@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/emit.rs +2 -2
- package/crates/team-agent/src/cli/mod.rs +12 -7
- package/crates/team-agent/src/cli/models.rs +52 -1
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +408 -95
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +39 -8
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +186 -40
- package/crates/team-agent/src/contract_runtime/framework.rs +38 -5
- package/crates/team-agent/src/contract_runtime/mod.rs +2 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +95 -33
- package/crates/team-agent/src/contract_runtime/registry.rs +171 -4
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/leader/start.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +99 -1
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -5
- package/crates/team-agent/src/lifecycle/launch.rs +6 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/package.json +4 -4
|
@@ -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::
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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:
|
|
64
|
-
bounds
|
|
80
|
+
source: descriptor.model.catalog.require("native catalog source").map_err(DiscoveryError::Contract)?.clone(),
|
|
81
|
+
bounds,
|
|
65
82
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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|
|
|
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:
|
|
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"],
|
|
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
|
|
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
|
|
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
|
|
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":"
|
|
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:
|
|
148
|
+
descriptor: selected.descriptor(),
|
|
108
149
|
hooks: &hooks,
|
|
109
|
-
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(
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
207
|
+
allow_first_business_bootstrap: bootstrap_allowed,
|
|
154
208
|
require_server_handshake: true,
|
|
155
209
|
channel: Channel::Tmux,
|
|
156
|
-
policy_sha256:
|
|
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) =>
|
|
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
|
}
|