From 0dcbe7f160d1f73493bc61c0c03faa60e1fe0c79 Mon Sep 17 00:00:00 2001 From: Miya Date: Tue, 15 Sep 2026 11:17:21 +0200 Subject: [PATCH] fix(kernel): carry agent transport in specs Session-Id: 01a09c40-ce3b-7f11-a7df-b6b7ccab6fd9 --- kernel/relayflowd-core/src/spec.rs | 13 ++++++++ kernel/relayflowd-core/tests/spec_parity.rs | 35 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/kernel/relayflowd-core/src/spec.rs b/kernel/relayflowd-core/src/spec.rs index d50f58ed..9680f6f1 100644 --- a/kernel/relayflowd-core/src/spec.rs +++ b/kernel/relayflowd-core/src/spec.rs @@ -313,6 +313,7 @@ const STEP_AGENT_FIELDS: &[&str] = &[ "instruction", "cli", "model", + "transport", "recovery_mode", "surfaces", "permissions", @@ -423,6 +424,11 @@ pub enum StepKind { /// then handed to the worker, which surfaces it to the CLI. #[serde(default, skip_serializing_if = "Option::is_none")] model: Option, + /// How the attached worker invokes the declared CLI. The kernel does + /// not implement either transport; it journals and dispatches the + /// choice so the worker can honor it deterministically. + #[serde(default, skip_serializing_if = "Option::is_none")] + transport: Option, #[serde(default)] recovery_mode: RecoveryMode, /// Declared mutable surfaces (RFC Appendix A rule 1) — names only. @@ -450,6 +456,13 @@ pub enum StepType { Agent, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AgentTransport { + Direct, + Relay, +} + #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum RecoveryMode { diff --git a/kernel/relayflowd-core/tests/spec_parity.rs b/kernel/relayflowd-core/tests/spec_parity.rs index 66c5d2dc..7e327879 100644 --- a/kernel/relayflowd-core/tests/spec_parity.rs +++ b/kernel/relayflowd-core/tests/spec_parity.rs @@ -67,6 +67,41 @@ fn the_kernel_parses_the_rung_c_agent_spec_and_stamps_the_same_hash() { ); } +#[test] +fn the_kernel_round_trips_declared_agent_transports_and_rejects_unknown_values() { + for transport in ["direct", "relay"] { + let value = serde_json::json!({ + "steps": [{ + "id": "agent", + "type": "agent", + "instruction": "work", + "transport": transport, + }], + }); + let parsed = RunSpec::parse(&value).expect("declared agent transport must parse"); + parsed + .validate() + .expect("declared agent transport must validate"); + assert_eq!( + serde_json::to_value(parsed).unwrap()["steps"][0]["transport"], + transport + ); + } + + let unknown = serde_json::json!({ + "steps": [{ + "id": "agent", + "type": "agent", + "instruction": "work", + "transport": "telepathy", + }], + }); + assert!( + RunSpec::parse(&unknown).is_err(), + "unknown transport must fail closed" + ); +} + #[test] fn the_kernel_parses_the_event_triggered_spec_and_stamps_the_same_hash() { assert_parity(