Skip to main content

runx_parser/skill/
types.rs

1// rust-style-allow: large-file - skill schema vocabulary is intentionally
2// centralized so parser fixtures, contract mirrors, and runtime front validation
3// share one typed source of truth.
4use std::collections::BTreeMap;
5
6use runx_contracts::{ExecutionSemantics, JsonObject, JsonValue};
7use runx_core::policy::{CwdPolicy, SandboxProfile};
8use serde::{Deserialize, Serialize};
9
10use crate::graph::MintAuthorityDirective;
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct RawSkillIr {
15    pub frontmatter: JsonObject,
16    pub raw_frontmatter: String,
17    pub body: String,
18}
19
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct SkillInput {
23    #[serde(rename = "type")]
24    pub input_type: String,
25    pub required: bool,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub description: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub default: Option<JsonValue>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct SkillRetryPolicy {
35    pub max_attempts: u64,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct SkillIdempotencyPolicy {
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub key: Option<String>,
43}
44
45/// Closed set of built-in skill source kinds. The extension lane is the
46/// `ExternalAdapter` variant; custom adapters are identified by the
47/// external-adapter manifest, not by an open `source.type` string. First-party
48/// governed fronts that carry their own protocol, such as thread outbox
49/// publication, get explicit variants.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
52pub enum SourceKind {
53    CliTool,
54    Mcp,
55    Catalog,
56    A2a,
57    Agent,
58    #[serde(rename = "agent-task")]
59    AgentStep,
60    HarnessHook,
61    Graph,
62    Http,
63    ExternalAdapter,
64    ThreadOutboxProvider,
65}
66
67impl SourceKind {
68    pub fn as_str(&self) -> &'static str {
69        match self {
70            SourceKind::CliTool => "cli-tool",
71            SourceKind::Mcp => "mcp",
72            SourceKind::Catalog => "catalog",
73            SourceKind::A2a => "a2a",
74            SourceKind::Agent => "agent",
75            SourceKind::AgentStep => "agent-task",
76            SourceKind::HarnessHook => "harness-hook",
77            SourceKind::Graph => "graph",
78            SourceKind::Http => "http",
79            SourceKind::ExternalAdapter => "external-adapter",
80            SourceKind::ThreadOutboxProvider => "thread-outbox-provider",
81        }
82    }
83}
84
85impl std::fmt::Display for SourceKind {
86    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        formatter.write_str(self.as_str())
88    }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum InputMode {
94    Args,
95    Stdin,
96    None,
97}
98
99impl InputMode {
100    pub fn as_str(&self) -> &'static str {
101        match self {
102            InputMode::Args => "args",
103            InputMode::Stdin => "stdin",
104            InputMode::None => "none",
105        }
106    }
107}
108
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct SkillSource {
112    #[serde(rename = "type")]
113    pub source_type: SourceKind,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub command: Option<String>,
116    pub args: Vec<String>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub cwd: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub timeout_seconds: Option<u64>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub input_mode: Option<InputMode>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub sandbox: Option<SkillSandbox>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub server: Option<SkillMcpServer>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub catalog_ref: Option<String>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub tool: Option<String>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub arguments: Option<JsonObject>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub agent_card_url: Option<String>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub agent_identity: Option<String>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub agent: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub task: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub hook: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub outputs: Option<JsonObject>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub graph: Option<crate::ExecutionGraph>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub http: Option<SkillHttpSource>,
149    /// The declared act this source performs, validated at load. `None` when no
150    /// `act:` block is declared (the run then seals a generic observation act).
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub act: Option<ActDeclaration>,
153    pub raw: JsonObject,
154}
155
156/// A skill's declared act: how a run describes the act it performs. The form and
157/// purpose, and how the target, decision, effect, actor, authority, and previous
158/// receipt are read, where `<field>_from` names a trusted input key and the bare
159/// field is a static literal (the driver-pinned input wins). The runtime fills
160/// the act's structure from these and the trusted inputs; the model authors only
161/// the reason prose. Absent an `act:` block, a run seals a generic observation.
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct ActDeclaration {
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub form: Option<String>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub form_from: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub purpose: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub purpose_from: Option<String>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub legitimacy: Option<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub legitimacy_from: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub reason_from: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub target_from: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub decision_from: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub effect_from: Option<String>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub effect_field_from: Option<String>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub effect_from_input: Option<String>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub effect_type: Option<String>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub effect_prefix: Option<String>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub effect_prefix_from: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub actor_from: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub authority_from: Option<String>,
199    /// Charter attenuation: the input key carrying the member's own authority term
200    /// (the child grant minted from the charter).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub authority_term_from: Option<String>,
203    /// The input key carrying the parent (charter) authority reference the child
204    /// term attenuates from.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub authority_parent_from: Option<String>,
207    /// The input key carrying the subset proof that the child term is no broader
208    /// than the parent charter.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub authority_subset_proof_from: Option<String>,
211    /// Compute path (mutually exclusive with the explicit `authority_*_from` keys
212    /// above): when present, the runtime mints the child authority term from the
213    /// charter (resolved from config) off the model path instead of receiving a
214    /// pre-built term. With `MintScopeSource::RequestedScope`, the requested child
215    /// scope is read from the input key named by `requested_scope_from`.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub mint_authority: Option<MintAuthorityDirective>,
218    /// The input key carrying the requested child scope, used only when
219    /// `mint_authority.source` is `MintScopeSource::RequestedScope`.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub requested_scope_from: Option<String>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub previous_from: Option<String>,
224    /// Graph turns only: the step whose output supplies the reason prose.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub reason_step: Option<String>,
227    /// Graph turns only: the step whose real result supplies the governed effect.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub effect_step: Option<String>,
230}
231
232/// Config for an `http` source: the endpoint, the method, static request headers
233/// (whose values may carry `${secret:NAME}` references resolved at invocation),
234/// and an explicit, default-off opt-in to reach private or loopback networks
235/// (the governed transport blocks them otherwise, mirroring the sandbox network
236/// opt-in).
237#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct SkillHttpSource {
240    pub url: String,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub method: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub headers: Option<BTreeMap<String, String>>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub allow_private_network: Option<bool>,
247}
248
249#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct SkillMcpServer {
252    pub command: String,
253    pub args: Vec<String>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub cwd: Option<String>,
256}
257
258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct SkillSandbox {
261    pub profile: SandboxProfile,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub cwd_policy: Option<CwdPolicy>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub env_allowlist: Option<Vec<String>>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub network: Option<bool>,
268    pub writable_paths: Vec<String>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub require_enforcement: Option<bool>,
271    #[serde(skip)]
272    pub approved_escalation: Option<bool>,
273    pub raw: JsonObject,
274}
275
276#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct SkillArtifactContract {
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub emits: Option<Vec<String>>,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub named_emits: Option<BTreeMap<String, String>>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub wrap_as: Option<String>,
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct SkillQualityProfile {
290    pub heading: String,
291    pub content: String,
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295pub enum ValidateSkillMode {
296    Strict,
297    Lenient,
298}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
301pub struct ValidateSkillOptions {
302    pub mode: ValidateSkillMode,
303}
304
305impl Default for ValidateSkillOptions {
306    fn default() -> Self {
307        Self {
308            mode: ValidateSkillMode::Strict,
309        }
310    }
311}
312
313impl ValidateSkillOptions {
314    #[must_use]
315    pub const fn strict() -> Self {
316        Self {
317            mode: ValidateSkillMode::Strict,
318        }
319    }
320
321    #[must_use]
322    pub const fn lenient() -> Self {
323        Self {
324            mode: ValidateSkillMode::Lenient,
325        }
326    }
327}
328
329#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct ValidatedSkill {
332    pub name: String,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub description: Option<String>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub category: Option<String>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub runx_category: Option<String>,
339    pub body: String,
340    pub source: SkillSource,
341    pub inputs: BTreeMap<String, SkillInput>,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub auth: Option<JsonValue>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub risk: Option<JsonValue>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub runtime: Option<JsonValue>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub retry: Option<SkillRetryPolicy>,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub idempotency: Option<SkillIdempotencyPolicy>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub mutating: Option<bool>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub artifacts: Option<SkillArtifactContract>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub quality_profile: Option<SkillQualityProfile>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub allowed_tools: Option<Vec<String>>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub execution: Option<ExecutionSemantics>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub runx: Option<JsonObject>,
364    pub raw: RawSkillIr,
365}
366
367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct SkillRunnerDefinition {
370    pub name: String,
371    pub default: bool,
372    pub source: SkillSource,
373    pub inputs: BTreeMap<String, SkillInput>,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub auth: Option<JsonValue>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub risk: Option<JsonValue>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub runtime: Option<JsonValue>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub retry: Option<SkillRetryPolicy>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub idempotency: Option<SkillIdempotencyPolicy>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub mutating: Option<bool>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub artifacts: Option<SkillArtifactContract>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub allowed_tools: Option<Vec<String>>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub execution: Option<ExecutionSemantics>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub runx: Option<JsonObject>,
394    pub raw: JsonObject,
395}