@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.
- 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 +155 -19
- package/crates/team-agent/src/cli/models.rs +52 -1
- package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
- package/crates/team-agent/src/contract_runtime/backend.rs +490 -97
- package/crates/team-agent/src/contract_runtime/commands.rs +326 -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 +218 -40
- package/crates/team-agent/src/contract_runtime/framework.rs +38 -5
- package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +2 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +97 -33
- package/crates/team-agent/src/contract_runtime/registry.rs +248 -4
- package/crates/team-agent/src/contract_runtime/workspace.rs +228 -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/agent.rs +4 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +109 -58
- package/crates/team-agent/src/lifecycle/restart/selection.rs +63 -7
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- 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 +5 -1
- 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::
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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:
|
|
64
|
-
bounds
|
|
94
|
+
source: descriptor.model.catalog.require("native catalog source").map_err(DiscoveryError::Contract)?.clone(),
|
|
95
|
+
bounds,
|
|
65
96
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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|
|
|
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:
|
|
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"],
|
|
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
|
|
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
|
|
@@ -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;
|