@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.
Files changed (52) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +57 -22
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +157 -8
  10. package/crates/team-agent/src/cli/models.rs +119 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
  17. package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
  18. package/crates/team-agent/src/contract_runtime/config.rs +340 -0
  19. package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
  20. package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
  21. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  22. package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
  23. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  24. package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
  25. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  26. package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
  27. package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
  28. package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
  29. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  30. package/crates/team-agent/src/leader/mod.rs +1 -0
  31. package/crates/team-agent/src/leader/prompt.rs +906 -0
  32. package/crates/team-agent/src/leader/start.rs +353 -21
  33. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  34. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  35. package/crates/team-agent/src/leader/types.rs +6 -0
  36. package/crates/team-agent/src/lib.rs +4 -0
  37. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
  38. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  39. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
  40. package/crates/team-agent/src/lifecycle/launch.rs +45 -28
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
  42. package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
  43. package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
  44. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  45. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  46. package/crates/team-agent/src/main.rs +6 -0
  47. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  48. package/crates/team-agent/src/messaging/send.rs +10 -0
  49. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  50. package/crates/team-agent/src/model/spec.rs +24 -12
  51. package/crates/team-agent/src/state/persist.rs +73 -0
  52. 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
+ }