@team-agent/installer 0.5.114 → 0.5.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,12 @@ use std::path::{Path, PathBuf};
3
3
 
4
4
  use team_agent_contract::contract::{hooks::*, plan::CatalogObservation, types::*};
5
5
  use team_agent_contract::host::{command::RealCommandRunner, process::resolve_cwd};
6
- use team_agent_contract::kiro::{native::*, KiroAdapter, McpStdio, KIRO_DESCRIPTOR};
6
+ use team_agent_contract::kiro::native as kiro;
7
+ use team_agent_contract::providers::cursor::native as cursor;
7
8
  use thiserror::Error;
8
9
 
10
+ use super::registry::{self, NativeProvider};
11
+
9
12
  #[derive(Debug, Error)]
10
13
  pub enum DiscoveryError {
11
14
  #[error("required executable is unavailable: {0}")]
@@ -13,7 +16,9 @@ pub enum DiscoveryError {
13
16
  #[error("native discovery failed: {0}")]
14
17
  Host(#[from] team_agent_contract::host::HostError),
15
18
  #[error("native engine probe failed: {0}")]
16
- Probe(#[from] EngineProbeError),
19
+ Probe(#[from] kiro::EngineProbeError),
20
+ #[error("native Cursor launcher probe failed: {0}")]
21
+ CursorProbe(#[from] cursor::ProbeError),
17
22
  #[error("native discovery contract: {0:?}")]
18
23
  Contract(ContractError),
19
24
  #[error("native model discovery: {0:?}")]
@@ -26,6 +31,7 @@ pub struct Discovery {
26
31
  pub engine: PathBuf,
27
32
  pub native: NativeIdentity,
28
33
  pub catalog: CatalogObservation,
34
+ pub runtime: Option<cursor::RuntimeInstallation>,
29
35
  }
30
36
 
31
37
  pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
@@ -43,44 +49,89 @@ pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
43
49
  Err(DiscoveryError::Executable(name))
44
50
  }
45
51
 
46
- pub fn discover(workspace: &Path) -> Result<Discovery, DiscoveryError> {
47
- let home = std::env::var_os("HOME")
48
- .map(PathBuf::from)
49
- .ok_or(DiscoveryError::Executable(
50
- "HOME for native installation lookup",
51
- ))?;
52
- let engine = resolve_helper(&executable(KIRO_DESCRIPTOR.identity.binary)?, &home)?;
53
- let native = probe_engine(
54
- &engine,
55
- &mut RealCommandRunner::default(),
56
- discovery_bounds(),
57
- )?;
52
+ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, DiscoveryError> {
53
+ let selected = registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)?;
54
+ let descriptor = selected.descriptor();
55
+ let bounds = ReadBounds {
56
+ deadline: std::time::Duration::from_secs(30),
57
+ max_output_bytes: 1024 * 1024,
58
+ };
59
+ let launcher = executable(descriptor.identity.binary)?;
60
+ let (engine, native) = match selected {
61
+ NativeProvider::Kiro => {
62
+ let home = std::env::var_os("HOME")
63
+ .map(PathBuf::from)
64
+ .ok_or(DiscoveryError::Executable("HOME for native installation lookup"))?;
65
+ let engine = kiro::resolve_helper(&launcher, &home)?;
66
+ let native = kiro::probe_engine(&engine, &mut RealCommandRunner::default(), bounds)?;
67
+ (engine, native)
68
+ }
69
+ NativeProvider::Cursor => {
70
+ let engine = cursor::resolve_launcher(&launcher)?;
71
+ let native = cursor::probe_launcher(&engine, &mut RealCommandRunner::default(), bounds)?;
72
+ (engine, native)
73
+ }
74
+ };
58
75
  let request = CatalogRequest {
59
- provider: ProviderId::new(KIRO_DESCRIPTOR.identity.id).map_err(DiscoveryError::Contract)?,
76
+ provider: ProviderId::new(descriptor.identity.id).map_err(DiscoveryError::Contract)?,
60
77
  native: native.clone(),
61
78
  executable: engine.clone(),
62
79
  cwd: resolve_cwd(workspace)?,
63
- source: CHAT_CATALOG_SOURCE,
64
- bounds: discovery_bounds(),
80
+ source: descriptor.model.catalog.require("native catalog source").map_err(DiscoveryError::Contract)?.clone(),
81
+ bounds,
65
82
  };
66
- let mut host = CatalogReader::new(request.clone(), RealCommandRunner::default())
67
- .map_err(DiscoveryError::Contract)?;
68
- // H1 does not use these pointers; keep the actual current executable and do
69
- // not manufacture an alternate CLI or execute an MCP server for discovery.
70
- let adapter = KiroAdapter::new(McpStdio {
71
- executable: std::env::current_exe()?.canonicalize()?,
72
- arguments: Vec::new(),
73
- environment: Default::default(),
74
- })
75
- .map_err(DiscoveryError::Contract)?;
76
- let catalog = adapter
77
- .discover(&request, &mut host)
78
- .map_err(DiscoveryError::Catalog)?;
79
- Ok(Discovery {
80
- engine,
81
- native,
82
- catalog,
83
- })
83
+ // H1 does not use these pointers or execute an MCP server. Cursor's
84
+ // launcher fingerprint authorizes only metadata reads, never worker launch.
85
+ let adapter = selected.adapter(
86
+ std::env::current_exe()?.canonicalize()?,
87
+ Vec::new(),
88
+ Default::default(),
89
+ ).map_err(DiscoveryError::Contract)?;
90
+ let hooks = adapter.hooks();
91
+ let catalog_hook = hooks.catalog.require("H1 catalog").map_err(DiscoveryError::Contract)?;
92
+ let catalog = match selected {
93
+ NativeProvider::Kiro => {
94
+ let mut host = kiro::CatalogReader::new(request.clone(), RealCommandRunner::default())
95
+ .map_err(DiscoveryError::Contract)?;
96
+ catalog_hook.discover(&request, &mut host)
97
+ }
98
+ NativeProvider::Cursor => {
99
+ let mut host = cursor::NativeCatalog::new(request.clone(), RealCommandRunner::default())
100
+ .map_err(DiscoveryError::Contract)?;
101
+ catalog_hook.discover(&request, &mut host)
102
+ }
103
+ }.map_err(DiscoveryError::Catalog)?;
104
+ let result = Discovery { engine, native, catalog, runtime: None };
105
+ validate_catalog(provider, &result).map_err(DiscoveryError::Contract)?;
106
+ Ok(result)
107
+ }
108
+
109
+ /// Runtime discovery is separate from public catalog metadata. Cursor explicitly
110
+ /// bridges the same installation's catalog to its proven Node/entry identity.
111
+ pub(super) fn discover_runtime(provider: &ProviderId, workspace: &Path) -> Result<Discovery, DiscoveryError> {
112
+ let mut discovery = discover(provider, workspace)?;
113
+ if registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)? == NativeProvider::Cursor {
114
+ let home = std::env::var_os("HOME").map(PathBuf::from)
115
+ .ok_or(DiscoveryError::Executable("HOME for native subscription"))?;
116
+ let runtime = cursor::probe_runtime(&discovery.engine, &home, &mut RealCommandRunner::default(),
117
+ ReadBounds { deadline: std::time::Duration::from_secs(30), max_output_bytes: 1024 * 1024 })?;
118
+ discovery.catalog = runtime.bind_catalog(&discovery.catalog).map_err(DiscoveryError::Contract)?;
119
+ discovery.engine = runtime.engine.clone();
120
+ discovery.native = runtime.native.clone();
121
+ discovery.runtime = Some(runtime);
122
+ }
123
+ Ok(discovery)
124
+ }
125
+
126
+ fn validate_catalog(provider: &ProviderId, discovery: &Discovery) -> Result<(), ContractError> {
127
+ let descriptor = registry::provider(provider.as_str())?.descriptor();
128
+ if discovery.catalog.provider != *provider
129
+ || discovery.catalog.native != discovery.native
130
+ || discovery.catalog.schema != descriptor.model.catalog.require("native catalog source")?.schema
131
+ {
132
+ return Err(ContractError::Mismatch("native catalog provider/identity/schema"));
133
+ }
134
+ Ok(())
84
135
  }
85
136
 
86
137
  pub fn models(
@@ -88,7 +139,10 @@ pub fn models(
88
139
  ) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
89
140
  let result = std::env::current_dir()
90
141
  .map_err(DiscoveryError::Io)
91
- .and_then(|cwd| discover(&cwd));
142
+ .and_then(|cwd| {
143
+ let provider = ProviderId::new(&args.provider).map_err(DiscoveryError::Contract)?;
144
+ discover(&provider, &cwd)
145
+ });
92
146
  render_models(args, result)
93
147
  }
94
148
 
@@ -98,6 +152,11 @@ fn render_models(
98
152
  ) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
99
153
  use crate::cli::{CmdOutput, CmdResult, ExitCode};
100
154
  use serde_json::json;
155
+ let result = result.and_then(|discovery| {
156
+ let provider = ProviderId::new(&args.provider).map_err(DiscoveryError::Contract)?;
157
+ validate_catalog(&provider, &discovery).map_err(DiscoveryError::Contract)?;
158
+ Ok(discovery)
159
+ });
101
160
  let (value, human, exit) = match result {
102
161
  Ok(discovery) => {
103
162
  let query = args.search.as_deref().map(str::to_lowercase);
@@ -114,7 +173,8 @@ fn render_models(
114
173
  let value = json!({"schema_version":"models.v1","ok":true,"provider":args.provider,
115
174
  "models":rows,"auth":"unknown","auth_basis":"native_catalog_visibility_is_not_authorization",
116
175
  "source":{"schema":discovery.catalog.schema,"executable":discovery.engine,
117
- "version":discovery.native.version,"executable_sha256":team_agent_contract::host::digest_hex(discovery.native.executable_sha256)},
176
+ "version":discovery.native.version,"executable_sha256":team_agent_contract::host::digest_hex(discovery.native.executable_sha256),
177
+ "identity_scope":"catalog_only"},
118
178
  "current_role_model":null,"search":args.search});
119
179
  let mut lines = vec![format!("models.v1 | {} native model IDs:", args.provider)];
120
180
  lines.extend(
@@ -163,12 +223,13 @@ mod tests {
163
223
  executable_sha256: Digest([1; 32]),
164
224
  };
165
225
  let discovery = Discovery {
226
+ runtime: None,
166
227
  engine: "/controlled/kiro-cli-chat".into(),
167
228
  native: native.clone(),
168
229
  catalog: CatalogObservation {
169
230
  provider: ProviderId::new("kiro").unwrap(),
170
231
  native,
171
- schema: "controlled-json".into(),
232
+ schema: kiro::CHAT_CATALOG_SOURCE.schema.into(),
172
233
  models: vec![ModelRecord {
173
234
  id: "Exact-Case-ID".into(),
174
235
  efforts: Support::Unverified(Reason {
@@ -192,7 +253,7 @@ mod tests {
192
253
  assert_eq!(json["auth"], "unknown");
193
254
  assert_eq!(json["models"][0]["model_id"], "Exact-Case-ID");
194
255
  assert_eq!(json["models"][0]["efforts"]["state"], "unverified");
195
- assert_eq!(json["source"]["schema"], "controlled-json");
256
+ assert_eq!(json["source"]["schema"], kiro::CHAT_CATALOG_SOURCE.schema);
196
257
  assert!(json["current_role_model"].is_null());
197
258
  }
198
259
 
@@ -203,7 +264,7 @@ mod tests {
203
264
  search: None,
204
265
  json: true,
205
266
  };
206
- let error = parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
267
+ let error = kiro::parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
207
268
  let result = render_models(&args, Err(DiscoveryError::Probe(error))).unwrap();
208
269
  assert_eq!(result.exit, ExitCode::Error);
209
270
  let CmdOutput::Human(text) = result.output else {
@@ -217,6 +278,91 @@ mod tests {
217
278
  assert_eq!(json["models"], serde_json::json!([]));
218
279
  }
219
280
 
281
+ fn cursor_catalog() -> Discovery {
282
+ let native = NativeIdentity {
283
+ version: "2026.09.29-a1b2c3".into(),
284
+ harness: cursor::METADATA_HARNESS.into(),
285
+ ui: cursor::METADATA_UI.into(),
286
+ platform: Platform::MacOs,
287
+ executable_sha256: Digest([2; 32]),
288
+ };
289
+ Discovery {
290
+ runtime: None,
291
+ engine: "/controlled/cursor-agent".into(),
292
+ native: native.clone(),
293
+ catalog: CatalogObservation {
294
+ provider: ProviderId::new("cursor").unwrap(),
295
+ native,
296
+ schema: cursor::CATALOG_SOURCE.schema.into(),
297
+ models: ["gpt-5.6-luna-xhigh", "gpt-5.6-luna-xhigh-fast", "Other-Exact-ID"]
298
+ .into_iter().map(|id| ModelRecord {
299
+ id: id.into(),
300
+ efforts: Support::Unverified(Reason {
301
+ code: "not-observed", message: "Native IDs are not separate effort flags",
302
+ }),
303
+ }).collect(),
304
+ },
305
+ }
306
+ }
307
+
308
+ #[test]
309
+ fn cursor_catalog_filters_exact_native_family_without_runtime_claims() {
310
+ let args = ModelsArgs {
311
+ provider: "cursor".into(), search: Some("GPT-5.6-LUNA-XHIGH".into()), json: true,
312
+ };
313
+ let result = render_models(&args, Ok(cursor_catalog())).unwrap();
314
+ assert_eq!(result.exit, ExitCode::Ok);
315
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
316
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
317
+ let ids: Vec<_> = json["models"].as_array().unwrap().iter()
318
+ .map(|row| row["model_id"].as_str().unwrap()).collect();
319
+ assert_eq!(ids, ["gpt-5.6-luna-xhigh", "gpt-5.6-luna-xhigh-fast"]);
320
+ assert_eq!(json["provider"], "cursor");
321
+ assert_eq!(json["auth"], "unknown");
322
+ assert_eq!(json["source"]["identity_scope"], "catalog_only");
323
+ assert_eq!(json["source"]["version"], "2026.09.29-a1b2c3");
324
+ assert!(json["models"].as_array().unwrap().iter().all(|row|
325
+ row["efforts"]["state"] == "unverified" && row["aliases"] == serde_json::json!([])));
326
+ assert!(json["current_role_model"].is_null());
327
+ }
328
+
329
+ #[test]
330
+ fn mismatched_catalog_provider_native_or_schema_is_not_projected() {
331
+ let args = ModelsArgs { provider: "cursor".into(), search: None, json: true };
332
+ for field in ["provider", "native", "schema"] {
333
+ let mut discovery = cursor_catalog();
334
+ match field {
335
+ "provider" => discovery.catalog.provider = ProviderId::new("kiro").unwrap(),
336
+ "native" => discovery.catalog.native.executable_sha256 = Digest([9; 32]),
337
+ "schema" => discovery.catalog.schema = kiro::CHAT_CATALOG_SOURCE.schema.into(),
338
+ _ => unreachable!(),
339
+ }
340
+ let result = render_models(&args, Ok(discovery)).unwrap();
341
+ assert_eq!(result.exit, ExitCode::Error, "{field}");
342
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
343
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
344
+ assert_eq!(json["ok"], false);
345
+ assert_eq!(json["models"], serde_json::json!([]));
346
+ }
347
+ }
348
+
349
+ #[test]
350
+ fn cursor_probe_failure_has_no_legacy_or_fixed_model_fallback() {
351
+ let args = ModelsArgs { provider: "cursor".into(), search: None, json: true };
352
+ for error in [
353
+ DiscoveryError::Executable("cursor-agent"),
354
+ DiscoveryError::CursorProbe(cursor::parse_version(b"Cursor release unknown").unwrap_err()),
355
+ ] {
356
+ let result = render_models(&args, Err(error)).unwrap();
357
+ assert_eq!(result.exit, ExitCode::Error);
358
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
359
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
360
+ assert_eq!(json["provider"], "cursor");
361
+ assert_eq!(json["models"], serde_json::json!([]));
362
+ assert_eq!(json["ok"], false);
363
+ }
364
+ }
365
+
220
366
  #[test]
221
367
  fn native_discovery_failure_has_no_catalog_or_fallback() {
222
368
  let args = ModelsArgs {
@@ -19,6 +19,14 @@ pub fn is_contract(agent: &Value) -> bool {
19
19
  .and_then(Value::as_str)
20
20
  .is_some_and(|provider| super::registry::descriptor(provider).is_some())
21
21
  }
22
+ /// Published paused roles without an instance never crossed native startup.
23
+ /// A bound instance is never skipped, even if a stale row says paused.
24
+ pub(crate) fn is_paused_unstarted(agent: &Value) -> bool {
25
+ is_contract(agent)
26
+ && agent.get("status").and_then(Value::as_str) == Some("paused")
27
+ && agent.get("contract_instance").is_none_or(Value::is_null)
28
+ }
29
+
22
30
  pub fn has_contract(state: &Value) -> bool {
23
31
  state
24
32
  .get("agents")
@@ -72,7 +80,7 @@ pub fn require_legacy_lifecycle(
72
80
  None => !config.roles.is_empty(),
73
81
  });
74
82
  if selected_native || configured_native {
75
- return Err(crate::cli::CliError::Runtime(format!("contract_capability_unverified: {operation} is not admitted for Kiro; no legacy fallback or fresh replacement was performed")));
83
+ return Err(crate::cli::CliError::Runtime(format!("contract_capability_unverified: {operation} is not admitted for contract-native seats; no legacy fallback or fresh replacement was performed")));
76
84
  }
77
85
  Ok(())
78
86
  }
@@ -93,7 +101,7 @@ pub fn require_legacy_role(
93
101
  .or(declared.as_deref())
94
102
  .is_some_and(super::registry::recognizes)
95
103
  {
96
- return Err(crate::cli::CliError::Runtime("contract_capability_unverified: mutable Kiro role lifecycle is not admitted; use initial quick-start; no role file was changed".into()));
104
+ return Err(crate::cli::CliError::Runtime("contract_capability_unverified: mutable contract-native role lifecycle is not admitted; use initial quick-start; no role file was changed".into()));
97
105
  }
98
106
  Ok(())
99
107
  }
@@ -141,16 +149,36 @@ pub fn start_role(
141
149
  }
142
150
 
143
151
  pub fn project_seat(workspace: &Path, team: &str, id: &str) -> Result<Value, BackendError> {
144
- let backend = Backend::open_readonly(workspace, team)?;
152
+ let mut backend = Backend::open_readonly(workspace, team)?;
145
153
  let seat = backend
146
154
  .store
147
155
  .seat(&SeatId::new(id)?)?
148
156
  .ok_or(BackendError::Metadata)?;
149
- let role = backend::read_role(&backend.binding, &seat.identity.seat)?;
157
+ let role = backend::read_role(&backend.binding, &seat.identity)?;
158
+ if role.provider != seat.provider {
159
+ return Err(BackendError::Metadata);
160
+ }
150
161
  let alive = seat
151
162
  .physical
152
163
  .as_ref()
153
164
  .is_some_and(|target| sample_process(&target.process) == ProcessState::Alive);
165
+ // Present only individually proven tools. A successful report_result
166
+ // never turns send_message/get_team_status into invented discoveries.
167
+ let mut observed = Vec::new();
168
+ let mut binding_source = None;
169
+ if super::registry::provider(seat.provider.as_str())?.uses_tool_roundtrips() {
170
+ let protocol = super::pump::protocol(&mut backend, &seat, &team_agent_contract::host::clock::RealClock::new())?;
171
+ if let team_agent_contract::contract::probe::ProbeOutcome::Observed(binding) = protocol.binding.outcome {
172
+ binding_source = Some(format!("{:?}", binding.source));
173
+ observed = binding.tools;
174
+ }
175
+ }
176
+ let tools: serde_json::Map<String, Value> = [
177
+ (team_agent_contract::contract::types::LogicalTool::SendMessage, "send_message"),
178
+ (team_agent_contract::contract::types::LogicalTool::ReportResult, "report_result"),
179
+ (team_agent_contract::contract::types::LogicalTool::GetTeamStatus, "get_team_status"),
180
+ ].into_iter().map(|(tool, name)| (name.into(), json!(if observed.contains(&tool) { "observed" } else { "unknown" }))).collect();
181
+ let binding_status = if observed.len() == 3 { "observed" } else if observed.is_empty() { "unverified" } else { "partial" };
154
182
  let status = if seat.status == SeatStatus::Stopped {
155
183
  "stopped"
156
184
  } else if seat.status != SeatStatus::Unknown && alive {
@@ -166,7 +194,8 @@ pub fn project_seat(workspace: &Path, team: &str, id: &str) -> Result<Value, Bac
166
194
  "session_id":seat.session.map(|session| session.native_session),
167
195
  "native_endpoint":seat.endpoint,"native_pane":seat.pane,
168
196
  "pane_id":null,"window":null,"session_name":null,
169
- "mcp_binding":"unverified","bootstrap_used":seat.bootstrap_used}),
197
+ "mcp_binding":binding_status,"mcp_binding_source":binding_source,"mcp_tools":tools,
198
+ "bootstrap_used":seat.bootstrap_used}),
170
199
  )
171
200
  }
172
201
 
@@ -235,6 +264,10 @@ pub fn stop_selected(workspace: &Path, state: &Value) -> Result<Vec<Value>, Back
235
264
  let mut receipts = Vec::new();
236
265
  if let Some(agents) = state.get("agents").and_then(Value::as_object) {
237
266
  for (id, agent) in agents.iter().filter(|(_, agent)| is_contract(agent)) {
267
+ if is_paused_unstarted(agent) {
268
+ receipts.push(json!({"agent_id":id,"status":"paused","stopped":false}));
269
+ continue;
270
+ }
238
271
  let backend = Backend::open(workspace, &team)?;
239
272
  let seat = backend
240
273
  .store
@@ -4,6 +4,7 @@
4
4
  //! Selection/preflight is read-only. Physical operations require a validated
5
5
  //! contract role and its real framework/native binding, not a provider guess.
6
6
  pub mod backend;
7
+ pub mod commands;
7
8
  pub mod config;
8
9
  pub mod discovery;
9
10
  pub mod forward;
@@ -12,3 +13,4 @@ pub mod mcp;
12
13
  pub mod prompt;
13
14
  pub mod pump;
14
15
  pub mod registry;
16
+ mod workspace;
@@ -2,7 +2,7 @@
2
2
  //! implicit business retry, queue flush, or replay of uncertain messages.
3
3
  use super::{
4
4
  backend::{self, Backend, BackendError},
5
- forward, framework,
5
+ forward, framework, registry,
6
6
  };
7
7
  use serde_json::Value;
8
8
  use std::path::Path;
@@ -12,11 +12,10 @@ use team_agent_contract::host::{
12
12
  clock::{Clock, RealClock},
13
13
  process::verify_native_ancestry,
14
14
  };
15
- use team_agent_contract::kiro::{interaction, KIRO_DESCRIPTOR};
16
15
  use team_agent_contract::orchestration::{physical::*, store::*, supervisor::*};
17
16
  use team_agent_contract::runtime::probes::*;
18
17
 
19
- fn protocol(
18
+ pub(super) fn protocol(
20
19
  backend: &mut Backend,
21
20
  seat: &SeatRecord,
22
21
  clock: &RealClock,
@@ -28,6 +27,9 @@ fn protocol(
28
27
  .into_iter()
29
28
  .filter(|record| {
30
29
  !record.closed
30
+ && record.identity == seat.identity
31
+ && record.server_key == seat.server_key
32
+ && record.binding_key == seat.binding_key
31
33
  && record.native_process == target.process
32
34
  && verify_native_ancestry(&record.process, &record.native_process).is_ok()
33
35
  })
@@ -75,6 +77,35 @@ fn protocol(
75
77
  });
76
78
  }
77
79
  }
80
+ if registry::provider(seat.provider.as_str())?.uses_tool_roundtrips() {
81
+ let session = seat.session.as_ref().ok_or(BackendError::Metadata)?;
82
+ if target.identity() != &seat.identity || target.binding != seat.binding_key
83
+ || target.provider != seat.provider || target.native != seat.native
84
+ || target.native_session.as_ref() != Some(&session.native_session)
85
+ || session.source != seat.identity || session.provider != seat.provider
86
+ || session.native != seat.native || session.cwd != target.cwd
87
+ || target.candidate_sha256 != backend.binding.candidate_sha256
88
+ {
89
+ return Err(ContractError::Mismatch("native roundtrip current binding").into());
90
+ }
91
+ // The core returns only per-tool invocation + actual successful
92
+ // write/flush pairs for THIS captured connection and generation.
93
+ // Server list writes or another connection's facts cannot qualify.
94
+ let tools = backend.store.completed_connection_tools(&seat.identity, &connection.connection)?;
95
+ if !tools.is_empty() {
96
+ events.push(ProtocolEvent {
97
+ scope: target.scope(events.len() as u64, clock.now(), Duration::from_secs(2),
98
+ "live-native-tool-roundtrip-ledger"),
99
+ fact: ProtocolFact::ClientBound {
100
+ server_instance: connection.connection.clone(),
101
+ evidence: ClientBindingEvidence {
102
+ server_key: seat.server_key.clone(), tools,
103
+ source: ClientBindingSource::NativeToolRoundtrip,
104
+ },
105
+ },
106
+ });
107
+ }
108
+ }
78
109
  Ok(collect_protocol(
79
110
  target,
80
111
  &connection.connection,
@@ -91,8 +122,18 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
91
122
  {
92
123
  return Ok(());
93
124
  }
125
+ let selected = registry::provider(seat.provider.as_str())?;
126
+ let bootstrap_allowed = selected.first_business_bootstrap() && !seat.bootstrap_used;
127
+ let profiles = selected.descriptor().input.profiles.require("native input profiles")?;
128
+ let profile = profiles.iter().find(|profile| profile.id == seat.profile && profile.matches_native(&seat.native))
129
+ .ok_or(ContractError::ProfileUnavailable)?;
130
+ profile.policy.require("native submit policy")?;
131
+ let role = backend::read_role(&backend.binding, &seat.identity)?;
132
+ if role.provider != seat.provider {
133
+ return Err(BackendError::Metadata);
134
+ }
94
135
  let clock = RealClock::new();
95
- let adapter = backend::adapter(&backend.binding, &seat.identity)?;
136
+ let adapter = backend::adapter(&backend.binding, &seat.identity, &seat.provider)?;
96
137
  let hooks = adapter.hooks();
97
138
  let operation = OperationId::new(format!(
98
139
  "deliver-{}",
@@ -104,15 +145,17 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
104
145
  ))?;
105
146
  let mut physical = PhysicalRuntime::new(
106
147
  Registration {
107
- descriptor: &KIRO_DESCRIPTOR,
148
+ descriptor: selected.descriptor(),
108
149
  hooks: &hooks,
109
- controls: interaction::CONTROLS,
150
+ controls: selected.controls(),
110
151
  },
111
- backend::settings(&backend.binding),
152
+ backend::settings(&backend.binding, selected),
112
153
  operation.clone(),
113
154
  &clock,
114
155
  )?;
115
- physical.evidence = backend::policy_evidence(&seat.native, backend.binding.candidate_sha256);
156
+ physical.evidence = backend::policy_evidence(
157
+ &seat.provider, &seat.native, backend.binding.candidate_sha256,
158
+ )?;
116
159
  physical.protocol = Some(protocol(backend, seat, &clock)?);
117
160
  let sample = physical.readiness(seat)?;
118
161
  if !matches!(sample.pane.outcome, ProbeOutcome::Observed(ref pane) if pane.surface == InputSurface::ComposerReady)
@@ -120,28 +163,39 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
120
163
  {
121
164
  return Ok(());
122
165
  }
123
- backend
124
- .store
125
- .begin_observation(&seat.identity, &operation)?;
126
- // A completed-but-uncertain inspector quarantines the seat, without an
127
- // orphan lease that would prevent explicit scoped shutdown. No replay or
128
- // blind Escape to clear an unrelated approval/composer is permitted.
129
- let binding = match physical.client_binding(seat, &interaction::TOOL_PANEL) {
130
- Ok(binding) => binding,
131
- Err(error) => {
132
- backend.store.fail_observation(&seat.identity, &operation)?;
133
- return Err(error.into());
166
+ // A roundtrip provider consumes only its captured protocol evidence. An
167
+ // Unknown/partial binding is left to the typed readiness gate; it never
168
+ // triggers a guessed TUI /tools command or a Kiro inspector fallback.
169
+ if !bootstrap_allowed && !selected.uses_tool_roundtrips() {
170
+ let panel = selected.tool_panel()?;
171
+ if panel.profile_id != seat.profile || panel.server_key != seat.server_key
172
+ || panel.policy_sha256 != profile.policy_sha256
173
+ {
174
+ return Err(ContractError::Mismatch("native inspector seat binding").into());
134
175
  }
135
- };
136
- backend
137
- .store
138
- .finish_observation(&seat.identity, &operation)?;
139
- if !matches!(binding.outcome, ProbeOutcome::Observed(_)) {
140
- return Ok(());
176
+ backend.store.begin_observation(&seat.identity, &operation)?;
177
+ // Uncertain inspection quarantines the seat without an orphan lease.
178
+ // No replay or blind Escape clears an unrelated composer/approval.
179
+ let binding = match physical.client_binding(seat, panel) {
180
+ Ok(binding) => binding,
181
+ Err(error) => {
182
+ backend.store.fail_observation(&seat.identity, &operation)?;
183
+ return Err(error.into());
184
+ }
185
+ };
186
+ backend.store.finish_observation(&seat.identity, &operation)?;
187
+ if !matches!(binding.outcome, ProbeOutcome::Observed(_)) {
188
+ return Ok(());
189
+ }
190
+ let mut current_protocol = protocol(backend, seat, &clock)?;
191
+ current_protocol.binding = binding;
192
+ physical.protocol = Some(current_protocol);
193
+ }
194
+ // H3's owned prompt receipt is independently checked by PhysicalRuntime;
195
+ // only the one durable first-business claim can carry this instruction.
196
+ if bootstrap_allowed {
197
+ physical.initial_prompt = Some(role.prompt.text.clone());
141
198
  }
142
- let mut current_protocol = protocol(backend, seat, &clock)?;
143
- current_protocol.binding = binding;
144
- physical.protocol = Some(current_protocol);
145
199
  let mut bootstrap = OutboxBootstrap::new(ContractStore::open(
146
200
  &backend.binding.transport_root,
147
201
  backend.binding.scope.clone(),
@@ -150,10 +204,10 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
150
204
  physical.bootstrap = Some(&mut bootstrap);
151
205
  let sample = physical.readiness(seat)?;
152
206
  let authorization = DeliveryAuthorization {
153
- allow_first_business_bootstrap: false,
207
+ allow_first_business_bootstrap: bootstrap_allowed,
154
208
  require_server_handshake: true,
155
209
  channel: Channel::Tmux,
156
- policy_sha256: interaction::POLICY,
210
+ policy_sha256: profile.policy_sha256,
157
211
  };
158
212
  let _ = team_agent_contract::orchestration::supervisor::tick(
159
213
  &mut backend.store,
@@ -170,15 +224,18 @@ pub fn tick(workspace: &Path, state: &mut Value) -> Result<(), BackendError> {
170
224
  return Ok(());
171
225
  }
172
226
  let team = crate::state::projection::team_state_key(state);
173
- let mut backend = Backend::open(workspace, &team)?;
174
227
  let ids: Vec<_> = state
175
228
  .get("agents")
176
229
  .and_then(Value::as_object)
177
230
  .into_iter()
178
231
  .flat_map(|agents| agents.iter())
179
- .filter(|(_, agent)| framework::is_contract(agent))
232
+ .filter(|(_, agent)| framework::is_contract(agent) && !framework::is_paused_unstarted(agent))
180
233
  .map(|(id, _)| id.clone())
181
234
  .collect();
235
+ if ids.is_empty() {
236
+ return Ok(());
237
+ }
238
+ let mut backend = Backend::open(workspace, &team)?;
182
239
  let mut failure = None;
183
240
  for id in ids {
184
241
  let Some(seat) = backend.store.seat(&SeatId::new(&id)?)? else {
@@ -192,7 +249,12 @@ pub fn tick(workspace: &Path, state: &mut Value) -> Result<(), BackendError> {
192
249
  .and_then(|agents| agents.get_mut(&id))
193
250
  {
194
251
  match framework::project_seat(workspace, &team, &id) {
195
- Ok(projected) => *agent = projected,
252
+ Ok(projected) => {
253
+ let row = agent.as_object_mut().ok_or(BackendError::Metadata)?;
254
+ for (key, value) in projected.as_object().ok_or(BackendError::Metadata)? {
255
+ row.insert(key.clone(), value.clone());
256
+ }
257
+ },
196
258
  Err(error) => failure = Some(error),
197
259
  }
198
260
  }