@team-agent/installer 0.5.114 → 0.5.116

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.
Files changed (33) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +2 -2
  4. package/crates/team-agent/src/cli/mod.rs +155 -19
  5. package/crates/team-agent/src/cli/models.rs +52 -1
  6. package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
  7. package/crates/team-agent/src/cli/spec.rs +1 -1
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
  9. package/crates/team-agent/src/contract_runtime/backend.rs +490 -97
  10. package/crates/team-agent/src/contract_runtime/commands.rs +326 -0
  11. package/crates/team-agent/src/contract_runtime/config.rs +39 -8
  12. package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
  13. package/crates/team-agent/src/contract_runtime/discovery.rs +218 -40
  14. package/crates/team-agent/src/contract_runtime/framework.rs +38 -5
  15. package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
  16. package/crates/team-agent/src/contract_runtime/mod.rs +2 -0
  17. package/crates/team-agent/src/contract_runtime/pump.rs +97 -33
  18. package/crates/team-agent/src/contract_runtime/registry.rs +248 -4
  19. package/crates/team-agent/src/contract_runtime/workspace.rs +228 -0
  20. package/crates/team-agent/src/leader/start.rs +93 -5
  21. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +99 -1
  22. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -5
  23. package/crates/team-agent/src/lifecycle/launch.rs +6 -0
  24. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +109 -58
  26. package/crates/team-agent/src/lifecycle/restart/selection.rs +63 -7
  27. package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
  28. package/crates/team-agent/src/provider/session/capture.rs +24 -1
  29. package/crates/team-agent/src/state/persist.rs +72 -2
  30. package/crates/team-agent/src/state/projection.rs +29 -0
  31. package/crates/team-agent/src/state/repository/tests.rs +70 -0
  32. package/crates/team-agent/src/state/repository.rs +5 -1
  33. package/package.json +4 -4
@@ -3,9 +3,13 @@ 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;
8
+ use team_agent_contract::providers::grok_build::native as grok;
7
9
  use thiserror::Error;
8
10
 
11
+ use super::registry::{self, NativeProvider, NativeRuntime};
12
+
9
13
  #[derive(Debug, Error)]
10
14
  pub enum DiscoveryError {
11
15
  #[error("required executable is unavailable: {0}")]
@@ -13,7 +17,11 @@ pub enum DiscoveryError {
13
17
  #[error("native discovery failed: {0}")]
14
18
  Host(#[from] team_agent_contract::host::HostError),
15
19
  #[error("native engine probe failed: {0}")]
16
- Probe(#[from] EngineProbeError),
20
+ Probe(#[from] kiro::EngineProbeError),
21
+ #[error("native Cursor launcher probe failed: {0}")]
22
+ CursorProbe(#[from] cursor::ProbeError),
23
+ #[error("native Grok launcher probe failed: {0}")]
24
+ GrokProbe(#[from] grok::ProbeError),
17
25
  #[error("native discovery contract: {0:?}")]
18
26
  Contract(ContractError),
19
27
  #[error("native model discovery: {0:?}")]
@@ -26,6 +34,7 @@ pub struct Discovery {
26
34
  pub engine: PathBuf,
27
35
  pub native: NativeIdentity,
28
36
  pub catalog: CatalogObservation,
37
+ pub runtime: Option<NativeRuntime>,
29
38
  }
30
39
 
31
40
  pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
@@ -43,44 +52,118 @@ pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
43
52
  Err(DiscoveryError::Executable(name))
44
53
  }
45
54
 
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
- )?;
55
+ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, DiscoveryError> {
56
+ let selected = registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)?;
57
+ let descriptor = selected.descriptor();
58
+ let bounds = ReadBounds {
59
+ deadline: std::time::Duration::from_secs(30),
60
+ max_output_bytes: 1024 * 1024,
61
+ };
62
+ let launcher = if selected == NativeProvider::GrokBuild {
63
+ // This macOS admission preserves the explicitly approved proxy entry.
64
+ // PATH may put the inner image first; it must never bypass the wrapper.
65
+ grok::resolve_launcher(Path::new("/usr/local/bin/grok"))?
66
+ } else {
67
+ executable(descriptor.identity.binary)?
68
+ };
69
+ let (engine, native) = match selected {
70
+ NativeProvider::Kiro => {
71
+ let home = std::env::var_os("HOME")
72
+ .map(PathBuf::from)
73
+ .ok_or(DiscoveryError::Executable("HOME for native installation lookup"))?;
74
+ let engine = kiro::resolve_helper(&launcher, &home)?;
75
+ let native = kiro::probe_engine(&engine, &mut RealCommandRunner::default(), bounds)?;
76
+ (engine, native)
77
+ }
78
+ NativeProvider::Cursor => {
79
+ let engine = cursor::resolve_launcher(&launcher)?;
80
+ let native = cursor::probe_launcher(&engine, &mut RealCommandRunner::default(), bounds)?;
81
+ (engine, native)
82
+ }
83
+ NativeProvider::GrokBuild => {
84
+ let engine = grok::resolve_launcher(&launcher)?;
85
+ let native = grok::probe_launcher(&engine, &mut RealCommandRunner::default(), bounds)?;
86
+ (engine, native)
87
+ }
88
+ };
58
89
  let request = CatalogRequest {
59
- provider: ProviderId::new(KIRO_DESCRIPTOR.identity.id).map_err(DiscoveryError::Contract)?,
90
+ provider: ProviderId::new(descriptor.identity.id).map_err(DiscoveryError::Contract)?,
60
91
  native: native.clone(),
61
92
  executable: engine.clone(),
62
93
  cwd: resolve_cwd(workspace)?,
63
- source: CHAT_CATALOG_SOURCE,
64
- bounds: discovery_bounds(),
94
+ source: descriptor.model.catalog.require("native catalog source").map_err(DiscoveryError::Contract)?.clone(),
95
+ bounds,
65
96
  };
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
- })
97
+ // H1 does not use these pointers or execute an MCP server. Cursor's
98
+ // launcher fingerprint authorizes only metadata reads, never worker launch.
99
+ let adapter = selected.adapter(
100
+ std::env::current_exe()?.canonicalize()?,
101
+ Vec::new(),
102
+ Default::default(),
103
+ ).map_err(DiscoveryError::Contract)?;
104
+ let hooks = adapter.hooks();
105
+ let catalog_hook = hooks.catalog.require("H1 catalog").map_err(DiscoveryError::Contract)?;
106
+ let catalog = match selected {
107
+ NativeProvider::Kiro => {
108
+ let mut host = kiro::CatalogReader::new(request.clone(), RealCommandRunner::default())
109
+ .map_err(DiscoveryError::Contract)?;
110
+ catalog_hook.discover(&request, &mut host)
111
+ }
112
+ NativeProvider::Cursor => {
113
+ let mut host = cursor::NativeCatalog::new(request.clone(), RealCommandRunner::default())
114
+ .map_err(DiscoveryError::Contract)?;
115
+ catalog_hook.discover(&request, &mut host)
116
+ }
117
+ NativeProvider::GrokBuild => {
118
+ let mut host = grok::NativeCatalog::new(request.clone(), RealCommandRunner::default())
119
+ .map_err(DiscoveryError::Contract)?;
120
+ catalog_hook.discover(&request, &mut host)
121
+ }
122
+ }.map_err(DiscoveryError::Catalog)?;
123
+ let result = Discovery { engine, native, catalog, runtime: None };
124
+ validate_catalog(provider, &result).map_err(DiscoveryError::Contract)?;
125
+ Ok(result)
126
+ }
127
+
128
+ /// Runtime discovery is separate from public catalog metadata. Cursor explicitly
129
+ /// bridges the same installation's catalog to its proven Node/entry identity.
130
+ pub(super) fn discover_runtime(provider: &ProviderId, workspace: &Path) -> Result<Discovery, DiscoveryError> {
131
+ let mut discovery = discover(provider, workspace)?;
132
+ let selected = registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)?;
133
+ if selected != NativeProvider::Kiro {
134
+ let home = std::env::var_os("HOME").map(PathBuf::from)
135
+ .ok_or(DiscoveryError::Executable("HOME for native subscription"))?;
136
+ let bounds = ReadBounds { deadline: std::time::Duration::from_secs(30), max_output_bytes: 1024 * 1024 };
137
+ match selected {
138
+ NativeProvider::Cursor => {
139
+ let runtime = cursor::probe_runtime(&discovery.engine, &home, &mut RealCommandRunner::default(), bounds)?;
140
+ discovery.catalog = runtime.bind_catalog(&discovery.catalog).map_err(DiscoveryError::Contract)?;
141
+ discovery.engine = runtime.engine.clone();
142
+ discovery.native = runtime.native.clone();
143
+ discovery.runtime = Some(NativeRuntime::Cursor(runtime));
144
+ }
145
+ NativeProvider::GrokBuild => {
146
+ let runtime = grok::probe_runtime(&discovery.engine, &home, &mut RealCommandRunner::default(), bounds)?;
147
+ discovery.catalog = runtime.bind_catalog(&discovery.catalog).map_err(DiscoveryError::Contract)?;
148
+ discovery.engine = runtime.engine.clone();
149
+ discovery.native = runtime.native.clone();
150
+ discovery.runtime = Some(NativeRuntime::GrokBuild(runtime));
151
+ }
152
+ NativeProvider::Kiro => unreachable!(),
153
+ }
154
+ }
155
+ Ok(discovery)
156
+ }
157
+
158
+ fn validate_catalog(provider: &ProviderId, discovery: &Discovery) -> Result<(), ContractError> {
159
+ let descriptor = registry::provider(provider.as_str())?.descriptor();
160
+ if discovery.catalog.provider != *provider
161
+ || discovery.catalog.native != discovery.native
162
+ || discovery.catalog.schema != descriptor.model.catalog.require("native catalog source")?.schema
163
+ {
164
+ return Err(ContractError::Mismatch("native catalog provider/identity/schema"));
165
+ }
166
+ Ok(())
84
167
  }
85
168
 
86
169
  pub fn models(
@@ -88,7 +171,10 @@ pub fn models(
88
171
  ) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
89
172
  let result = std::env::current_dir()
90
173
  .map_err(DiscoveryError::Io)
91
- .and_then(|cwd| discover(&cwd));
174
+ .and_then(|cwd| {
175
+ let provider = ProviderId::new(&args.provider).map_err(DiscoveryError::Contract)?;
176
+ discover(&provider, &cwd)
177
+ });
92
178
  render_models(args, result)
93
179
  }
94
180
 
@@ -98,6 +184,11 @@ fn render_models(
98
184
  ) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
99
185
  use crate::cli::{CmdOutput, CmdResult, ExitCode};
100
186
  use serde_json::json;
187
+ let result = result.and_then(|discovery| {
188
+ let provider = ProviderId::new(&args.provider).map_err(DiscoveryError::Contract)?;
189
+ validate_catalog(&provider, &discovery).map_err(DiscoveryError::Contract)?;
190
+ Ok(discovery)
191
+ });
101
192
  let (value, human, exit) = match result {
102
193
  Ok(discovery) => {
103
194
  let query = args.search.as_deref().map(str::to_lowercase);
@@ -114,7 +205,8 @@ fn render_models(
114
205
  let value = json!({"schema_version":"models.v1","ok":true,"provider":args.provider,
115
206
  "models":rows,"auth":"unknown","auth_basis":"native_catalog_visibility_is_not_authorization",
116
207
  "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)},
208
+ "version":discovery.native.version,"executable_sha256":team_agent_contract::host::digest_hex(discovery.native.executable_sha256),
209
+ "identity_scope":"catalog_only"},
118
210
  "current_role_model":null,"search":args.search});
119
211
  let mut lines = vec![format!("models.v1 | {} native model IDs:", args.provider)];
120
212
  lines.extend(
@@ -163,12 +255,13 @@ mod tests {
163
255
  executable_sha256: Digest([1; 32]),
164
256
  };
165
257
  let discovery = Discovery {
258
+ runtime: None,
166
259
  engine: "/controlled/kiro-cli-chat".into(),
167
260
  native: native.clone(),
168
261
  catalog: CatalogObservation {
169
262
  provider: ProviderId::new("kiro").unwrap(),
170
263
  native,
171
- schema: "controlled-json".into(),
264
+ schema: kiro::CHAT_CATALOG_SOURCE.schema.into(),
172
265
  models: vec![ModelRecord {
173
266
  id: "Exact-Case-ID".into(),
174
267
  efforts: Support::Unverified(Reason {
@@ -192,7 +285,7 @@ mod tests {
192
285
  assert_eq!(json["auth"], "unknown");
193
286
  assert_eq!(json["models"][0]["model_id"], "Exact-Case-ID");
194
287
  assert_eq!(json["models"][0]["efforts"]["state"], "unverified");
195
- assert_eq!(json["source"]["schema"], "controlled-json");
288
+ assert_eq!(json["source"]["schema"], kiro::CHAT_CATALOG_SOURCE.schema);
196
289
  assert!(json["current_role_model"].is_null());
197
290
  }
198
291
 
@@ -203,7 +296,7 @@ mod tests {
203
296
  search: None,
204
297
  json: true,
205
298
  };
206
- let error = parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
299
+ let error = kiro::parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
207
300
  let result = render_models(&args, Err(DiscoveryError::Probe(error))).unwrap();
208
301
  assert_eq!(result.exit, ExitCode::Error);
209
302
  let CmdOutput::Human(text) = result.output else {
@@ -217,6 +310,91 @@ mod tests {
217
310
  assert_eq!(json["models"], serde_json::json!([]));
218
311
  }
219
312
 
313
+ fn cursor_catalog() -> Discovery {
314
+ let native = NativeIdentity {
315
+ version: "2026.09.29-a1b2c3".into(),
316
+ harness: cursor::METADATA_HARNESS.into(),
317
+ ui: cursor::METADATA_UI.into(),
318
+ platform: Platform::MacOs,
319
+ executable_sha256: Digest([2; 32]),
320
+ };
321
+ Discovery {
322
+ runtime: None,
323
+ engine: "/controlled/cursor-agent".into(),
324
+ native: native.clone(),
325
+ catalog: CatalogObservation {
326
+ provider: ProviderId::new("cursor").unwrap(),
327
+ native,
328
+ schema: cursor::CATALOG_SOURCE.schema.into(),
329
+ models: ["gpt-5.6-luna-xhigh", "gpt-5.6-luna-xhigh-fast", "Other-Exact-ID"]
330
+ .into_iter().map(|id| ModelRecord {
331
+ id: id.into(),
332
+ efforts: Support::Unverified(Reason {
333
+ code: "not-observed", message: "Native IDs are not separate effort flags",
334
+ }),
335
+ }).collect(),
336
+ },
337
+ }
338
+ }
339
+
340
+ #[test]
341
+ fn cursor_catalog_filters_exact_native_family_without_runtime_claims() {
342
+ let args = ModelsArgs {
343
+ provider: "cursor".into(), search: Some("GPT-5.6-LUNA-XHIGH".into()), json: true,
344
+ };
345
+ let result = render_models(&args, Ok(cursor_catalog())).unwrap();
346
+ assert_eq!(result.exit, ExitCode::Ok);
347
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
348
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
349
+ let ids: Vec<_> = json["models"].as_array().unwrap().iter()
350
+ .map(|row| row["model_id"].as_str().unwrap()).collect();
351
+ assert_eq!(ids, ["gpt-5.6-luna-xhigh", "gpt-5.6-luna-xhigh-fast"]);
352
+ assert_eq!(json["provider"], "cursor");
353
+ assert_eq!(json["auth"], "unknown");
354
+ assert_eq!(json["source"]["identity_scope"], "catalog_only");
355
+ assert_eq!(json["source"]["version"], "2026.09.29-a1b2c3");
356
+ assert!(json["models"].as_array().unwrap().iter().all(|row|
357
+ row["efforts"]["state"] == "unverified" && row["aliases"] == serde_json::json!([])));
358
+ assert!(json["current_role_model"].is_null());
359
+ }
360
+
361
+ #[test]
362
+ fn mismatched_catalog_provider_native_or_schema_is_not_projected() {
363
+ let args = ModelsArgs { provider: "cursor".into(), search: None, json: true };
364
+ for field in ["provider", "native", "schema"] {
365
+ let mut discovery = cursor_catalog();
366
+ match field {
367
+ "provider" => discovery.catalog.provider = ProviderId::new("kiro").unwrap(),
368
+ "native" => discovery.catalog.native.executable_sha256 = Digest([9; 32]),
369
+ "schema" => discovery.catalog.schema = kiro::CHAT_CATALOG_SOURCE.schema.into(),
370
+ _ => unreachable!(),
371
+ }
372
+ let result = render_models(&args, Ok(discovery)).unwrap();
373
+ assert_eq!(result.exit, ExitCode::Error, "{field}");
374
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
375
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
376
+ assert_eq!(json["ok"], false);
377
+ assert_eq!(json["models"], serde_json::json!([]));
378
+ }
379
+ }
380
+
381
+ #[test]
382
+ fn cursor_probe_failure_has_no_legacy_or_fixed_model_fallback() {
383
+ let args = ModelsArgs { provider: "cursor".into(), search: None, json: true };
384
+ for error in [
385
+ DiscoveryError::Executable("cursor-agent"),
386
+ DiscoveryError::CursorProbe(cursor::parse_version(b"Cursor release unknown").unwrap_err()),
387
+ ] {
388
+ let result = render_models(&args, Err(error)).unwrap();
389
+ assert_eq!(result.exit, ExitCode::Error);
390
+ let CmdOutput::Human(text) = result.output else { panic!("JSON text projection"); };
391
+ let json: serde_json::Value = serde_json::from_str(&text).unwrap();
392
+ assert_eq!(json["provider"], "cursor");
393
+ assert_eq!(json["models"], serde_json::json!([]));
394
+ assert_eq!(json["ok"], false);
395
+ }
396
+ }
397
+
220
398
  #[test]
221
399
  fn native_discovery_failure_has_no_catalog_or_fallback() {
222
400
  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
@@ -187,6 +187,9 @@ fn run_observed(args: &[String], diagnostics: &mut Diagnostics) -> Result<(), Ba
187
187
  };
188
188
  diagnostics.fields["scope"] = json!(identity.scope.as_str());
189
189
  let seat = diagnostics.step("target_wait", || wait_target(&backend.store, &identity))?;
190
+ if seat.server_key != super::registry::provider(seat.provider.as_str())?.server_key_for(&identity)? {
191
+ return Err(Error::Fence.into());
192
+ }
190
193
  let target = seat
191
194
  .physical
192
195
  .as_ref()
@@ -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;