@team-agent/installer 0.5.112 → 0.5.114
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 +119 -50
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +173 -109
- 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/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +158 -14
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +35 -29
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +94 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +8 -8
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +65 -26
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -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 +265 -16
- package/crates/team-agent/src/leader/tests/identity.rs +21 -0
- 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/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- 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/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
//! Framework scope and owned native startup. No legacy Provider enum or adapter
|
|
2
|
+
//! is used here. Native admission is resolved before creating the new store.
|
|
3
|
+
use std::collections::BTreeMap;
|
|
4
|
+
use std::io::{Read, Write};
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::time::Duration;
|
|
7
|
+
|
|
8
|
+
use serde::{Deserialize, Serialize};
|
|
9
|
+
use team_agent_contract::contract::{
|
|
10
|
+
descriptor::AuthMode,
|
|
11
|
+
hooks::*,
|
|
12
|
+
plan::*,
|
|
13
|
+
session::{CwdIdentity, ResumeBinding},
|
|
14
|
+
types::*,
|
|
15
|
+
};
|
|
16
|
+
use team_agent_contract::host::{
|
|
17
|
+
clock::RealClock,
|
|
18
|
+
command::*,
|
|
19
|
+
digest, digest_hex,
|
|
20
|
+
materialize::ScopedMaterializer,
|
|
21
|
+
process::{fingerprint_file, resolve_cwd},
|
|
22
|
+
tmux::HostLimits,
|
|
23
|
+
};
|
|
24
|
+
use team_agent_contract::kiro::{KiroAdapter, McpStdio, KIRO_DESCRIPTOR};
|
|
25
|
+
use team_agent_contract::orchestration::{lifecycle::*, physical::*, store::*, Error};
|
|
26
|
+
use thiserror::Error as ThisError;
|
|
27
|
+
|
|
28
|
+
use super::{
|
|
29
|
+
config::{MemberRoute, RoleConfig, RuntimeFamily},
|
|
30
|
+
discovery::{self, Discovery, DiscoveryError},
|
|
31
|
+
forward::FrameworkContext,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, ThisError)]
|
|
35
|
+
pub enum BackendError {
|
|
36
|
+
#[error("native discovery: {0}")]
|
|
37
|
+
Discovery(#[from] DiscoveryError),
|
|
38
|
+
#[error("contract runtime: {0}")]
|
|
39
|
+
Runtime(#[from] Error),
|
|
40
|
+
#[error("native contract: {0:?}")]
|
|
41
|
+
Contract(#[from] ContractError),
|
|
42
|
+
#[error("native host: {0}")]
|
|
43
|
+
Host(#[from] team_agent_contract::host::HostError),
|
|
44
|
+
#[error("native runtime I/O: {0}")]
|
|
45
|
+
Io(#[from] std::io::Error),
|
|
46
|
+
#[error("native runtime metadata is invalid")]
|
|
47
|
+
Metadata,
|
|
48
|
+
#[error("native shutdown operation={operation:?} phase={phase:?} outcome={outcome:?} failure={failure:?}")]
|
|
49
|
+
Shutdown {
|
|
50
|
+
operation: OperationId,
|
|
51
|
+
phase: Phase,
|
|
52
|
+
outcome: Outcome,
|
|
53
|
+
failure: Option<String>,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[derive(Clone, Serialize, Deserialize)]
|
|
58
|
+
#[serde(deny_unknown_fields)]
|
|
59
|
+
pub struct Binding {
|
|
60
|
+
pub version: u32,
|
|
61
|
+
pub workspace: CwdIdentity,
|
|
62
|
+
pub team: String,
|
|
63
|
+
pub scope: ScopeId,
|
|
64
|
+
pub endpoint: String,
|
|
65
|
+
pub candidate: PathBuf,
|
|
66
|
+
pub candidate_sha256: Digest,
|
|
67
|
+
pub tmux: PathBuf,
|
|
68
|
+
pub transport_root: PathBuf,
|
|
69
|
+
}
|
|
70
|
+
impl Binding {
|
|
71
|
+
pub fn framework(&self) -> FrameworkContext {
|
|
72
|
+
FrameworkContext {
|
|
73
|
+
workspace: self.workspace.clone(),
|
|
74
|
+
team: self.team.clone(),
|
|
75
|
+
native_scope: self.scope.clone(),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub struct Backend {
|
|
81
|
+
pub store: ContractStore,
|
|
82
|
+
pub binding: Binding,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn scope_for(workspace: &CwdIdentity, team: &str) -> Result<ScopeId, BackendError> {
|
|
86
|
+
if team.trim().is_empty() || team.chars().any(char::is_control) {
|
|
87
|
+
return Err(BackendError::Metadata);
|
|
88
|
+
}
|
|
89
|
+
let encoded = serde_json::to_vec(&(workspace, team)).map_err(|_| BackendError::Metadata)?;
|
|
90
|
+
Ok(ScopeId::new(format!(
|
|
91
|
+
"contract-{}",
|
|
92
|
+
&digest_hex(digest(&encoded))[..32]
|
|
93
|
+
))?)
|
|
94
|
+
}
|
|
95
|
+
pub fn root_for(workspace: &Path, scope: &ScopeId) -> PathBuf {
|
|
96
|
+
crate::model::paths::runtime_dir(workspace)
|
|
97
|
+
.join("contract")
|
|
98
|
+
.join(scope.as_str())
|
|
99
|
+
}
|
|
100
|
+
fn endpoint(scope: &ScopeId) -> String {
|
|
101
|
+
format!("framework:{}", scope.as_str())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Backend {
|
|
105
|
+
pub fn open(workspace: &Path, team: &str) -> Result<Self, BackendError> {
|
|
106
|
+
Self::open_mode(workspace, team, false)
|
|
107
|
+
}
|
|
108
|
+
pub fn open_readonly(workspace: &Path, team: &str) -> Result<Self, BackendError> {
|
|
109
|
+
Self::open_mode(workspace, team, true)
|
|
110
|
+
}
|
|
111
|
+
fn open_mode(workspace: &Path, team: &str, readonly: bool) -> Result<Self, BackendError> {
|
|
112
|
+
let workspace = resolve_cwd(workspace)?;
|
|
113
|
+
let scope = scope_for(&workspace, team)?;
|
|
114
|
+
let root = root_for(&workspace.path, &scope);
|
|
115
|
+
let file_path = root.join("framework-binding.json");
|
|
116
|
+
let metadata = std::fs::symlink_metadata(&file_path)?;
|
|
117
|
+
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
|
118
|
+
return Err(BackendError::Metadata);
|
|
119
|
+
}
|
|
120
|
+
let mut bytes = Vec::new();
|
|
121
|
+
std::fs::File::open(&file_path)?
|
|
122
|
+
.take(65537)
|
|
123
|
+
.read_to_end(&mut bytes)?;
|
|
124
|
+
if bytes.len() > 65536 {
|
|
125
|
+
return Err(BackendError::Metadata);
|
|
126
|
+
}
|
|
127
|
+
let binding: Binding =
|
|
128
|
+
serde_json::from_slice(&bytes).map_err(|_| BackendError::Metadata)?;
|
|
129
|
+
if binding.version != 1
|
|
130
|
+
|| binding.workspace != workspace
|
|
131
|
+
|| binding.team != team
|
|
132
|
+
|| binding.scope != scope
|
|
133
|
+
|| binding.endpoint != endpoint(&scope)
|
|
134
|
+
{
|
|
135
|
+
return Err(BackendError::Metadata);
|
|
136
|
+
}
|
|
137
|
+
let store = if readonly {
|
|
138
|
+
ContractStore::open_readonly(&binding.transport_root, scope, &binding.endpoint)?
|
|
139
|
+
} else {
|
|
140
|
+
ContractStore::open(&binding.transport_root, scope, &binding.endpoint)?
|
|
141
|
+
};
|
|
142
|
+
Ok(Self { store, binding })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Called only after the caller's pure/native-discovery preflight. Exclusive
|
|
146
|
+
/// roots are not adopted; partial initialization stays visible on failure.
|
|
147
|
+
fn create(
|
|
148
|
+
workspace: CwdIdentity,
|
|
149
|
+
team: &str,
|
|
150
|
+
candidate: PathBuf,
|
|
151
|
+
tmux: PathBuf,
|
|
152
|
+
) -> Result<Self, BackendError> {
|
|
153
|
+
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
|
|
154
|
+
let scope = scope_for(&workspace, team)?;
|
|
155
|
+
let root = root_for(&workspace.path, &scope);
|
|
156
|
+
let parent = root.parent().ok_or(BackendError::Metadata)?;
|
|
157
|
+
let base = parent.parent().ok_or(BackendError::Metadata)?;
|
|
158
|
+
if base.canonicalize()? != base {
|
|
159
|
+
return Err(BackendError::Metadata);
|
|
160
|
+
}
|
|
161
|
+
match std::fs::symlink_metadata(parent) {
|
|
162
|
+
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
|
|
163
|
+
Ok(_) => return Err(BackendError::Metadata),
|
|
164
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
165
|
+
std::fs::DirBuilder::new().mode(0o700).create(parent)?;
|
|
166
|
+
}
|
|
167
|
+
Err(error) => return Err(error.into()),
|
|
168
|
+
}
|
|
169
|
+
let candidate_sha256 =
|
|
170
|
+
fingerprint_file(&candidate, 1024 * 1024 * 1024, Duration::from_secs(10))?;
|
|
171
|
+
std::fs::DirBuilder::new().mode(0o700).create(&root)?;
|
|
172
|
+
// OS entropy gives this transport parent a fresh incarnation even when
|
|
173
|
+
// a logical team/name is reused. This is not a native session identity.
|
|
174
|
+
let mut nonce = [0u8; 32];
|
|
175
|
+
std::fs::File::open("/dev/urandom")?.read_exact(&mut nonce)?;
|
|
176
|
+
let transport_root = Path::new("/tmp")
|
|
177
|
+
.canonicalize()?
|
|
178
|
+
.join(format!("ta-{}", &digest_hex(digest(&nonce))[..24]));
|
|
179
|
+
// K3 and K2 share the same owned short root. Keeping only the pointer
|
|
180
|
+
// under the project avoids macOS sockaddr_un limits without weakening
|
|
181
|
+
// lifecycle's store-root fence or adopting an existing transport.
|
|
182
|
+
let store = ContractStore::create(&transport_root, scope.clone(), &endpoint(&scope))?;
|
|
183
|
+
let binding = Binding {
|
|
184
|
+
version: 1,
|
|
185
|
+
workspace,
|
|
186
|
+
team: team.into(),
|
|
187
|
+
scope: scope.clone(),
|
|
188
|
+
endpoint: endpoint(&scope),
|
|
189
|
+
candidate,
|
|
190
|
+
candidate_sha256,
|
|
191
|
+
tmux,
|
|
192
|
+
transport_root,
|
|
193
|
+
};
|
|
194
|
+
let bytes = serde_json::to_vec(&binding).map_err(|_| BackendError::Metadata)?;
|
|
195
|
+
let mut file = std::fs::OpenOptions::new()
|
|
196
|
+
.write(true)
|
|
197
|
+
.create_new(true)
|
|
198
|
+
.mode(0o600)
|
|
199
|
+
.open(root.join("framework-binding.json"))?;
|
|
200
|
+
file.write_all(&bytes)?;
|
|
201
|
+
file.sync_all()?;
|
|
202
|
+
Ok(Self { store, binding })
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub(super) fn settings(binding: &Binding) -> PhysicalSettings {
|
|
207
|
+
PhysicalSettings {
|
|
208
|
+
tmux: binding.tmux.clone(),
|
|
209
|
+
limits: HostLimits {
|
|
210
|
+
command_output: OutputLimits {
|
|
211
|
+
stdout: 256 * 1024,
|
|
212
|
+
stderr: 65536,
|
|
213
|
+
stdin: 2 * 1024 * 1024,
|
|
214
|
+
},
|
|
215
|
+
max_script_bytes: 128 * 1024,
|
|
216
|
+
max_capture_bytes: 256 * 1024,
|
|
217
|
+
max_payload_bytes: 1024 * 1024,
|
|
218
|
+
max_executable_bytes: 1024 * 1024 * 1024,
|
|
219
|
+
poll_interval: Duration::from_millis(100),
|
|
220
|
+
columns: 120,
|
|
221
|
+
rows: 40,
|
|
222
|
+
},
|
|
223
|
+
action_budget: Duration::from_secs(30),
|
|
224
|
+
freshness: Duration::from_secs(2),
|
|
225
|
+
journal_bytes: 2 * 1024 * 1024,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
pub(super) fn adapter(
|
|
229
|
+
binding: &Binding,
|
|
230
|
+
identity: &InstanceIdentity,
|
|
231
|
+
) -> Result<KiroAdapter, BackendError> {
|
|
232
|
+
Ok(KiroAdapter::new(McpStdio {
|
|
233
|
+
executable: binding.candidate.clone(),
|
|
234
|
+
arguments: vec![
|
|
235
|
+
"contract-mcp".into(),
|
|
236
|
+
"--workspace".into(),
|
|
237
|
+
binding
|
|
238
|
+
.workspace
|
|
239
|
+
.path
|
|
240
|
+
.to_str()
|
|
241
|
+
.ok_or(BackendError::Metadata)?
|
|
242
|
+
.into(),
|
|
243
|
+
"--team".into(),
|
|
244
|
+
binding.team.clone(),
|
|
245
|
+
"--seat".into(),
|
|
246
|
+
identity.seat.as_str().into(),
|
|
247
|
+
"--instance".into(),
|
|
248
|
+
identity.instance.as_str().into(),
|
|
249
|
+
"--generation".into(),
|
|
250
|
+
identity.generation.0.to_string(),
|
|
251
|
+
],
|
|
252
|
+
environment: BTreeMap::new(),
|
|
253
|
+
})?)
|
|
254
|
+
}
|
|
255
|
+
fn request(
|
|
256
|
+
binding: &Binding,
|
|
257
|
+
role: &RoleConfig,
|
|
258
|
+
identity: InstanceIdentity,
|
|
259
|
+
discovered: &Discovery,
|
|
260
|
+
) -> Result<LaunchRequest, BackendError> {
|
|
261
|
+
let profiles = KIRO_DESCRIPTOR
|
|
262
|
+
.input
|
|
263
|
+
.profiles
|
|
264
|
+
.require("Kiro input profiles")?;
|
|
265
|
+
let mut matches = profiles.iter().filter(|profile| {
|
|
266
|
+
profile.matches_native(&discovered.native)
|
|
267
|
+
&& profile.channel == Channel::Tmux
|
|
268
|
+
&& profile.operations.contains(&Operation::FirstBusiness)
|
|
269
|
+
});
|
|
270
|
+
let profile = matches.next().ok_or(ContractError::ProfileUnavailable)?;
|
|
271
|
+
if matches.next().is_some() {
|
|
272
|
+
return Err(ContractError::AmbiguousProfile.into());
|
|
273
|
+
}
|
|
274
|
+
Ok(LaunchRequest {
|
|
275
|
+
provider: role.provider.as_str().into(),
|
|
276
|
+
operation: Operation::Fresh,
|
|
277
|
+
mode: LaunchMode::FullWorker,
|
|
278
|
+
auth: AuthMode::NativeSubscription,
|
|
279
|
+
model: Some(role.model.clone()),
|
|
280
|
+
role_effort: role.effort.clone(),
|
|
281
|
+
team_effort: None,
|
|
282
|
+
bypass: role.bypass,
|
|
283
|
+
prompt: Some(role.prompt.text.clone()),
|
|
284
|
+
identity,
|
|
285
|
+
native: discovered.native.clone(),
|
|
286
|
+
paths: LaunchPaths {
|
|
287
|
+
executable: discovered.engine.clone(),
|
|
288
|
+
candidate: binding.candidate.clone(),
|
|
289
|
+
cwd: binding.workspace.clone(),
|
|
290
|
+
runtime_root: binding.transport_root.clone(),
|
|
291
|
+
},
|
|
292
|
+
channel: Channel::Tmux,
|
|
293
|
+
input_profile: Some(profile.id.into()),
|
|
294
|
+
evidence_kind: EvidenceKind::Native,
|
|
295
|
+
preassigned_session: None,
|
|
296
|
+
resume: None,
|
|
297
|
+
fork: None,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Team-wide native discovery/admission before the shared launcher creates any
|
|
302
|
+
/// seats (including legacy peers). Bounded read-only native commands run; no
|
|
303
|
+
/// runtime store, worker config, private socket, or worker process is created.
|
|
304
|
+
pub fn preflight(team: &super::config::TeamConfig) -> Result<(), BackendError> {
|
|
305
|
+
let discovered = discovery::discover(&team.workspace.path)?;
|
|
306
|
+
let candidate = std::env::current_exe()?.canonicalize()?;
|
|
307
|
+
let candidate_sha256 =
|
|
308
|
+
fingerprint_file(&candidate, 1024 * 1024 * 1024, Duration::from_secs(10))?;
|
|
309
|
+
let binding = Binding {
|
|
310
|
+
version: 1,
|
|
311
|
+
workspace: team.workspace.clone(),
|
|
312
|
+
team: team.selector.clone(),
|
|
313
|
+
scope: team.scope.clone(),
|
|
314
|
+
endpoint: endpoint(&team.scope),
|
|
315
|
+
candidate,
|
|
316
|
+
candidate_sha256,
|
|
317
|
+
tmux: discovery::executable("tmux")?,
|
|
318
|
+
transport_root: "/tmp/preflight-no-write".into(),
|
|
319
|
+
};
|
|
320
|
+
for role in &team.roles {
|
|
321
|
+
let identity = InstanceIdentity {
|
|
322
|
+
scope: team.scope.clone(),
|
|
323
|
+
seat: role.id.clone(),
|
|
324
|
+
instance: InstanceId::new("preflight")?,
|
|
325
|
+
generation: Generation(1),
|
|
326
|
+
};
|
|
327
|
+
let adapter = adapter(&binding, &identity)?;
|
|
328
|
+
let hooks = adapter.hooks();
|
|
329
|
+
let request = request(&binding, role, identity, &discovered)?;
|
|
330
|
+
let resolved = resolve_launch(
|
|
331
|
+
&KIRO_DESCRIPTOR,
|
|
332
|
+
&hooks,
|
|
333
|
+
&request,
|
|
334
|
+
Some(&discovered.catalog),
|
|
335
|
+
)?;
|
|
336
|
+
validate_launch_plan(&KIRO_DESCRIPTOR, &resolved, &adapter.plan(&resolved)?)?;
|
|
337
|
+
}
|
|
338
|
+
Ok(())
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// This first wiring is fresh-only. Resume/fork must use their typed contracts;
|
|
342
|
+
/// missing evidence cannot be bypassed by silently launching another fresh seat.
|
|
343
|
+
pub fn start(
|
|
344
|
+
workspace: &Path,
|
|
345
|
+
team: &str,
|
|
346
|
+
role: &RoleConfig,
|
|
347
|
+
members: &[MemberRoute],
|
|
348
|
+
) -> Result<(Binding, OperationRecord), BackendError> {
|
|
349
|
+
let discovered = discovery::discover(workspace)?;
|
|
350
|
+
let cwd = resolve_cwd(workspace)?;
|
|
351
|
+
let scope = scope_for(&cwd, team)?;
|
|
352
|
+
let candidate = std::env::current_exe()?.canonicalize()?;
|
|
353
|
+
let tmux = discovery::executable("tmux")?;
|
|
354
|
+
let candidate_sha256 =
|
|
355
|
+
fingerprint_file(&candidate, 1024 * 1024 * 1024, Duration::from_secs(10))?;
|
|
356
|
+
let provisional = Binding {
|
|
357
|
+
version: 1,
|
|
358
|
+
workspace: cwd.clone(),
|
|
359
|
+
team: team.into(),
|
|
360
|
+
scope: scope.clone(),
|
|
361
|
+
endpoint: endpoint(&scope),
|
|
362
|
+
candidate: candidate.clone(),
|
|
363
|
+
candidate_sha256,
|
|
364
|
+
tmux: tmux.clone(),
|
|
365
|
+
transport_root: PathBuf::from("/tmp/preflight-no-write"),
|
|
366
|
+
};
|
|
367
|
+
let provisional_identity = InstanceIdentity {
|
|
368
|
+
scope: scope.clone(),
|
|
369
|
+
seat: role.id.clone(),
|
|
370
|
+
instance: InstanceId::new("preflight")?,
|
|
371
|
+
generation: Generation(1),
|
|
372
|
+
};
|
|
373
|
+
let probe_adapter = adapter(&provisional, &provisional_identity)?;
|
|
374
|
+
let probe_hooks = probe_adapter.hooks();
|
|
375
|
+
let preflight = request(&provisional, role, provisional_identity, &discovered)?;
|
|
376
|
+
let preflight = resolve_launch(
|
|
377
|
+
&KIRO_DESCRIPTOR,
|
|
378
|
+
&probe_hooks,
|
|
379
|
+
&preflight,
|
|
380
|
+
Some(&discovered.catalog),
|
|
381
|
+
)?;
|
|
382
|
+
probe_adapter.plan(&preflight)?;
|
|
383
|
+
let mut backend = if root_for(&cwd.path, &scope).exists() {
|
|
384
|
+
Backend::open(&cwd.path, team)?
|
|
385
|
+
} else {
|
|
386
|
+
Backend::create(cwd, team, candidate, tmux)?
|
|
387
|
+
};
|
|
388
|
+
if backend.binding.candidate != provisional.candidate
|
|
389
|
+
|| backend.binding.candidate_sha256 != candidate_sha256
|
|
390
|
+
{
|
|
391
|
+
return Err(Error::Fence.into());
|
|
392
|
+
}
|
|
393
|
+
if backend.store.seat(&role.id)?.is_some() {
|
|
394
|
+
return Err(Error::Conflict.into());
|
|
395
|
+
}
|
|
396
|
+
// A private immutable source snapshot binds model/effort and all four exact
|
|
397
|
+
// instruction layers to this startup. Never print its prompt body.
|
|
398
|
+
use std::os::unix::fs::OpenOptionsExt;
|
|
399
|
+
let snapshot = serde_json::to_vec(role).map_err(|_| BackendError::Metadata)?;
|
|
400
|
+
let mut file = std::fs::OpenOptions::new()
|
|
401
|
+
.write(true)
|
|
402
|
+
.create_new(true)
|
|
403
|
+
.mode(0o600)
|
|
404
|
+
.open(
|
|
405
|
+
root_for(&backend.binding.workspace.path, &backend.binding.scope)
|
|
406
|
+
.join(format!("role-{}.json", role.id.as_str())),
|
|
407
|
+
)?;
|
|
408
|
+
file.write_all(&snapshot)?;
|
|
409
|
+
file.sync_all()?;
|
|
410
|
+
let route = backend.binding.framework().route()?;
|
|
411
|
+
let mut peers: Vec<_> = members
|
|
412
|
+
.iter()
|
|
413
|
+
.filter(|member| member.runtime == RuntimeFamily::Legacy)
|
|
414
|
+
.map(
|
|
415
|
+
|member| team_agent_contract::orchestration::forward::FrameworkPeer {
|
|
416
|
+
recipient: member.id.clone(),
|
|
417
|
+
route: route.clone(),
|
|
418
|
+
provider: member.provider.clone(),
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
.collect();
|
|
422
|
+
peers.push(team_agent_contract::orchestration::forward::FrameworkPeer {
|
|
423
|
+
recipient: "leader".into(),
|
|
424
|
+
route: route.clone(),
|
|
425
|
+
provider: "framework".into(),
|
|
426
|
+
});
|
|
427
|
+
backend.store.set_framework_routes(&peers, Some(&route))?;
|
|
428
|
+
let identity = backend.store.propose_identity(&role.id)?;
|
|
429
|
+
let adapter = adapter(&backend.binding, &identity)?;
|
|
430
|
+
let hooks = adapter.hooks();
|
|
431
|
+
let request = request(&backend.binding, role, identity, &discovered)?;
|
|
432
|
+
let resolved = resolve_launch(
|
|
433
|
+
&KIRO_DESCRIPTOR,
|
|
434
|
+
&hooks,
|
|
435
|
+
&request,
|
|
436
|
+
Some(&discovered.catalog),
|
|
437
|
+
)?;
|
|
438
|
+
let plan = adapter.plan(&resolved)?;
|
|
439
|
+
let operation = OperationId::new(format!("start-{}", request.identity.instance.as_str()))?;
|
|
440
|
+
let mut io = ScopedMaterializer::new(
|
|
441
|
+
&KIRO_DESCRIPTOR,
|
|
442
|
+
&resolved,
|
|
443
|
+
&plan,
|
|
444
|
+
operation.clone(),
|
|
445
|
+
RealCommandRunner::default(),
|
|
446
|
+
)?;
|
|
447
|
+
let clock = RealClock::new();
|
|
448
|
+
let mut physical = PhysicalRuntime::new(
|
|
449
|
+
Registration {
|
|
450
|
+
descriptor: &KIRO_DESCRIPTOR,
|
|
451
|
+
hooks: &hooks,
|
|
452
|
+
controls: team_agent_contract::kiro::interaction::CONTROLS,
|
|
453
|
+
},
|
|
454
|
+
settings(&backend.binding),
|
|
455
|
+
operation.clone(),
|
|
456
|
+
&clock,
|
|
457
|
+
)?;
|
|
458
|
+
physical.evidence = policy_evidence(&request.native, candidate_sha256);
|
|
459
|
+
let record = Lifecycle {
|
|
460
|
+
store: &mut backend.store,
|
|
461
|
+
host: &mut physical,
|
|
462
|
+
io: &mut io,
|
|
463
|
+
}
|
|
464
|
+
.startup(
|
|
465
|
+
&Adapter {
|
|
466
|
+
descriptor: &KIRO_DESCRIPTOR,
|
|
467
|
+
hooks: &hooks,
|
|
468
|
+
catalog: Some(&discovered.catalog),
|
|
469
|
+
},
|
|
470
|
+
&request,
|
|
471
|
+
Routing {
|
|
472
|
+
pane: "unpublished".into(),
|
|
473
|
+
binding_key: format!("binding-{}", request.identity.instance.as_str()),
|
|
474
|
+
server_key: "team".into(),
|
|
475
|
+
},
|
|
476
|
+
operation,
|
|
477
|
+
)?;
|
|
478
|
+
Ok((backend.binding, record))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
pub(super) fn read_role(binding: &Binding, id: &SeatId) -> Result<RoleConfig, BackendError> {
|
|
482
|
+
let path = root_for(&binding.workspace.path, &binding.scope)
|
|
483
|
+
.join(format!("role-{}.json", id.as_str()));
|
|
484
|
+
let metadata = std::fs::symlink_metadata(&path)?;
|
|
485
|
+
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
|
486
|
+
return Err(BackendError::Metadata);
|
|
487
|
+
}
|
|
488
|
+
let mut bytes = Vec::new();
|
|
489
|
+
std::fs::File::open(path)?
|
|
490
|
+
.take(4 * 1024 * 1024 + 1)
|
|
491
|
+
.read_to_end(&mut bytes)?;
|
|
492
|
+
if bytes.len() > 4 * 1024 * 1024 {
|
|
493
|
+
return Err(BackendError::Metadata);
|
|
494
|
+
}
|
|
495
|
+
let role: RoleConfig = serde_json::from_slice(&bytes).map_err(|_| BackendError::Metadata)?;
|
|
496
|
+
if &role.id != id
|
|
497
|
+
|| role.provider.as_str() != "kiro"
|
|
498
|
+
|| digest(role.prompt.text.as_bytes()) != role.prompt.sha256
|
|
499
|
+
{
|
|
500
|
+
return Err(BackendError::Metadata);
|
|
501
|
+
}
|
|
502
|
+
Ok(role)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
pub(super) fn policy_evidence(
|
|
506
|
+
native: &NativeIdentity,
|
|
507
|
+
candidate_sha256: Digest,
|
|
508
|
+
) -> Vec<CapabilityEvidence> {
|
|
509
|
+
team_agent_contract::kiro::interaction::PROFILES
|
|
510
|
+
.iter()
|
|
511
|
+
.filter(|profile| profile.matches_native(native))
|
|
512
|
+
.flat_map(|profile| {
|
|
513
|
+
// Bind this observed executable and candidate to the stable recipe.
|
|
514
|
+
// This is not the historical R0 receipt, nor a business/MCP PASS.
|
|
515
|
+
let binding_sha256 = digest(
|
|
516
|
+
format!(
|
|
517
|
+
"kiro-runtime-policy-v1\n{native:?}\n{}\n{:?}\n{candidate_sha256:?}",
|
|
518
|
+
profile.id, profile.policy_sha256,
|
|
519
|
+
)
|
|
520
|
+
.as_bytes(),
|
|
521
|
+
);
|
|
522
|
+
profile
|
|
523
|
+
.operations
|
|
524
|
+
.iter()
|
|
525
|
+
.map(move |operation| CapabilityEvidence {
|
|
526
|
+
provider: ProviderId::new("kiro").expect("static provider"),
|
|
527
|
+
native: native.clone(),
|
|
528
|
+
profile_id: profile.id.into(),
|
|
529
|
+
policy_sha256: profile.policy_sha256,
|
|
530
|
+
channel: profile.channel,
|
|
531
|
+
operation: *operation,
|
|
532
|
+
kind: EvidenceKind::Native,
|
|
533
|
+
candidate_sha256,
|
|
534
|
+
evidence_sha256: binding_sha256,
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
.collect()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
struct NoNewIo;
|
|
541
|
+
impl OwnedIo for NoNewIo {
|
|
542
|
+
fn create_exclusive(
|
|
543
|
+
&mut self,
|
|
544
|
+
_: &OwnedResourceRequest,
|
|
545
|
+
) -> Result<OwnedResourceReceipt, PartialFailure> {
|
|
546
|
+
Err(PartialFailure {
|
|
547
|
+
error: ContractError::Invalid("teardown cannot materialize"),
|
|
548
|
+
receipt: MaterializeReceipt {
|
|
549
|
+
resources: Vec::new(),
|
|
550
|
+
},
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
fn read_bound_session(
|
|
554
|
+
&mut self,
|
|
555
|
+
_: &ResumeBinding,
|
|
556
|
+
_: ReadBounds,
|
|
557
|
+
) -> Result<ReadOutput, ReadFailure> {
|
|
558
|
+
Err(ReadFailure::Error(ContractError::Invalid(
|
|
559
|
+
"teardown cannot read session backing",
|
|
560
|
+
)))
|
|
561
|
+
}
|
|
562
|
+
fn validate_configuration(
|
|
563
|
+
&mut self,
|
|
564
|
+
_: &OwnedValidationRequest,
|
|
565
|
+
) -> Result<ReadOutput, ReadFailure> {
|
|
566
|
+
Err(ReadFailure::Error(ContractError::Invalid(
|
|
567
|
+
"teardown cannot run a validator",
|
|
568
|
+
)))
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
pub fn stop(workspace: &Path, team: &str, seat: &SeatId) -> Result<OperationRecord, BackendError> {
|
|
573
|
+
let mut backend = Backend::open(workspace, team)?;
|
|
574
|
+
let target = backend
|
|
575
|
+
.store
|
|
576
|
+
.seat(seat)?
|
|
577
|
+
.ok_or(Error::Invalid("unknown native seat"))?;
|
|
578
|
+
let nonce = backend.store.propose_identity(seat)?.instance;
|
|
579
|
+
let operation = OperationId::new(format!("stop-{}", nonce.as_str()))?;
|
|
580
|
+
let adapter = adapter(&backend.binding, &target.identity)?;
|
|
581
|
+
let hooks = adapter.hooks();
|
|
582
|
+
let clock = RealClock::new();
|
|
583
|
+
let mut physical = PhysicalRuntime::new(
|
|
584
|
+
Registration {
|
|
585
|
+
descriptor: &KIRO_DESCRIPTOR,
|
|
586
|
+
hooks: &hooks,
|
|
587
|
+
controls: &[],
|
|
588
|
+
},
|
|
589
|
+
settings(&backend.binding),
|
|
590
|
+
operation.clone(),
|
|
591
|
+
&clock,
|
|
592
|
+
)?;
|
|
593
|
+
Ok(Lifecycle {
|
|
594
|
+
store: &mut backend.store,
|
|
595
|
+
host: &mut physical,
|
|
596
|
+
io: &mut NoNewIo,
|
|
597
|
+
}
|
|
598
|
+
.teardown(&target.identity, operation)?)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[cfg(test)]
|
|
602
|
+
mod tests {
|
|
603
|
+
use super::*;
|
|
604
|
+
|
|
605
|
+
#[test]
|
|
606
|
+
fn shutdown_error_exposes_the_durable_operation_failure_not_metadata() {
|
|
607
|
+
let failure = "native instance not alive or replaced: Unknown; stage=check-bound; expected_pid=44123; process_state=Some(Replaced)";
|
|
608
|
+
let error = BackendError::Shutdown {
|
|
609
|
+
operation: OperationId::new("stop-instance-test").unwrap(),
|
|
610
|
+
phase: Phase::F0Preflight,
|
|
611
|
+
outcome: Outcome::NeedsRecovery,
|
|
612
|
+
failure: Some(failure.into()),
|
|
613
|
+
};
|
|
614
|
+
let shown = error.to_string();
|
|
615
|
+
for expected in ["stop-instance-test", "F0Preflight", "NeedsRecovery", failure] {
|
|
616
|
+
assert!(shown.contains(expected), "{shown}");
|
|
617
|
+
}
|
|
618
|
+
assert!(!shown.contains("metadata is invalid"));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[test]
|
|
622
|
+
fn policy_bindings_track_runtime_native_identity_not_a_release_receipt() {
|
|
623
|
+
let mut native = NativeIdentity {
|
|
624
|
+
version: "2.28.0".into(),
|
|
625
|
+
harness: "v2".into(),
|
|
626
|
+
ui: "tui".into(),
|
|
627
|
+
platform: Platform::MacOs,
|
|
628
|
+
executable_sha256: digest(b"old image"),
|
|
629
|
+
};
|
|
630
|
+
let candidate = digest(b"candidate");
|
|
631
|
+
let old = policy_evidence(&native, candidate);
|
|
632
|
+
assert!(!old.is_empty());
|
|
633
|
+
native.version = "2.29.0".into();
|
|
634
|
+
native.executable_sha256 = digest(b"updated image");
|
|
635
|
+
let new = policy_evidence(&native, candidate);
|
|
636
|
+
assert_eq!(new.len(), old.len());
|
|
637
|
+
for (old, new) in old.iter().zip(&new) {
|
|
638
|
+
assert_eq!(new.native, native);
|
|
639
|
+
assert_eq!(new.candidate_sha256, candidate);
|
|
640
|
+
assert_eq!(new.policy_sha256, old.policy_sha256);
|
|
641
|
+
assert_ne!(new.evidence_sha256, old.evidence_sha256);
|
|
642
|
+
}
|
|
643
|
+
native.harness = "v3".into();
|
|
644
|
+
assert!(policy_evidence(&native, candidate).is_empty());
|
|
645
|
+
}
|
|
646
|
+
}
|