@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,16 @@
1
+ //! Per-agent boundary into the contract runtime. Existing provider internals
2
+ //! stay on their path, including mixed teams; a seat never has two executors.
3
+ //!
4
+ //! Selection/preflight is read-only. Physical operations require a validated
5
+ //! contract role and its real framework/native binding, not a provider guess.
6
+ pub mod backend;
7
+ pub mod commands;
8
+ pub mod config;
9
+ pub mod discovery;
10
+ pub mod forward;
11
+ pub mod framework;
12
+ pub mod mcp;
13
+ pub mod prompt;
14
+ pub mod pump;
15
+ pub mod registry;
16
+ mod workspace;
@@ -0,0 +1,216 @@
1
+ //! Provider-independent instruction source policy. The Kiro adapter receives
2
+ //! one resolved prompt, never a provider-private home/path search algorithm.
3
+ use std::io::Read;
4
+ use std::path::{Path, PathBuf};
5
+
6
+ use serde::{Deserialize, Serialize};
7
+ use team_agent_contract::contract::types::Digest;
8
+ use team_agent_contract::host::digest;
9
+
10
+ use super::config::ConfigError;
11
+ use crate::communication_mode::CommunicationMode;
12
+ use crate::lifecycle::worker_command_context::{runtime_contract_section, worker_identity_section};
13
+ use crate::model::yaml::Value;
14
+
15
+ pub const MAX_PROMPT_BYTES: usize = 1024 * 1024;
16
+
17
+ /// Source contents are private; public diagnostics project only their receipts.
18
+ #[derive(Clone)]
19
+ pub struct Source {
20
+ pub path: PathBuf,
21
+ pub contents: String,
22
+ }
23
+ impl Source {
24
+ pub fn read(path: &Path) -> Result<Self, ConfigError> {
25
+ let resolved = path.canonicalize()?;
26
+ let mut bytes = Vec::new();
27
+ std::fs::File::open(&resolved)?
28
+ .take(MAX_PROMPT_BYTES as u64 + 1)
29
+ .read_to_end(&mut bytes)?;
30
+ if bytes.len() > MAX_PROMPT_BYTES {
31
+ return Err(ConfigError::Unsupported(
32
+ "instruction source exceeds the bounded prompt limit; no truncation is performed",
33
+ ));
34
+ }
35
+ let contents = String::from_utf8(bytes).map_err(|_| ConfigError::Invalid {
36
+ path: path.into(),
37
+ field: "UTF-8 instructions",
38
+ })?;
39
+ if contents.contains('\0') {
40
+ return Err(ConfigError::Invalid {
41
+ path: path.into(),
42
+ field: "NUL in instructions",
43
+ });
44
+ }
45
+ Ok(Self {
46
+ path: resolved,
47
+ contents,
48
+ })
49
+ }
50
+ fn optional(path: &Path) -> Result<Option<Self>, ConfigError> {
51
+ match std::fs::symlink_metadata(path) {
52
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
53
+ Err(error) => Err(error.into()),
54
+ Ok(_) => Self::read(path).map(Some),
55
+ }
56
+ }
57
+ }
58
+
59
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
60
+ pub enum Layer {
61
+ UserGlobal,
62
+ Project,
63
+ Role,
64
+ TeamExplicit,
65
+ }
66
+
67
+ #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68
+ pub struct InstructionReceipt {
69
+ pub layer: Layer,
70
+ pub path: PathBuf,
71
+ pub sha256: Digest,
72
+ pub bytes: usize,
73
+ /// Exact UTF-8 byte range in the assembled native prompt, excluding headings.
74
+ pub start: usize,
75
+ pub end: usize,
76
+ }
77
+
78
+ #[derive(Clone, Serialize, Deserialize)]
79
+ pub struct Prompt {
80
+ pub text: String,
81
+ pub sha256: Digest,
82
+ pub sources: Vec<InstructionReceipt>,
83
+ }
84
+
85
+ /// Central compatibility default, not a Kiro adapter constant. An explicit
86
+ /// unified TEAM_AGENT_USER_INSTRUCTIONS file takes precedence and is required
87
+ /// to exist. Without an override the authorized user-level AGENTS is optional.
88
+ pub fn user_source() -> Result<Option<Source>, ConfigError> {
89
+ if let Some(path) = std::env::var_os("TEAM_AGENT_USER_INSTRUCTIONS") {
90
+ if path.is_empty() {
91
+ return Err(ConfigError::Unsupported(
92
+ "TEAM_AGENT_USER_INSTRUCTIONS must name a file",
93
+ ));
94
+ }
95
+ return Source::read(Path::new(&path)).map(Some);
96
+ }
97
+ match std::env::var_os("HOME") {
98
+ Some(home) => Source::optional(&PathBuf::from(home).join(".pi/agent/AGENTS.md")),
99
+ None => Ok(None),
100
+ }
101
+ }
102
+
103
+ /// Selection follows the explicit project workspace, not the roles directory,
104
+ /// generated .kiro directory, current shell directory, or an ancestor guess.
105
+ pub fn project_source(workspace: &Path) -> Result<Option<Source>, ConfigError> {
106
+ Source::optional(&workspace.join("AGENTS.md"))
107
+ }
108
+
109
+ /// instruction_files is an explicit path or ordered path list. instructions is
110
+ /// either inline text (retained verbatim in TEAM.md) or an ordered file-list
111
+ /// alias. Paths are relative to TEAM.md, never the generated native config.
112
+ pub fn team_sources(team: &Source, meta: &Value) -> Result<Vec<Source>, ConfigError> {
113
+ let mut paths = Vec::new();
114
+ for field in ["instruction_files", "instructions"] {
115
+ match meta.get(field) {
116
+ None | Some(Value::Null) => {}
117
+ Some(Value::Str(_)) if field == "instructions" => {}
118
+ Some(Value::Str(path)) => paths.push(path),
119
+ Some(Value::List(values)) => {
120
+ for value in values {
121
+ match value {
122
+ Value::Str(path) => paths.push(path),
123
+ _ => {
124
+ return Err(ConfigError::Invalid {
125
+ path: team.path.clone(),
126
+ field,
127
+ })
128
+ }
129
+ }
130
+ }
131
+ }
132
+ _ => {
133
+ return Err(ConfigError::Invalid {
134
+ path: team.path.clone(),
135
+ field,
136
+ })
137
+ }
138
+ }
139
+ }
140
+ let mut sources = vec![team.clone()];
141
+ for path in paths {
142
+ if path.trim().is_empty() || path.contains('\0') {
143
+ return Err(ConfigError::Invalid {
144
+ path: team.path.clone(),
145
+ field: "instruction file path",
146
+ });
147
+ }
148
+ let path = Path::new(path);
149
+ let path = if path.is_absolute() {
150
+ path.to_owned()
151
+ } else {
152
+ team.path
153
+ .parent()
154
+ .ok_or(ConfigError::Unsupported("TEAM.md has no parent"))?
155
+ .join(path)
156
+ };
157
+ sources.push(Source::read(&path)?);
158
+ }
159
+ Ok(sources)
160
+ }
161
+
162
+ /// Framework binding precedes the four ordered user-policy layers. Original
163
+ /// documents (including role front matter and CR/LF bytes) are copied whole;
164
+ /// only section separators are new bytes. Dynamic tasks are NOT repeated here:
165
+ /// they enter the same native context later through the one durable outbox.
166
+ pub fn assemble(
167
+ id: &str,
168
+ role_name: &str,
169
+ mode: CommunicationMode,
170
+ user: Option<&Source>,
171
+ project: Option<&Source>,
172
+ role: &Source,
173
+ team: &[Source],
174
+ ) -> Result<Prompt, ConfigError> {
175
+ let mut text = [
176
+ worker_identity_section(id, role_name),
177
+ runtime_contract_section("send_message", "report_result", "get_team_status()"),
178
+ mode.runtime_contract("send_message"),
179
+ "The operations above are logical names. Use the bound MCP client's advertised tool names, not guessed prefixes. Original role-template metadata below does not change your bound worker identity or selected native launch parameters.\nFinal completion calls report_result exactly once. Durable storage and actual leader presentation are separate facts; do not claim delivery from registration or persistence alone.".into(),
180
+ ].join("\n\n");
181
+ let mut ordered = Vec::new();
182
+ if let Some(source) = user {
183
+ ordered.push((Layer::UserGlobal, source));
184
+ }
185
+ if let Some(source) = project {
186
+ ordered.push((Layer::Project, source));
187
+ }
188
+ ordered.push((Layer::Role, role));
189
+ ordered.extend(team.iter().map(|source| (Layer::TeamExplicit, source)));
190
+ let mut sources = Vec::new();
191
+ for (layer, source) in ordered {
192
+ text.push_str(&format!(
193
+ "\n\n# Team Agent instruction layer: {layer:?}\n\n"
194
+ ));
195
+ let start = text.len();
196
+ text.push_str(&source.contents);
197
+ if text.len() > MAX_PROMPT_BYTES {
198
+ return Err(ConfigError::Unsupported(
199
+ "assembled prompt exceeds the bounded limit; no truncation is performed",
200
+ ));
201
+ }
202
+ sources.push(InstructionReceipt {
203
+ layer,
204
+ path: source.path.clone(),
205
+ sha256: digest(source.contents.as_bytes()),
206
+ bytes: source.contents.len(),
207
+ start,
208
+ end: text.len(),
209
+ });
210
+ }
211
+ Ok(Prompt {
212
+ sha256: digest(text.as_bytes()),
213
+ text,
214
+ sources,
215
+ })
216
+ }
@@ -0,0 +1,269 @@
1
+ //! One coordinator-owned consumer of the K3 outbox. No CLI send side channel,
2
+ //! implicit business retry, queue flush, or replay of uncertain messages.
3
+ use super::{
4
+ backend::{self, Backend, BackendError},
5
+ forward, framework, registry,
6
+ };
7
+ use serde_json::Value;
8
+ use std::path::Path;
9
+ use std::time::Duration;
10
+ use team_agent_contract::contract::{delivery::InputSurface, probe::*, types::*};
11
+ use team_agent_contract::host::{
12
+ clock::{Clock, RealClock},
13
+ process::verify_native_ancestry,
14
+ };
15
+ use team_agent_contract::orchestration::{physical::*, store::*, supervisor::*};
16
+ use team_agent_contract::runtime::probes::*;
17
+
18
+ pub(super) fn protocol(
19
+ backend: &mut Backend,
20
+ seat: &SeatRecord,
21
+ clock: &RealClock,
22
+ ) -> Result<ProtocolSnapshot, BackendError> {
23
+ let target = seat.physical.as_ref().ok_or(BackendError::Metadata)?;
24
+ let active: Vec<_> = backend
25
+ .store
26
+ .connections(&seat.identity)?
27
+ .into_iter()
28
+ .filter(|record| {
29
+ !record.closed
30
+ && record.identity == seat.identity
31
+ && record.server_key == seat.server_key
32
+ && record.binding_key == seat.binding_key
33
+ && record.native_process == target.process
34
+ && verify_native_ancestry(&record.process, &record.native_process).is_ok()
35
+ })
36
+ .collect();
37
+ if active.len() != 1 {
38
+ return Ok(collect_protocol(
39
+ target,
40
+ &seat.identity.instance,
41
+ &seat.server_key,
42
+ &[],
43
+ clock.now(),
44
+ Duration::from_secs(2),
45
+ ));
46
+ }
47
+ let connection = &active[0];
48
+ let facts = backend
49
+ .store
50
+ .connection_facts(&seat.identity, &connection.connection)?;
51
+ let mut events = Vec::new();
52
+ // T3a is connection state, sampled against BOTH captured process births.
53
+ // Completed server writes are never promoted to client discovery.
54
+ for (fact, protocol) in [
55
+ (
56
+ "initialize_response_written",
57
+ ProtocolFact::InitializeResponseWritten {
58
+ server_instance: connection.connection.clone(),
59
+ },
60
+ ),
61
+ (
62
+ "tools_list_response_written",
63
+ ProtocolFact::ToolsListResponseWritten {
64
+ server_instance: connection.connection.clone(),
65
+ },
66
+ ),
67
+ ] {
68
+ if facts.iter().any(|value| value == fact) {
69
+ events.push(ProtocolEvent {
70
+ scope: target.scope(
71
+ events.len() as u64,
72
+ clock.now(),
73
+ Duration::from_secs(2),
74
+ "live-owned-stdio-state",
75
+ ),
76
+ fact: protocol,
77
+ });
78
+ }
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
+ }
109
+ Ok(collect_protocol(
110
+ target,
111
+ &connection.connection,
112
+ &seat.server_key,
113
+ &events,
114
+ clock.now(),
115
+ Duration::from_secs(2),
116
+ ))
117
+ }
118
+
119
+ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendError> {
120
+ if matches!(seat.status, SeatStatus::Stopped | SeatStatus::Unknown)
121
+ || !backend.store.has_queued_for(&seat.identity)?
122
+ {
123
+ return Ok(());
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
+ }
135
+ let clock = RealClock::new();
136
+ let adapter = backend::adapter(&backend.binding, &seat.identity, &seat.provider)?;
137
+ let hooks = adapter.hooks();
138
+ let operation = OperationId::new(format!(
139
+ "deliver-{}",
140
+ backend
141
+ .store
142
+ .propose_identity(&seat.identity.seat)?
143
+ .instance
144
+ .as_str()
145
+ ))?;
146
+ let mut physical = PhysicalRuntime::new(
147
+ Registration {
148
+ descriptor: selected.descriptor(),
149
+ hooks: &hooks,
150
+ controls: selected.controls(),
151
+ },
152
+ backend::settings(&backend.binding, selected),
153
+ operation.clone(),
154
+ &clock,
155
+ )?;
156
+ physical.evidence = backend::policy_evidence(
157
+ &seat.provider, &seat.native, backend.binding.candidate_sha256,
158
+ )?;
159
+ physical.protocol = Some(protocol(backend, seat, &clock)?);
160
+ let sample = physical.readiness(seat)?;
161
+ if !matches!(sample.pane.outcome, ProbeOutcome::Observed(ref pane) if pane.surface == InputSurface::ComposerReady)
162
+ || !matches!(sample.server.outcome, ProbeOutcome::Observed(ref server) if server.initialize_response_written && server.tools_list_response_written)
163
+ {
164
+ return Ok(());
165
+ }
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());
175
+ }
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());
198
+ }
199
+ let mut bootstrap = OutboxBootstrap::new(ContractStore::open(
200
+ &backend.binding.transport_root,
201
+ backend.binding.scope.clone(),
202
+ &backend.binding.endpoint,
203
+ )?);
204
+ physical.bootstrap = Some(&mut bootstrap);
205
+ let sample = physical.readiness(seat)?;
206
+ let authorization = DeliveryAuthorization {
207
+ allow_first_business_bootstrap: bootstrap_allowed,
208
+ require_server_handshake: true,
209
+ channel: Channel::Tmux,
210
+ policy_sha256: profile.policy_sha256,
211
+ };
212
+ let _ = team_agent_contract::orchestration::supervisor::tick(
213
+ &mut backend.store,
214
+ &seat.identity,
215
+ &sample,
216
+ &authorization,
217
+ &mut physical,
218
+ )?;
219
+ Ok(())
220
+ }
221
+
222
+ pub fn tick(workspace: &Path, state: &mut Value) -> Result<(), BackendError> {
223
+ if !framework::has_contract(state) {
224
+ return Ok(());
225
+ }
226
+ let team = crate::state::projection::team_state_key(state);
227
+ let ids: Vec<_> = state
228
+ .get("agents")
229
+ .and_then(Value::as_object)
230
+ .into_iter()
231
+ .flat_map(|agents| agents.iter())
232
+ .filter(|(_, agent)| framework::is_contract(agent) && !framework::is_paused_unstarted(agent))
233
+ .map(|(id, _)| id.clone())
234
+ .collect();
235
+ if ids.is_empty() {
236
+ return Ok(());
237
+ }
238
+ let mut backend = Backend::open(workspace, &team)?;
239
+ let mut failure = None;
240
+ for id in ids {
241
+ let Some(seat) = backend.store.seat(&SeatId::new(&id)?)? else {
242
+ continue;
243
+ };
244
+ if let Err(error) = deliver_one(&mut backend, &seat) {
245
+ failure = Some(error);
246
+ }
247
+ if let Some(agent) = state
248
+ .get_mut("agents")
249
+ .and_then(|agents| agents.get_mut(&id))
250
+ {
251
+ match framework::project_seat(workspace, &team, &id) {
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
+ },
258
+ Err(error) => failure = Some(error),
259
+ }
260
+ }
261
+ }
262
+ // One blocked seat must not prevent already committed results from reaching
263
+ // the shared framework task-finalization/leader-presentation path.
264
+ forward::drain(&backend.binding.framework(), &mut backend.store, 32)?;
265
+ match failure {
266
+ Some(error) => Err(error),
267
+ None => Ok(()),
268
+ }
269
+ }
@@ -0,0 +1,197 @@
1
+ //! The sole new-runtime provider registry. This is not an admission claim:
2
+ //! the descriptor's Unverified gates still apply to each requested operation.
3
+ use std::collections::BTreeMap;
4
+ use std::path::PathBuf;
5
+ use team_agent_contract::contract::{
6
+ descriptor::{PromptCarrier, ProviderDescriptor, ToolNaming},
7
+ hooks::{PlanHook, ProviderHooks},
8
+ plan::{LaunchPlan, ResolvedLaunch},
9
+ types::{ContractError, Support},
10
+ };
11
+ use team_agent_contract::kiro::{self, KiroAdapter, KIRO_DESCRIPTOR};
12
+ use team_agent_contract::providers::cursor::{self, CursorAdapter, CURSOR_DESCRIPTOR};
13
+ use team_agent_contract::runtime::{delivery::ControlDefinition, native_panel::NativePanelPolicy};
14
+
15
+ const DESCRIPTORS: &[&ProviderDescriptor] = &[&KIRO_DESCRIPTOR, &CURSOR_DESCRIPTOR];
16
+
17
+ /// Leaf selection stays here; the framework, lifecycle and outbox remain shared.
18
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
19
+ pub(super) enum NativeProvider {
20
+ Kiro,
21
+ Cursor,
22
+ }
23
+
24
+ impl NativeProvider {
25
+ pub fn descriptor(self) -> &'static ProviderDescriptor {
26
+ match self {
27
+ Self::Kiro => &KIRO_DESCRIPTOR,
28
+ Self::Cursor => &CURSOR_DESCRIPTOR,
29
+ }
30
+ }
31
+
32
+ pub fn action_budget(self) -> std::time::Duration {
33
+ // Kiro's existing budget is unchanged; Cursor's measured cold-ready
34
+ // path needs a larger finite bound, not an unbounded readiness wait.
35
+ std::time::Duration::from_secs(match self { Self::Kiro => 30, Self::Cursor => 90 })
36
+ }
37
+
38
+ pub fn controls(self) -> &'static [ControlDefinition] {
39
+ match self {
40
+ Self::Kiro => kiro::interaction::CONTROLS,
41
+ Self::Cursor => cursor::interaction::CONTROLS,
42
+ }
43
+ }
44
+
45
+ /// A terminal instruction bridge needs one durable, authorized first packet
46
+ /// before the native client can demonstrate its Team-tool registry.
47
+ pub fn first_business_bootstrap(self) -> bool {
48
+ matches!(self.descriptor().prompt.carrier, Support::Supported(PromptCarrier::RuntimeBridge))
49
+ }
50
+
51
+ /// Cursor has no measured neutral TUI tools panel. Its protocol binding
52
+ /// must come from the separately fenced native tool-roundtrip ledger.
53
+ pub fn uses_tool_roundtrips(self) -> bool {
54
+ matches!(self, Self::Cursor)
55
+ }
56
+
57
+ pub fn tool_panel(self) -> Result<&'static NativePanelPolicy, ContractError> {
58
+ match self {
59
+ Self::Kiro => Ok(&kiro::interaction::TOOL_PANEL),
60
+ Self::Cursor => Err(ContractError::Unverified {
61
+ field: "Cursor native Team-tool inspector",
62
+ reason: cursor::UNVERIFIED,
63
+ }),
64
+ }
65
+ }
66
+
67
+ pub fn server_key(self) -> Result<&'static str, ContractError> {
68
+ match self.descriptor().tool_names.naming.require("tool naming")? {
69
+ ToolNaming::StaticServer { key } => Ok(key),
70
+ ToolNaming::RuntimeBound => Err(ContractError::Invalid("runtime server-key receipt required")),
71
+ }
72
+ }
73
+
74
+ pub fn adapter(
75
+ self,
76
+ executable: PathBuf,
77
+ arguments: Vec<String>,
78
+ environment: BTreeMap<String, String>,
79
+ ) -> Result<NativeAdapter, ContractError> {
80
+ match self {
81
+ Self::Kiro => KiroAdapter::new(kiro::McpStdio {
82
+ executable, arguments, environment,
83
+ }).map(NativeAdapter::Kiro),
84
+ Self::Cursor => CursorAdapter::new(cursor::McpStdio {
85
+ executable, arguments, environment,
86
+ }).map(NativeAdapter::Cursor),
87
+ }
88
+ }
89
+ }
90
+
91
+ pub(super) enum NativeAdapter {
92
+ Kiro(KiroAdapter),
93
+ Cursor(CursorAdapter),
94
+ }
95
+
96
+ impl NativeAdapter {
97
+ pub fn with_launch_context(
98
+ self,
99
+ project: PathBuf,
100
+ runtime: Option<cursor::native::RuntimeInstallation>,
101
+ ) -> Result<Self, ContractError> {
102
+ match self {
103
+ Self::Kiro(adapter) if runtime.is_none() => Ok(Self::Kiro(adapter)),
104
+ Self::Kiro(_) => Err(ContractError::Mismatch("native runtime provider")),
105
+ Self::Cursor(adapter) => {
106
+ let adapter = adapter.with_project(project)?;
107
+ Ok(Self::Cursor(match runtime {
108
+ Some(runtime) => adapter.with_runtime(runtime),
109
+ None => adapter,
110
+ }))
111
+ }
112
+ }
113
+ }
114
+
115
+ pub fn hooks(&self) -> ProviderHooks<'_> {
116
+ match self {
117
+ Self::Kiro(adapter) => adapter.hooks(),
118
+ Self::Cursor(adapter) => adapter.hooks(),
119
+ }
120
+ }
121
+
122
+ pub fn plan(&self, request: &ResolvedLaunch) -> Result<LaunchPlan, ContractError> {
123
+ match self {
124
+ Self::Kiro(adapter) => adapter.plan(request),
125
+ Self::Cursor(adapter) => adapter.plan(request),
126
+ }
127
+ }
128
+ }
129
+
130
+ pub(super) fn provider(key: &str) -> Result<NativeProvider, ContractError> {
131
+ let descriptor = descriptor(key).ok_or(ContractError::Invalid("unregistered contract provider"))?;
132
+ match descriptor.identity.id {
133
+ "kiro" => Ok(NativeProvider::Kiro),
134
+ "cursor" => Ok(NativeProvider::Cursor),
135
+ _ => Err(ContractError::Invalid("contract provider dispatcher")),
136
+ }
137
+ }
138
+
139
+ pub(crate) fn descriptors() -> &'static [&'static ProviderDescriptor] {
140
+ DESCRIPTORS
141
+ }
142
+
143
+ pub fn descriptor(key: &str) -> Option<&'static ProviderDescriptor> {
144
+ DESCRIPTORS.iter().copied().find(|descriptor| {
145
+ descriptor.identity.id == key || descriptor.identity.aliases.contains(&key)
146
+ })
147
+ }
148
+
149
+ /// Recognize a misspelled/case-modified contract declaration so it can fail
150
+ /// explicitly instead of accidentally selecting a legacy default provider.
151
+ pub fn recognizes(key: &str) -> bool {
152
+ let key = key.trim();
153
+ DESCRIPTORS.iter().any(|descriptor| {
154
+ descriptor.identity.id.eq_ignore_ascii_case(key)
155
+ || descriptor
156
+ .identity
157
+ .aliases
158
+ .iter()
159
+ .any(|alias| alias.eq_ignore_ascii_case(key))
160
+ })
161
+ }
162
+
163
+ #[cfg(test)]
164
+ mod tests {
165
+ use super::*;
166
+
167
+ #[test]
168
+ fn canonical_cursor_does_not_reclassify_legacy_names() {
169
+ assert_eq!(provider("cursor").unwrap(), NativeProvider::Cursor);
170
+ assert_eq!(provider("kiro").unwrap(), NativeProvider::Kiro);
171
+ for legacy in ["cursor_agent", "agent", "pi", "codex"] {
172
+ assert!(descriptor(legacy).is_none());
173
+ assert!(!recognizes(legacy));
174
+ assert!(provider(legacy).is_err());
175
+ }
176
+ assert!(recognizes(" Cursor "));
177
+ assert!(descriptor("Cursor").is_none());
178
+ for descriptor in descriptors() {
179
+ assert_eq!(provider(descriptor.identity.id).unwrap().descriptor(), *descriptor);
180
+ }
181
+ }
182
+
183
+ #[test]
184
+ fn cursor_has_no_kiro_control_or_tool_inspector_fallback() {
185
+ assert!(NativeProvider::Cursor.controls().is_empty());
186
+ assert!(matches!(NativeProvider::Cursor.tool_panel(), Err(ContractError::Unverified { .. })));
187
+ assert_eq!(NativeProvider::Cursor.server_key().unwrap(), "team");
188
+ assert!(NativeProvider::Cursor.first_business_bootstrap());
189
+ assert!(NativeProvider::Cursor.uses_tool_roundtrips());
190
+ assert!(!NativeProvider::Kiro.first_business_bootstrap());
191
+ assert!(!NativeProvider::Kiro.uses_tool_roundtrips());
192
+ let kiro_panel = NativeProvider::Kiro.tool_panel().unwrap();
193
+ assert_eq!(kiro_panel.server_key, NativeProvider::Kiro.server_key().unwrap());
194
+ assert_eq!(kiro_panel.policy_sha256, kiro::interaction::POLICY);
195
+ assert!(!NativeProvider::Kiro.controls().is_empty());
196
+ }
197
+ }