@team-agent/installer 0.5.115 → 0.5.117

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 (36) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/mod.rs +143 -12
  4. package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
  5. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
  6. package/crates/team-agent/src/contract_runtime/backend.rs +88 -8
  7. package/crates/team-agent/src/contract_runtime/commands.rs +88 -7
  8. package/crates/team-agent/src/contract_runtime/discovery.rs +42 -10
  9. package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
  10. package/crates/team-agent/src/contract_runtime/pump.rs +2 -0
  11. package/crates/team-agent/src/contract_runtime/registry.rs +86 -9
  12. package/crates/team-agent/src/contract_runtime/workspace.rs +79 -11
  13. package/crates/team-agent/src/leader/lease.rs +1 -18
  14. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +86 -8
  15. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
  16. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +101 -72
  17. package/crates/team-agent/src/lifecycle/restart/selection.rs +30 -8
  18. package/crates/team-agent/src/messaging/delivery.rs +132 -27
  19. package/crates/team-agent/src/messaging/enter_rescue.rs +334 -0
  20. package/crates/team-agent/src/messaging/enter_rescue_tests.rs +566 -0
  21. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  22. package/crates/team-agent/src/messaging/mod.rs +1 -0
  23. package/crates/team-agent/src/messaging/scheduler.rs +8 -0
  24. package/crates/team-agent/src/messaging/tests/basic.rs +2 -0
  25. package/crates/team-agent/src/messaging/types.rs +1 -0
  26. package/crates/team-agent/src/provider/session/capture.rs +24 -1
  27. package/crates/team-agent/src/state/persist.rs +72 -2
  28. package/crates/team-agent/src/state/projection.rs +29 -0
  29. package/crates/team-agent/src/state/repository/tests.rs +70 -0
  30. package/crates/team-agent/src/state/repository.rs +52 -1
  31. package/crates/team-agent/src/tmux_backend/native_input.rs +494 -0
  32. package/crates/team-agent/src/tmux_backend/native_input_tests.rs +781 -0
  33. package/crates/team-agent/src/tmux_backend.rs +49 -6
  34. package/crates/team-agent/src/transport/native_input.rs +594 -0
  35. package/crates/team-agent/src/transport.rs +19 -0
  36. package/package.json +4 -4
@@ -317,6 +317,23 @@ fn request(
317
317
  })
318
318
  }
319
319
 
320
+ fn preflight_request(
321
+ binding: &Binding,
322
+ role: &RoleConfig,
323
+ identity: InstanceIdentity,
324
+ discovered: &Discovery,
325
+ ) -> Result<LaunchRequest, BackendError> {
326
+ let mut request = request(binding, role, identity, discovered)?;
327
+ if registry::provider(role.provider.as_str())? == registry::NativeProvider::GrokBuild {
328
+ // Pure H2 only: this is a proposed private path, not a filesystem receipt.
329
+ request.paths.cwd = team_agent_contract::contract::session::CwdIdentity {
330
+ path: binding.transport_root.join("workspaces").join(role.id.as_str()),
331
+ identity: digest(b"effect-free-planned-private-cwd"),
332
+ };
333
+ }
334
+ Ok(request)
335
+ }
336
+
320
337
  /// Team-wide native discovery/admission before the shared launcher creates any
321
338
  /// seats (including legacy peers). Bounded read-only native commands run; no
322
339
  /// runtime store, worker config, private socket, or worker process is created.
@@ -356,7 +373,7 @@ pub fn preflight(team: &super::config::TeamConfig) -> Result<(), BackendError> {
356
373
  };
357
374
  let adapter = launch_adapter(&binding, &identity, &role.provider, discovered)?;
358
375
  let hooks = adapter.hooks();
359
- let request = request(&binding, role, identity, discovered)?;
376
+ let request = preflight_request(&binding, role, identity, discovered)?;
360
377
  let resolved = resolve_launch_preflight(
361
378
  selected.descriptor(),
362
379
  &hooks,
@@ -404,7 +421,7 @@ pub fn start(
404
421
  };
405
422
  let probe_adapter = launch_adapter(&provisional, &provisional_identity, &role.provider, &discovered)?;
406
423
  let probe_hooks = probe_adapter.hooks();
407
- let preflight = request(&provisional, role, provisional_identity, &discovered)?;
424
+ let preflight = preflight_request(&provisional, role, provisional_identity, &discovered)?;
408
425
  let preflight = resolve_launch_preflight(
409
426
  descriptor,
410
427
  &probe_hooks,
@@ -413,7 +430,6 @@ pub fn start(
413
430
  )?;
414
431
  let preflight_plan = probe_adapter.plan(&preflight)?;
415
432
  validate_launch_plan(descriptor, &preflight, &preflight_plan)?;
416
- let server_key = selected.server_key()?;
417
433
  let mut backend = if root_for(&cwd.path, &scope).exists() {
418
434
  Backend::open(&cwd.path, team)?
419
435
  } else {
@@ -446,6 +462,7 @@ pub fn start(
446
462
  });
447
463
  backend.store.set_framework_routes(&peers, Some(&route))?;
448
464
  let identity = backend.store.propose_identity(&role.id)?;
465
+ let server_key = selected.server_key_for(&identity)?;
449
466
  write_role(&backend.binding, &identity, role)?;
450
467
  let adapter = launch_adapter(&backend.binding, &identity, &role.provider, &discovered)?;
451
468
  let hooks = adapter.hooks();
@@ -453,7 +470,10 @@ pub fn start(
453
470
  if !descriptor.workspace.shared_cwd {
454
471
  request.paths.cwd = super::workspace::prepare(&backend.binding, &role.id)?;
455
472
  }
456
- if let Some(runtime) = &discovered.runtime {
473
+ if selected == registry::NativeProvider::GrokBuild {
474
+ team_agent_contract::providers::grok_build::native::require_private_primary(&request.paths.cwd)?;
475
+ }
476
+ if let Some(registry::NativeRuntime::Cursor(runtime)) = &discovered.runtime {
457
477
  request.preassigned_session = Some(
458
478
  team_agent_contract::providers::cursor::native::allocate_chat(
459
479
  runtime, &request.paths.cwd, &mut RealCommandRunner::default(),
@@ -484,6 +504,8 @@ pub fn start(
484
504
  descriptor,
485
505
  hooks: &hooks,
486
506
  controls: selected.controls(),
507
+ startup: selected.startup(),
508
+ session_panel: selected.session_panel(),
487
509
  },
488
510
  settings(&backend.binding, selected),
489
511
  operation.clone(),
@@ -537,12 +559,20 @@ pub(crate) fn prepare_restart(
537
559
  let config = super::framework::team_config(workspace, directory, team)?.ok_or(BackendError::Metadata)?;
538
560
  let mut prepared = Vec::new();
539
561
  let agents = state.get("agents").and_then(serde_json::Value::as_object).ok_or(BackendError::Metadata)?;
540
- for (id, _) in agents.iter().filter(|(_, row)| super::framework::is_contract(row)) {
562
+ for (id, _) in agents.iter().filter(|(_, row)| {
563
+ super::framework::is_contract(row)
564
+ && !crate::state::projection::agent_stopped_for_restart(row)
565
+ }) {
541
566
  let Some(role) = config.roles.iter().find(|role| role.id.as_str() == id) else {
542
567
  // Removed roles are not silently re-created from an old snapshot.
543
568
  continue;
544
569
  };
545
570
  if role.paused { continue; }
571
+ // The native store is authoritative even if its framework mirror is
572
+ // stale. Only explicit start-agent may resume a stopped generation.
573
+ let backend = Backend::open_readonly(workspace, team)?;
574
+ let seat = backend.store.seat(&role.id)?.ok_or(BackendError::Metadata)?;
575
+ if seat.status == SeatStatus::Stopped { continue; }
546
576
  prepared.push(prepare_resume(workspace, team, &role.id, Some(role))?);
547
577
  }
548
578
  Ok(prepared)
@@ -586,6 +616,9 @@ pub fn prepare_resume(
586
616
  binding: source, expected_source: parent.identity,
587
617
  });
588
618
  let selected = registry::provider(role.provider.as_str())?;
619
+ if selected == registry::NativeProvider::GrokBuild {
620
+ team_agent_contract::providers::grok_build::native::require_private_primary(&request.paths.cwd)?;
621
+ }
589
622
  let resolved = resolve_launch(selected.descriptor(), &hooks, &request, Some(&discovered.catalog))?;
590
623
  validate_launch_plan(selected.descriptor(), &resolved, &adapter.plan(&resolved)?)?;
591
624
  Ok(PreparedResume { binding: backend.binding, role, discovered, request })
@@ -616,7 +649,10 @@ pub fn resume(prepared: PreparedResume) -> Result<OperationRecord, BackendError>
616
649
  let mut io = ScopedMaterializer::new(descriptor, &resolved, &plan, operation.clone(), RealCommandRunner::default())?;
617
650
  let clock = RealClock::new();
618
651
  let mut physical = PhysicalRuntime::new(
619
- Registration { descriptor, hooks: &hooks, controls: selected.controls() },
652
+ Registration {
653
+ descriptor, hooks: &hooks, controls: selected.controls(),
654
+ startup: selected.startup(), session_panel: selected.session_panel(),
655
+ },
620
656
  settings(&backend.binding, selected), operation.clone(), &clock,
621
657
  )?;
622
658
  physical.evidence = policy_evidence(&prepared.role.provider, &prepared.request.native, backend.binding.candidate_sha256)?;
@@ -626,7 +662,7 @@ pub fn resume(prepared: PreparedResume) -> Result<OperationRecord, BackendError>
626
662
  Routing {
627
663
  pane: "unpublished".into(),
628
664
  binding_key: format!("binding-{}", prepared.request.identity.instance.as_str()),
629
- server_key: selected.server_key()?.into(),
665
+ server_key: selected.server_key_for(&prepared.request.identity)?,
630
666
  },
631
667
  operation,
632
668
  )?;
@@ -774,12 +810,15 @@ pub fn stop(workspace: &Path, team: &str, seat: &SeatId) -> Result<OperationReco
774
810
  Registration {
775
811
  descriptor: selected.descriptor(),
776
812
  hooks: &hooks,
777
- controls: &[],
813
+ controls: selected.controls(),
814
+ startup: selected.startup(),
815
+ session_panel: selected.session_panel(),
778
816
  },
779
817
  settings(&backend.binding, selected),
780
818
  operation.clone(),
781
819
  &clock,
782
820
  )?;
821
+ physical.evidence = policy_evidence(&target.provider, &target.native, backend.binding.candidate_sha256)?;
783
822
  Ok(Lifecycle {
784
823
  store: &mut backend.store,
785
824
  host: &mut physical,
@@ -817,6 +856,47 @@ mod tests {
817
856
  }
818
857
  impl Drop for RoleFixture { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.root); } }
819
858
 
859
+ #[test]
860
+ fn restart_skips_stopped_mirror_and_stopped_native_store_before_resume_discovery() {
861
+ for provider in ["cursor", "grok_build"] {
862
+ let mut fixture = RoleFixture::new();
863
+ let directory = fixture.root.join("roles");
864
+ std::fs::create_dir_all(directory.join("agents")).unwrap();
865
+ std::fs::write(directory.join("TEAM.md"), "---\nname: test\n---\nFixture\n").unwrap();
866
+ std::fs::write(directory.join("agents/worker.md"),
867
+ format!("---\nprovider: {provider}\nmodel: unavailable-fixture-model\n---\nFixture\n")).unwrap();
868
+ let stopped = serde_json::json!({"agents":{"worker":{"provider":provider,"status":"stopped","session_id":"preserved","generation":1}}});
869
+ // No framework binding/store/native installation exists yet. A
870
+ // stopped mirror must not even attempt to open the native store.
871
+ assert!(prepare_restart(&fixture.root, "test", &directory, &stopped).unwrap().is_empty());
872
+
873
+ fixture.binding.transport_root = fixture.binding.workspace.path.join("native-store");
874
+ fixture.binding.endpoint = endpoint(&fixture.binding.scope);
875
+ let store = ContractStore::create(&fixture.binding.transport_root, fixture.binding.scope.clone(), &fixture.binding.endpoint).unwrap();
876
+ let native = NativeIdentity { version: "fixture".into(), harness: "unavailable".into(), ui: "tui".into(),
877
+ platform: Platform::MacOs, executable_sha256: digest(b"fixture") };
878
+ let seat = SeatRecord {
879
+ identity: fixture.identity.clone(), provider: ProviderId::new(provider).unwrap(), native,
880
+ cwd: fixture.binding.workspace.clone(), endpoint: fixture.binding.endpoint.clone(),
881
+ pane: "unpublished".into(), binding_key: "fixture".into(), profile: "unavailable".into(), server_key: "fixture".into(),
882
+ evidence_kind: EvidenceKind::Native, status: SeatStatus::Stopped, process: None, physical: None,
883
+ session: None, resources: vec![], workspace_resources: vec![], bootstrap_used: false,
884
+ };
885
+ // Test-only canonical fixture, never a live-store recovery shortcut.
886
+ let db = rusqlite::Connection::open(fixture.binding.transport_root.join("contract.db")).unwrap();
887
+ db.execute("INSERT INTO contract_seats(seat,record) VALUES(?1,?2)",
888
+ rusqlite::params![fixture.identity.seat.as_str(), serde_json::to_string(&seat).unwrap()]).unwrap();
889
+ db.execute("INSERT INTO contract_generations(seat,generation) VALUES(?1,1)", [fixture.identity.seat.as_str()]).unwrap();
890
+ drop(db);
891
+ std::fs::write(root_for(&fixture.binding.workspace.path, &fixture.binding.scope).join("framework-binding.json"),
892
+ serde_json::to_vec(&fixture.binding).unwrap()).unwrap();
893
+ let stale_running = serde_json::json!({"agents":{"worker":{"provider":provider,"status":"running"}}});
894
+ assert!(prepare_restart(&fixture.root, "test", &directory, &stale_running).unwrap().is_empty());
895
+ assert_eq!(store.seat(&fixture.identity.seat).unwrap(), Some(seat));
896
+ assert_eq!(store.propose_identity(&fixture.identity.seat).unwrap().generation, Generation(2));
897
+ }
898
+ }
899
+
820
900
  #[test]
821
901
  fn role_snapshots_are_instance_scoped_and_exclusive() {
822
902
  let fixture = RoleFixture::new();
@@ -51,10 +51,10 @@ fn with_agent(
51
51
  apply(&latest, &SeatId::new(agent).map_err(error)?).map(Some)
52
52
  }
53
53
 
54
- fn publish(selected: &SelectedTeam, id: &SeatId) -> Result<Value, CliError> {
55
- publish_at(&selected.run_workspace, &selected.team_key, id)
54
+ fn publish(selected: &SelectedTeam, id: &SeatId, clear_stop: bool) -> Result<Value, CliError> {
55
+ publish_at(&selected.run_workspace, &selected.team_key, id, clear_stop)
56
56
  }
57
- fn publish_at(workspace: &Path, team: &str, id: &SeatId) -> Result<Value, CliError> {
57
+ fn publish_at(workspace: &Path, team: &str, id: &SeatId, clear_stop: bool) -> Result<Value, CliError> {
58
58
  let projected = framework::project_seat(workspace, team, id.as_str()).map_err(error)?;
59
59
  StateRepository::new(workspace).commit(
60
60
  StateWriteIntent::StartAgent { team_key: team, agent_id: id.as_str() },
@@ -64,6 +64,9 @@ fn publish_at(workspace: &Path, team: &str, id: &SeatId) -> Result<Value, CliErr
64
64
  row.insert(key.clone(), value.clone());
65
65
  }
66
66
  row.insert("paused".into(), Value::Bool(false));
67
+ if clear_stop {
68
+ row.insert("explicit_stop".into(), Value::Bool(false));
69
+ }
67
70
  }
68
71
  },
69
72
  ).map_err(error)?;
@@ -79,6 +82,16 @@ fn check_record(receipt: &team_agent_contract::orchestration::store::OperationRe
79
82
  Ok(())
80
83
  }
81
84
 
85
+ // A partial native operation may not have a session for projection yet. Its
86
+ // original receipt must win over any secondary projection/metadata error.
87
+ fn checked_projection(
88
+ receipt: &team_agent_contract::orchestration::store::OperationRecord,
89
+ project: impl FnOnce() -> Result<Value, CliError>,
90
+ ) -> Result<Value, CliError> {
91
+ check_record(receipt)?;
92
+ project()
93
+ }
94
+
82
95
  pub fn start_agent(
83
96
  workspace: &Path,
84
97
  team: Option<&str>,
@@ -130,8 +143,7 @@ pub fn start_agent(
130
143
  };
131
144
  // An uncertain operation never rolls the authoritative native store back
132
145
  // to the old generation. The receipt remains available for recovery.
133
- let projection = publish(selected, id)?;
134
- check_record(&receipt)?;
146
+ let projection = checked_projection(&receipt, || publish(selected, id, true))?;
135
147
  let coordinator = crate::coordinator::start_coordinator_with_team(
136
148
  &crate::coordinator::WorkspacePath::new(selected.run_workspace.clone()), Some(&selected.team_key),
137
149
  ).map_err(error)?;
@@ -162,13 +174,13 @@ pub(crate) fn restart_prepared(
162
174
  let session_id = prepared.session().map(str::to_owned);
163
175
  let outcome = (|| {
164
176
  let receipt = backend::resume(prepared).map_err(error)?;
165
- let projection = publish_at(workspace, team, &id)?;
177
+ let projection = checked_projection(&receipt, || publish_at(workspace, team, &id, false))?;
166
178
  if let Some(row) = state.get_mut("agents").and_then(|agents| agents.get_mut(id.as_str())).and_then(Value::as_object_mut) {
167
179
  for (key, value) in projection.as_object().expect("native projection object") {
168
180
  row.insert(key.clone(), value.clone());
169
181
  }
170
182
  }
171
- check_record(&receipt)
183
+ Ok::<(), CliError>(())
172
184
  })().map_err(|error| error.to_string());
173
185
  NativeRestartResult { agent_id: id.as_str().into(), session_id, outcome }
174
186
  }).collect()
@@ -235,6 +247,7 @@ pub fn stop_agent(
235
247
  for (key, value) in projected.as_object().expect("native projection object") {
236
248
  row.insert(key.clone(), value.clone());
237
249
  }
250
+ row.insert("explicit_stop".into(), Value::Bool(true));
238
251
  StateRepository::new(&selected.run_workspace).save(
239
252
  StateWriteIntent::StopAgent { team_key: &selected.team_key, agent_id: agent }, &state,
240
253
  ).map_err(error)?;
@@ -243,3 +256,71 @@ pub fn stop_agent(
243
256
  "runtime":"contract","operation":operation}))
244
257
  })
245
258
  }
259
+
260
+ #[cfg(test)]
261
+ mod tests {
262
+ use super::*;
263
+ use team_agent_contract::contract::{delivery::DeliveryEffect, session::CwdIdentity, types::*};
264
+ use team_agent_contract::orchestration::store::{OperationRecord, Phase, SeatRecord, TransactionKind};
265
+
266
+ fn receipt(outcome: Outcome) -> OperationRecord {
267
+ let identity = InstanceIdentity {
268
+ scope: ScopeId::new("scope").unwrap(), seat: SeatId::new("grok-worker").unwrap(),
269
+ instance: InstanceId::new("instance-two").unwrap(), generation: Generation(2),
270
+ };
271
+ OperationRecord {
272
+ id: OperationId::new("resume-instance-two").unwrap(), kind: TransactionKind::Restart,
273
+ target: SeatRecord {
274
+ identity, provider: ProviderId::new("grok_build").unwrap(),
275
+ native: NativeIdentity { version: "fixture".into(), harness: "fixture".into(), ui: "tui".into(),
276
+ platform: Platform::MacOs, executable_sha256: Digest([0; 32]) },
277
+ cwd: CwdIdentity { path: "/controlled/native-cwd".into(), identity: Digest([0; 32]) },
278
+ endpoint: "fixture".into(), pane: "%0".into(), binding_key: "fixture".into(),
279
+ profile: "fixture".into(), server_key: "fixture".into(), evidence_kind: EvidenceKind::Fixture,
280
+ status: SeatStatus::Starting, process: None, physical: None, session: None,
281
+ resources: vec![], workspace_resources: vec![], bootstrap_used: false,
282
+ },
283
+ parent: None, phase: Phase::F3Spawn, pending: Some("compensating-stop".into()),
284
+ resources: vec![], effect: DeliveryEffect::NoEffect, outcome, preserved: vec![],
285
+ failure: Some("native control SessionInspect guard-deadline".into()),
286
+ }
287
+ }
288
+
289
+ #[test]
290
+ fn uncommitted_receipt_precedes_projection_and_preserves_original_failure() {
291
+ for outcome in [Outcome::Running, Outcome::Compensated, Outcome::NeedsRecovery] {
292
+ let record = receipt(outcome);
293
+ let before = record.clone();
294
+ let mut projected = false;
295
+ let mut state = json!({"status":"stopped","explicit_stop":true,"generation":1});
296
+ let original = state.clone();
297
+ let error = checked_projection(&record, || {
298
+ projected = true;
299
+ state["explicit_stop"] = Value::Bool(false);
300
+ Err(super::error(BackendError::Metadata))
301
+ }).unwrap_err().to_string();
302
+ assert!(!projected);
303
+ assert_eq!(state, original);
304
+ assert_eq!(record, before);
305
+ for value in ["resume-instance-two", "F3Spawn", "SessionInspect", "guard-deadline"] {
306
+ assert!(error.contains(value), "{error}");
307
+ }
308
+ assert!(!error.contains("metadata is invalid"), "{error}");
309
+ }
310
+ }
311
+
312
+ #[test]
313
+ fn committed_receipt_retains_projection_success_and_error() {
314
+ let record = receipt(Outcome::Committed);
315
+ let mut calls = 0;
316
+ let projected = checked_projection(&record, || {
317
+ calls += 1;
318
+ Ok(json!({"status":"running","explicit_stop":false,"generation":2}))
319
+ }).unwrap();
320
+ assert_eq!(calls, 1);
321
+ assert_eq!(projected["explicit_stop"], false);
322
+ let error = checked_projection(&record, || Err(super::error(BackendError::Metadata)))
323
+ .unwrap_err().to_string();
324
+ assert!(error.contains("metadata is invalid"));
325
+ }
326
+ }
@@ -5,9 +5,10 @@ use team_agent_contract::contract::{hooks::*, plan::CatalogObservation, types::*
5
5
  use team_agent_contract::host::{command::RealCommandRunner, process::resolve_cwd};
6
6
  use team_agent_contract::kiro::native as kiro;
7
7
  use team_agent_contract::providers::cursor::native as cursor;
8
+ use team_agent_contract::providers::grok_build::native as grok;
8
9
  use thiserror::Error;
9
10
 
10
- use super::registry::{self, NativeProvider};
11
+ use super::registry::{self, NativeProvider, NativeRuntime};
11
12
 
12
13
  #[derive(Debug, Error)]
13
14
  pub enum DiscoveryError {
@@ -19,6 +20,8 @@ pub enum DiscoveryError {
19
20
  Probe(#[from] kiro::EngineProbeError),
20
21
  #[error("native Cursor launcher probe failed: {0}")]
21
22
  CursorProbe(#[from] cursor::ProbeError),
23
+ #[error("native Grok launcher probe failed: {0}")]
24
+ GrokProbe(#[from] grok::ProbeError),
22
25
  #[error("native discovery contract: {0:?}")]
23
26
  Contract(ContractError),
24
27
  #[error("native model discovery: {0:?}")]
@@ -31,7 +34,7 @@ pub struct Discovery {
31
34
  pub engine: PathBuf,
32
35
  pub native: NativeIdentity,
33
36
  pub catalog: CatalogObservation,
34
- pub runtime: Option<cursor::RuntimeInstallation>,
37
+ pub runtime: Option<NativeRuntime>,
35
38
  }
36
39
 
37
40
  pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
@@ -56,7 +59,13 @@ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, Di
56
59
  deadline: std::time::Duration::from_secs(30),
57
60
  max_output_bytes: 1024 * 1024,
58
61
  };
59
- let launcher = executable(descriptor.identity.binary)?;
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
+ };
60
69
  let (engine, native) = match selected {
61
70
  NativeProvider::Kiro => {
62
71
  let home = std::env::var_os("HOME")
@@ -71,6 +80,11 @@ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, Di
71
80
  let native = cursor::probe_launcher(&engine, &mut RealCommandRunner::default(), bounds)?;
72
81
  (engine, native)
73
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
+ }
74
88
  };
75
89
  let request = CatalogRequest {
76
90
  provider: ProviderId::new(descriptor.identity.id).map_err(DiscoveryError::Contract)?,
@@ -100,6 +114,11 @@ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, Di
100
114
  .map_err(DiscoveryError::Contract)?;
101
115
  catalog_hook.discover(&request, &mut host)
102
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
+ }
103
122
  }.map_err(DiscoveryError::Catalog)?;
104
123
  let result = Discovery { engine, native, catalog, runtime: None };
105
124
  validate_catalog(provider, &result).map_err(DiscoveryError::Contract)?;
@@ -110,15 +129,28 @@ pub fn discover(provider: &ProviderId, workspace: &Path) -> Result<Discovery, Di
110
129
  /// bridges the same installation's catalog to its proven Node/entry identity.
111
130
  pub(super) fn discover_runtime(provider: &ProviderId, workspace: &Path) -> Result<Discovery, DiscoveryError> {
112
131
  let mut discovery = discover(provider, workspace)?;
113
- if registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)? == NativeProvider::Cursor {
132
+ let selected = registry::provider(provider.as_str()).map_err(DiscoveryError::Contract)?;
133
+ if selected != NativeProvider::Kiro {
114
134
  let home = std::env::var_os("HOME").map(PathBuf::from)
115
135
  .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);
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
+ }
122
154
  }
123
155
  Ok(discovery)
124
156
  }
@@ -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()
@@ -148,6 +148,8 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
148
148
  descriptor: selected.descriptor(),
149
149
  hooks: &hooks,
150
150
  controls: selected.controls(),
151
+ startup: selected.startup(),
152
+ session_panel: selected.session_panel(),
151
153
  },
152
154
  backend::settings(&backend.binding, selected),
153
155
  operation.clone(),