Skip to main content

runx_contracts/
external_adapter.rs

1//! External adapter contract types.
2use serde::{Deserialize, Deserializer, Serialize};
3
4use crate::schema::{IsoDateTime, NonEmptyString, RunxSchema};
5use crate::{JsonNumber, JsonObject, Reference, ResolutionRequest};
6
7pub const EXTERNAL_ADAPTER_PROTOCOL_VERSION: &str = "runx.external_adapter.v1";
8
9/// The const `protocol_version` discriminant shared by every external-adapter
10/// frame.
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
12pub enum ExternalAdapterProtocolVersion {
13    #[serde(rename = "runx.external_adapter.v1")]
14    V1,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
18pub enum ExternalAdapterManifestSchema {
19    #[serde(rename = "runx.external_adapter.manifest.v1")]
20    V1,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
24pub enum ExternalAdapterCredentialRequestSchema {
25    #[serde(rename = "runx.external_adapter.credential_request.v1")]
26    V1,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
30pub enum ExternalAdapterInvocationSchema {
31    #[serde(rename = "runx.external_adapter.invocation.v1")]
32    V1,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
36pub enum ExternalAdapterHostResolutionSchema {
37    #[serde(rename = "runx.external_adapter.host_resolution.v1")]
38    V1,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
42pub enum ExternalAdapterCancellationSchema {
43    #[serde(rename = "runx.external_adapter.cancellation.v1")]
44    V1,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
48#[serde(rename_all = "snake_case")]
49pub enum ExternalAdapterTransportKind {
50    Process,
51    Http,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
55#[serde(rename_all = "snake_case")]
56pub enum ExternalAdapterStatus {
57    Completed,
58    Failed,
59    HostResolutionRequested,
60    Cancelled,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
64#[serde(rename_all = "snake_case")]
65pub enum ExternalAdapterCredentialPurpose {
66    ProviderApi,
67    Registry,
68    ArtifactStore,
69    WebhookVerification,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
73#[serde(deny_unknown_fields)]
74pub struct ExternalAdapterTransport {
75    pub kind: ExternalAdapterTransportKind,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub command: Option<NonEmptyString>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub args: Option<Vec<String>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub endpoint: Option<NonEmptyString>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
85#[serde(deny_unknown_fields)]
86pub struct ExternalAdapterCredentialNeed {
87    pub purpose: ExternalAdapterCredentialPurpose,
88    pub provider: NonEmptyString,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub scope_refs: Option<Vec<Reference>>,
91    pub required: bool,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
95#[serde(deny_unknown_fields)]
96pub struct ExternalAdapterSandboxIntent {
97    pub profile: NonEmptyString,
98    pub network: bool,
99    pub cwd_policy: NonEmptyString,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub writable_paths: Option<Vec<NonEmptyString>>,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
105#[serde(deny_unknown_fields)]
106pub struct ExternalAdapterTimeouts {
107    pub startup_ms: u64,
108    pub invocation_ms: u64,
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
112#[serde(deny_unknown_fields)]
113#[runx_schema(id = "runx.external_adapter.manifest.v1")]
114pub struct ExternalAdapterManifest {
115    pub schema: ExternalAdapterManifestSchema,
116    pub protocol_version: ExternalAdapterProtocolVersion,
117    pub adapter_id: NonEmptyString,
118    pub name: NonEmptyString,
119    pub version: NonEmptyString,
120    pub supported_source_types: Vec<NonEmptyString>,
121    pub transport: ExternalAdapterTransport,
122    pub timeouts: ExternalAdapterTimeouts,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub credential_needs: Option<Vec<ExternalAdapterCredentialNeed>>,
125    pub sandbox_intent: ExternalAdapterSandboxIntent,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub metadata: Option<JsonObject>,
128}
129
130#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
131#[serde(deny_unknown_fields)]
132pub struct ExternalAdapterCredentialReference {
133    pub credential_ref: Reference,
134    pub provider: NonEmptyString,
135    pub purpose: ExternalAdapterCredentialPurpose,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
139#[serde(deny_unknown_fields)]
140#[runx_schema(id = "runx.external_adapter.credential_request.v1")]
141pub struct ExternalAdapterCredentialRequest {
142    pub schema: ExternalAdapterCredentialRequestSchema,
143    pub protocol_version: ExternalAdapterProtocolVersion,
144    pub request_id: NonEmptyString,
145    pub adapter_id: NonEmptyString,
146    pub invocation_id: NonEmptyString,
147    pub credential_refs: Vec<ExternalAdapterCredentialReference>,
148    pub requested_at: IsoDateTime,
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
152#[serde(deny_unknown_fields)]
153#[runx_schema(id = "runx.external_adapter.invocation.v1")]
154pub struct ExternalAdapterInvocation {
155    pub schema: ExternalAdapterInvocationSchema,
156    pub protocol_version: ExternalAdapterProtocolVersion,
157    pub invocation_id: NonEmptyString,
158    pub adapter_id: NonEmptyString,
159    pub run_id: NonEmptyString,
160    pub step_id: NonEmptyString,
161    pub source_type: NonEmptyString,
162    pub skill_ref: NonEmptyString,
163    pub harness_ref: Reference,
164    pub host_ref: Reference,
165    pub inputs: JsonObject,
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub resolved_inputs: Option<JsonObject>,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub cwd: Option<NonEmptyString>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub receipt_dir: Option<NonEmptyString>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub env: Option<JsonObject>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub credential_refs: Option<Vec<ExternalAdapterCredentialReference>>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub metadata: Option<JsonObject>,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
181#[serde(deny_unknown_fields)]
182pub struct ExternalAdapterArtifactObservation {
183    pub artifact_ref: Reference,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub summary: Option<String>,
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
189#[serde(deny_unknown_fields)]
190pub struct ExternalAdapterErrorObservation {
191    pub code: String,
192    pub message: String,
193    pub retryable: bool,
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
197#[serde(untagged)]
198pub enum ExternalAdapterTelemetryValue {
199    Number(JsonNumber),
200    String(String),
201    Bool(bool),
202}
203
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
205#[serde(deny_unknown_fields)]
206pub struct ExternalAdapterTelemetryObservation {
207    pub name: String,
208    pub value: ExternalAdapterTelemetryValue,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub unit: Option<String>,
211}
212
213#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
214#[serde(deny_unknown_fields)]
215#[runx_schema(id = "runx.external_adapter.response.v1")]
216pub struct ExternalAdapterResponse {
217    pub schema: String,
218    pub protocol_version: String,
219    pub invocation_id: String,
220    pub adapter_id: String,
221    pub status: ExternalAdapterStatus,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub stdout: Option<String>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub stderr: Option<String>,
226    #[serde(
227        default,
228        deserialize_with = "deserialize_optional_nullable_i64",
229        skip_serializing_if = "Option::is_none"
230    )]
231    pub exit_code: Option<Option<i64>>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub output: Option<JsonObject>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub artifacts: Option<Vec<ExternalAdapterArtifactObservation>>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub errors: Option<Vec<ExternalAdapterErrorObservation>>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub telemetry: Option<Vec<ExternalAdapterTelemetryObservation>>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub metadata: Option<JsonObject>,
242    pub observed_at: String,
243}
244
245#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
246#[serde(deny_unknown_fields)]
247#[runx_schema(id = "runx.external_adapter.host_resolution.v1")]
248pub struct ExternalAdapterHostResolutionFrame {
249    pub schema: ExternalAdapterHostResolutionSchema,
250    pub protocol_version: ExternalAdapterProtocolVersion,
251    pub frame_id: NonEmptyString,
252    pub invocation_id: NonEmptyString,
253    pub adapter_id: NonEmptyString,
254    pub request: ResolutionRequest,
255    pub requested_at: IsoDateTime,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
259#[serde(deny_unknown_fields)]
260#[runx_schema(id = "runx.external_adapter.cancellation.v1")]
261pub struct ExternalAdapterCancellationFrame {
262    pub schema: ExternalAdapterCancellationSchema,
263    pub protocol_version: ExternalAdapterProtocolVersion,
264    pub frame_id: NonEmptyString,
265    pub invocation_id: NonEmptyString,
266    pub adapter_id: NonEmptyString,
267    pub reason: NonEmptyString,
268    pub requested_at: IsoDateTime,
269}
270
271fn deserialize_optional_nullable_i64<'de, D>(
272    deserializer: D,
273) -> Result<Option<Option<i64>>, D::Error>
274where
275    D: Deserializer<'de>,
276{
277    Option::<i64>::deserialize(deserializer).map(Some)
278}