Skip to main content

runx_contracts/
host_protocol.rs

1//! Host protocol contracts: execution events, resolution requests, host-run lifecycle.
2use serde::{Deserialize, Serialize};
3
4use crate::schema::{NonEmptyString, RunxSchema};
5use crate::{AgentContextEnvelope, JsonObject, JsonValue};
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
9pub enum ExecutionEvent {
10    SkillLoaded {
11        message: String,
12        #[serde(skip_serializing_if = "Option::is_none")]
13        data: Option<JsonValue>,
14    },
15    InputsResolved {
16        message: String,
17        #[serde(skip_serializing_if = "Option::is_none")]
18        data: Option<JsonValue>,
19    },
20    AuthResolved {
21        message: String,
22        #[serde(skip_serializing_if = "Option::is_none")]
23        data: Option<JsonValue>,
24    },
25    ResolutionRequested {
26        message: String,
27        #[serde(skip_serializing_if = "Option::is_none")]
28        data: Option<JsonValue>,
29    },
30    ResolutionResolved {
31        message: String,
32        #[serde(skip_serializing_if = "Option::is_none")]
33        data: Option<JsonValue>,
34    },
35    Admitted {
36        message: String,
37        #[serde(skip_serializing_if = "Option::is_none")]
38        data: Option<JsonValue>,
39    },
40    Executing {
41        message: String,
42        #[serde(skip_serializing_if = "Option::is_none")]
43        data: Option<JsonValue>,
44    },
45    StepStarted {
46        message: String,
47        #[serde(skip_serializing_if = "Option::is_none")]
48        data: Option<JsonValue>,
49    },
50    StepWaitingResolution {
51        message: String,
52        #[serde(skip_serializing_if = "Option::is_none")]
53        data: Option<JsonValue>,
54    },
55    StepCompleted {
56        message: String,
57        #[serde(skip_serializing_if = "Option::is_none")]
58        data: Option<JsonValue>,
59    },
60    Warning {
61        message: String,
62        #[serde(skip_serializing_if = "Option::is_none")]
63        data: Option<JsonValue>,
64    },
65    Completed {
66        message: String,
67        #[serde(skip_serializing_if = "Option::is_none")]
68        data: Option<JsonValue>,
69    },
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
73#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
74#[runx_schema(spec_id = "https://runx.ai/spec/resolution-request.schema.json")]
75pub enum ResolutionRequest {
76    Input {
77        id: NonEmptyString,
78        questions: Vec<Question>,
79    },
80    Approval {
81        id: NonEmptyString,
82        gate: ApprovalGate,
83    },
84    AgentAct {
85        id: NonEmptyString,
86        invocation: Box<AgentActInvocation>,
87    },
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
91#[serde(deny_unknown_fields)]
92#[runx_schema(spec_id = "https://runx.ai/spec/question.schema.json")]
93pub struct Question {
94    pub id: NonEmptyString,
95    pub prompt: NonEmptyString,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub description: Option<String>,
98    pub required: bool,
99    #[serde(rename = "type")]
100    pub question_type: NonEmptyString,
101}
102
103/// Host protocol approval request gate carried by `ResolutionRequest::Approval`.
104///
105/// This is distinct from `authority_proof.approval_gate`, which records an
106/// approval decision with `gate_id`, `gate_type`, and `decision` fields after
107/// policy evaluation. Keep this shape aligned with the host resolution request
108/// schema; do not use it as the authority-proof decision record.
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
110#[serde(deny_unknown_fields)]
111#[runx_schema(spec_id = "https://runx.ai/spec/approval-gate.schema.json")]
112pub struct ApprovalGate {
113    pub id: NonEmptyString,
114    pub reason: NonEmptyString,
115    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
116    pub gate_type: Option<String>,
117    /// Shallow summary payload. `rust-resolution-payload-parity` owns any
118    /// future deep typing for approval summaries.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub summary: Option<JsonObject>,
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
124#[serde(deny_unknown_fields)]
125#[runx_schema(spec_id = "https://runx.ai/spec/agent-act-invocation.schema.json")]
126pub struct AgentActInvocation {
127    pub id: NonEmptyString,
128    pub source_type: AgentActSourceType,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub agent: Option<NonEmptyString>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub task: Option<NonEmptyString>,
133    pub envelope: AgentContextEnvelope,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
137#[serde(rename_all = "kebab-case")]
138pub enum AgentActSourceType {
139    Agent,
140    #[serde(rename = "agent-task")]
141    AgentStep,
142}
143
144#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
145#[serde(deny_unknown_fields)]
146#[runx_schema(spec_id = "https://runx.ai/spec/resolution-response.schema.json")]
147pub struct ResolutionResponse {
148    pub actor: ResolutionResponseActor,
149    /// Shallow response payload. `rust-resolution-payload-parity` owns any
150    /// future deep typing for resolution responses.
151    pub payload: JsonValue,
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
155#[serde(rename_all = "snake_case")]
156pub enum ResolutionResponseActor {
157    Human,
158    Agent,
159}
160
161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ApprovalDecision {
164    pub gate: ApprovalGate,
165    pub approved: bool,
166}
167
168#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
169#[serde(
170    tag = "status",
171    rename_all = "snake_case",
172    rename_all_fields = "camelCase",
173    deny_unknown_fields
174)]
175pub enum HostRunResult {
176    NeedsAgent {
177        skill_name: String,
178        run_id: String,
179        requests: Vec<ResolutionRequest>,
180        #[serde(skip_serializing_if = "Option::is_none")]
181        step_ids: Option<Vec<String>>,
182        #[serde(skip_serializing_if = "Option::is_none")]
183        step_labels: Option<Vec<String>>,
184        events: Vec<ExecutionEvent>,
185    },
186    Completed {
187        skill_name: String,
188        receipt_id: String,
189        output: String,
190        events: Vec<ExecutionEvent>,
191    },
192    Failed {
193        skill_name: String,
194        #[serde(skip_serializing_if = "Option::is_none")]
195        receipt_id: Option<String>,
196        error: String,
197        events: Vec<ExecutionEvent>,
198    },
199    Escalated {
200        skill_name: String,
201        receipt_id: String,
202        error: String,
203        events: Vec<ExecutionEvent>,
204    },
205    Denied {
206        skill_name: String,
207        reasons: Vec<String>,
208        #[serde(skip_serializing_if = "Option::is_none")]
209        receipt_id: Option<String>,
210        events: Vec<ExecutionEvent>,
211    },
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
215#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
216pub enum HostRunState {
217    NeedsAgent(HostNeedsAgentState),
218    Completed(HostTerminalState),
219    Failed(HostTerminalState),
220    Escalated(HostTerminalState),
221    Denied(HostTerminalState),
222}
223
224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase", deny_unknown_fields)]
226pub struct HostNeedsAgentState {
227    pub skill_name: String,
228    pub run_id: String,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub requested_path: Option<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub resolved_path: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub selected_runner: Option<String>,
235    pub requests: Vec<ResolutionRequest>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub step_ids: Option<Vec<String>>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub step_labels: Option<Vec<String>>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub lineage: Option<HostRunLineage>,
242}
243
244#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase", deny_unknown_fields)]
246pub struct HostTerminalState {
247    pub kind: HostRunKind,
248    pub skill_name: String,
249    pub run_id: String,
250    pub receipt_id: String,
251    pub verification: HostRunVerification,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub source_type: Option<String>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub started_at: Option<String>,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub completed_at: Option<String>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub disposition: Option<String>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub outcome_state: Option<String>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub actors: Option<Vec<String>>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub artifact_types: Option<Vec<String>>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub runner_provider: Option<String>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub approval: Option<HostRunApproval>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub lineage: Option<HostRunLineage>,
272}
273
274#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum HostRunKind {
277    Harness,
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
281#[serde(deny_unknown_fields)]
282pub struct HostRunVerification {
283    pub status: HostRunVerificationStatus,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub reason: Option<String>,
286}
287
288#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum HostRunVerificationStatus {
291    Verified,
292    Unverified,
293    Invalid,
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase", deny_unknown_fields)]
298pub struct HostRunLineage {
299    pub kind: HostRunLineageKind,
300    pub source_run_id: String,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub source_receipt_id: Option<String>,
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "snake_case")]
307pub enum HostRunLineageKind {
308    Rerun,
309}
310
311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase", deny_unknown_fields)]
313pub struct HostRunApproval {
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub gate_id: Option<String>,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub gate_type: Option<String>,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub decision: Option<HostRunApprovalDecision>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub reason: Option<String>,
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
325#[serde(rename_all = "snake_case")]
326pub enum HostRunApprovalDecision {
327    Approved,
328    Denied,
329}