@team-agent/installer 0.5.60 → 0.5.62

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.
Files changed (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +3 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +5 -1
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,869 @@
1
+ //! P0 RED contract: external diagnostic projections never expose credential values.
2
+ //!
3
+ //! Source: `.team/artifacts/status-credential-redact-locate.md` §8 RED1-RED7.
4
+
5
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
6
+
7
+ #[path = "../../../tests/support/hermetic.rs"]
8
+ mod hermetic_guard;
9
+
10
+ #[path = "../../../tests/support/composite_source.rs"]
11
+ mod composite_source;
12
+ use std::fs;
13
+ use std::io::{Read, Write};
14
+ use std::net::TcpListener;
15
+ #[cfg(unix)]
16
+ use std::os::unix::fs::PermissionsExt;
17
+ use std::path::{Path, PathBuf};
18
+ use std::process::{Command, Stdio};
19
+ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20
+ use std::sync::Arc;
21
+ use std::thread;
22
+ use std::time::{Duration, Instant};
23
+
24
+ use serde_json::{json, Value};
25
+ use serial_test::serial;
26
+ use team_agent::cli::{cmd_preflight, emit, CmdOutput, PreflightArgs};
27
+ use team_agent::event_log::EventLog;
28
+ use team_agent::message_store::MessageStore;
29
+ use team_agent::state::persist::save_runtime_state;
30
+ #[cfg(unix)]
31
+ use team_agent::tmux_backend::TmuxBackend;
32
+ #[cfg(unix)]
33
+ use team_agent::transport::{InjectPayload, Key, PaneId, Target, Transport};
34
+
35
+ const REDACTED: &str = "[REDACTED]";
36
+
37
+ #[test]
38
+ #[serial(env)]
39
+ fn red1_external_redaction_has_one_shared_module_and_two_crate_entry_points() {
40
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
41
+ let redaction = fs::read_to_string(root.join("src/redaction.rs")).unwrap_or_default();
42
+ let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
43
+ let crate_entries = redaction
44
+ .lines()
45
+ .filter_map(|line| line.trim().strip_prefix("pub(crate) fn "))
46
+ .filter_map(|line| line.split('(').next())
47
+ .collect::<Vec<_>>();
48
+ let marker = synthetic_marker();
49
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red1");
50
+ env.scrub_tmux();
51
+ env.assert_no_real_tmux();
52
+ let log = EventLog::new(&env.workspace("red1"));
53
+ let written = log
54
+ .write("redaction.contract", mixed_sensitive_value(&marker))
55
+ .unwrap();
56
+ let tail = log.tail(0).unwrap();
57
+ let text = Value::Array(tail.clone()).to_string();
58
+ let idempotent = tail.first() == Some(&written);
59
+
60
+ assert!(
61
+ lib.lines().any(|line| line.trim() == "mod redaction;")
62
+ && crate_entries == ["redact_external_value", "redact_external_text"]
63
+ && !text.contains(&marker)
64
+ && text.contains(REDACTED)
65
+ && text.contains("https://[REDACTED]@proxy.invalid:8443/path")
66
+ && idempotent,
67
+ "RED1: one internal module with exactly two crate entry points must recursively mask mixed values and remain idempotent; no third public crate scrubber is allowed"
68
+ );
69
+ }
70
+
71
+ #[test]
72
+ #[serial(env)]
73
+ fn red2_false_positive_guard_preserves_team_provider_and_model_truth() {
74
+ let marker = synthetic_marker();
75
+ let (env, workspace) = redaction_case("red2", &marker, "current");
76
+ let status = team_agent::cli::status_port::status(&workspace, false, true)
77
+ .expect("detail status must remain available");
78
+ let text = status.to_string();
79
+ let diagnostics = status
80
+ .pointer("/teams/current/diagnostics")
81
+ .expect("raw selected-team diagnostics remain present");
82
+ let structural_fields_preserved = [
83
+ ("/team_key", json!("current")),
84
+ ("/auth_mode", json!("subscription")),
85
+ ("/provider", json!("codex")),
86
+ ("/model", json!("gpt-contract")),
87
+ ("/provider_source", json!("role")),
88
+ ("/model_source", json!("role")),
89
+ ("/model_stale", json!(true)),
90
+ ("/session_id", json!("session-contract")),
91
+ ("/pane_id", json!("%contract")),
92
+ ("/monkey", json!("banana")),
93
+ ("/author", json!("operator")),
94
+ ("/normal_url", json!("https://proxy.invalid/health")),
95
+ ("/already_redacted", json!(REDACTED)),
96
+ ]
97
+ .iter()
98
+ .all(|(pointer, expected)| diagnostics.pointer(pointer) == Some(expected));
99
+ let marker_absent = !text.contains(&marker);
100
+ let replacement_present = text.contains(REDACTED);
101
+ let url_structure_preserved = text.contains("https://[REDACTED]@proxy.invalid:8443/path");
102
+ assert!(
103
+ marker_absent
104
+ && replacement_present
105
+ && structural_fields_preserved
106
+ && url_structure_preserved,
107
+ "RED2: recursive masking must remove the synthetic marker while preserving structural provider/model/identity fields and URL host/path; marker_absent={marker_absent} replacement_present={replacement_present} structural_fields_preserved={structural_fields_preserved} url_structure_preserved={url_structure_preserved}"
108
+ );
109
+ drop(env);
110
+ }
111
+
112
+ #[test]
113
+ #[serial(env)]
114
+ fn red3_full_and_compact_status_never_expose_restart_credentials() {
115
+ let marker = synthetic_marker();
116
+ let (env, workspace) = redaction_case("red3", &marker, "current");
117
+ let full = team_agent::cli::status_port::status(&workspace, false, true)
118
+ .expect("full status must remain available");
119
+ let compact = team_agent::cli::status_port::status(&workspace, true, false)
120
+ .expect("compact status must remain available");
121
+ let full_text = full.to_string();
122
+ let compact_text = compact.to_string();
123
+ let full_safe = !full_text.contains(&marker) && full_text.contains(REDACTED);
124
+ let compact_safe = !compact_text.contains(&marker);
125
+ let diagnostics_preserved = [
126
+ "/agents/worker/restart_error",
127
+ "/teams/current/agents/worker/restart_error",
128
+ ]
129
+ .iter()
130
+ .all(|pointer| {
131
+ full.pointer(pointer)
132
+ .and_then(Value::as_str)
133
+ .is_some_and(|error| {
134
+ error.contains("subprocess exited 37")
135
+ && error.contains("HTTPS_PROXY")
136
+ && error.contains("proxy.invalid:8443/path")
137
+ && error.contains(REDACTED)
138
+ })
139
+ });
140
+ assert!(
141
+ full_safe && compact_safe && diagnostics_preserved,
142
+ "RED3: full/compact status must mask credentials and full detail must retain restart classification/key/host/path; full_safe={full_safe} compact_safe={compact_safe} diagnostics_preserved={diagnostics_preserved}"
143
+ );
144
+ drop(env);
145
+ }
146
+
147
+ #[test]
148
+ #[serial(env)]
149
+ fn red4_jsonrpc_and_legacy_mcp_status_never_expose_raw_team_credentials() {
150
+ let marker = synthetic_marker();
151
+ let team_key = "redaction-contract";
152
+ let (env, workspace) = redaction_case("red4", &marker, team_key);
153
+ let frames = run_mcp_status_frames(&env, &workspace, team_key);
154
+ assert_eq!(
155
+ frames.len(),
156
+ 2,
157
+ "RED4: real MCP stdio must answer JSON-RPC and legacy requests"
158
+ );
159
+
160
+ let outer = frames[0].to_string();
161
+ let content_text = frames[0]
162
+ .pointer("/result/content/0/text")
163
+ .and_then(Value::as_str)
164
+ .expect("JSON-RPC tools/call content text");
165
+ let body: Value = serde_json::from_str(content_text).expect("decoded MCP status body");
166
+ let outer_safe = !outer.contains(&marker);
167
+ let content_safe = !content_text.contains(&marker);
168
+ let body_preserved = body
169
+ .pointer(&format!("/teams/{team_key}/agents/worker/restart_error"))
170
+ .and_then(Value::as_str)
171
+ .is_some_and(|error| error.contains(REDACTED));
172
+ let legacy = frames[1].to_string();
173
+ let legacy_safe = !legacy.contains(&marker);
174
+ let legacy_preserved = frames[1]
175
+ .pointer(&format!("/teams/{team_key}/agents/worker/restart_error"))
176
+ .and_then(Value::as_str)
177
+ .is_some_and(|error| error.contains(REDACTED));
178
+ assert!(
179
+ outer_safe && content_safe && body_preserved && legacy_safe && legacy_preserved,
180
+ "RED4: JSON-RPC outer/text and legacy frames must all mask the synthetic marker while preserving selected raw-team diagnostics; outer_safe={outer_safe} content_safe={content_safe} body_preserved={body_preserved} legacy_safe={legacy_safe} legacy_preserved={legacy_preserved}"
181
+ );
182
+ drop(env);
183
+ }
184
+
185
+ #[test]
186
+ #[serial(env)]
187
+ fn red5_event_write_and_legacy_tail_are_redacted_and_idempotent() {
188
+ let marker = synthetic_marker();
189
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red5");
190
+ env.scrub_tmux();
191
+ env.assert_no_real_tmux();
192
+ let workspace = env.workspace("red5");
193
+ let log = EventLog::new(&workspace);
194
+ let diagnostic = restart_diagnostic(&marker);
195
+ let written = [
196
+ log.write(
197
+ "provider.worker.spawn_argv",
198
+ json!({
199
+ "authorization_header": format!("Bearer {marker}"),
200
+ "credential_blob": marker.clone(),
201
+ "config": format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\""),
202
+ "argv": ["provider", "--api-key", marker.clone()],
203
+ }),
204
+ )
205
+ .unwrap(),
206
+ log.write(
207
+ "provider.worker.spawn_argv",
208
+ json!({"argv": ["provider", diagnostic.clone()]}),
209
+ )
210
+ .unwrap(),
211
+ log.write(
212
+ "start_agent.agent_start",
213
+ json!({"command": ["provider", diagnostic.clone()]}),
214
+ )
215
+ .unwrap(),
216
+ log.write(
217
+ "restart.agent_failed",
218
+ json!({"agent_id": "worker", "error": diagnostic.clone()}),
219
+ )
220
+ .unwrap(),
221
+ log.write(
222
+ "restart.completed",
223
+ json!({"failed_agents": [{"agent_id": "worker", "error": diagnostic}]}),
224
+ )
225
+ .unwrap(),
226
+ ];
227
+ let path = workspace.join(".team/logs/events.jsonl");
228
+ let new_bytes = fs::read_to_string(&path).unwrap();
229
+ let new_bytes_safe = !new_bytes.contains(&marker) && new_bytes.contains(REDACTED);
230
+
231
+ let mut file = fs::OpenOptions::new().append(true).open(&path).unwrap();
232
+ writeln!(
233
+ file,
234
+ "{}",
235
+ json!({"event": "legacy.restart", "error": restart_diagnostic(&marker)})
236
+ )
237
+ .unwrap();
238
+ writeln!(
239
+ file,
240
+ "{}",
241
+ json!({
242
+ "event": "provider.session.converging",
243
+ "required_missing": ["worker"],
244
+ "pending": ["worker"]
245
+ })
246
+ )
247
+ .unwrap();
248
+ writeln!(file, "legacy malformed {}", restart_diagnostic(&marker)).unwrap();
249
+ let tail = log.tail(0).unwrap();
250
+ let tail_text = Value::Array(tail.clone()).to_string();
251
+ let tail_safe = !tail_text.contains(&marker) && tail_text.contains(REDACTED);
252
+ let double_boundary_idempotent = tail.first() == written.first();
253
+ let legacy_aliases_readable = tail.iter().any(|event| {
254
+ event.get("event").and_then(Value::as_str) == Some("provider.session.converging")
255
+ && event.get("required_missing") == Some(&json!(["worker"]))
256
+ && event.get("pending") == Some(&json!(["worker"]))
257
+ });
258
+
259
+ assert!(
260
+ new_bytes_safe && tail_safe && double_boundary_idempotent && legacy_aliases_readable,
261
+ "RED5: EventLog write bytes and legacy/malformed tail output must be masked; applying redaction at write then tail must be idempotent"
262
+ );
263
+ }
264
+
265
+ #[test]
266
+ fn eventlog_producers_share_the_central_writer_and_canonical_schema() {
267
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
268
+ let launch = composite_source::composite_source("src/lifecycle/launch.rs");
269
+ let common = fs::read_to_string(root.join("src/lifecycle/restart/common.rs")).unwrap();
270
+ let rebuild = fs::read_to_string(root.join("src/lifecycle/restart/rebuild.rs")).unwrap();
271
+ let helper_calls = [&launch, &common, &rebuild]
272
+ .iter()
273
+ .map(|source| source.matches("provider_worker_spawn_argv_fields(").count())
274
+ .sum::<usize>();
275
+ let convergence_writer = source_section(
276
+ &common,
277
+ "fn write_session_convergence_progress_event(",
278
+ "pub(crate) fn restart_required_missing_session_agent_ids(",
279
+ );
280
+ let resume_end = ["pub(crate) fn restart_candidates(", "pub fn restart_candidates("]
281
+ .into_iter()
282
+ .find(|marker| rebuild.contains(marker))
283
+ .expect("restart_candidates source section end");
284
+ let resume_writer = source_section(
285
+ &rebuild,
286
+ "fn write_restart_resume_decision_event(",
287
+ resume_end,
288
+ );
289
+
290
+ assert_eq!(
291
+ helper_calls, 3,
292
+ "launch/restart/fake must share one spawn schema helper"
293
+ );
294
+ assert!(!common.contains("\"required_missing\": progress"));
295
+ assert!(!common.contains("\"pending\": pending_agent_ids"));
296
+ for writer in [convergence_writer, resume_writer] {
297
+ assert!(writer.contains("EventLog::new"));
298
+ assert!(!writer.contains("OpenOptions"));
299
+ assert!(!writer.contains("write_all"));
300
+ assert!(!writer.contains("events.jsonl"));
301
+ }
302
+ }
303
+
304
+ fn source_section<'a>(source: &'a str, start: &str, end: &str) -> &'a str {
305
+ let start = source.find(start).expect("source section start");
306
+ let rest = &source[start..];
307
+ &rest[..rest.find(end).expect("source section end")]
308
+ }
309
+
310
+ #[test]
311
+ #[serial(env)]
312
+ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
313
+ let marker = synthetic_marker();
314
+ let diagnostic = restart_diagnostic(&marker);
315
+ let mut regular_reports_safe = true;
316
+ for status in ["partial", "failed"] {
317
+ let output = CmdOutput::Json(json!({
318
+ "ok": false,
319
+ "status": status,
320
+ "reason": "restart_failed",
321
+ "failed_agents": [{"agent_id": "worker", "error": diagnostic.clone()}],
322
+ "action": "inspect the failed spawn and retry"
323
+ }));
324
+ for as_json in [true, false] {
325
+ let rendered = emit(&output, as_json).expect("restart report renders");
326
+ regular_reports_safe &= !rendered.contains(&marker)
327
+ && rendered.contains("restart_failed")
328
+ && rendered.contains("worker")
329
+ && rendered.contains(REDACTED);
330
+ }
331
+ }
332
+
333
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red6");
334
+ env.scrub_tmux();
335
+ env.assert_no_real_tmux();
336
+ let workspace = env.workspace("red6");
337
+ let spec = write_invalid_validate_spec(&workspace, &marker);
338
+ let mut combined = String::new();
339
+ for json_mode in [true, false] {
340
+ let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
341
+ command.arg("validate").arg(&spec);
342
+ if json_mode {
343
+ command.arg("--json");
344
+ }
345
+ let output = command
346
+ .current_dir(&workspace)
347
+ .env("HOME", env.home())
348
+ .env_remove("TMUX")
349
+ .env_remove("TMUX_PANE")
350
+ .output()
351
+ .expect("run top-level CLI error boundary");
352
+ assert!(
353
+ !output.status.success(),
354
+ "invalid spec must reach CLI error"
355
+ );
356
+ combined.push_str(&String::from_utf8_lossy(&output.stdout));
357
+ combined.push_str(&String::from_utf8_lossy(&output.stderr));
358
+ }
359
+ let logs = read_cli_error_logs(&workspace);
360
+ let output_safe = !combined.contains(&marker) && combined.contains(REDACTED);
361
+ let logs_safe = !logs.contains(&marker) && logs.contains(REDACTED);
362
+ assert!(
363
+ regular_reports_safe && output_safe && logs_safe,
364
+ "RED6: regular JSON/human reports plus top-level stdout/stderr and physical error logs must mask before emission; regular_reports_safe={regular_reports_safe} output_safe={output_safe} logs_safe={logs_safe} output_had_marker={} logs_had_marker={}",
365
+ combined.contains(&marker),
366
+ logs.contains(&marker)
367
+ );
368
+ }
369
+
370
+ #[test]
371
+ #[serial(env)]
372
+ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
373
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
374
+ let tmux = fs::read_to_string(root.join("src/tmux_backend.rs")).unwrap();
375
+ let profile = fs::read_to_string(root.join("src/lifecycle/profile_smoke.rs")).unwrap();
376
+ let delegates_to_shared =
377
+ tmux.contains("redact_external_text") && profile.contains("redact_external_text");
378
+
379
+ let marker = synthetic_marker();
380
+ let profile_token = format!("synthetic-profile-token-{}", std::process::id());
381
+ let tail_sentinel = "synthetic-truncation-tail";
382
+ let body = json!({"error": {"message": format!(
383
+ "HTTPS_PROXY='{}' explicit={} {}{}",
384
+ credential_url(&marker),
385
+ profile_token,
386
+ "x".repeat(600),
387
+ tail_sentinel
388
+ )}})
389
+ .to_string();
390
+ let server = MockLlmServer::new(401, body);
391
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
392
+ let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
393
+ let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
394
+ let result = cmd_preflight(&PreflightArgs { team, json: true })
395
+ .expect("profile smoke returns a diagnostic report");
396
+ let report = match result.output {
397
+ CmdOutput::Json(value) => value,
398
+ other => panic!("expected JSON preflight output, got {other:?}"),
399
+ };
400
+ let text = report.to_string();
401
+ let profile_policy_preserved = !text.contains(&marker)
402
+ && !text.contains(&profile_token)
403
+ && !text.contains(tail_sentinel)
404
+ && text.contains(REDACTED)
405
+ && text.contains("proxy.invalid:8443/path")
406
+ && server.was_called();
407
+
408
+ assert!(
409
+ delegates_to_shared && pane_policy_preserved && profile_policy_preserved,
410
+ "RED7: pane/profile scrubbers must delegate generic env/URL masking to the shared module while retaining pane token/hex policy and profile explicit-token/truncation policy"
411
+ );
412
+ }
413
+
414
+ fn redaction_case(
415
+ tag: &str,
416
+ marker: &str,
417
+ team_key: &str,
418
+ ) -> (hermetic_guard::HermeticTestEnv, PathBuf) {
419
+ let env = hermetic_guard::HermeticTestEnv::enter(tag);
420
+ env.scrub_tmux();
421
+ env.assert_no_real_tmux();
422
+ let workspace = env.workspace(tag);
423
+ seed_redaction_state(&workspace, marker, team_key);
424
+ (env, workspace)
425
+ }
426
+
427
+ fn seed_redaction_state(workspace: &Path, marker: &str, team_key: &str) {
428
+ let diagnostic = restart_diagnostic(marker);
429
+ let mixed = mixed_sensitive_value(marker);
430
+ let agent = json!({
431
+ "agent_id": "worker",
432
+ "provider": "codex",
433
+ "model": "gpt-contract",
434
+ "provider_source": "role",
435
+ "model_source": "role",
436
+ "model_stale": true,
437
+ "auth_mode": "subscription",
438
+ "status": "failed",
439
+ "restart_error": diagnostic,
440
+ "session_id": "session-contract",
441
+ "pane_id": "%contract",
442
+ });
443
+ save_runtime_state(
444
+ workspace,
445
+ &json!({
446
+ "active_team_key": team_key,
447
+ "session_name": "team-redaction-contract",
448
+ "leader": {"id": "leader"},
449
+ "agents": {"worker": agent.clone()},
450
+ "teams": {
451
+ (team_key): {
452
+ "agents": {"worker": agent},
453
+ "diagnostics": mixed,
454
+ "tasks": []
455
+ }
456
+ },
457
+ "tasks": []
458
+ }),
459
+ )
460
+ .unwrap();
461
+ let _ = MessageStore::open(workspace).unwrap();
462
+ }
463
+
464
+ fn mixed_sensitive_value(marker: &str) -> Value {
465
+ let diagnostic = restart_diagnostic(marker);
466
+ json!({
467
+ "HTTPS_PROXY": marker,
468
+ "http_proxy": marker,
469
+ "Copilot_Github_Token": marker,
470
+ "OPENAI_API_KEY": marker,
471
+ "db_password": marker,
472
+ "client_auth": marker,
473
+ "client_secret": marker,
474
+ "service_credential": marker,
475
+ "ordinary_diagnostic": format!(
476
+ "HTTPS_PROXY='{}' http_proxy=\"{}\" OPENAI_API_KEY={} endpoint={}",
477
+ credential_url(marker), credential_url(marker), marker, credential_url(marker)
478
+ ),
479
+ "nested": [{"values": [diagnostic.clone(), credential_url(marker)]}],
480
+ "team_key": "current",
481
+ "auth_mode": "subscription",
482
+ "provider": "codex",
483
+ "model": "gpt-contract",
484
+ "provider_source": "role",
485
+ "model_source": "role",
486
+ "model_stale": true,
487
+ "session_id": "session-contract",
488
+ "pane_id": "%contract",
489
+ "monkey": "banana",
490
+ "author": "operator",
491
+ "normal_url": "https://proxy.invalid/health",
492
+ "already_redacted": REDACTED,
493
+ })
494
+ }
495
+
496
+ fn synthetic_marker() -> String {
497
+ static NEXT: AtomicU64 = AtomicU64::new(0);
498
+ format!(
499
+ "synthetic-redact-marker-{}-{}",
500
+ std::process::id(),
501
+ NEXT.fetch_add(1, Ordering::Relaxed)
502
+ )
503
+ }
504
+
505
+ fn credential_url(marker: &str) -> String {
506
+ format!("https://demo-user:{marker}@proxy.invalid:8443/path")
507
+ }
508
+
509
+ fn restart_diagnostic(marker: &str) -> String {
510
+ format!(
511
+ "subprocess exited 37: argv=[tmux, new-window, HTTPS_PROXY='{}']; endpoint={}; stderr=synthetic transport failure",
512
+ credential_url(marker),
513
+ credential_url(marker)
514
+ )
515
+ }
516
+
517
+ fn run_mcp_status_frames(
518
+ env: &hermetic_guard::HermeticTestEnv,
519
+ workspace: &Path,
520
+ team_key: &str,
521
+ ) -> Vec<Value> {
522
+ let mut child = Command::new(crate::lifecycle::tests::test_binary_path())
523
+ .args(["mcp-server", "--workspace"])
524
+ .arg(workspace)
525
+ .current_dir(workspace)
526
+ .env("HOME", env.home())
527
+ .env("TEAM_AGENT_ID", "worker")
528
+ .env("TEAM_AGENT_OWNER_TEAM_ID", team_key)
529
+ .env_remove("TMUX")
530
+ .env_remove("TMUX_PANE")
531
+ .stdin(Stdio::piped())
532
+ .stdout(Stdio::piped())
533
+ .stderr(Stdio::null())
534
+ .spawn()
535
+ .expect("spawn real MCP stdio server");
536
+ let rpc = json!({
537
+ "jsonrpc": "2.0",
538
+ "id": 1,
539
+ "method": "tools/call",
540
+ "params": {"name": "get_team_status", "arguments": {}}
541
+ });
542
+ let legacy = json!({"tool": "get_team_status", "arguments": {}});
543
+ {
544
+ let stdin = child.stdin.as_mut().expect("MCP stdin");
545
+ writeln!(stdin, "{rpc}").unwrap();
546
+ writeln!(stdin, "{legacy}").unwrap();
547
+ }
548
+ drop(child.stdin.take());
549
+ let output = child.wait_with_output().expect("wait for MCP EOF");
550
+ assert!(output.status.success(), "MCP stdio exits cleanly at EOF");
551
+ String::from_utf8(output.stdout)
552
+ .unwrap()
553
+ .lines()
554
+ .map(|line| serde_json::from_str(line).expect("MCP response JSON"))
555
+ .collect()
556
+ }
557
+
558
+ fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
559
+ let path = workspace.join("invalid-provider.spec.yaml");
560
+ fs::write(
561
+ &path,
562
+ format!(
563
+ "version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
564
+ workspace.display(),
565
+ credential_url(marker)
566
+ ),
567
+ )
568
+ .unwrap();
569
+ path
570
+ }
571
+
572
+ fn read_cli_error_logs(workspace: &Path) -> String {
573
+ let dir = workspace.join(".team/logs");
574
+ fs::read_dir(dir)
575
+ .into_iter()
576
+ .flatten()
577
+ .filter_map(Result::ok)
578
+ .filter(|entry| {
579
+ entry
580
+ .file_name()
581
+ .to_string_lossy()
582
+ .starts_with("cli-error-")
583
+ })
584
+ .filter_map(|entry| fs::read_to_string(entry.path()).ok())
585
+ .collect::<Vec<_>>()
586
+ .join("\n")
587
+ }
588
+
589
+ #[cfg(unix)]
590
+ fn pane_excerpt_uses_shared_redaction(env: &hermetic_guard::HermeticTestEnv, marker: &str) -> bool {
591
+ let workspace = env.workspace("red7-pane");
592
+ let shim_dir = env.root().join("pane-bin");
593
+ fs::create_dir_all(&shim_dir).unwrap();
594
+ let shim = shim_dir.join("tmux");
595
+ let submitted = env.root().join("pane-submitted");
596
+ fs::write(
597
+ &shim,
598
+ r#"#!/bin/sh
599
+ case " $* " in
600
+ *" capture-pane "*)
601
+ if [ -f "$TEAM_AGENT_TEST_PANE_SUBMITTED" ]; then
602
+ printf '%s\n' "$TEAM_AGENT_TEST_PANE_AFTER"
603
+ else
604
+ printf '%s\n' "$TEAM_AGENT_TEST_PANE_BEFORE"
605
+ fi
606
+ exit 0
607
+ ;;
608
+ *" send-keys "*)
609
+ : > "$TEAM_AGENT_TEST_PANE_SUBMITTED"
610
+ exit 0
611
+ ;;
612
+ *) exit 0 ;;
613
+ esac
614
+ "#,
615
+ )
616
+ .unwrap();
617
+ let mut permissions = fs::metadata(&shim).unwrap().permissions();
618
+ permissions.set_mode(0o755);
619
+ fs::set_permissions(&shim, permissions).unwrap();
620
+ let real_path = std::env::var_os("PATH").unwrap_or_default();
621
+ let token = "[team-agent-token:synthetic-redact-pane]";
622
+ let diagnostic = restart_diagnostic(marker);
623
+ let before = format!("message {token}\n{diagnostic}");
624
+ let after = format!(
625
+ "{diagnostic}\n{}{}\n{}{}\nBearer {}.part\nhex {}\nplain pane diagnostic",
626
+ "sk-",
627
+ marker,
628
+ "ghp_",
629
+ marker,
630
+ marker,
631
+ "a".repeat(40)
632
+ );
633
+ let _guards = [
634
+ env.with_env(
635
+ "PATH",
636
+ &format!("{}:{}", shim_dir.display(), real_path.to_string_lossy()),
637
+ ),
638
+ env.with_env(
639
+ "TEAM_AGENT_TEST_PANE_SUBMITTED",
640
+ submitted.to_str().unwrap(),
641
+ ),
642
+ env.with_env("TEAM_AGENT_TEST_PANE_BEFORE", &before),
643
+ env.with_env("TEAM_AGENT_TEST_PANE_AFTER", &after),
644
+ ];
645
+ let backend = TmuxBackend::for_workspace(&workspace);
646
+ let report = backend
647
+ .inject(
648
+ &Target::Pane(PaneId::new("%redact-contract")),
649
+ &InjectPayload::Text(format!("message {token}")),
650
+ Key::Enter,
651
+ false,
652
+ )
653
+ .expect("scripted pane injection reaches submit diagnostics");
654
+ let excerpts = report
655
+ .submit_diagnostics
656
+ .into_iter()
657
+ .flat_map(|diagnostics| diagnostics.attempts_detail)
658
+ .map(|observation| observation.pane_tail_excerpt)
659
+ .collect::<Vec<_>>()
660
+ .join("\n");
661
+ !excerpts.contains(marker)
662
+ && excerpts.contains(REDACTED)
663
+ && excerpts.contains("REDACTED_HEX")
664
+ && excerpts.contains("plain pane diagnostic")
665
+ }
666
+
667
+ #[cfg(not(unix))]
668
+ fn pane_excerpt_uses_shared_redaction(
669
+ _env: &hermetic_guard::HermeticTestEnv,
670
+ _marker: &str,
671
+ ) -> bool {
672
+ true
673
+ }
674
+
675
+ fn write_profile_fixture(root: &Path, base_url: &str, token: &str) -> PathBuf {
676
+ let team = root.join("profile-team");
677
+ fs::create_dir_all(team.join("agents")).unwrap();
678
+ fs::create_dir_all(team.join("profiles")).unwrap();
679
+ fs::write(
680
+ team.join("profiles/local.env"),
681
+ format!(
682
+ "ANTHROPIC_BASE_URL={base_url}\nANTHROPIC_AUTH_TOKEN={token}\nANTHROPIC_MODEL=mock-model\n"
683
+ ),
684
+ )
685
+ .unwrap();
686
+ fs::write(
687
+ team.join("TEAM.md"),
688
+ "---\nname: redact-profile\nobjective: Synthetic redaction profile contract.\nprovider: claude\nauth_mode: compatible_api\n---\n\nTeam.\n",
689
+ )
690
+ .unwrap();
691
+ fs::write(
692
+ team.join("agents/worker.md"),
693
+ "---\nname: worker\nrole: Worker\nprovider: claude\nauth_mode: compatible_api\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n",
694
+ )
695
+ .unwrap();
696
+ team
697
+ }
698
+
699
+ struct MockLlmServer {
700
+ url: String,
701
+ called: Arc<AtomicBool>,
702
+ handle: Option<thread::JoinHandle<()>>,
703
+ }
704
+
705
+ impl MockLlmServer {
706
+ fn new(status: u16, body: String) -> Self {
707
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
708
+ listener.set_nonblocking(true).unwrap();
709
+ let addr = listener.local_addr().unwrap();
710
+ let called = Arc::new(AtomicBool::new(false));
711
+ let called_in_thread = called.clone();
712
+ let handle = thread::spawn(move || {
713
+ let deadline = Instant::now() + Duration::from_secs(2);
714
+ while Instant::now() < deadline {
715
+ match listener.accept() {
716
+ Ok((mut stream, _)) => {
717
+ called_in_thread.store(true, Ordering::SeqCst);
718
+ let mut buffer = [0_u8; 4096];
719
+ let _ = stream.read(&mut buffer);
720
+ let response = format!(
721
+ "HTTP/1.1 {status} test\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
722
+ body.len()
723
+ );
724
+ let _ = stream.write_all(response.as_bytes());
725
+ return;
726
+ }
727
+ Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
728
+ thread::sleep(Duration::from_millis(20));
729
+ }
730
+ Err(_) => return,
731
+ }
732
+ }
733
+ });
734
+ Self {
735
+ url: format!("http://{addr}/v1"),
736
+ called,
737
+ handle: Some(handle),
738
+ }
739
+ }
740
+
741
+ fn base_url(&self) -> &str {
742
+ &self.url
743
+ }
744
+
745
+ fn was_called(&self) -> bool {
746
+ self.called.load(Ordering::SeqCst)
747
+ }
748
+ }
749
+
750
+ impl Drop for MockLlmServer {
751
+ fn drop(&mut self) {
752
+ if let Some(handle) = self.handle.take() {
753
+ let _ = handle.join();
754
+ }
755
+ }
756
+ }
757
+
758
+ #[test]
759
+ #[serial(env)]
760
+ fn red8_bearer_token_inside_free_text_is_masked_without_over_redaction() {
761
+ let marker = synthetic_marker();
762
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red8");
763
+ env.scrub_tmux();
764
+ env.assert_no_real_tmux();
765
+ let workspace = env.workspace("red8");
766
+ let log = EventLog::new(&workspace);
767
+ // Attack-input shape (case.md section 2.4 / arch delta-smell6 probe): a
768
+ // user-controlled prompt travels as free text INSIDE an argv value
769
+ // (worker_command_context -> codex.rs `-c developer_instructions=<prompt>`
770
+ // -> launch.rs spawn_argv event). The bearer token hides in prose - not
771
+ // under a sensitive key, assignment key, or flag-value pair - so the
772
+ // shape matrix must map the attack input, not the scrubber branches.
773
+ let written = log
774
+ .write(
775
+ "provider.worker.spawn_argv",
776
+ json!({
777
+ "argv": [
778
+ "codex",
779
+ "-c",
780
+ format!(
781
+ "developer_instructions=You are w1. Call the api with Bearer {marker} and report back."
782
+ ),
783
+ ],
784
+ }),
785
+ )
786
+ .unwrap();
787
+ let events_path = workspace.join(".team").join("logs").join("events.jsonl");
788
+ let bytes = std::fs::read_to_string(&events_path).unwrap();
789
+ let leak_free = !bytes.contains(&marker) && bytes.contains(REDACTED);
790
+
791
+ // Over-redaction anchor: prose where "Bearer" is ordinary language (the
792
+ // successor word is not token-shaped) must survive verbatim.
793
+ log.write(
794
+ "provider.worker.spawn_argv",
795
+ json!({
796
+ "argv": [
797
+ "codex",
798
+ "-c",
799
+ "developer_instructions=A bearer of good news, Bearer of the ring.",
800
+ ],
801
+ }),
802
+ )
803
+ .unwrap();
804
+ let bytes = std::fs::read_to_string(&events_path).unwrap();
805
+ let benign_preserved = bytes.contains("Bearer of the ring");
806
+
807
+ // Idempotency at the tail boundary, same discipline as RED5.
808
+ let tail = log.tail(0).unwrap();
809
+ let idempotent = tail.first() == Some(&written);
810
+
811
+ assert!(
812
+ leak_free && benign_preserved && idempotent,
813
+ "RED8: a bearer token inside user-controlled free text must be masked at event write while ordinary Bearer prose survives and redaction stays idempotent; leak_free={leak_free} benign_preserved={benign_preserved} idempotent={idempotent}"
814
+ );
815
+ }
816
+
817
+ #[test]
818
+ #[serial(env)]
819
+ fn red8b_bearer_terminal_matrix_masks_every_rfc6750_tail_with_zero_remnant() {
820
+ let env = hermetic_guard::HermeticTestEnv::enter("redact-red8b");
821
+ env.scrub_tmux();
822
+ env.assert_no_real_tmux();
823
+ let workspace = env.workspace("red8b");
824
+ let log = EventLog::new(&workspace);
825
+ // Boundary-value matrix (leader final ruling via r10): the token tail may
826
+ // be ANY RFC6750 b64token character. Exactly-min-length tokens and long
827
+ // tokens must both be fully masked with ZERO token remnant - over-masking
828
+ // a sentence period is accepted, a surviving token fragment is not.
829
+ let tails = ['-', '.', '~', '+', '/', '='];
830
+ for (index, tail) in tails.iter().enumerate() {
831
+ let core = format!("bnd{index}fake{index}core");
832
+ let min_token = format!("ab{core}{tail}");
833
+ let long_token = format!("ab{core}x{core}{tail}");
834
+ for token in [min_token, long_token] {
835
+ log.write(
836
+ "provider.worker.spawn_argv",
837
+ json!({
838
+ "argv": [
839
+ "codex",
840
+ "-c",
841
+ format!("developer_instructions=call the api with Bearer {token} then stop."),
842
+ ],
843
+ }),
844
+ )
845
+ .unwrap();
846
+ }
847
+ }
848
+ let events_path = workspace.join(".team").join("logs").join("events.jsonl");
849
+ let bytes = std::fs::read_to_string(&events_path).unwrap();
850
+ let mut failures = Vec::new();
851
+ for (index, tail) in tails.iter().enumerate() {
852
+ let core = format!("bnd{index}fake{index}core");
853
+ if bytes.contains(&core) {
854
+ failures.push(format!("token body leaked for tail {tail:?}: {core}"));
855
+ }
856
+ let remnant = format!("{REDACTED}{tail}");
857
+ if bytes.contains(&remnant) {
858
+ failures.push(format!("token tail remnant survived masking: {remnant}"));
859
+ }
860
+ }
861
+ if !bytes.contains(REDACTED) {
862
+ failures.push("no [REDACTED] marker written at all".to_string());
863
+ }
864
+ assert!(
865
+ failures.is_empty(),
866
+ "RED8B: every RFC6750 terminal character must be masked with zero token remnant at exact-min and long lengths.\n{}",
867
+ failures.join("\n")
868
+ );
869
+ }