@team-agent/installer 0.5.49 → 0.5.51
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -18,6 +18,28 @@
|
|
|
18
18
|
//! sites can reference the phases by name. The phase fns themselves
|
|
19
19
|
//! remain in launch.rs until the next batch of relocations.
|
|
20
20
|
|
|
21
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
22
|
+
use std::path::{Path, PathBuf};
|
|
23
|
+
use std::process::Command;
|
|
24
|
+
|
|
25
|
+
use crate::lifecycle::*;
|
|
26
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
27
|
+
use crate::model::ids::AgentId;
|
|
28
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
29
|
+
use crate::model::yaml::{self, Value};
|
|
30
|
+
use crate::state::persist::load_runtime_state;
|
|
31
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
32
|
+
|
|
33
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
34
|
+
|
|
35
|
+
use super::approval::detect_dangerous_approval;
|
|
36
|
+
use super::identity::spec_display_backend;
|
|
37
|
+
use super::leader_context::{
|
|
38
|
+
attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
|
|
39
|
+
seed_unbound_launched_owner,
|
|
40
|
+
};
|
|
41
|
+
use super::worker_env::spawn_timestamp;
|
|
42
|
+
|
|
21
43
|
/// Named launch phases under spec_state. Used in phase labels for logs
|
|
22
44
|
/// and (future) for the orchestrator's step dispatcher.
|
|
23
45
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
@@ -49,3 +71,415 @@ mod tests {
|
|
|
49
71
|
.starts_with("launch.spec_state."));
|
|
50
72
|
}
|
|
51
73
|
}
|
|
74
|
+
|
|
75
|
+
pub(super) fn initial_runtime_state(
|
|
76
|
+
spec: &Value,
|
|
77
|
+
spec_path: &Path,
|
|
78
|
+
workspace: &Path,
|
|
79
|
+
team_dir: &Path,
|
|
80
|
+
team_key: &str,
|
|
81
|
+
) -> serde_json::Value {
|
|
82
|
+
let mut agents = serde_json::Map::new();
|
|
83
|
+
for agent in spec_agent_values(spec) {
|
|
84
|
+
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
85
|
+
continue;
|
|
86
|
+
};
|
|
87
|
+
let provider = agent
|
|
88
|
+
.get("provider")
|
|
89
|
+
.and_then(Value::as_str)
|
|
90
|
+
.unwrap_or("codex");
|
|
91
|
+
let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
|
|
92
|
+
let model = agent.get("model").and_then(Value::as_str);
|
|
93
|
+
let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
|
|
94
|
+
let mut value = serde_json::json!({
|
|
95
|
+
"provider": provider,
|
|
96
|
+
"role": role,
|
|
97
|
+
});
|
|
98
|
+
if let Some(obj) = value.as_object_mut() {
|
|
99
|
+
if let Some(model) = model {
|
|
100
|
+
obj.insert("model".to_string(), serde_json::json!(model));
|
|
101
|
+
}
|
|
102
|
+
if let Some(auth_mode) = auth_mode {
|
|
103
|
+
obj.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
agents.insert(id.to_string(), value);
|
|
107
|
+
}
|
|
108
|
+
let display_backend = spec_display_backend(spec);
|
|
109
|
+
let mut state = serde_json::Map::new();
|
|
110
|
+
state.insert(
|
|
111
|
+
"spec_path".to_string(),
|
|
112
|
+
serde_json::json!(spec_path.to_string_lossy().to_string()),
|
|
113
|
+
);
|
|
114
|
+
state.insert(
|
|
115
|
+
"workspace".to_string(),
|
|
116
|
+
serde_json::json!(workspace.to_string_lossy().to_string()),
|
|
117
|
+
);
|
|
118
|
+
state.insert(
|
|
119
|
+
"team_dir".to_string(),
|
|
120
|
+
serde_json::json!(team_dir.to_string_lossy().to_string()),
|
|
121
|
+
);
|
|
122
|
+
state.insert("team_key".to_string(), serde_json::json!(team_key));
|
|
123
|
+
state.insert(
|
|
124
|
+
"session_name".to_string(),
|
|
125
|
+
serde_json::json!(spec_session_name(spec).as_str()),
|
|
126
|
+
);
|
|
127
|
+
state.insert(
|
|
128
|
+
"leader".to_string(),
|
|
129
|
+
spec.get("leader")
|
|
130
|
+
.map(yaml_value_to_json)
|
|
131
|
+
.unwrap_or(serde_json::Value::Null),
|
|
132
|
+
);
|
|
133
|
+
state.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
134
|
+
state.insert("tasks".to_string(), spec_tasks_json(spec));
|
|
135
|
+
state.insert(
|
|
136
|
+
"display_backend".to_string(),
|
|
137
|
+
serde_json::json!(display_backend),
|
|
138
|
+
);
|
|
139
|
+
state.insert("is_external_leader".to_string(), serde_json::json!(false));
|
|
140
|
+
let mut state = serde_json::Value::Object(state);
|
|
141
|
+
if !seed_launched_owner_from_env(&mut state) {
|
|
142
|
+
let team_id = crate::state::projection::team_state_key(&state);
|
|
143
|
+
seed_unbound_launched_owner(&mut state, &team_id);
|
|
144
|
+
}
|
|
145
|
+
state
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub(super) fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
149
|
+
let team_id = crate::state::projection::team_state_key(state);
|
|
150
|
+
let Ok(caller) = crate::state::identity::caller_identity_from_env(
|
|
151
|
+
Some(state),
|
|
152
|
+
&crate::state::identity::SystemEnv,
|
|
153
|
+
Some(&team_id),
|
|
154
|
+
None,
|
|
155
|
+
) else {
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
seed_launched_owner_from_caller_with_provider_lookup(
|
|
159
|
+
state,
|
|
160
|
+
caller,
|
|
161
|
+
attributed_provider_for_pane_across_tmux_sockets,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub(super) fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
166
|
+
state: &mut serde_json::Value,
|
|
167
|
+
caller: crate::state::owner_gate::CallerIdentity,
|
|
168
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
169
|
+
) -> bool {
|
|
170
|
+
if caller.pane_id.is_empty() {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
|
|
174
|
+
let pane_id = caller.pane_id;
|
|
175
|
+
let owner_epoch = 1u64;
|
|
176
|
+
let mut owner = serde_json::json!({
|
|
177
|
+
"pane_id": pane_id,
|
|
178
|
+
"machine_fingerprint": caller.machine_fingerprint,
|
|
179
|
+
"leader_session_uuid": caller.leader_session_uuid,
|
|
180
|
+
"owner_epoch": owner_epoch,
|
|
181
|
+
"claimed_at": spawn_timestamp(),
|
|
182
|
+
"claimed_via": "quick-start",
|
|
183
|
+
"os_user": std::env::var("USER")
|
|
184
|
+
.or_else(|_| std::env::var("USERNAME"))
|
|
185
|
+
.unwrap_or_default(),
|
|
186
|
+
});
|
|
187
|
+
let mut receiver = serde_json::json!({
|
|
188
|
+
"mode": "direct_tmux",
|
|
189
|
+
"status": "attached",
|
|
190
|
+
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
191
|
+
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
192
|
+
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
193
|
+
"owner_epoch": owner_epoch,
|
|
194
|
+
"discovery": "quick_start",
|
|
195
|
+
});
|
|
196
|
+
if let Some(provider) = provider.as_ref() {
|
|
197
|
+
if let Some(owner) = owner.as_object_mut() {
|
|
198
|
+
owner.insert("provider".to_string(), serde_json::json!(provider));
|
|
199
|
+
}
|
|
200
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
201
|
+
receiver.insert("provider".to_string(), serde_json::json!(provider));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if let (Some(receiver), Some(socket)) = (
|
|
205
|
+
receiver.as_object_mut(),
|
|
206
|
+
crate::tmux_backend::socket_name_from_tmux_env(),
|
|
207
|
+
) {
|
|
208
|
+
receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
209
|
+
}
|
|
210
|
+
// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
|
|
211
|
+
// route quick-start attached-from-env seed through ownership repository.
|
|
212
|
+
let team_key = crate::state::projection::team_state_key(state);
|
|
213
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
214
|
+
.with_leader_receiver(receiver)
|
|
215
|
+
.with_team_owner(owner)
|
|
216
|
+
.with_owner_epoch(owner_epoch);
|
|
217
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
218
|
+
true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pub(super) fn has_positive_caller_leader_env() -> bool {
|
|
222
|
+
env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
|
|
223
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
|
|
224
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
|
|
225
|
+
|| env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
pub(super) fn env_nonempty(key: &str) -> bool {
|
|
229
|
+
std::env::var(key)
|
|
230
|
+
.ok()
|
|
231
|
+
.is_some_and(|value| !value.is_empty())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pub(super) fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
235
|
+
spec.get("tasks")
|
|
236
|
+
.and_then(Value::as_list)
|
|
237
|
+
.map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
|
|
238
|
+
.unwrap_or_else(|| serde_json::json!([]))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pub(super) fn yaml_value_to_json(value: &Value) -> serde_json::Value {
|
|
242
|
+
match value {
|
|
243
|
+
Value::Null => serde_json::Value::Null,
|
|
244
|
+
Value::Bool(v) => serde_json::json!(v),
|
|
245
|
+
Value::Int(v) => serde_json::json!(v),
|
|
246
|
+
Value::Float(v) => serde_json::json!(v),
|
|
247
|
+
Value::Str(v) => serde_json::json!(v),
|
|
248
|
+
Value::List(values) => {
|
|
249
|
+
serde_json::Value::Array(values.iter().map(yaml_value_to_json).collect())
|
|
250
|
+
}
|
|
251
|
+
Value::Map(entries) => {
|
|
252
|
+
let mut out = serde_json::Map::new();
|
|
253
|
+
for (key, item) in entries {
|
|
254
|
+
out.insert(key.clone(), yaml_value_to_json(item));
|
|
255
|
+
}
|
|
256
|
+
serde_json::Value::Object(out)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Set `runtime.session_name` on the compiled spec to `session_name`, creating the
|
|
262
|
+
/// `runtime` map and/or the `session_name` entry if absent. Used by quick-start to
|
|
263
|
+
/// derive the tmux session from the REQUESTED team identity (CR-040/042) rather
|
|
264
|
+
/// than the template's compiled-in name.
|
|
265
|
+
/// E5 Bug2(atomic 真修):原子写 runtime spec —— 写 `<spec>.tmp-<pid>` 再 rename 覆盖,
|
|
266
|
+
/// 避免崩溃/并发留下半截 spec(plain fs::write 会 in-place truncate 后逐字节写)。
|
|
267
|
+
/// rename 失败时清理 tmp,原 spec(若有)不动。
|
|
268
|
+
pub(crate) fn write_spec_atomic(spec_path: &Path, spec: &Value) -> Result<(), LifecycleError> {
|
|
269
|
+
if let Some(parent) = spec_path.parent() {
|
|
270
|
+
std::fs::create_dir_all(parent)
|
|
271
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", parent.display())))?;
|
|
272
|
+
}
|
|
273
|
+
let tmp = spec_path.with_extension(format!("tmp-{}", std::process::id()));
|
|
274
|
+
std::fs::write(&tmp, yaml::dumps(spec))
|
|
275
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", tmp.display())))?;
|
|
276
|
+
if let Err(e) = std::fs::rename(&tmp, spec_path) {
|
|
277
|
+
let _ = std::fs::remove_file(&tmp);
|
|
278
|
+
return Err(LifecycleError::StatePersist(format!(
|
|
279
|
+
"{}: {e}",
|
|
280
|
+
spec_path.display()
|
|
281
|
+
)));
|
|
282
|
+
}
|
|
283
|
+
Ok(())
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
287
|
+
override_spec_runtime_str(spec, "session_name", session_name);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pub(crate) fn override_spec_workspace(spec: &mut Value, workspace: &Path) {
|
|
291
|
+
let workspace_s = workspace.to_string_lossy().to_string();
|
|
292
|
+
let Value::Map(root) = spec else { return };
|
|
293
|
+
if let Some((_, Value::Map(team))) = root.iter_mut().find(|(k, _)| k == "team") {
|
|
294
|
+
if let Some((_, value)) = team.iter_mut().find(|(k, _)| k == "workspace") {
|
|
295
|
+
*value = Value::Str(workspace_s.clone());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if let Some((_, Value::List(agents))) = root.iter_mut().find(|(k, _)| k == "agents") {
|
|
299
|
+
for agent in agents {
|
|
300
|
+
if let Value::Map(fields) = agent {
|
|
301
|
+
if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory")
|
|
302
|
+
{
|
|
303
|
+
*value = Value::Str(workspace_s.clone());
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
pub(super) fn override_spec_display_backend(spec: &mut Value, display_backend: &str) {
|
|
311
|
+
override_spec_runtime_str(spec, "display_backend", display_backend);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
pub(super) fn override_spec_runtime_str(spec: &mut Value, key: &str, value: &str) {
|
|
315
|
+
let Value::Map(root) = spec else { return };
|
|
316
|
+
let runtime_slot = root
|
|
317
|
+
.iter_mut()
|
|
318
|
+
.find(|(k, _)| k == "runtime")
|
|
319
|
+
.map(|(_, v)| v);
|
|
320
|
+
match runtime_slot {
|
|
321
|
+
Some(Value::Map(runtime)) => {
|
|
322
|
+
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == key) {
|
|
323
|
+
*existing = Value::Str(value.to_string());
|
|
324
|
+
} else {
|
|
325
|
+
runtime.push((key.to_string(), Value::Str(value.to_string())));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
Some(other) => {
|
|
329
|
+
*other = Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]);
|
|
330
|
+
}
|
|
331
|
+
None => {
|
|
332
|
+
root.push((
|
|
333
|
+
"runtime".to_string(),
|
|
334
|
+
Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]),
|
|
335
|
+
));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
pub(super) fn spec_session_name(spec: &Value) -> SessionName {
|
|
341
|
+
if let Some(name) = spec
|
|
342
|
+
.get("runtime")
|
|
343
|
+
.and_then(|v| v.get("session_name"))
|
|
344
|
+
.and_then(Value::as_str)
|
|
345
|
+
.filter(|name| !name.is_empty())
|
|
346
|
+
{
|
|
347
|
+
return SessionName::new(name);
|
|
348
|
+
}
|
|
349
|
+
// Python launch/core.py:56 — fallback derives from the team name, not a constant.
|
|
350
|
+
let team_name = spec
|
|
351
|
+
.get("team")
|
|
352
|
+
.and_then(|team| team.get("name"))
|
|
353
|
+
.and_then(Value::as_str)
|
|
354
|
+
.filter(|name| !name.is_empty())
|
|
355
|
+
.unwrap_or("agent");
|
|
356
|
+
SessionName::new(format!("team-{team_name}"))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// 0.3.28 layout step 1: pub re-export of `spec_session_name` for the new
|
|
360
|
+
/// `layout::sessions::worker_session_name` to delegate to. Single underlying
|
|
361
|
+
/// impl; this just widens visibility without duplicating logic.
|
|
362
|
+
pub fn worker_session_name_pub(spec: &Value) -> SessionName {
|
|
363
|
+
spec_session_name(spec)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pub(super) fn spec_agents(spec: &Value) -> Vec<AgentId> {
|
|
367
|
+
spec_agent_values(spec)
|
|
368
|
+
.into_iter()
|
|
369
|
+
.filter_map(|agent| agent.get("id").and_then(Value::as_str).map(AgentId::new))
|
|
370
|
+
.collect()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Bug 1 (0.4.2): expose spec agent id set so the restart path can filter
|
|
374
|
+
/// state.agents to only the agents currently defined in the rebuilt spec.
|
|
375
|
+
/// Returns a `BTreeSet<String>` for O(log n) membership checks.
|
|
376
|
+
pub(crate) fn spec_agent_id_set(spec: &Value) -> std::collections::BTreeSet<String> {
|
|
377
|
+
spec_agent_values(spec)
|
|
378
|
+
.into_iter()
|
|
379
|
+
.filter_map(|agent| agent.get("id").and_then(Value::as_str).map(str::to_string))
|
|
380
|
+
.collect()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
pub(super) fn spec_agent_values(spec: &Value) -> Vec<&Value> {
|
|
384
|
+
spec.get("agents")
|
|
385
|
+
.and_then(Value::as_list)
|
|
386
|
+
.map(|agents| agents.iter().collect())
|
|
387
|
+
.unwrap_or_default()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pub(super) fn spec_routes(spec: &Value) -> Vec<RoutingDecision> {
|
|
391
|
+
spec.get("tasks")
|
|
392
|
+
.and_then(Value::as_list)
|
|
393
|
+
.map(|tasks| {
|
|
394
|
+
tasks
|
|
395
|
+
.iter()
|
|
396
|
+
.map(|task| {
|
|
397
|
+
let routed = crate::model::routing::route_task(spec, task);
|
|
398
|
+
RoutingDecision {
|
|
399
|
+
task_id: task.get("id").and_then(Value::as_str).map(str::to_string),
|
|
400
|
+
selected_agent: routed.agent_id,
|
|
401
|
+
reason: routed.reason,
|
|
402
|
+
manual_override: false,
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
.collect()
|
|
406
|
+
})
|
|
407
|
+
.unwrap_or_default()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
pub(super) fn spec_default_assignee(spec: &Value) -> Option<AgentId> {
|
|
411
|
+
spec.get("routing")
|
|
412
|
+
.and_then(|v| v.get("default_assignee"))
|
|
413
|
+
.and_then(Value::as_str)
|
|
414
|
+
.map(AgentId::new)
|
|
415
|
+
.or_else(|| spec_agents(spec).into_iter().next())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
pub(crate) fn effective_runtime_config(spec: &Value) -> Result<DangerousApproval, LifecycleError> {
|
|
419
|
+
let enabled = spec
|
|
420
|
+
.get("runtime")
|
|
421
|
+
.and_then(|v| v.get("dangerous_auto_approve"))
|
|
422
|
+
.is_some_and(Value::is_truthy);
|
|
423
|
+
if enabled {
|
|
424
|
+
let leader = detect_dangerous_approval()?;
|
|
425
|
+
Ok(DangerousApproval {
|
|
426
|
+
enabled: true,
|
|
427
|
+
source: DangerousApprovalSource::RuntimeConfig,
|
|
428
|
+
inherited: false,
|
|
429
|
+
provider: None,
|
|
430
|
+
flag: None,
|
|
431
|
+
worker_capability_above_leader: !leader.enabled,
|
|
432
|
+
ancestry_binary_name: leader.ancestry_binary_name,
|
|
433
|
+
unexpected_binary: false,
|
|
434
|
+
})
|
|
435
|
+
} else {
|
|
436
|
+
Ok(detect_dangerous_approval()?)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
441
|
+
) -> Result<DangerousApproval, LifecycleError> {
|
|
442
|
+
detect_dangerous_approval()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
pub(super) fn write_launch_permission_audit(
|
|
446
|
+
workspace: &Path,
|
|
447
|
+
safety: &DangerousApproval,
|
|
448
|
+
) -> Result<(), LifecycleError> {
|
|
449
|
+
crate::event_log::EventLog::new(workspace)
|
|
450
|
+
.write(
|
|
451
|
+
"launch.permissions_resolved",
|
|
452
|
+
serde_json::json!({
|
|
453
|
+
"dangerous_auto_approve": safety.enabled,
|
|
454
|
+
"dangerous_auto_approve_source": safety.source,
|
|
455
|
+
"dangerous_auto_approve_inherited": safety.inherited,
|
|
456
|
+
"dangerous_auto_approve_provider": safety.provider,
|
|
457
|
+
"dangerous_auto_approve_flag": safety.flag,
|
|
458
|
+
"worker_capability_above_leader": safety.worker_capability_above_leader,
|
|
459
|
+
"ancestry_binary_name": safety.ancestry_binary_name,
|
|
460
|
+
}),
|
|
461
|
+
)
|
|
462
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
463
|
+
if safety.unexpected_binary {
|
|
464
|
+
crate::event_log::EventLog::new(workspace)
|
|
465
|
+
.write(
|
|
466
|
+
"dangerous_flag_in_unexpected_binary",
|
|
467
|
+
serde_json::json!({
|
|
468
|
+
"provider": safety.provider,
|
|
469
|
+
"flag": safety.flag,
|
|
470
|
+
"ancestry_binary_name": safety.ancestry_binary_name,
|
|
471
|
+
}),
|
|
472
|
+
)
|
|
473
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
474
|
+
}
|
|
475
|
+
Ok(())
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
479
|
+
crate::model::paths::team_workspace(team_dir).unwrap_or_else(|_| {
|
|
480
|
+
team_dir
|
|
481
|
+
.parent()
|
|
482
|
+
.map(Path::to_path_buf)
|
|
483
|
+
.unwrap_or_else(|| team_dir.to_path_buf())
|
|
484
|
+
})
|
|
485
|
+
}
|