@team-agent/installer 0.5.111 → 0.5.113
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 +93 -779
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +158 -373
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +8 -15
- 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 +29 -32
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/tests/base.rs +6 -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/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +12 -116
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +0 -9
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/start.rs +5 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +21 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
- package/crates/team-agent/src/lifecycle/launch.rs +2 -2
- 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/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- 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/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//!
|
|
2
2
|
//! `team.db` schema 初始化(真相源 `message_store/schema.py` + `schema_migration.py`)。
|
|
3
3
|
//!
|
|
4
|
-
//! slice 1:fresh DB schema ——
|
|
4
|
+
//! slice 1:fresh DB schema —— 7 表 DDL、6 索引、ALTER 式列迁移、agent_health 重建、
|
|
5
5
|
//! WAL/busy_timeout pragmas、`user_version = SCHEMA_VERSION`。
|
|
6
6
|
//! slice 2(待做):列**顺序**漂移的整表 rebuild(`ensure_table_layout`/`_rebuild_tables`)
|
|
7
7
|
//! 与 `schema_diagnosis`,验 legacy_team_db_fixture + schema_migration 契约。
|
|
@@ -15,14 +15,13 @@ use crate::db::DbError;
|
|
|
15
15
|
/// `schema.py:90`。
|
|
16
16
|
pub const SCHEMA_VERSION: i64 = 5;
|
|
17
17
|
|
|
18
|
-
///
|
|
18
|
+
/// 7 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
|
|
19
19
|
/// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
|
|
20
20
|
const CREATE_MESSAGES: &str = "create table if not exists messages (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}',\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )";
|
|
21
21
|
const CREATE_RESULTS: &str = "create table if not exists results (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )";
|
|
22
22
|
const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_events (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )";
|
|
23
23
|
const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
|
|
24
24
|
const CREATE_AGENT_HEALTH: &str = "create table if not exists agent_health (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
25
|
-
const CREATE_PEER_ALLOWLIST: &str = "create table if not exists peer_allowlist (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )";
|
|
26
25
|
const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
|
|
27
26
|
const CREATE_LEADER_NOTIFICATION_LOG: &str = "create table if not exists leader_notification_log (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )";
|
|
28
27
|
const CREATE_AGENT_HEALTH_NEW: &str = "create table agent_health_new (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
@@ -96,7 +95,6 @@ const AGENT_HEALTH_COLUMNS: &[&str] = &[
|
|
|
96
95
|
"current_task_id",
|
|
97
96
|
"updated_at",
|
|
98
97
|
];
|
|
99
|
-
const PEER_ALLOWLIST_COLUMNS: &[&str] = &["a", "b", "created_at"];
|
|
100
98
|
const RESULT_WATCHER_COLUMNS: &[&str] = &[
|
|
101
99
|
"owner_team_id",
|
|
102
100
|
"watcher_id",
|
|
@@ -249,7 +247,6 @@ pub fn initialize_schema(
|
|
|
249
247
|
CREATE_SCHEDULED_EVENTS,
|
|
250
248
|
CREATE_DELIVERY_TOKENS,
|
|
251
249
|
CREATE_AGENT_HEALTH,
|
|
252
|
-
CREATE_PEER_ALLOWLIST,
|
|
253
250
|
CREATE_RESULT_WATCHERS,
|
|
254
251
|
] {
|
|
255
252
|
tx.execute(ddl, [])?;
|
|
@@ -293,7 +290,6 @@ pub fn initialize_schema(
|
|
|
293
290
|
)?;
|
|
294
291
|
ensure_table_columns(&tx, "delivery_tokens", DELIVERY_TOKEN_COLUMNS, &[])?;
|
|
295
292
|
migrate_agent_health_owner_team_id(&tx)?;
|
|
296
|
-
ensure_table_columns(&tx, "peer_allowlist", PEER_ALLOWLIST_COLUMNS, &[])?;
|
|
297
293
|
ensure_table_columns(
|
|
298
294
|
&tx,
|
|
299
295
|
"result_watchers",
|
|
@@ -333,7 +329,28 @@ mod tests {
|
|
|
333
329
|
conn
|
|
334
330
|
}
|
|
335
331
|
|
|
336
|
-
|
|
332
|
+
#[test]
|
|
333
|
+
fn retired_peer_allowlist_is_neither_created_nor_managed() {
|
|
334
|
+
let conn = fresh();
|
|
335
|
+
assert!(table_layout(&conn, "peer_allowlist").unwrap().is_empty());
|
|
336
|
+
// Legacy residue may have an obsolete layout. Initialization must leave
|
|
337
|
+
// its data alone, not rebuild/drop it or require it for schema health.
|
|
338
|
+
conn.execute_batch(
|
|
339
|
+
"create table peer_allowlist (legacy_payload text);\n insert into peer_allowlist values ('preserve-this-row');",
|
|
340
|
+
)
|
|
341
|
+
.unwrap();
|
|
342
|
+
initialize_schema(&conn, None).unwrap();
|
|
343
|
+
assert!(crate::db::migration::ensure_table_layout(&conn, SCHEMA_VERSION, None)
|
|
344
|
+
.unwrap()
|
|
345
|
+
.is_empty());
|
|
346
|
+
assert_eq!(table_layout(&conn, "peer_allowlist").unwrap(), ["legacy_payload"]);
|
|
347
|
+
let value: String = conn.query_row("select legacy_payload from peer_allowlist", [], |row| row.get(0)).unwrap();
|
|
348
|
+
assert_eq!(value, "preserve-this-row");
|
|
349
|
+
let version: i64 = conn.query_row("pragma user_version", [], |row| row.get(0)).unwrap();
|
|
350
|
+
assert_eq!(version, 5);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fresh managed schema, excluding retired legacy tables.
|
|
337
354
|
#[test]
|
|
338
355
|
fn fresh_schema_matches_python_sqlite_master() {
|
|
339
356
|
let conn = fresh();
|
|
@@ -356,7 +373,6 @@ mod tests {
|
|
|
356
373
|
("idx_scheduled_events_owner_team_id", "index"),
|
|
357
374
|
("leader_notification_log", "table"),
|
|
358
375
|
("messages", "table"),
|
|
359
|
-
("peer_allowlist", "table"),
|
|
360
376
|
("result_watchers", "table"),
|
|
361
377
|
("results", "table"),
|
|
362
378
|
("scheduled_events", "table"),
|
|
@@ -419,9 +435,9 @@ mod tests {
|
|
|
419
435
|
assert_eq!(idx, 6, "二次 init 后 6 个 named index 仍在");
|
|
420
436
|
}
|
|
421
437
|
|
|
422
|
-
//
|
|
438
|
+
// Managed tables: (name, type, notnull, dflt, pk).
|
|
423
439
|
#[test]
|
|
424
|
-
fn
|
|
440
|
+
fn per_column_table_info_managed_tables() {
|
|
425
441
|
type Col = (&'static str, &'static str, i64, Option<&'static str>, i64);
|
|
426
442
|
let golden: &[(&str, &[Col])] = &[
|
|
427
443
|
(
|
|
@@ -503,14 +519,6 @@ mod tests {
|
|
|
503
519
|
("updated_at", "TEXT", 1, None, 0),
|
|
504
520
|
],
|
|
505
521
|
),
|
|
506
|
-
(
|
|
507
|
-
"peer_allowlist",
|
|
508
|
-
&[
|
|
509
|
-
("a", "TEXT", 1, None, 1),
|
|
510
|
-
("b", "TEXT", 1, None, 2),
|
|
511
|
-
("created_at", "TEXT", 1, None, 0),
|
|
512
|
-
],
|
|
513
|
-
),
|
|
514
522
|
(
|
|
515
523
|
"result_watchers",
|
|
516
524
|
&[
|
|
@@ -582,7 +590,7 @@ mod tests {
|
|
|
582
590
|
];
|
|
583
591
|
let got: Vec<(&str, &str)> = idx.iter().map(|(n, s)| (n.as_str(), s.as_str())).collect();
|
|
584
592
|
assert_eq!(got, want);
|
|
585
|
-
//
|
|
593
|
+
// 6 个 sqlite_autoindex(每个有 pk/unique 约束的表各一)。
|
|
586
594
|
let auto: Vec<String> = conn
|
|
587
595
|
.prepare(
|
|
588
596
|
"select name from sqlite_master where type='index' and sql is null order by name",
|
|
@@ -599,7 +607,6 @@ mod tests {
|
|
|
599
607
|
"sqlite_autoindex_delivery_tokens_1",
|
|
600
608
|
"sqlite_autoindex_leader_notification_log_1",
|
|
601
609
|
"sqlite_autoindex_messages_1",
|
|
602
|
-
"sqlite_autoindex_peer_allowlist_1",
|
|
603
610
|
"sqlite_autoindex_result_watchers_1",
|
|
604
611
|
"sqlite_autoindex_results_1",
|
|
605
612
|
]
|
|
@@ -17,30 +17,6 @@ use super::{
|
|
|
17
17
|
OwnerBindResult, TeamOwner,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
// ── leader::identity — leader_identity / 身份上下文 ──
|
|
21
|
-
|
|
22
|
-
/// `leader_identity`(card §47;`__init__.py:355`)。`team-agent identity` 入口。
|
|
23
|
-
/// 返回 uuid_prefix + 身份字段(JSON dict,CLI 直出)。
|
|
24
|
-
pub fn leader_identity(workspace: &Path, team: Option<&str>) -> Result<Value, LeaderError> {
|
|
25
|
-
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
26
|
-
let identity = leader_identity_context(workspace, team, Some(&state))?;
|
|
27
|
-
Ok(json!({
|
|
28
|
-
"ok": true,
|
|
29
|
-
"uuid_prefix": prefix(identity.leader_session_uuid.as_str(), 12),
|
|
30
|
-
"machine_fingerprint": identity.machine_fingerprint,
|
|
31
|
-
"workspace_abspath": identity.workspace_abspath.to_string_lossy(),
|
|
32
|
-
"os_user": identity.os_user,
|
|
33
|
-
"team_id": identity.team_id.as_str(),
|
|
34
|
-
"current_pane_id": std::env::var("TEAM_AGENT_LEADER_PANE_ID")
|
|
35
|
-
.ok()
|
|
36
|
-
.filter(|v| !v.is_empty())
|
|
37
|
-
.or_else(|| std::env::var("TMUX_PANE").ok().filter(|v| !v.is_empty())),
|
|
38
|
-
"last_seen_at": get_path_str(&state, &["leader_receiver", "attached_at"])
|
|
39
|
-
.or_else(|| get_path_str(&state, &["leader_receiver", "last_seen_at"])),
|
|
40
|
-
"source": serde_json::to_value(identity.leader_session_uuid_source)?,
|
|
41
|
-
}))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
20
|
/// `_leader_identity_context`(`__init__.py:192`)。派生 leader 身份上下文(override / state / derive)。
|
|
45
21
|
pub fn leader_identity_context(
|
|
46
22
|
workspace: &Path,
|
|
@@ -403,6 +403,11 @@ pub(crate) fn leader_env_for_identity(
|
|
|
403
403
|
"1".to_string(),
|
|
404
404
|
);
|
|
405
405
|
}
|
|
406
|
+
// Managed providers run after `sh -lc`; restore the launcher's native
|
|
407
|
+
// executable lookup path at invocation, not just in the tmux client env.
|
|
408
|
+
if let Some(path) = std::env::var_os("PATH").and_then(|path| path.into_string().ok()) {
|
|
409
|
+
leader_env.insert("PATH".to_string(), path);
|
|
410
|
+
}
|
|
406
411
|
leader_env
|
|
407
412
|
}
|
|
408
413
|
|
|
@@ -353,23 +353,3 @@ fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
|
|
|
353
353
|
"non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4"
|
|
354
354
|
);
|
|
355
355
|
}
|
|
356
|
-
|
|
357
|
-
// C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
|
|
358
|
-
// :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
|
|
359
|
-
// TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
|
|
360
|
-
// the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
|
|
361
|
-
// Rust reads attached_at only.)
|
|
362
|
-
#[test]
|
|
363
|
-
#[serial_test::serial(env)]
|
|
364
|
-
fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
|
|
365
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
366
|
-
let _e = EnvGuard::apply(&[
|
|
367
|
-
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
368
|
-
("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
|
|
369
|
-
("TMUX_PANE", None),
|
|
370
|
-
]);
|
|
371
|
-
let ws = p2_temp_ws("c3_pane_env");
|
|
372
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
373
|
-
assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
|
|
374
|
-
"golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
|
|
375
|
-
}
|
|
@@ -23,38 +23,6 @@ fn leader_identity_context_derives_when_no_override_no_state() {
|
|
|
23
23
|
assert_eq!(id.leader_session_uuid.as_str().len(), 32);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
|
|
27
|
-
// 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
|
|
28
|
-
// 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
|
|
29
|
-
#[test]
|
|
30
|
-
#[serial_test::serial(env)]
|
|
31
|
-
fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
|
|
32
|
-
let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
|
|
33
|
-
std::fs::create_dir_all(&ws).unwrap();
|
|
34
|
-
// 空 state → context 走 derive(无 override/无 state uuid)。
|
|
35
|
-
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
36
|
-
let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
|
|
37
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
38
|
-
assert_eq!(v["ok"], serde_json::json!(true));
|
|
39
|
-
// uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
|
|
40
|
-
assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
|
|
41
|
-
// 其余身份字段与 context 字节一致。
|
|
42
|
-
assert_eq!(
|
|
43
|
-
v["machine_fingerprint"].as_str().unwrap(),
|
|
44
|
-
ctx.machine_fingerprint
|
|
45
|
-
);
|
|
46
|
-
assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
|
|
47
|
-
assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
|
|
48
|
-
// source == 派生侧 "derived"(无 override → 不是 "override"/"env")。
|
|
49
|
-
assert_eq!(v["source"], serde_json::json!("derived"));
|
|
50
|
-
assert_eq!(
|
|
51
|
-
ctx.leader_session_uuid_source,
|
|
52
|
-
LeaderSessionUuidSource::Derived
|
|
53
|
-
);
|
|
54
|
-
// CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
|
|
55
|
-
assert_eq!(v["last_seen_at"], serde_json::Value::Null);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
26
|
// =====================================================================
|
|
59
27
|
// 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
|
|
60
28
|
// =====================================================================
|
|
@@ -323,6 +291,27 @@ fn copilot_leader_env_disables_terminal_title_only_for_copilot() {
|
|
|
323
291
|
}
|
|
324
292
|
}
|
|
325
293
|
|
|
294
|
+
#[test]
|
|
295
|
+
#[serial_test::serial(env)]
|
|
296
|
+
fn leader_env_preserves_native_path_for_managed_shell_wrapper() {
|
|
297
|
+
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
298
|
+
let native_path = "/tmp/native leader/bin:/usr/bin:/bin";
|
|
299
|
+
let _env = EnvGuard::apply(&[("PATH", Some(native_path))]);
|
|
300
|
+
let ws = p2_temp_ws("native_path");
|
|
301
|
+
let identity = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
302
|
+
let env = leader_env_for_identity(Provider::Pi, &identity);
|
|
303
|
+
assert_eq!(env.get("PATH").map(String::as_str), Some(native_path));
|
|
304
|
+
let command = crate::tmux_backend::leader_shell_wrapper_command(
|
|
305
|
+
&["pi".to_string(), "--name".to_string(), "leader".to_string()],
|
|
306
|
+
&ws, &env, &[], "pi",
|
|
307
|
+
);
|
|
308
|
+
assert!(command.contains("PATH='/tmp/native leader/bin:/usr/bin:/bin'"));
|
|
309
|
+
assert!(command.find("PATH=").unwrap() < command.find(" pi --name leader").unwrap());
|
|
310
|
+
assert_eq!(env.get("TEAM_AGENT_TEAM_ID").map(String::as_str), Some(identity.team_id.as_str()));
|
|
311
|
+
let _unset = EnvGuard::apply(&[("PATH", None)]);
|
|
312
|
+
assert!(!leader_env_for_identity(Provider::Pi, &identity).contains_key("PATH"));
|
|
313
|
+
}
|
|
314
|
+
|
|
326
315
|
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
327
316
|
// Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
|
|
328
317
|
// Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
|
|
@@ -398,61 +398,6 @@ fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
|
|
|
398
398
|
assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
// ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
|
|
402
|
-
//
|
|
403
|
-
// LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
|
|
404
|
-
// OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
|
|
405
|
-
// machine_fingerprint and os_user). This pins the missing key so the porter
|
|
406
|
-
// must add it. Fails today (key absent → null) = RED against the omission.
|
|
407
|
-
#[test]
|
|
408
|
-
#[serial_test::serial(env)]
|
|
409
|
-
fn leader_identity_dict_includes_workspace_abspath_key() {
|
|
410
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
411
|
-
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
412
|
-
let ws = p2_temp_ws("lid_wsabs");
|
|
413
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
414
|
-
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
415
|
-
// golden key present + equals the resolved workspace abspath from context.
|
|
416
|
-
assert!(
|
|
417
|
-
v.get("workspace_abspath").is_some(),
|
|
418
|
-
"golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
|
|
419
|
-
);
|
|
420
|
-
assert_eq!(
|
|
421
|
-
v["workspace_abspath"].as_str().unwrap(),
|
|
422
|
-
ctx.workspace_abspath.to_string_lossy(),
|
|
423
|
-
"workspace_abspath must equal the context's resolved abspath"
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
|
|
428
|
-
// [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
|
|
429
|
-
// current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
|
|
430
|
-
#[test]
|
|
431
|
-
#[serial_test::serial(env)]
|
|
432
|
-
fn leader_identity_dict_has_exact_nine_golden_keys() {
|
|
433
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
434
|
-
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
435
|
-
let ws = p2_temp_ws("lid_keys");
|
|
436
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
437
|
-
let obj = v.as_object().expect("leader_identity → JSON object");
|
|
438
|
-
for key in [
|
|
439
|
-
"ok",
|
|
440
|
-
"uuid_prefix",
|
|
441
|
-
"machine_fingerprint",
|
|
442
|
-
"workspace_abspath",
|
|
443
|
-
"os_user",
|
|
444
|
-
"team_id",
|
|
445
|
-
"current_pane_id",
|
|
446
|
-
"last_seen_at",
|
|
447
|
-
"source",
|
|
448
|
-
] {
|
|
449
|
-
assert!(
|
|
450
|
-
obj.contains_key(key),
|
|
451
|
-
"golden leader_identity dict must carry '{key}'"
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
401
|
// ═════════════════════════════════════════════════════════════════════════
|
|
457
402
|
// WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
|
|
458
403
|
// The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
|
|
@@ -476,26 +476,62 @@ pub(super) fn annotate_persisted_team_depth(
|
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
/// ---
|
|
479
|
-
/// purpose:
|
|
480
|
-
/// returns:
|
|
479
|
+
/// purpose: 判断目标团队是否已有 worker runtime,已证明的 Leader-only 登记不算初始化
|
|
480
|
+
/// returns: 未初始化的有效 Leader-only 登记为 false;已有或无法分类的目标 runtime 为 true
|
|
481
481
|
/// ---
|
|
482
482
|
pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
483
|
+
let entry = state
|
|
484
|
+
.get("teams")
|
|
485
|
+
.and_then(serde_json::Value::as_object)
|
|
486
|
+
.and_then(|teams| teams.get(team));
|
|
487
|
+
if let Some(entry) = entry {
|
|
488
|
+
return !is_quick_start_leader_bootstrap(entry);
|
|
489
|
+
}
|
|
490
|
+
// Legacy aliases still identify existing/ambiguous runtime, not a proven
|
|
491
|
+
// canonical bootstrap record. Do not grant fresh creation through an alias.
|
|
492
|
+
if state
|
|
493
|
+
.get("teams")
|
|
494
|
+
.and_then(serde_json::Value::as_object)
|
|
495
|
+
.is_some_and(|teams| teams.values().any(|entry| json_team_identity_matches(entry, team)))
|
|
496
|
+
{
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
let matches = explicit_active_team_key(state).as_deref() == Some(team)
|
|
493
500
|
|| crate::state::projection::team_state_key(state) == team
|
|
494
501
|
|| json_team_identity_matches(state, team)
|
|
495
502
|
|| state
|
|
496
503
|
.get("session_name")
|
|
497
504
|
.and_then(serde_json::Value::as_str)
|
|
498
|
-
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
505
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team));
|
|
506
|
+
matches && !is_quick_start_leader_bootstrap(state)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// An empty agent table alone is not fresh: stopped/removed/partial teams keep
|
|
510
|
+
// their spec, session or task history. Only a coherent attached leader binding
|
|
511
|
+
// with no worker initialization evidence may proceed through first creation.
|
|
512
|
+
pub(super) fn is_quick_start_leader_bootstrap(state: &serde_json::Value) -> bool {
|
|
513
|
+
if !state.is_object()
|
|
514
|
+
|| state.get("teams").is_some_and(|teams| !teams.is_object())
|
|
515
|
+
|| !state.get("agents").and_then(serde_json::Value::as_object).is_some_and(|agents| agents.is_empty())
|
|
516
|
+
|| state.get("spec_path").is_some()
|
|
517
|
+
|| state.get("team_dir").is_some_and(|dir| !dir.is_null())
|
|
518
|
+
|| state.get("session_name").is_some_and(|session| !session.is_null())
|
|
519
|
+
|| state.get("tasks").is_some_and(|tasks| {
|
|
520
|
+
!tasks.as_object().is_some_and(|tasks| tasks.is_empty())
|
|
521
|
+
&& !tasks.as_array().is_some_and(|tasks| tasks.is_empty())
|
|
522
|
+
})
|
|
523
|
+
|| state.get("status").is_some_and(|status| status.as_str() != Some("alive"))
|
|
524
|
+
|| state.get("archived_at").is_some_and(|value| !value.is_null())
|
|
525
|
+
{
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
let owner = state.get("team_owner");
|
|
529
|
+
let receiver = state.get("leader_receiver");
|
|
530
|
+
let owner_pane = owner.and_then(|owner| owner.get("pane_id")).and_then(serde_json::Value::as_str).filter(|pane| !pane.is_empty());
|
|
531
|
+
let receiver_pane = receiver.and_then(|receiver| receiver.get("pane_id")).and_then(serde_json::Value::as_str);
|
|
532
|
+
owner_pane.is_some()
|
|
533
|
+
&& owner_pane == receiver_pane
|
|
534
|
+
&& receiver.and_then(|receiver| receiver.get("status")).and_then(serde_json::Value::as_str) == Some("attached")
|
|
499
535
|
}
|
|
500
536
|
|
|
501
537
|
/// ---
|
|
@@ -513,3 +549,126 @@ pub(super) fn json_team_identity_matches(state: &serde_json::Value, team: &str)
|
|
|
513
549
|
.and_then(serde_json::Value::as_str)
|
|
514
550
|
.is_some_and(|value| value == team)
|
|
515
551
|
}
|
|
552
|
+
|
|
553
|
+
#[cfg(test)]
|
|
554
|
+
mod quick_start_runtime_identity_tests {
|
|
555
|
+
use super::runtime_state_has_quick_start_team;
|
|
556
|
+
use serde_json::{json, Value};
|
|
557
|
+
|
|
558
|
+
fn leader_bootstrap() -> Value {
|
|
559
|
+
json!({
|
|
560
|
+
"agents": {},
|
|
561
|
+
"session_name": null,
|
|
562
|
+
"tasks": {},
|
|
563
|
+
"team_owner": {"pane_id": "%0", "provider": "pi", "owner_epoch": 1},
|
|
564
|
+
"leader_receiver": {"pane_id": "%0", "status": "attached", "session_name": "team-agent-leader-pi-fixture"},
|
|
565
|
+
"leader_client": {"diagnostic_only": true}
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn current_launcher_seed_is_not_initialized_and_is_not_mutated() {
|
|
571
|
+
let seed = leader_bootstrap();
|
|
572
|
+
let state = json!({"active_team_key": "current", "team_key": "current", "teams": {"current": seed}});
|
|
573
|
+
let before = state.clone();
|
|
574
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
575
|
+
assert_eq!(state, before);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
#[test]
|
|
579
|
+
fn real_native_managed_launcher_empty_array_shape_is_fresh_and_unchanged() {
|
|
580
|
+
let mut entry = leader_bootstrap();
|
|
581
|
+
entry["tasks"] = json!([]);
|
|
582
|
+
entry["is_external_leader"] = json!(false);
|
|
583
|
+
entry["team_key"] = json!("current");
|
|
584
|
+
entry["tmux_socket"] = json!("/private/tmp/fixture-native-leader");
|
|
585
|
+
entry["owner_epoch"] = json!(1);
|
|
586
|
+
entry["team_owner"]["machine_fingerprint"] = json!("");
|
|
587
|
+
entry["team_owner"]["leader_session_uuid"] = json!("fixture-native-uuid");
|
|
588
|
+
entry["leader_receiver"]["owner_epoch"] = json!(1);
|
|
589
|
+
entry["leader_receiver"]["leader_session_uuid"] = json!("fixture-native-uuid");
|
|
590
|
+
let state = json!({
|
|
591
|
+
"agents": {}, "tasks": [], "session_name": null,
|
|
592
|
+
"active_team_key": "current", "team_key": "current",
|
|
593
|
+
"teams": {"current": entry}
|
|
594
|
+
});
|
|
595
|
+
let before = state.clone();
|
|
596
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
597
|
+
assert_eq!(state, before);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
fn legacy_flat_leader_binding_can_initialize() {
|
|
602
|
+
let mut state = leader_bootstrap();
|
|
603
|
+
state["active_team_key"] = json!("current");
|
|
604
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn stopped_removed_partial_and_unknown_teams_remain_existing() {
|
|
609
|
+
let cases = [
|
|
610
|
+
("agents", json!({"worker": {"status": "stopped"}})),
|
|
611
|
+
("spec_path", json!("/tmp/current/team.spec.yaml")),
|
|
612
|
+
("spec_path", Value::Null),
|
|
613
|
+
("team_dir", json!("/tmp/.team/current")),
|
|
614
|
+
("session_name", json!("team-current")),
|
|
615
|
+
("session_name", json!("team-agent-leader-stale-worker-field")),
|
|
616
|
+
("tasks", json!({"finished_task": {"status": "done"}})),
|
|
617
|
+
("tasks", json!([{"status": "done"}])),
|
|
618
|
+
("tasks", Value::Null),
|
|
619
|
+
("tasks", json!("unknown")),
|
|
620
|
+
("tasks", json!(false)),
|
|
621
|
+
("tasks", json!(0)),
|
|
622
|
+
("status", json!("stopped")),
|
|
623
|
+
("archived_at", json!("2026-10-08")),
|
|
624
|
+
("agents", Value::Null),
|
|
625
|
+
("agents", json!([])),
|
|
626
|
+
("team_owner", Value::Null),
|
|
627
|
+
("leader_receiver", json!({"pane_id": "%99", "status": "attached"})),
|
|
628
|
+
("leader_receiver", json!({"pane_id": "%0", "status": "pending"})),
|
|
629
|
+
];
|
|
630
|
+
for (key, value) in cases {
|
|
631
|
+
let mut entry = leader_bootstrap();
|
|
632
|
+
entry[key] = value;
|
|
633
|
+
let state = json!({"active_team_key": "current", "teams": {"current": entry}});
|
|
634
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"), "must protect {key}: {state}");
|
|
635
|
+
}
|
|
636
|
+
for entry in [json!({}), Value::Null, json!({"agents": {}, "session_name": null})] {
|
|
637
|
+
let state = json!({"teams": {"current": entry}});
|
|
638
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
fn malformed_team_tables_do_not_allow_flat_bootstrap_to_bypass_the_guard() {
|
|
644
|
+
for teams in [Value::Null, json!([]), json!("damaged")] {
|
|
645
|
+
let mut state = leader_bootstrap();
|
|
646
|
+
state["active_team_key"] = json!("current");
|
|
647
|
+
state["teams"] = teams;
|
|
648
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
#[test]
|
|
653
|
+
fn only_the_requested_team_supplies_initialization_evidence() {
|
|
654
|
+
let state = json!({
|
|
655
|
+
"active_team_key": "other", "spec_path": "/tmp/other/spec.yaml",
|
|
656
|
+
"session_name": "team-other", "agents": {"other_worker": {}},
|
|
657
|
+
"teams": {
|
|
658
|
+
"other": {"spec_path": "/tmp/other/spec.yaml", "agents": {"other_worker": {}}},
|
|
659
|
+
"current": leader_bootstrap()
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
663
|
+
assert!(runtime_state_has_quick_start_team(&state, "other"));
|
|
664
|
+
assert!(!runtime_state_has_quick_start_team(&state, "new-team"));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#[test]
|
|
668
|
+
fn ambiguous_legacy_identity_is_not_fresh() {
|
|
669
|
+
let mut first = leader_bootstrap();
|
|
670
|
+
first["name"] = json!("current");
|
|
671
|
+
let state = json!({"teams": {"first": first, "second": {"name": "current"}}});
|
|
672
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
673
|
+
}
|
|
674
|
+
}
|