Skip to main content

max / makenotwork

bento: wire the agent protocol-version skew check (Run 3 wiring) HealthResponse.version / PROTOCOL_VERSION were advertised and documented as the mechanism to "detect a skewed agent at preflight" but no client ever compared the value — preflight discarded it. preflight() now checks it: version 0 (a legacy agent predating the field) is tolerated, a mismatched non-zero version is refused with a clear upgrade message, so skew surfaces before a run instead of mid-stream. e2e test added. ops-exec 41 tests; clippy -D clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-23 02:49 UTC
Commit: dff1bfc2c9a98c5bb4598882cae7e023add8b17d
Parent: cb10468
2 files changed, +31 insertions, -2 deletions
@@ -172,12 +172,23 @@ impl Executor for AgentRpc {
172 172 }
173 173
174 174 async fn preflight(&self) -> Result<()> {
175 - self.health().await.map(|_| ()).with_context(|| {
175 + let health = self.health().await.with_context(|| {
176 176 format!(
177 177 "agent /health failed for `{}` — is ops-agent running in the Aqua session on the build host?",
178 178 self.host_label,
179 179 )
180 - })
180 + })?;
181 + // Detect a skewed agent here rather than mid-stream. version 0 = a legacy
182 + // agent that predates the field (HealthResponse.version defaults to 0);
183 + // tolerate it, but refuse a future major we don't speak.
184 + anyhow::ensure!(
185 + health.version == 0 || health.version == crate::wire::PROTOCOL_VERSION,
186 + "agent `{}` speaks wire protocol v{}, but this client expects v{} — upgrade the mismatched side",
187 + self.host_label,
188 + health.version,
189 + crate::wire::PROTOCOL_VERSION,
190 + );
191 + Ok(())
181 192 }
182 193
183 194 fn capabilities(&self) -> &CapabilitySet {
@@ -82,6 +82,24 @@ async fn agent_runs_a_granted_step_and_streams_output() {
82 82 }
83 83
84 84 #[tokio::test]
85 + async fn preflight_accepts_a_matching_protocol_version() {
86 + let base = spawn_agent(
87 + vec![CallerGrant {
88 + identity: "fw13".into(),
89 + actuate: vec!["build".into()],
90 + observe: vec![],
91 + }],
92 + builder_grant(),
93 + )
94 + .await;
95 + let rpc = AgentRpc::new(base, "mbp", CapabilitySet::from_tokens(["build"], Vec::<&str>::new()));
96 + // The agent advertises PROTOCOL_VERSION; preflight must accept it.
97 + let health = rpc.health().await.unwrap();
98 + assert_eq!(health.version, ops_exec::wire::PROTOCOL_VERSION);
99 + rpc.preflight().await.expect("preflight accepts a same-version agent");
100 + }
101 +
102 + #[tokio::test]
85 103 async fn agent_denies_action_outside_its_grant() {
86 104 // The agent host grants build/sign only; the caller asks to deploy. Even
87 105 // though the client-side caps below include deploy, the agent must refuse.