@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +143 -12
- package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
- package/crates/team-agent/src/contract_runtime/backend.rs +88 -8
- package/crates/team-agent/src/contract_runtime/commands.rs +88 -7
- package/crates/team-agent/src/contract_runtime/discovery.rs +42 -10
- package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +2 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +86 -9
- package/crates/team-agent/src/contract_runtime/workspace.rs +79 -11
- package/crates/team-agent/src/leader/lease.rs +1 -18
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +86 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +101 -72
- package/crates/team-agent/src/lifecycle/restart/selection.rs +30 -8
- package/crates/team-agent/src/messaging/delivery.rs +132 -27
- package/crates/team-agent/src/messaging/enter_rescue.rs +334 -0
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +566 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +8 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +2 -0
- package/crates/team-agent/src/messaging/types.rs +1 -0
- package/crates/team-agent/src/provider/session/capture.rs +24 -1
- package/crates/team-agent/src/state/persist.rs +72 -2
- package/crates/team-agent/src/state/projection.rs +29 -0
- package/crates/team-agent/src/state/repository/tests.rs +70 -0
- package/crates/team-agent/src/state/repository.rs +52 -1
- package/crates/team-agent/src/tmux_backend/native_input.rs +494 -0
- package/crates/team-agent/src/tmux_backend/native_input_tests.rs +781 -0
- package/crates/team-agent/src/tmux_backend.rs +49 -6
- package/crates/team-agent/src/transport/native_input.rs +594 -0
- package/crates/team-agent/src/transport.rs +19 -0
- 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 =
|
|
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 =
|
|
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
|
|
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)|
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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<
|
|
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 =
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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(),
|