@team-agent/installer 0.5.113 → 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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +57 -22
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +157 -8
- package/crates/team-agent/src/cli/models.rs +119 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +340 -0
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +353 -21
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
- package/crates/team-agent/src/lifecycle/launch.rs +45 -28
- 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/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
//! Bounded native discovery. Nothing here creates a runtime or claims T2/T3.
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
|
|
4
|
+
use team_agent_contract::contract::{hooks::*, plan::CatalogObservation, types::*};
|
|
5
|
+
use team_agent_contract::host::{command::RealCommandRunner, process::resolve_cwd};
|
|
6
|
+
use team_agent_contract::kiro::native as kiro;
|
|
7
|
+
use team_agent_contract::providers::cursor::native as cursor;
|
|
8
|
+
use thiserror::Error;
|
|
9
|
+
|
|
10
|
+
use super::registry::{self, NativeProvider};
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Error)]
|
|
13
|
+
pub enum DiscoveryError {
|
|
14
|
+
#[error("required executable is unavailable: {0}")]
|
|
15
|
+
Executable(&'static str),
|
|
16
|
+
#[error("native discovery failed: {0}")]
|
|
17
|
+
Host(#[from] team_agent_contract::host::HostError),
|
|
18
|
+
#[error("native engine probe failed: {0}")]
|
|
19
|
+
Probe(#[from] kiro::EngineProbeError),
|
|
20
|
+
#[error("native Cursor launcher probe failed: {0}")]
|
|
21
|
+
CursorProbe(#[from] cursor::ProbeError),
|
|
22
|
+
#[error("native discovery contract: {0:?}")]
|
|
23
|
+
Contract(ContractError),
|
|
24
|
+
#[error("native model discovery: {0:?}")]
|
|
25
|
+
Catalog(ReadFailure),
|
|
26
|
+
#[error("native discovery I/O: {0}")]
|
|
27
|
+
Io(#[from] std::io::Error),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub struct Discovery {
|
|
31
|
+
pub engine: PathBuf,
|
|
32
|
+
pub native: NativeIdentity,
|
|
33
|
+
pub catalog: CatalogObservation,
|
|
34
|
+
pub runtime: Option<cursor::RuntimeInstallation>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
|
|
38
|
+
let path = std::env::var_os("PATH").ok_or(DiscoveryError::Executable(name))?;
|
|
39
|
+
for directory in std::env::split_paths(&path) {
|
|
40
|
+
let candidate = directory.join(name);
|
|
41
|
+
let Ok(metadata) = std::fs::metadata(&candidate) else {
|
|
42
|
+
continue;
|
|
43
|
+
};
|
|
44
|
+
use std::os::unix::fs::PermissionsExt;
|
|
45
|
+
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
|
|
46
|
+
return Ok(candidate.canonicalize()?);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
Err(DiscoveryError::Executable(name))
|
|
50
|
+
}
|
|
51
|
+
|
|
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
|
+
};
|
|
75
|
+
let request = CatalogRequest {
|
|
76
|
+
provider: ProviderId::new(descriptor.identity.id).map_err(DiscoveryError::Contract)?,
|
|
77
|
+
native: native.clone(),
|
|
78
|
+
executable: engine.clone(),
|
|
79
|
+
cwd: resolve_cwd(workspace)?,
|
|
80
|
+
source: descriptor.model.catalog.require("native catalog source").map_err(DiscoveryError::Contract)?.clone(),
|
|
81
|
+
bounds,
|
|
82
|
+
};
|
|
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(())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn models(
|
|
138
|
+
args: &crate::cli::ModelsArgs,
|
|
139
|
+
) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
|
|
140
|
+
let result = std::env::current_dir()
|
|
141
|
+
.map_err(DiscoveryError::Io)
|
|
142
|
+
.and_then(|cwd| {
|
|
143
|
+
let provider = ProviderId::new(&args.provider).map_err(DiscoveryError::Contract)?;
|
|
144
|
+
discover(&provider, &cwd)
|
|
145
|
+
});
|
|
146
|
+
render_models(args, result)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn render_models(
|
|
150
|
+
args: &crate::cli::ModelsArgs,
|
|
151
|
+
result: Result<Discovery, DiscoveryError>,
|
|
152
|
+
) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
|
|
153
|
+
use crate::cli::{CmdOutput, CmdResult, ExitCode};
|
|
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
|
+
});
|
|
160
|
+
let (value, human, exit) = match result {
|
|
161
|
+
Ok(discovery) => {
|
|
162
|
+
let query = args.search.as_deref().map(str::to_lowercase);
|
|
163
|
+
let rows: Vec<_> = discovery.catalog.models.iter()
|
|
164
|
+
.filter(|model| query.as_ref().is_none_or(|query| model.id.to_lowercase().contains(query)))
|
|
165
|
+
.map(|model| {
|
|
166
|
+
let efforts = match &model.efforts {
|
|
167
|
+
Support::Supported(values) => json!({"state":"supported","values":values.iter().map(|value| value.as_str()).collect::<Vec<_>>()}),
|
|
168
|
+
Support::Unsupported(reason) => json!({"state":"unsupported","reason":reason.code}),
|
|
169
|
+
Support::Unverified(reason) => json!({"state":"unverified","reason":reason.code}),
|
|
170
|
+
};
|
|
171
|
+
json!({"provider":args.provider,"model_id":model.id,"role_model":model.id,"aliases":[],"efforts":efforts})
|
|
172
|
+
}).collect();
|
|
173
|
+
let value = json!({"schema_version":"models.v1","ok":true,"provider":args.provider,
|
|
174
|
+
"models":rows,"auth":"unknown","auth_basis":"native_catalog_visibility_is_not_authorization",
|
|
175
|
+
"source":{"schema":discovery.catalog.schema,"executable":discovery.engine,
|
|
176
|
+
"version":discovery.native.version,"executable_sha256":team_agent_contract::host::digest_hex(discovery.native.executable_sha256),
|
|
177
|
+
"identity_scope":"catalog_only"},
|
|
178
|
+
"current_role_model":null,"search":args.search});
|
|
179
|
+
let mut lines = vec![format!("models.v1 | {} native model IDs:", args.provider)];
|
|
180
|
+
lines.extend(
|
|
181
|
+
rows.iter()
|
|
182
|
+
.filter_map(|row| row["model_id"].as_str())
|
|
183
|
+
.map(str::to_owned),
|
|
184
|
+
);
|
|
185
|
+
lines.push("Catalog visibility does not prove subscription authorization, runtime model selection, or effort support.".into());
|
|
186
|
+
(value, lines.join("\n"), ExitCode::Ok)
|
|
187
|
+
}
|
|
188
|
+
Err(error) => {
|
|
189
|
+
let message = error.to_string();
|
|
190
|
+
let action = "Check the selected native executable and existing subscription; no model fallback or login retry was performed.";
|
|
191
|
+
(
|
|
192
|
+
json!({"schema_version":"models.v1","ok":false,"provider":args.provider,"models":[],"auth":"not_ready","error":message,"action":action}),
|
|
193
|
+
format!("models.v1 | {message}\n{action}"),
|
|
194
|
+
ExitCode::Error,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
Ok(CmdResult {
|
|
199
|
+
output: CmdOutput::Human(if args.json {
|
|
200
|
+
serde_json::to_string_pretty(&value)?
|
|
201
|
+
} else {
|
|
202
|
+
human
|
|
203
|
+
}),
|
|
204
|
+
exit,
|
|
205
|
+
as_json: false,
|
|
206
|
+
preserve_json_order: true,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(test)]
|
|
211
|
+
mod tests {
|
|
212
|
+
use super::*;
|
|
213
|
+
use crate::cli::{CmdOutput, ExitCode, ModelsArgs};
|
|
214
|
+
use team_agent_contract::contract::plan::ModelRecord;
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn native_catalog_projection_preserves_ids_without_claiming_auth_or_effort() {
|
|
218
|
+
let native = NativeIdentity {
|
|
219
|
+
version: "controlled".into(),
|
|
220
|
+
harness: "v3".into(),
|
|
221
|
+
ui: "tui".into(),
|
|
222
|
+
platform: Platform::Linux,
|
|
223
|
+
executable_sha256: Digest([1; 32]),
|
|
224
|
+
};
|
|
225
|
+
let discovery = Discovery {
|
|
226
|
+
runtime: None,
|
|
227
|
+
engine: "/controlled/kiro-cli-chat".into(),
|
|
228
|
+
native: native.clone(),
|
|
229
|
+
catalog: CatalogObservation {
|
|
230
|
+
provider: ProviderId::new("kiro").unwrap(),
|
|
231
|
+
native,
|
|
232
|
+
schema: kiro::CHAT_CATALOG_SOURCE.schema.into(),
|
|
233
|
+
models: vec![ModelRecord {
|
|
234
|
+
id: "Exact-Case-ID".into(),
|
|
235
|
+
efforts: Support::Unverified(Reason {
|
|
236
|
+
code: "not-observed",
|
|
237
|
+
message: "No effort evidence",
|
|
238
|
+
}),
|
|
239
|
+
}],
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
let args = ModelsArgs {
|
|
243
|
+
provider: "kiro".into(),
|
|
244
|
+
search: Some("exact".into()),
|
|
245
|
+
json: true,
|
|
246
|
+
};
|
|
247
|
+
let result = render_models(&args, Ok(discovery)).unwrap();
|
|
248
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
249
|
+
let CmdOutput::Human(text) = result.output else {
|
|
250
|
+
panic!("JSON text projection");
|
|
251
|
+
};
|
|
252
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
253
|
+
assert_eq!(json["auth"], "unknown");
|
|
254
|
+
assert_eq!(json["models"][0]["model_id"], "Exact-Case-ID");
|
|
255
|
+
assert_eq!(json["models"][0]["efforts"]["state"], "unverified");
|
|
256
|
+
assert_eq!(json["source"]["schema"], kiro::CHAT_CATALOG_SOURCE.schema);
|
|
257
|
+
assert!(json["current_role_model"].is_null());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn version_failure_projection_preserves_the_received_version() {
|
|
262
|
+
let args = ModelsArgs {
|
|
263
|
+
provider: "kiro".into(),
|
|
264
|
+
search: None,
|
|
265
|
+
json: true,
|
|
266
|
+
};
|
|
267
|
+
let error = kiro::parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
|
|
268
|
+
let result = render_models(&args, Err(DiscoveryError::Probe(error))).unwrap();
|
|
269
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
270
|
+
let CmdOutput::Human(text) = result.output else {
|
|
271
|
+
panic!("JSON text projection");
|
|
272
|
+
};
|
|
273
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
274
|
+
assert!(json["error"]
|
|
275
|
+
.as_str()
|
|
276
|
+
.unwrap()
|
|
277
|
+
.contains("kiro-cli-chat 2.29.invalid"));
|
|
278
|
+
assert_eq!(json["models"], serde_json::json!([]));
|
|
279
|
+
}
|
|
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
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn native_discovery_failure_has_no_catalog_or_fallback() {
|
|
368
|
+
let args = ModelsArgs {
|
|
369
|
+
provider: "kiro".into(),
|
|
370
|
+
search: None,
|
|
371
|
+
json: true,
|
|
372
|
+
};
|
|
373
|
+
let result = render_models(&args, Err(DiscoveryError::Executable("kiro-cli"))).unwrap();
|
|
374
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
375
|
+
let CmdOutput::Human(text) = result.output else {
|
|
376
|
+
panic!("JSON text projection");
|
|
377
|
+
};
|
|
378
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
379
|
+
assert_eq!(json["ok"], false);
|
|
380
|
+
assert_eq!(json["models"], serde_json::json!([]));
|
|
381
|
+
assert!(json["error"].as_str().unwrap().contains("kiro-cli"));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//! New-runtime egress into the existing shared framework business APIs. A
|
|
2
|
+
//! forwarding intent is not a legacy physical queue, nor proof of presentation.
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
use team_agent_contract::contract::{session::CwdIdentity, types::ScopeId};
|
|
5
|
+
use team_agent_contract::host::{digest, digest_hex, process::resolve_cwd};
|
|
6
|
+
use team_agent_contract::orchestration::{forward::*, store::ContractStore, Error};
|
|
7
|
+
|
|
8
|
+
use crate::messaging::{self, MessageTarget, SendOptions, SendOrigin, TrustedSender};
|
|
9
|
+
use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
10
|
+
|
|
11
|
+
/// Created from the selected framework Team, never from MCP tool arguments.
|
|
12
|
+
pub struct FrameworkContext {
|
|
13
|
+
pub workspace: CwdIdentity,
|
|
14
|
+
pub team: String,
|
|
15
|
+
pub native_scope: ScopeId,
|
|
16
|
+
}
|
|
17
|
+
impl FrameworkContext {
|
|
18
|
+
pub fn route(&self) -> Result<String, Error> {
|
|
19
|
+
Ok(format!(
|
|
20
|
+
"framework-{}",
|
|
21
|
+
digest_hex(digest(&serde_json::to_vec(&(
|
|
22
|
+
&self.workspace,
|
|
23
|
+
&self.team,
|
|
24
|
+
&self.native_scope
|
|
25
|
+
))?))
|
|
26
|
+
))
|
|
27
|
+
}
|
|
28
|
+
fn validate(&self, store: &ContractStore) -> Result<(), Error> {
|
|
29
|
+
if self.team.trim().is_empty()
|
|
30
|
+
|| self.team.chars().any(char::is_control)
|
|
31
|
+
|| store.scope() != &self.native_scope
|
|
32
|
+
|| resolve_cwd(&self.workspace.path).map_err(|_| Error::Fence)? != self.workspace
|
|
33
|
+
{
|
|
34
|
+
return Err(Error::Fence);
|
|
35
|
+
}
|
|
36
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
37
|
+
&self.workspace.path,
|
|
38
|
+
Some(&self.team),
|
|
39
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
40
|
+
)
|
|
41
|
+
.map_err(|_| Error::Fence)?;
|
|
42
|
+
if selected.team_key != self.team
|
|
43
|
+
|| resolve_cwd(&selected.run_workspace).map_err(|_| Error::Fence)? != self.workspace
|
|
44
|
+
{
|
|
45
|
+
return Err(Error::Fence);
|
|
46
|
+
}
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn framework_result(result_id: &str, source: &str, envelope: &Value) -> Result<Value, Error> {
|
|
52
|
+
if envelope.get("agent_id").and_then(Value::as_str) != Some(source)
|
|
53
|
+
|| envelope
|
|
54
|
+
.get("task_id")
|
|
55
|
+
.and_then(Value::as_str)
|
|
56
|
+
.is_none_or(|task| task.trim().is_empty())
|
|
57
|
+
{
|
|
58
|
+
return Err(Error::Fence);
|
|
59
|
+
}
|
|
60
|
+
// K3's compact wire (schema 1 / completed) is not the root business envelope.
|
|
61
|
+
// Reuse the shared normalizer rather than maintaining a second alias/schema
|
|
62
|
+
// implementation or claiming that a persisted K3 result was forwarded.
|
|
63
|
+
let normalized = crate::mcp_server::normalize::normalize_report_envelope(envelope);
|
|
64
|
+
if normalized.presentation_error.is_some() {
|
|
65
|
+
return Err(Error::Invalid("result presentation"));
|
|
66
|
+
}
|
|
67
|
+
let mut value = serde_json::to_value(normalized)?;
|
|
68
|
+
value["result_id"] = json!(result_id);
|
|
69
|
+
Ok(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Reuses the common root send/result operations. No provider implementation is
|
|
73
|
+
/// selected here; the common per-recipient dispatcher remains authoritative.
|
|
74
|
+
fn forward_one(context: &FrameworkContext, intent: &ForwardIntent) -> (ForwardState, Value) {
|
|
75
|
+
match &intent.payload {
|
|
76
|
+
ForwardPayload::Message {
|
|
77
|
+
message,
|
|
78
|
+
task,
|
|
79
|
+
sender,
|
|
80
|
+
recipient,
|
|
81
|
+
content,
|
|
82
|
+
mailbox,
|
|
83
|
+
} => {
|
|
84
|
+
if sender != intent.source.seat.as_str() {
|
|
85
|
+
return (
|
|
86
|
+
ForwardState::Refused,
|
|
87
|
+
json!({"code":"sender_binding_mismatch"}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
let mut options = SendOptions {
|
|
91
|
+
origin: SendOrigin::Mcp,
|
|
92
|
+
task_id: task.as_ref().map(|task| TaskId::new(task.clone())),
|
|
93
|
+
route_task_id: false,
|
|
94
|
+
sender: TrustedSender::from_runtime_identity(AgentId::new(sender)),
|
|
95
|
+
team: Some(TeamKey::new(&context.team)),
|
|
96
|
+
message_id: Some(message.as_str().into()),
|
|
97
|
+
wait_visible: false,
|
|
98
|
+
block_until_delivered: false,
|
|
99
|
+
timeout: 2.0,
|
|
100
|
+
..Default::default()
|
|
101
|
+
};
|
|
102
|
+
if *mailbox {
|
|
103
|
+
options.presentation.sink = messaging::presentation::PresentationSink::Casefile;
|
|
104
|
+
}
|
|
105
|
+
match messaging::send_message(
|
|
106
|
+
&context.workspace.path,
|
|
107
|
+
&MessageTarget::Single(recipient.clone()),
|
|
108
|
+
content,
|
|
109
|
+
&options,
|
|
110
|
+
) {
|
|
111
|
+
Ok(outcome) => {
|
|
112
|
+
let accepted = outcome.message_id.as_deref() == Some(message.as_str())
|
|
113
|
+
&& matches!(
|
|
114
|
+
outcome.status,
|
|
115
|
+
messaging::DeliveryStatus::Queued
|
|
116
|
+
| messaging::DeliveryStatus::StoredOnly
|
|
117
|
+
| messaging::DeliveryStatus::Delivered
|
|
118
|
+
| messaging::DeliveryStatus::AlreadyDelivered
|
|
119
|
+
| messaging::DeliveryStatus::RetryScheduled
|
|
120
|
+
| messaging::DeliveryStatus::FallbackLog
|
|
121
|
+
);
|
|
122
|
+
let state = if accepted {
|
|
123
|
+
ForwardState::Accepted
|
|
124
|
+
} else if outcome.message_id.is_none()
|
|
125
|
+
&& matches!(outcome.status, messaging::DeliveryStatus::Refused)
|
|
126
|
+
{
|
|
127
|
+
ForwardState::Refused
|
|
128
|
+
} else {
|
|
129
|
+
ForwardState::Unknown
|
|
130
|
+
};
|
|
131
|
+
(
|
|
132
|
+
state,
|
|
133
|
+
json!({"ok":outcome.ok,"message_id":outcome.message_id,
|
|
134
|
+
"status":outcome.status,"message_status":outcome.message_status.0,
|
|
135
|
+
"reason":outcome.reason,"verification":outcome.verification}),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
// Error alone is not proof that no destination row was written.
|
|
139
|
+
Err(_) => (
|
|
140
|
+
ForwardState::Unknown,
|
|
141
|
+
json!({"code":"framework_send_outcome_unknown"}),
|
|
142
|
+
),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
ForwardPayload::Result {
|
|
146
|
+
result_id,
|
|
147
|
+
envelope,
|
|
148
|
+
} => {
|
|
149
|
+
let envelope = match framework_result(result_id, intent.source.seat.as_str(), envelope)
|
|
150
|
+
{
|
|
151
|
+
Ok(envelope) => envelope,
|
|
152
|
+
Err(_) => {
|
|
153
|
+
return (
|
|
154
|
+
ForwardState::Refused,
|
|
155
|
+
json!({"code":"invalid_result_envelope"}),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
match messaging::results::report_result_for_owner_team(
|
|
160
|
+
&context.workspace.path,
|
|
161
|
+
&envelope,
|
|
162
|
+
Some(&context.team),
|
|
163
|
+
) {
|
|
164
|
+
Ok(receipt) => (ForwardState::Accepted, receipt),
|
|
165
|
+
Err(_) => (
|
|
166
|
+
ForwardState::Unknown,
|
|
167
|
+
json!({"code":"framework_result_outcome_unknown"}),
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The root coordinator owns this consumer. Claim commits before the delegate
|
|
175
|
+
/// is called, so no K3 transaction is held across root framework locks/writes.
|
|
176
|
+
/// InFlight/Unknown items require explicit exact-ID reconciliation, not replay.
|
|
177
|
+
pub fn drain(
|
|
178
|
+
context: &FrameworkContext,
|
|
179
|
+
store: &mut ContractStore,
|
|
180
|
+
limit: usize,
|
|
181
|
+
) -> Result<usize, Error> {
|
|
182
|
+
context.validate(store)?;
|
|
183
|
+
let route = context.route()?;
|
|
184
|
+
let pending = store.pending_forwards(limit)?;
|
|
185
|
+
let mut completed = 0;
|
|
186
|
+
for next in pending {
|
|
187
|
+
if next.source.scope != context.native_scope {
|
|
188
|
+
return Err(Error::Fence);
|
|
189
|
+
}
|
|
190
|
+
let intent = match store.claim_forward(&next.id) {
|
|
191
|
+
Ok(intent) => intent,
|
|
192
|
+
Err(Error::Conflict) => continue,
|
|
193
|
+
Err(error) => return Err(error),
|
|
194
|
+
};
|
|
195
|
+
let (state, receipt) = if intent.route != route {
|
|
196
|
+
(
|
|
197
|
+
ForwardState::Refused,
|
|
198
|
+
json!({"code":"framework_route_changed"}),
|
|
199
|
+
)
|
|
200
|
+
} else {
|
|
201
|
+
// Recheck the selected directory before every external operation.
|
|
202
|
+
context.validate(store)?;
|
|
203
|
+
forward_one(context, &intent)
|
|
204
|
+
};
|
|
205
|
+
store.finish_forward(&intent.id, state, receipt)?;
|
|
206
|
+
completed += 1;
|
|
207
|
+
}
|
|
208
|
+
Ok(completed)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[cfg(test)]
|
|
212
|
+
mod tests {
|
|
213
|
+
use super::*;
|
|
214
|
+
#[test]
|
|
215
|
+
fn compact_native_result_becomes_the_existing_framework_business_schema() {
|
|
216
|
+
let compact = json!({"schema_version":1,"agent_id":"worker","task_id":"msg-bound","status":"completed","summary":"5535"});
|
|
217
|
+
let result = framework_result("result-fixed", "worker", &compact).unwrap();
|
|
218
|
+
assert_eq!(result["schema_version"], "result_envelope_v1");
|
|
219
|
+
assert_eq!(result["status"], "success");
|
|
220
|
+
assert_eq!(result["task_id"], "msg-bound");
|
|
221
|
+
assert_eq!(result["summary"], "5535");
|
|
222
|
+
assert_eq!(result["result_id"], "result-fixed");
|
|
223
|
+
for field in ["changes", "tests", "risks", "artifacts", "next_actions"] {
|
|
224
|
+
assert!(result[field].is_array());
|
|
225
|
+
}
|
|
226
|
+
crate::messaging::helpers::validate_result_envelope(&result).unwrap();
|
|
227
|
+
assert!(framework_result("result-fixed", "other", &compact).is_err());
|
|
228
|
+
}
|
|
229
|
+
}
|