@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,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
|
+
}
|