@team-agent/installer 0.5.84 → 0.5.86
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/mod.rs +0 -1
- package/crates/team-agent/src/cli/profile.rs +4 -12
- package/crates/team-agent/src/cli/tests/verb_profile.rs +66 -0
- package/crates/team-agent/src/compiler/tests.rs +13 -24
- package/crates/team-agent/src/compiler.rs +1 -50
- package/crates/team-agent/src/leader/start.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +0 -6
- package/crates/team-agent/src/lifecycle/launch/identity.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/layout.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -89
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +0 -3
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +14 -24
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch.rs +0 -9
- package/crates/team-agent/src/lifecycle/profile_launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +42 -46
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +209 -52
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +32 -77
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -13
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +0 -2
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -85
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests.rs +0 -1
- package/crates/team-agent/src/lifecycle/types.rs +0 -10
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -248
- package/crates/team-agent/src/messaging/results/collection_tests.rs +719 -0
- package/crates/team-agent/src/messaging/results.rs +104 -27
- package/crates/team-agent/src/model/enums.rs +0 -35
- package/crates/team-agent/src/model/mod.rs +0 -1
- package/crates/team-agent/src/model/spec.rs +23 -84
- package/crates/team-agent/src/provider/adapter.rs +48 -50
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -30
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -25
- package/crates/team-agent/src/provider/adapters/copilot.rs +9 -42
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +4 -8
- package/crates/team-agent/src/provider/adapters/grok.rs +5 -58
- package/crates/team-agent/src/provider/adapters/mod.rs +5 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +1 -56
- package/crates/team-agent/src/provider/bypass_flags.rs +1 -2
- package/crates/team-agent/src/provider/session/capture.rs +4 -4
- package/crates/team-agent/src/provider/tests/adapter.rs +7 -24
- package/crates/team-agent/src/provider/types.rs +1 -1
- package/crates/team-agent/src/state/persist.rs +15 -1
- package/package.json +4 -4
- package/schemas/team.schema.json +4 -9
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +0 -784
- package/crates/team-agent/src/model/permissions.rs +0 -721
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//! 1. worker argv golden (§C1 + C-5-1/3) — copilot_argv_* tests
|
|
18
18
|
//! 2. per-worker AGENTS.md == compiled prompt — copilot_agents_md_* tests (C-1-1/2,
|
|
19
19
|
//! identity-first per the 0.3.4 B2/D6 lock)
|
|
20
|
-
//! 3.
|
|
20
|
+
//! 3. tools metadata transparency (C-2-1, no prompt permission note or argv deny flags)
|
|
21
21
|
//! 4. fork = structured CapabilityUnsupported (C-4-2/3)
|
|
22
22
|
//! 5. startup recognizer supports copilot trust/ready; turn-state Unknown never idles
|
|
23
23
|
//! (N11, C-3-1/4)
|
|
@@ -62,12 +62,12 @@ const ANCESTRY_KEY: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
|
|
|
62
62
|
const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
|
|
63
63
|
const COPILOT_TMP_PREFIX: &str = "ta-rs-copilot";
|
|
64
64
|
|
|
65
|
-
/// Face 1+3 (§C1 golden + C-5-3, verdict
|
|
66
|
-
///
|
|
67
|
-
///
|
|
68
|
-
///
|
|
69
|
-
///
|
|
70
|
-
///
|
|
65
|
+
/// Face 1+3 (§C1 golden + C-5-3, verdict case `copilot_argv_golden_per_role_tools`):
|
|
66
|
+
/// a copilot worker (mcp_team only) gets the full §C1 argv — noise control,
|
|
67
|
+
/// inline/`@file` MCP config carrying team_orchestrator, a pre-assigned
|
|
68
|
+
/// `--session-id` equal to the persisted `_pending_session_id`, and `-C <workspace>`.
|
|
69
|
+
/// Tools metadata must not derive deny flags; non-dangerous workers still omit the
|
|
70
|
+
/// --allow-all family flag.
|
|
71
71
|
#[test]
|
|
72
72
|
#[serial(env)]
|
|
73
73
|
fn copilot_argv_golden_restricted_role() {
|
|
@@ -176,21 +176,11 @@ team_orchestrator or @<.team/runtime/mcp/worker_a.json>; got {value:?}"
|
|
|
176
176
|
flag_value(argv, "-C")
|
|
177
177
|
));
|
|
178
178
|
}
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
for denied in ["shell", "write"] {
|
|
183
|
-
if !deny_tools.iter().any(|tool| tool.contains(denied)) {
|
|
184
|
-
failures.push(format!(
|
|
185
|
-
"C-5-3: restricted copilot worker must carry --deny-tool '{denied}'; deny={deny_tools:?}"
|
|
186
|
-
));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// RC-15 (C-5-2 v2): network deny is `--deny-tool 'url'` (the help has only shell/
|
|
190
|
-
// write/mcp/url tool kinds; v1's --deny-url is not the v2 form).
|
|
191
|
-
if !deny_tools.iter().any(|tool| tool.contains("url")) {
|
|
179
|
+
// Tools metadata is transparent compatibility data; it must not derive
|
|
180
|
+
// provider-specific deny flags (shell/write/url or any other category).
|
|
181
|
+
if argv.iter().any(|arg| arg == "--deny-tool") {
|
|
192
182
|
failures.push(format!(
|
|
193
|
-
"
|
|
183
|
+
"C-5-3: tools metadata must not derive copilot --deny-tool flags; argv={argv:?}"
|
|
194
184
|
));
|
|
195
185
|
}
|
|
196
186
|
// RC-10 (C-3-5/C-5-2): mcp_team is allowlisted by server name (no approval prompt).
|
|
@@ -289,8 +279,8 @@ used for the dangerous tier; argv={argv:?}"
|
|
|
289
279
|
/// the spawn env must carry COPILOT_CUSTOM_INSTRUCTIONS_DIRS pointing at the
|
|
290
280
|
/// per-worker dir (path contains the agent id; works with NO profile configured), the
|
|
291
281
|
/// AGENTS.md there must BE the compiled worker system prompt (single source: identity
|
|
292
|
-
/// first, runtime contract, role body, output contract,
|
|
293
|
-
///
|
|
282
|
+
/// first, runtime contract, role body, and output contract), and no global
|
|
283
|
+
/// ~/.copilot/AGENTS.md may be written.
|
|
294
284
|
#[test]
|
|
295
285
|
#[serial(env)]
|
|
296
286
|
fn copilot_agents_md_is_the_compiled_prompt_and_env_points_at_it() {
|
|
@@ -362,7 +352,6 @@ with the identity section FIRST; head={:?}",
|
|
|
362
352
|
"# Team Agent Teammate Runtime Contract",
|
|
363
353
|
"COPILOT ROLE BODY SENTINEL",
|
|
364
354
|
"report_result exactly once",
|
|
365
|
-
"Permission note: these tools are prompt-only for this provider and not hard-enforced:",
|
|
366
355
|
] {
|
|
367
356
|
if !agents_md.contains(marker) {
|
|
368
357
|
failures.push(format!(
|
|
@@ -386,14 +375,11 @@ with the identity section FIRST; head={:?}",
|
|
|
386
375
|
);
|
|
387
376
|
}
|
|
388
377
|
|
|
389
|
-
/// Face 3
|
|
390
|
-
///
|
|
391
|
-
/// prompt-only. Observable single-source: the compiled prompt's Permission note (built
|
|
392
|
-
/// from PROVIDER_ENFORCEMENT) for a full-capability role must list exactly
|
|
393
|
-
/// fs_list/fs_read/git_diff/provider_builtin (sorted).
|
|
378
|
+
/// Face 3: tools metadata is transparent compatibility data and must not produce
|
|
379
|
+
/// a provider-specific Permission note in the compiled Copilot prompt.
|
|
394
380
|
#[test]
|
|
395
381
|
#[serial(env)]
|
|
396
|
-
fn
|
|
382
|
+
fn copilot_prompt_omits_removed_permission_note() {
|
|
397
383
|
let _hermetic = enter_hermetic("copilot-permission-note");
|
|
398
384
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
399
385
|
let ws = tmp_ws("enforcement");
|
|
@@ -428,21 +414,21 @@ fn copilot_permission_note_registers_fs_tools_prompt_only() {
|
|
|
428
414
|
.env
|
|
429
415
|
.get("COPILOT_CUSTOM_INSTRUCTIONS_DIRS")
|
|
430
416
|
.cloned()
|
|
431
|
-
.
|
|
432
|
-
let agents_md = std::fs::read_to_string(Path::new(&dir).join("AGENTS.md")).unwrap_or_default();
|
|
433
|
-
|
|
434
|
-
let note_line = agents_md
|
|
435
|
-
.lines()
|
|
436
|
-
.find(|line| line.starts_with("Permission note:"))
|
|
437
|
-
.unwrap_or("")
|
|
438
|
-
.to_string();
|
|
417
|
+
.expect("Copilot spawn must carry COPILOT_CUSTOM_INSTRUCTIONS_DIRS");
|
|
439
418
|
assert!(
|
|
440
|
-
|
|
441
|
-
"
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
419
|
+
!dir.is_empty(),
|
|
420
|
+
"Copilot instructions directory must not be empty"
|
|
421
|
+
);
|
|
422
|
+
let agents_md = std::fs::read_to_string(Path::new(&dir).join("AGENTS.md"))
|
|
423
|
+
.expect("per-worker Copilot AGENTS.md must be readable");
|
|
424
|
+
let identity = "You are Team Agent worker `worker_a` with role `Copilot Worker`.";
|
|
425
|
+
assert!(
|
|
426
|
+
agents_md.starts_with(identity) && agents_md.contains("COPILOT ROLE BODY SENTINEL"),
|
|
427
|
+
"C-2-1: compiled Copilot prompt must include its identity and role body; content={agents_md:?}"
|
|
428
|
+
);
|
|
429
|
+
assert!(
|
|
430
|
+
!agents_md.contains("Permission note:"),
|
|
431
|
+
"C-2-1: removed provider tool enforcement must not emit a Permission note; content={agents_md:?}"
|
|
446
432
|
);
|
|
447
433
|
}
|
|
448
434
|
|
|
@@ -993,6 +979,145 @@ fn copilot_mcp_config_uses_copilot_field_name() {
|
|
|
993
979
|
);
|
|
994
980
|
}
|
|
995
981
|
|
|
982
|
+
fn assert_rules_t02_final_mcp(argv: &[String], ws: &Path, agent_id: &str, team: &str) {
|
|
983
|
+
let argument = flag_value(argv, "--additional-mcp-config").expect("final MCP flag");
|
|
984
|
+
let path = argument
|
|
985
|
+
.strip_prefix('@')
|
|
986
|
+
.expect("final plan references a file");
|
|
987
|
+
let config: serde_json::Value =
|
|
988
|
+
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
|
989
|
+
let server = &config["mcpServers"]["team_orchestrator"];
|
|
990
|
+
assert_eq!(server["transport"], "stdio", "{config}");
|
|
991
|
+
assert!(server.get("type").is_none(), "{config}");
|
|
992
|
+
assert!(Path::new(server["command"].as_str().unwrap()).is_absolute());
|
|
993
|
+
assert_eq!(
|
|
994
|
+
server["args"],
|
|
995
|
+
serde_json::json!(["mcp-server", "--workspace", ws])
|
|
996
|
+
);
|
|
997
|
+
assert_eq!(
|
|
998
|
+
server["env"]["TEAM_AGENT_WORKSPACE"],
|
|
999
|
+
ws.to_string_lossy().as_ref()
|
|
1000
|
+
);
|
|
1001
|
+
assert_eq!(server["env"]["TEAM_AGENT_ID"], agent_id);
|
|
1002
|
+
assert_eq!(server["env"]["TEAM_AGENT_OWNER_TEAM_ID"], team);
|
|
1003
|
+
eprintln!("T02 final MCP: agent={agent_id} team={team} argv={argv:?} config={config}");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
#[test]
|
|
1007
|
+
#[serial(env)]
|
|
1008
|
+
fn rules_t02_final_mcp_across_fresh_start_restart_and_add() {
|
|
1009
|
+
let hermetic = enter_hermetic("rules-t02-final-mcp");
|
|
1010
|
+
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
1011
|
+
let ws = hermetic.workspace("mcp");
|
|
1012
|
+
let team_dir = write_copilot_team(&ws, "cp-resume", &["mcp_team"], false);
|
|
1013
|
+
seed_healthy_coordinator(&ws);
|
|
1014
|
+
let health = team_agent::coordinator::coordinator_health(
|
|
1015
|
+
&team_agent::coordinator::WorkspacePath::new(ws.clone()),
|
|
1016
|
+
);
|
|
1017
|
+
assert!(
|
|
1018
|
+
health.ok,
|
|
1019
|
+
"seeded coordinator must prevent daemon spawn: {health:?}"
|
|
1020
|
+
);
|
|
1021
|
+
let fresh = RecordingTransport::new();
|
|
1022
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("cpresume"), &fresh)
|
|
1023
|
+
.expect("fresh entry");
|
|
1024
|
+
let argv = fresh.single_spawn().argv;
|
|
1025
|
+
assert_rules_t02_final_mcp(&argv, &ws, "worker_a", "cpresume");
|
|
1026
|
+
assert!(flag_value(&argv, "--session-id").is_some());
|
|
1027
|
+
assert!(flag_value(&argv, "--resume").is_none());
|
|
1028
|
+
|
|
1029
|
+
seed_running_resumable(&ws, "worker_a");
|
|
1030
|
+
seed_copilot_session_store(hermetic.home(), "99999999-aaaa-4bbb-8ccc-dddddddddddd");
|
|
1031
|
+
for single in [true, false] {
|
|
1032
|
+
// A previous file is an output, never the authority for the next spawn.
|
|
1033
|
+
std::fs::write(ws.join(".team/runtime/mcp/worker_a.json"),
|
|
1034
|
+
r#"{"mcpServers":{"team_orchestrator":{"type":"stdio","env":{"TEAM_AGENT_ID":"foreign"}}}}"#,
|
|
1035
|
+
).unwrap();
|
|
1036
|
+
let transport = RecordingTransport::new();
|
|
1037
|
+
if single {
|
|
1038
|
+
team_agent::lifecycle::start_agent_with_transport(
|
|
1039
|
+
&ws,
|
|
1040
|
+
&team_agent::model::ids::AgentId::new("worker_a"),
|
|
1041
|
+
true,
|
|
1042
|
+
false,
|
|
1043
|
+
false,
|
|
1044
|
+
Some("cpresume"),
|
|
1045
|
+
&transport,
|
|
1046
|
+
)
|
|
1047
|
+
.expect("single start entry");
|
|
1048
|
+
} else {
|
|
1049
|
+
team_agent::lifecycle::restart_with_transport(&ws, true, Some("cpresume"), &transport)
|
|
1050
|
+
.expect("team restart entry");
|
|
1051
|
+
}
|
|
1052
|
+
let argv = transport.single_spawn().argv;
|
|
1053
|
+
assert_rules_t02_final_mcp(&argv, &ws, "worker_a", "cpresume");
|
|
1054
|
+
assert_eq!(
|
|
1055
|
+
flag_value(&argv, "--resume").as_deref(),
|
|
1056
|
+
Some("99999999-aaaa-4bbb-8ccc-dddddddddddd")
|
|
1057
|
+
);
|
|
1058
|
+
assert!(flag_value(&argv, "--session-id").is_none());
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
let role_file = ws.join("worker_b.md");
|
|
1062
|
+
let role = std::fs::read_to_string(team_dir.join("agents/worker_a.md")).unwrap();
|
|
1063
|
+
std::fs::write(&role_file, role.replace("worker_a", "worker_b")).unwrap();
|
|
1064
|
+
let added = RecordingTransport::with_session_present();
|
|
1065
|
+
team_agent::lifecycle::add_agent_with_transport(
|
|
1066
|
+
&team_dir,
|
|
1067
|
+
&team_agent::model::ids::AgentId::new("worker_b"),
|
|
1068
|
+
&role_file,
|
|
1069
|
+
false,
|
|
1070
|
+
Some("cpresume"),
|
|
1071
|
+
&added,
|
|
1072
|
+
)
|
|
1073
|
+
.expect("add entry");
|
|
1074
|
+
assert_rules_t02_final_mcp(&added.single_spawn().argv, &ws, "worker_b", "cpresume");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
#[test]
|
|
1078
|
+
fn rules_t02_inline_server_map_preserves_unknown_and_non_object_values() {
|
|
1079
|
+
use team_agent::provider::adapters::copilot::copilot_translate_mcp_config;
|
|
1080
|
+
let raw = serde_json::json!({
|
|
1081
|
+
"stdio": {"type": "stdio", "command": "synthetic", "unknown": {"type": "nested"}},
|
|
1082
|
+
"http": {"type": "http", "url": "https://example.invalid"},
|
|
1083
|
+
"opaque": 7,
|
|
1084
|
+
});
|
|
1085
|
+
let translated = copilot_translate_mcp_config(&raw);
|
|
1086
|
+
assert_eq!(
|
|
1087
|
+
translated,
|
|
1088
|
+
serde_json::json!({
|
|
1089
|
+
"stdio": {"transport": "stdio", "command": "synthetic", "unknown": {"type": "nested"}},
|
|
1090
|
+
"http": {"transport": "http", "url": "https://example.invalid"},
|
|
1091
|
+
"opaque": 7,
|
|
1092
|
+
})
|
|
1093
|
+
);
|
|
1094
|
+
assert!(translated.get("mcpServers").is_none());
|
|
1095
|
+
for raw in [serde_json::json!(null), serde_json::json!([1, "opaque"])] {
|
|
1096
|
+
assert_eq!(copilot_translate_mcp_config(&raw), raw);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
#[test]
|
|
1101
|
+
#[serial(env)]
|
|
1102
|
+
fn rules_t02_non_copilot_writer_keeps_canonical_schema() {
|
|
1103
|
+
let hermetic = enter_hermetic("rules-t02-canonical");
|
|
1104
|
+
let ws = hermetic.workspace("canonical");
|
|
1105
|
+
let config = team_agent::provider::get_adapter(Provider::ClaudeCode)
|
|
1106
|
+
.mcp_config(team_agent::provider::AuthMode::Subscription)
|
|
1107
|
+
.unwrap();
|
|
1108
|
+
let path = team_agent::lifecycle::launch::write_worker_mcp_config_for_provider(
|
|
1109
|
+
&ws,
|
|
1110
|
+
"synthetic",
|
|
1111
|
+
&config,
|
|
1112
|
+
Some(Provider::ClaudeCode),
|
|
1113
|
+
)
|
|
1114
|
+
.unwrap();
|
|
1115
|
+
let written: serde_json::Value =
|
|
1116
|
+
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
|
1117
|
+
assert_eq!(written["mcpServers"], config.raw);
|
|
1118
|
+
assert_eq!(written["mcpServers"]["team_orchestrator"]["type"], "stdio");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
996
1121
|
/// C-7-3 / RC-24: a resume command line must not carry `--session-id` and `--resume`
|
|
997
1122
|
/// together (conflicting session semantics). Driven through the real restart path so
|
|
998
1123
|
/// the public resume builder is exercised end to end.
|
|
@@ -1436,11 +1561,7 @@ fn write_copilot_team(ws: &Path, name: &str, tools: &[&str], dangerous: bool) ->
|
|
|
1436
1561
|
let team = ws.join(name);
|
|
1437
1562
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1438
1563
|
// 0.5.66 bypass 单源:bypass 由角色字段驱动,不再是 TEAM.md dangerous_auto_approve。
|
|
1439
|
-
let role_bypass = if dangerous {
|
|
1440
|
-
"true"
|
|
1441
|
-
} else {
|
|
1442
|
-
"false"
|
|
1443
|
-
};
|
|
1564
|
+
let role_bypass = if dangerous { "true" } else { "false" };
|
|
1444
1565
|
std::fs::write(
|
|
1445
1566
|
team.join("TEAM.md"),
|
|
1446
1567
|
format!(
|
|
@@ -1637,6 +1758,7 @@ struct RecordedSpawn {
|
|
|
1637
1758
|
struct RecordingTransport {
|
|
1638
1759
|
spawns: Mutex<Vec<RecordedSpawn>>,
|
|
1639
1760
|
session_present: Mutex<bool>,
|
|
1761
|
+
panes: Mutex<Vec<PaneInfo>>,
|
|
1640
1762
|
}
|
|
1641
1763
|
|
|
1642
1764
|
impl RecordingTransport {
|
|
@@ -1681,8 +1803,22 @@ impl Transport for RecordingTransport {
|
|
|
1681
1803
|
window: window.as_str().to_string(),
|
|
1682
1804
|
});
|
|
1683
1805
|
*self.session_present.lock().unwrap() = true;
|
|
1806
|
+
let pane_id = PaneId::new(format!("%{}", spawns.len()));
|
|
1807
|
+
self.panes.lock().unwrap().push(PaneInfo {
|
|
1808
|
+
pane_id: pane_id.clone(),
|
|
1809
|
+
session: session.clone(),
|
|
1810
|
+
window_index: None,
|
|
1811
|
+
window_name: Some(window.clone()),
|
|
1812
|
+
pane_index: None,
|
|
1813
|
+
tty: None,
|
|
1814
|
+
current_command: None,
|
|
1815
|
+
current_path: None,
|
|
1816
|
+
active: false,
|
|
1817
|
+
pane_pid: Some(30_000 + spawns.len() as u32),
|
|
1818
|
+
leader_env: BTreeMap::new(),
|
|
1819
|
+
});
|
|
1684
1820
|
Ok(SpawnResult {
|
|
1685
|
-
pane_id
|
|
1821
|
+
pane_id,
|
|
1686
1822
|
session: session.clone(),
|
|
1687
1823
|
window: window.clone(),
|
|
1688
1824
|
child_pid: Some(30_000 + spawns.len() as u32),
|
|
@@ -1747,7 +1883,7 @@ impl Transport for RecordingTransport {
|
|
|
1747
1883
|
}
|
|
1748
1884
|
|
|
1749
1885
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1750
|
-
Ok(
|
|
1886
|
+
Ok(self.panes.lock().unwrap().clone())
|
|
1751
1887
|
}
|
|
1752
1888
|
|
|
1753
1889
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -1769,10 +1905,20 @@ impl Transport for RecordingTransport {
|
|
|
1769
1905
|
|
|
1770
1906
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
1771
1907
|
*self.session_present.lock().unwrap() = false;
|
|
1908
|
+
self.panes
|
|
1909
|
+
.lock()
|
|
1910
|
+
.unwrap()
|
|
1911
|
+
.retain(|pane| &pane.session != _session);
|
|
1772
1912
|
Ok(())
|
|
1773
1913
|
}
|
|
1774
1914
|
|
|
1775
|
-
fn kill_window(&self,
|
|
1915
|
+
fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
|
|
1916
|
+
self.panes.lock().unwrap().retain(|pane| match target {
|
|
1917
|
+
Target::Pane(id) => &pane.pane_id != id,
|
|
1918
|
+
Target::SessionWindow { session, window } => {
|
|
1919
|
+
&pane.session != session || pane.window_name.as_ref() != Some(window)
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1776
1922
|
Ok(())
|
|
1777
1923
|
}
|
|
1778
1924
|
|
|
@@ -1796,6 +1942,17 @@ fn copilot_empty_mcp_list_shim(tag: &str) -> TempWorkspace {
|
|
|
1796
1942
|
#[cfg(unix)]
|
|
1797
1943
|
{
|
|
1798
1944
|
use std::os::unix::fs::PermissionsExt;
|
|
1945
|
+
// coordinator health needs the seeded test PID's real non-zombie status.
|
|
1946
|
+
// Keep PATH isolated and refuse any broader process inspection.
|
|
1947
|
+
std::fs::write(
|
|
1948
|
+
bin.join("ps"),
|
|
1949
|
+
format!(
|
|
1950
|
+
"#!/bin/sh\nif [ \"$#\" = 4 ] && [ \"$1\" = -p ] && [ \"$2\" = {} ] && [ \"$3\" = -o ] && [ \"$4\" = stat= ]; then\n exec /bin/ps \"$@\"\nfi\nexit 1\n",
|
|
1951
|
+
std::process::id(),
|
|
1952
|
+
),
|
|
1953
|
+
)
|
|
1954
|
+
.unwrap();
|
|
1955
|
+
std::fs::set_permissions(bin.join("ps"), std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
1799
1956
|
std::fs::set_permissions(bin.join("copilot"), std::fs::Permissions::from_mode(0o755))
|
|
1800
1957
|
.unwrap();
|
|
1801
1958
|
}
|
|
@@ -37,7 +37,7 @@ fn bin() -> &'static str {
|
|
|
37
37
|
|
|
38
38
|
#[test]
|
|
39
39
|
#[serial(env)]
|
|
40
|
-
fn
|
|
40
|
+
fn claude_worker_spawn_argv_uses_compiled_prompt_without_tool_restrictions() {
|
|
41
41
|
let _hermetic = enter_hermetic("core034-claude-argv");
|
|
42
42
|
let _ancestry = EnvGuard::set(
|
|
43
43
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
@@ -80,7 +80,6 @@ fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
|
80
80
|
"send_message(to='leader'",
|
|
81
81
|
"report_result exactly once",
|
|
82
82
|
"ROLE BODY SENTINEL",
|
|
83
|
-
"Permission note",
|
|
84
83
|
] {
|
|
85
84
|
if !prompt.contains(marker) {
|
|
86
85
|
failures.push(format!("B2 missing compiled prompt marker {marker:?}"));
|
|
@@ -90,31 +89,15 @@ fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
|
90
89
|
failures.push("B2 prompt collapsed to the bare role label".to_string());
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"Bash",
|
|
96
|
-
"Read",
|
|
97
|
-
"Edit",
|
|
98
|
-
"Write",
|
|
99
|
-
"MultiEdit",
|
|
100
|
-
"NotebookEdit",
|
|
101
|
-
"Glob",
|
|
102
|
-
"Grep",
|
|
103
|
-
] {
|
|
104
|
-
if disallowed.iter().any(|tool| tool == forbidden) {
|
|
105
|
-
failures.push(format!(
|
|
106
|
-
"B1 declared capability still disallowed via --disallowedTools {forbidden:?}"
|
|
107
|
-
));
|
|
108
|
-
}
|
|
92
|
+
if argv_has_flag(&spawn.argv, "--disallowedTools") {
|
|
93
|
+
failures.push("B1 Claude argv must not derive --disallowedTools from tools metadata".to_string());
|
|
109
94
|
}
|
|
110
95
|
if argv_has_flag(&spawn.argv, "--allowedTools") {
|
|
111
|
-
failures.push(
|
|
112
|
-
"B1 Python parity violated: Claude argv must not switch to --allowedTools".to_string(),
|
|
113
|
-
);
|
|
96
|
+
failures.push("B1 Claude argv must not derive --allowedTools from tools metadata".to_string());
|
|
114
97
|
}
|
|
115
98
|
assert!(
|
|
116
99
|
failures.is_empty(),
|
|
117
|
-
"B1/B2 worker argv contract failed:\n{}\nprompt={prompt:?}\
|
|
100
|
+
"B1/B2 worker argv contract failed:\n{}\nprompt={prompt:?}\nargv={:?}",
|
|
118
101
|
failures.join("\n"),
|
|
119
102
|
spawn.argv
|
|
120
103
|
);
|
|
@@ -122,7 +105,7 @@ fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
|
122
105
|
|
|
123
106
|
#[test]
|
|
124
107
|
#[serial(env)]
|
|
125
|
-
fn
|
|
108
|
+
fn claude_control_role_does_not_derive_native_tool_restrictions() {
|
|
126
109
|
let _hermetic = enter_hermetic("core034-claude-control");
|
|
127
110
|
let _ancestry = EnvGuard::set(
|
|
128
111
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
@@ -156,29 +139,21 @@ fn claude_control_role_without_native_tools_still_disallows_native_tools() {
|
|
|
156
139
|
.expect("quick-start should record the control worker command");
|
|
157
140
|
|
|
158
141
|
let spawn = transport.single_spawn();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
] {
|
|
170
|
-
assert!(
|
|
171
|
-
disallowed.iter().any(|tool| tool == expected),
|
|
172
|
-
"B1 negative case: mcp_team-only control roles must still disallow native Claude tools; \
|
|
173
|
-
missing={expected} disallowed={disallowed:?}\nargv={:?}",
|
|
174
|
-
spawn.argv
|
|
175
|
-
);
|
|
176
|
-
}
|
|
142
|
+
assert!(
|
|
143
|
+
!argv_has_flag(&spawn.argv, "--disallowedTools"),
|
|
144
|
+
"B1 control role must not derive native-tool restrictions from tools metadata; argv={:?}",
|
|
145
|
+
spawn.argv
|
|
146
|
+
);
|
|
147
|
+
assert!(
|
|
148
|
+
!argv_has_flag(&spawn.argv, "--allowedTools"),
|
|
149
|
+
"B1 control role must not derive an allowlist from tools metadata; argv={:?}",
|
|
150
|
+
spawn.argv
|
|
151
|
+
);
|
|
177
152
|
}
|
|
178
153
|
|
|
179
154
|
#[test]
|
|
180
155
|
#[serial(env)]
|
|
181
|
-
fn
|
|
156
|
+
fn restart_claude_worker_does_not_derive_tool_restrictions_when_runtime_state_has_no_raw_tools() {
|
|
182
157
|
let _hermetic = enter_hermetic("core034-restart-tools");
|
|
183
158
|
let _ancestry = EnvGuard::set(
|
|
184
159
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
@@ -215,34 +190,20 @@ fn restart_claude_worker_resolves_tools_from_spec_when_runtime_state_has_no_raw_
|
|
|
215
190
|
let report = restart_with_transport(&ws, true, Some("restarttools"), &restart_transport)
|
|
216
191
|
.expect("restart should reach worker command construction");
|
|
217
192
|
let spawn = restart_transport.single_spawn();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"Edit",
|
|
224
|
-
"Write",
|
|
225
|
-
"MultiEdit",
|
|
226
|
-
"NotebookEdit",
|
|
227
|
-
"Glob",
|
|
228
|
-
"Grep",
|
|
229
|
-
] {
|
|
230
|
-
assert!(
|
|
231
|
-
!disallowed.iter().any(|tool| tool == forbidden),
|
|
232
|
-
"B1 restart contract: when runtime state lacks raw tools, restart must resolve tools from the selected spec/role through resolve_permissions. \
|
|
233
|
-
{forbidden} must not be disallowed for this role; disallowed={disallowed:?}\nargv={:?}\nreport={report:?}",
|
|
234
|
-
spawn.argv
|
|
235
|
-
);
|
|
236
|
-
}
|
|
193
|
+
assert!(
|
|
194
|
+
!argv_has_flag(&spawn.argv, "--disallowedTools"),
|
|
195
|
+
"B1 restart must not derive native-tool restrictions from tools metadata; argv={:?} report={report:?}",
|
|
196
|
+
spawn.argv
|
|
197
|
+
);
|
|
237
198
|
assert!(
|
|
238
199
|
!argv_has_flag(&spawn.argv, "--allowedTools"),
|
|
239
|
-
"B1
|
|
200
|
+
"B1 restart must not derive an allowlist from tools metadata; argv={:?} report={report:?}",
|
|
240
201
|
spawn.argv
|
|
241
202
|
);
|
|
242
203
|
}
|
|
243
204
|
|
|
244
205
|
#[test]
|
|
245
|
-
fn
|
|
206
|
+
fn worker_command_context_uses_single_prompt_without_permission_sources() {
|
|
246
207
|
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
247
208
|
let src_root = crate_root.join("src");
|
|
248
209
|
let launch = composite_source::composite_source("src/lifecycle/launch.rs");
|
|
@@ -276,10 +237,9 @@ fn worker_command_context_uses_single_prompt_and_permission_sources() {
|
|
|
276
237
|
&& all_src.contains("runtime_contract_section")
|
|
277
238
|
&& all_src.contains("identity_section")
|
|
278
239
|
&& all_src.contains("role_body")
|
|
279
|
-
&& all_src.contains("output_contract")
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
(runtime_contract_section, identity_section, role_body, output_contract, permission_notes)."
|
|
240
|
+
&& all_src.contains("output_contract"),
|
|
241
|
+
"B2 grep guard: worker command construction needs one compiled prompt helper with the CR-approved four sections \
|
|
242
|
+
(runtime_contract_section, identity_section, role_body, output_contract)."
|
|
283
243
|
);
|
|
284
244
|
assert!(
|
|
285
245
|
!command_sources.contains("system_prompt: role"),
|
|
@@ -296,8 +256,9 @@ fn worker_command_context_uses_single_prompt_and_permission_sources() {
|
|
|
296
256
|
);
|
|
297
257
|
|
|
298
258
|
assert!(
|
|
299
|
-
|
|
300
|
-
|
|
259
|
+
!command_sources.contains("resolve_permissions")
|
|
260
|
+
&& !command_sources.contains("resolved_tool_strings"),
|
|
261
|
+
"B1 grep guard: command construction must not derive argv restrictions from tools metadata."
|
|
301
262
|
);
|
|
302
263
|
assert!(
|
|
303
264
|
!command_sources.contains("worker_tool_refs(agent_tool_strings")
|
|
@@ -305,8 +266,8 @@ fn worker_command_context_uses_single_prompt_and_permission_sources() {
|
|
|
305
266
|
"B1 grep guard: command construction must not raw-pass agent.tools/tool_refs into ProviderCommandContext."
|
|
306
267
|
);
|
|
307
268
|
assert!(
|
|
308
|
-
adapter.contains("--disallowedTools") && !adapter.contains("--allowedTools"),
|
|
309
|
-
"B1
|
|
269
|
+
!adapter.contains("--disallowedTools") && !adapter.contains("--allowedTools"),
|
|
270
|
+
"B1 provider adapter must not derive Claude tool restrictions from tools metadata."
|
|
310
271
|
);
|
|
311
272
|
assert!(
|
|
312
273
|
!adapter.contains("resolve_permissions"),
|
|
@@ -837,12 +798,6 @@ fn flag_value(argv: &[String], flag: &str) -> Option<String> {
|
|
|
837
798
|
.find_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
|
|
838
799
|
}
|
|
839
800
|
|
|
840
|
-
fn flag_values(argv: &[String], flag: &str) -> Vec<String> {
|
|
841
|
-
argv.windows(2)
|
|
842
|
-
.filter_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
|
|
843
|
-
.collect()
|
|
844
|
-
}
|
|
845
|
-
|
|
846
801
|
fn argv_has_flag(argv: &[String], flag: &str) -> bool {
|
|
847
802
|
argv.iter().any(|arg| arg == flag)
|
|
848
803
|
}
|
|
@@ -520,8 +520,8 @@ fn launch_dry_run_resolves_real_plan_not_stub_error() {
|
|
|
520
520
|
Ok(report) => {
|
|
521
521
|
assert!(report.dry_run, "dry_run launch must report dry_run=true");
|
|
522
522
|
assert!(
|
|
523
|
-
!report.routes.is_empty()
|
|
524
|
-
"dry_run launch must resolve a real route
|
|
523
|
+
!report.routes.is_empty(),
|
|
524
|
+
"dry_run launch must resolve a real route plan from the compiled spec"
|
|
525
525
|
);
|
|
526
526
|
}
|
|
527
527
|
// any OTHER Err (a real requirement/transport error) still proves the stub is gone.
|
|
@@ -3210,7 +3210,6 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3210
3210
|
"profile",
|
|
3211
3211
|
"window",
|
|
3212
3212
|
"mcp_config",
|
|
3213
|
-
"permissions",
|
|
3214
3213
|
"effective_approval_policy",
|
|
3215
3214
|
// 0.5.66 bypass 单源:fresh spawn 记录运行面 bypass 值(restart drift 挡板)。
|
|
3216
3215
|
"as_launched_dangerously_skip_permissions",
|
|
@@ -3239,16 +3238,6 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3239
3238
|
.join(".team/runtime/mcp/implementer.json")
|
|
3240
3239
|
.to_string_lossy())
|
|
3241
3240
|
);
|
|
3242
|
-
assert_eq!(
|
|
3243
|
-
agent["permissions"],
|
|
3244
|
-
json!({
|
|
3245
|
-
"agent_id": "implementer",
|
|
3246
|
-
"provider": "codex",
|
|
3247
|
-
"tools": ["mcp_team"],
|
|
3248
|
-
"resolved_tools": [{"tool": "mcp_team", "enforcement": "prompt_only"}],
|
|
3249
|
-
"has_prompt_only": true
|
|
3250
|
-
})
|
|
3251
|
-
);
|
|
3252
3241
|
assert!(agent["session_id"].is_null());
|
|
3253
3242
|
assert!(agent["rollout_path"].is_null());
|
|
3254
3243
|
assert!(agent["captured_at"].is_null());
|
|
@@ -91,7 +91,6 @@ fn command_for_model(model: &str) -> Vec<String> {
|
|
|
91
91
|
model: Some(model),
|
|
92
92
|
effort: Some(ProviderEffort::Medium),
|
|
93
93
|
system_prompt: "worker contract",
|
|
94
|
-
tool_categories: &["mcp_team"],
|
|
95
94
|
session_dir: Some(Path::new("/workspace/.team/runtime/pi/t1/w1/sessions")),
|
|
96
95
|
session: PiSessionSelector::Fresh {
|
|
97
96
|
session_id: "716ba0cb-c491-4471-b41a-43c8d5e1f05a",
|
|
@@ -10,7 +10,6 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
|
|
|
10
10
|
model: Some("team-agent/qwen3.8-27b"),
|
|
11
11
|
effort: Some(ProviderEffort::High),
|
|
12
12
|
system_prompt: "frozen worker prompt",
|
|
13
|
-
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
14
13
|
session_dir: Some(Path::new(
|
|
15
14
|
"/workspace/.team/runtime/pi/team-a/worker-a/sessions",
|
|
16
15
|
)),
|
|
@@ -38,7 +37,6 @@ fn leader_request() -> PiCommandRequest<'static> {
|
|
|
38
37
|
model: None,
|
|
39
38
|
effort: None,
|
|
40
39
|
system_prompt: "frozen leader prompt",
|
|
41
|
-
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
42
40
|
session_dir: None,
|
|
43
41
|
session: PiSessionSelector::Fresh {
|
|
44
42
|
session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
|