@team-agent/installer 0.5.61 → 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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
- package/crates/team-agent/src/coordinator/tick.rs +24 -28
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
//! agent-clone-fork · successor RED batch 4 (verifier) — R8 Copilot fork
|
|
2
|
+
//! (caps re-charter) + R7 permission-clamp GUARDRAIL.
|
|
3
|
+
//!
|
|
4
|
+
//! From locate.md §7 R7/R8 + §1.4 (Copilot 1.0.61 real-machine proof: directory
|
|
5
|
+
//! copy + SQLite transaction under COPILOT_HOME is feasible) + §3 surface
|
|
6
|
+
//! (`provider/adapter.rs` Copilot caps=false; `provider/session_scan/copilot.rs`
|
|
7
|
+
//! hard-reads `$HOME/.copilot`; `fork_agent.rs:173-288` provider/permission
|
|
8
|
+
//! inheritance) + MUST-16, NOT the §4 design. Baseline `9e6be51` (v0.5.52).
|
|
9
|
+
//!
|
|
10
|
+
//! - **R8 Copilot fork caps RE-CHARTER (RED + verifier signature)**: baseline
|
|
11
|
+
//! locks "Copilot fork MUST be refused (capability unsupported)"
|
|
12
|
+
//! (`copilot_provider_red.rs::copilot_fork_returns_structured_capability_unsupported`).
|
|
13
|
+
//! §1.4 proves Copilot CAN fork (directory copy + atomic SQLite transaction
|
|
14
|
+
//! under an isolated COPILOT_HOME). This batch RED asserts that a Copilot fork
|
|
15
|
+
//! is no longer rejected by a blanket capability gate. Baseline red: the fork
|
|
16
|
+
//! is refused with a copilot/fork capability-unsupported error. Retiring the old
|
|
17
|
+
//! "must refuse" contract is a re-charter that REQUIRES a verifier signature —
|
|
18
|
+
//! recorded in
|
|
19
|
+
//! `.team/artifacts/pipeline-runs/agent-clone-fork/verifier-r8-caps-recharter-signoff.md`.
|
|
20
|
+
//! (The SQLite foreign-key table integrity + exact-nonce recall are the GREEN
|
|
21
|
+
//! half / subscription-E2E per §1.4 — not asserted offline here.)
|
|
22
|
+
//!
|
|
23
|
+
//! - **R7 permission-clamp GUARDRAIL (naturally-green, not a RED, per L1 r2)**: a
|
|
24
|
+
//! fork's effective approval policy must be derived from the LEADER
|
|
25
|
+
//! (`source=leader_process`, `worker_capability_above_leader=false`), never a
|
|
26
|
+
//! raw clone of a source-declared capability above the leader (MUST-16).
|
|
27
|
+
//! Measured: baseline already derives approval from the leader and marks
|
|
28
|
+
//! `worker_capability_above_leader=false`, so there is no offline structural
|
|
29
|
+
//! red. Following the L1 r2 naturally-green-guardrail precedent, this pins the
|
|
30
|
+
//! clamp as a guardrail whose teeth engage after implement recompiles the role
|
|
31
|
+
//! via RoleSourceResolver (§4.2): any impl that raw-clones a compiled permission
|
|
32
|
+
//! above the leader trips it on the spot. R7's escalation scenario (a source
|
|
33
|
+
//! role declaring capability above the leader) cannot be constructed offline —
|
|
34
|
+
//! the role model forces `permission_mode=restricted` and inherits approval from
|
|
35
|
+
//! the leader — so the true escalation guard is a subscription/real-machine
|
|
36
|
+
//! concern; the guardrail defends the invariant that already holds.
|
|
37
|
+
//!
|
|
38
|
+
//! R9 clone-semantics is NOT frozen in this batch: offline, `clone-agent` does not
|
|
39
|
+
//! exist (already RED in batch1 R10), and its distinguishing semantics
|
|
40
|
+
//! (fresh-session-distinct-from-fork, carries no source nonce) are subscription
|
|
41
|
+
//! real-machine concerns (locate §4.5, §5 obs 6). It is recorded against R10 +
|
|
42
|
+
//! the E2E gate, not asserted offline here.
|
|
43
|
+
|
|
44
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
45
|
+
|
|
46
|
+
use std::collections::BTreeMap;
|
|
47
|
+
use std::path::{Path, PathBuf};
|
|
48
|
+
use std::process::Output;
|
|
49
|
+
use std::sync::Mutex;
|
|
50
|
+
|
|
51
|
+
use serde_json::{json, Value};
|
|
52
|
+
use team_agent::lifecycle::launch::fork_agent_with_transport;
|
|
53
|
+
use team_agent::lifecycle::quick_start_with_transport_in_workspace;
|
|
54
|
+
use team_agent::model::ids::AgentId;
|
|
55
|
+
use team_agent::state::persist::{load_runtime_state, save_runtime_state};
|
|
56
|
+
use team_agent::transport::{
|
|
57
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
58
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
|
|
59
|
+
SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
|
|
60
|
+
TurnVerification, WindowName,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
64
|
+
mod hermetic_guard;
|
|
65
|
+
use hermetic_guard::HermeticTestEnv;
|
|
66
|
+
|
|
67
|
+
const TEAM_NAME: &str = "cf-batch4";
|
|
68
|
+
const SOURCE: &str = "cp_worker";
|
|
69
|
+
const NEW: &str = "cp_fork";
|
|
70
|
+
|
|
71
|
+
/// Gate6 CI-hermeticity revision: the R7 fixture must pin the process-ancestry
|
|
72
|
+
/// seam explicitly (`TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON`) instead of
|
|
73
|
+
/// depending on the REAL ancestry of the test-runner host. Unpinned, a local
|
|
74
|
+
/// runner descending from a dangerous-bypass leader yields
|
|
75
|
+
/// `source=leader_process` (historical local green) while a clean CI runner
|
|
76
|
+
/// correctly yields `source=disabled` — a non-hermetic assertion, not a product
|
|
77
|
+
/// regression. Precedent: `codex_mcp_approval_inheritance_red.rs` `MockAncestry`
|
|
78
|
+
/// (pins both bypass and restricted leader argv).
|
|
79
|
+
const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
|
|
80
|
+
const CODEX_BYPASS: &str = "--dangerously-bypass-approvals-and-sandbox";
|
|
81
|
+
const RESTRICTED_LEADER_ARGV: &[&str] = &[
|
|
82
|
+
"codex",
|
|
83
|
+
"--sandbox",
|
|
84
|
+
"read-only",
|
|
85
|
+
"--ask-for-approval",
|
|
86
|
+
"on-request",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
struct Case {
|
|
90
|
+
env: HermeticTestEnv,
|
|
91
|
+
workspace: PathBuf,
|
|
92
|
+
shim_path: String,
|
|
93
|
+
ancestry_json: String,
|
|
94
|
+
socket: Option<PathBuf>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Case {
|
|
98
|
+
/// R7 (permission-clamp guardrail) uses a claude source with a success-path
|
|
99
|
+
/// backing shim so the fork actually produces a NEW row whose permission clamp
|
|
100
|
+
/// can be inspected — without a real fork the guardrail would read no row and
|
|
101
|
+
/// pass vacuously. (R8 does not use Case: it exercises the library entry with
|
|
102
|
+
/// an in-process RecordingTransport — see `r8_...`.)
|
|
103
|
+
fn start(tag: &str) -> Self {
|
|
104
|
+
Self::start_with_ancestry(tag, &["codex", CODEX_BYPASS])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Every CLI invocation of this Case pins `ancestry_argv` through the
|
|
108
|
+
/// product's existing test seam so the derived approval policy is a
|
|
109
|
+
/// function of the FIXTURE, never of the host process tree.
|
|
110
|
+
fn start_with_ancestry(tag: &str, ancestry_argv: &[&str]) -> Self {
|
|
111
|
+
let env = HermeticTestEnv::enter(tag);
|
|
112
|
+
let workspace = env.workspace("ws");
|
|
113
|
+
write_team_docs(&workspace, "claude");
|
|
114
|
+
let shim_dir = write_claude_backing_shim(&workspace);
|
|
115
|
+
let shim_path = format!(
|
|
116
|
+
"{}:{}",
|
|
117
|
+
shim_dir.display(),
|
|
118
|
+
std::env::var("PATH").expect("PATH present")
|
|
119
|
+
);
|
|
120
|
+
let mut case = Self {
|
|
121
|
+
env,
|
|
122
|
+
workspace,
|
|
123
|
+
shim_path,
|
|
124
|
+
ancestry_json: serde_json::to_string(ancestry_argv).expect("ancestry argv json"),
|
|
125
|
+
socket: None,
|
|
126
|
+
};
|
|
127
|
+
case.quick_start();
|
|
128
|
+
case.seed_source_session_tuple();
|
|
129
|
+
case
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn ws(&self) -> &str {
|
|
133
|
+
self.workspace.to_str().expect("ws utf8")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn state_path(&self) -> PathBuf {
|
|
137
|
+
self.workspace
|
|
138
|
+
.join(".team")
|
|
139
|
+
.join("runtime")
|
|
140
|
+
.join("state.json")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn run(&self, args: &[&str]) -> Output {
|
|
144
|
+
self.env.run_cli_env(
|
|
145
|
+
&self.workspace,
|
|
146
|
+
args,
|
|
147
|
+
&[
|
|
148
|
+
("PATH", self.shim_path.as_str()),
|
|
149
|
+
(ANCESTRY_ENV, self.ancestry_json.as_str()),
|
|
150
|
+
],
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn quick_start(&mut self) {
|
|
155
|
+
let out = self.run(&[
|
|
156
|
+
"quick-start",
|
|
157
|
+
self.ws(),
|
|
158
|
+
"--workspace",
|
|
159
|
+
self.ws(),
|
|
160
|
+
"--name",
|
|
161
|
+
TEAM_NAME,
|
|
162
|
+
"--yes",
|
|
163
|
+
"--json",
|
|
164
|
+
]);
|
|
165
|
+
if let Ok(v) = serde_json::from_slice::<Value>(&out.stdout) {
|
|
166
|
+
if let Some(cmd) = v
|
|
167
|
+
.get("attach_commands")
|
|
168
|
+
.and_then(|a| a.as_array())
|
|
169
|
+
.and_then(|a| a.first())
|
|
170
|
+
.and_then(|c| c.as_str())
|
|
171
|
+
.or_else(|| v.get("leader_attach_command").and_then(|c| c.as_str()))
|
|
172
|
+
{
|
|
173
|
+
let toks: Vec<&str> = cmd.split_whitespace().collect();
|
|
174
|
+
if let Some(i) = toks.iter().position(|t| *t == "-S") {
|
|
175
|
+
if let Some(sock) = toks.get(i + 1) {
|
|
176
|
+
self.socket = Some(PathBuf::from(*sock));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Seed the SOURCE session tuple so the fork reaches the capability gate
|
|
184
|
+
/// (not a missing-session gate) — mirrors the old refusal contract's
|
|
185
|
+
/// `seed_session_id`, which existed precisely so "the capability gate is what
|
|
186
|
+
/// fires".
|
|
187
|
+
fn seed_source_session_tuple(&self) {
|
|
188
|
+
// A real rollout file the source tuple points at.
|
|
189
|
+
let rollout_file = self.workspace.join("fixture-source-rollout.jsonl");
|
|
190
|
+
let _ = std::fs::write(&rollout_file, "{\"type\":\"fixture-source\"}\n");
|
|
191
|
+
let Ok(raw) = std::fs::read_to_string(self.state_path()) else {
|
|
192
|
+
return;
|
|
193
|
+
};
|
|
194
|
+
let Ok(mut state) = serde_json::from_str::<Value>(&raw) else {
|
|
195
|
+
return;
|
|
196
|
+
};
|
|
197
|
+
let tuple = json!({
|
|
198
|
+
"session_id": "sess-cf-batch4-source",
|
|
199
|
+
"rollout_path": rollout_file.to_string_lossy(),
|
|
200
|
+
"captured_at": "2026-07-21T00:00:00Z",
|
|
201
|
+
"captured_via": "contract-fixture"
|
|
202
|
+
});
|
|
203
|
+
let mut patch = |row: &mut Value| {
|
|
204
|
+
if let Some(obj) = row.as_object_mut() {
|
|
205
|
+
for (k, v) in tuple.as_object().unwrap() {
|
|
206
|
+
obj.insert(k.clone(), v.clone());
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
if let Some(row) = state.get_mut("agents").and_then(|a| a.get_mut(SOURCE)) {
|
|
211
|
+
patch(row);
|
|
212
|
+
}
|
|
213
|
+
if let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) {
|
|
214
|
+
for team in teams.values_mut() {
|
|
215
|
+
if let Some(row) = team.get_mut("agents").and_then(|a| a.get_mut(SOURCE)) {
|
|
216
|
+
patch(row);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
let _ = std::fs::write(
|
|
221
|
+
self.state_path(),
|
|
222
|
+
serde_json::to_string_pretty(&state).unwrap(),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn fork(&self, as_name: &str) -> Value {
|
|
227
|
+
let out = self.run(&[
|
|
228
|
+
"fork-agent",
|
|
229
|
+
SOURCE,
|
|
230
|
+
"--as",
|
|
231
|
+
as_name,
|
|
232
|
+
"--workspace",
|
|
233
|
+
self.ws(),
|
|
234
|
+
"--team",
|
|
235
|
+
TEAM_NAME,
|
|
236
|
+
"--no-display",
|
|
237
|
+
"--json",
|
|
238
|
+
]);
|
|
239
|
+
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
240
|
+
serde_json::from_slice::<Value>(&out.stdout).unwrap_or_else(|_| {
|
|
241
|
+
json!({
|
|
242
|
+
"ok": out.status.success(),
|
|
243
|
+
"_exit": out.status.code(),
|
|
244
|
+
"_stderr": stderr,
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn team_agent_rows(&self) -> serde_json::Map<String, Value> {
|
|
250
|
+
let Ok(raw) = std::fs::read_to_string(self.state_path()) else {
|
|
251
|
+
return serde_json::Map::new();
|
|
252
|
+
};
|
|
253
|
+
let Ok(v) = serde_json::from_str::<Value>(&raw) else {
|
|
254
|
+
return serde_json::Map::new();
|
|
255
|
+
};
|
|
256
|
+
if let Some(teams) = v.get("teams").and_then(Value::as_object) {
|
|
257
|
+
for team in teams.values() {
|
|
258
|
+
if let Some(agents) = team.get("agents").and_then(Value::as_object) {
|
|
259
|
+
if agents.contains_key(SOURCE) {
|
|
260
|
+
return agents.clone();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
serde_json::Map::new()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
impl Drop for Case {
|
|
270
|
+
fn drop(&mut self) {
|
|
271
|
+
if let Some(sock) = &self.socket {
|
|
272
|
+
let _ = std::process::Command::new("tmux")
|
|
273
|
+
.args(["-S", sock.to_str().unwrap_or(""), "kill-server"])
|
|
274
|
+
.output();
|
|
275
|
+
}
|
|
276
|
+
let ws = self.workspace.to_string_lossy().to_string();
|
|
277
|
+
if let Ok(out) = std::process::Command::new("ps")
|
|
278
|
+
.args(["-axo", "pid=,command="])
|
|
279
|
+
.output()
|
|
280
|
+
{
|
|
281
|
+
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
|
282
|
+
if line.contains(&ws) {
|
|
283
|
+
if let Some(pid) = line.split_whitespace().next() {
|
|
284
|
+
let _ = std::process::Command::new("kill")
|
|
285
|
+
.args(["-TERM", pid])
|
|
286
|
+
.output();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let _ = &self.env;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn write_team_docs(workspace: &Path, provider: &str) {
|
|
296
|
+
std::fs::create_dir_all(workspace.join("agents")).expect("create agents dir");
|
|
297
|
+
std::fs::write(
|
|
298
|
+
workspace.join("TEAM.md"),
|
|
299
|
+
format!(
|
|
300
|
+
"---\nname: {TEAM_NAME}\nobjective: clone-fork batch4.\nprovider: {provider}\n---\n"
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
.expect("write TEAM.md");
|
|
304
|
+
std::fs::write(
|
|
305
|
+
workspace.join("agents").join(format!("{SOURCE}.md")),
|
|
306
|
+
format!(
|
|
307
|
+
"---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
.expect("write source role doc");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Copilot team docs for the library-entry R8 (a separate agents dir so the
|
|
314
|
+
/// OfflineTransport quick-start compiles a copilot source).
|
|
315
|
+
fn write_copilot_team_docs(workspace: &Path) -> PathBuf {
|
|
316
|
+
let team_dir = workspace.join("cf-r8-team");
|
|
317
|
+
std::fs::create_dir_all(team_dir.join("agents")).expect("create team agents dir");
|
|
318
|
+
std::fs::write(
|
|
319
|
+
team_dir.join("TEAM.md"),
|
|
320
|
+
format!("---\nname: {TEAM_NAME}\nobjective: clone-fork batch4 copilot recharter.\nprovider: copilot\n---\n\nTeam fixture.\n"),
|
|
321
|
+
)
|
|
322
|
+
.expect("write TEAM.md");
|
|
323
|
+
std::fs::write(
|
|
324
|
+
team_dir.join("agents").join(format!("{SOURCE}.md")),
|
|
325
|
+
format!(
|
|
326
|
+
"---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
.expect("write source role doc");
|
|
330
|
+
team_dir
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Seed the SOURCE's complete session tuple directly in runtime state (library
|
|
334
|
+
/// path) so the copilot fork reaches the capability gate, not a missing-session
|
|
335
|
+
/// gate. Mirrors the old refusal contract's `seed_session_id`.
|
|
336
|
+
fn seed_source_tuple_in_state(workspace: &Path, agent: &str) {
|
|
337
|
+
let mut state = load_runtime_state(workspace).expect("load runtime state");
|
|
338
|
+
let source_session_id = "99999999-aaaa-4bbb-8ccc-dddddddddddd";
|
|
339
|
+
let copilot_home =
|
|
340
|
+
PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME")).join(".copilot");
|
|
341
|
+
let source_dir = copilot_home.join("session-state").join(source_session_id);
|
|
342
|
+
std::fs::create_dir_all(&source_dir).expect("create Copilot source state");
|
|
343
|
+
std::fs::write(
|
|
344
|
+
source_dir.join("context.json"),
|
|
345
|
+
format!("{{\"session_id\":\"{source_session_id}\"}}\n"),
|
|
346
|
+
)
|
|
347
|
+
.expect("write Copilot source state");
|
|
348
|
+
let store = rusqlite::Connection::open(copilot_home.join("session-store.db"))
|
|
349
|
+
.expect("open Copilot source store");
|
|
350
|
+
store
|
|
351
|
+
.execute_batch(
|
|
352
|
+
"create table sessions (id text primary key);
|
|
353
|
+
create table turns (session_id text);
|
|
354
|
+
create table checkpoints (session_id text);
|
|
355
|
+
create table session_files (session_id text);
|
|
356
|
+
create table session_refs (session_id text);
|
|
357
|
+
create table forge_trajectory_events (session_id text);
|
|
358
|
+
create table search_index (session_id text);",
|
|
359
|
+
)
|
|
360
|
+
.expect("create Copilot source schema");
|
|
361
|
+
store
|
|
362
|
+
.execute(
|
|
363
|
+
"insert into sessions(id) values (?1)",
|
|
364
|
+
rusqlite::params![source_session_id],
|
|
365
|
+
)
|
|
366
|
+
.expect("seed Copilot source session");
|
|
367
|
+
drop(store);
|
|
368
|
+
let tuple = json!({
|
|
369
|
+
"status": "running",
|
|
370
|
+
"provider": "copilot",
|
|
371
|
+
"session_id": source_session_id,
|
|
372
|
+
"rollout_path": copilot_home.join("session-store.db").to_string_lossy(),
|
|
373
|
+
"captured_at": "2026-07-21T00:00:00Z",
|
|
374
|
+
"captured_via": "contract-fixture",
|
|
375
|
+
});
|
|
376
|
+
let apply = |obj: &mut serde_json::Map<String, Value>| {
|
|
377
|
+
for (k, v) in tuple.as_object().unwrap() {
|
|
378
|
+
obj.insert(k.clone(), v.clone());
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
if let Some(obj) = state
|
|
382
|
+
.pointer_mut(&format!("/agents/{agent}"))
|
|
383
|
+
.and_then(Value::as_object_mut)
|
|
384
|
+
{
|
|
385
|
+
apply(obj);
|
|
386
|
+
}
|
|
387
|
+
if let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) {
|
|
388
|
+
for team in teams.values_mut() {
|
|
389
|
+
if let Some(obj) = team
|
|
390
|
+
.get_mut("agents")
|
|
391
|
+
.and_then(|a| a.get_mut(agent))
|
|
392
|
+
.and_then(Value::as_object_mut)
|
|
393
|
+
{
|
|
394
|
+
apply(obj);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
save_runtime_state(workspace, &state).expect("save seeded state");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Claude success-path shim producing a NEW backing keyed to the predetermined
|
|
402
|
+
/// `--session-id` (same mechanism as batch3): the R7 fork must actually complete
|
|
403
|
+
/// so a NEW state row exists to inspect for the permission clamp — otherwise the
|
|
404
|
+
/// guardrail reads no row and passes vacuously (false-green-hole type 8).
|
|
405
|
+
fn write_claude_backing_shim(workspace: &Path) -> PathBuf {
|
|
406
|
+
let bin_dir = workspace.join("shim-bin");
|
|
407
|
+
std::fs::create_dir_all(&bin_dir).expect("create shim dir");
|
|
408
|
+
let shim = bin_dir.join("claude");
|
|
409
|
+
std::fs::write(
|
|
410
|
+
&shim,
|
|
411
|
+
r#"#!/bin/sh
|
|
412
|
+
sid=""
|
|
413
|
+
prev=""
|
|
414
|
+
for a in "$@"; do
|
|
415
|
+
if [ "$prev" = "--session-id" ]; then sid="$a"; fi
|
|
416
|
+
prev="$a"
|
|
417
|
+
done
|
|
418
|
+
if [ -n "$sid" ]; then
|
|
419
|
+
enc=$(printf '%s' "$PWD" | sed 's/[^a-zA-Z0-9]/-/g')
|
|
420
|
+
dir="$HOME/.claude/projects/$enc"
|
|
421
|
+
mkdir -p "$dir"
|
|
422
|
+
printf '{"sessionId":"%s","type":"fork-backing"}\n' "$sid" > "$dir/$sid.jsonl"
|
|
423
|
+
fi
|
|
424
|
+
echo 'claude shim ready'
|
|
425
|
+
exec sleep 3600
|
|
426
|
+
"#,
|
|
427
|
+
)
|
|
428
|
+
.expect("write claude shim");
|
|
429
|
+
#[cfg(unix)]
|
|
430
|
+
{
|
|
431
|
+
use std::os::unix::fs::PermissionsExt;
|
|
432
|
+
std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755))
|
|
433
|
+
.expect("chmod claude shim");
|
|
434
|
+
}
|
|
435
|
+
bin_dir
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// R8 Copilot fork caps RE-CHARTER — a Copilot fork must no longer be refused by a
|
|
439
|
+
/// blanket capability gate (§1.4 proves the directory-copy + SQLite-transaction
|
|
440
|
+
/// primitive is feasible under an isolated COPILOT_HOME). Baseline red: the fork
|
|
441
|
+
/// is refused with a copilot/fork capability-unsupported error.
|
|
442
|
+
///
|
|
443
|
+
/// Retiring the old "Copilot fork must be refused" contract is a re-charter
|
|
444
|
+
/// requiring a verifier signature — see
|
|
445
|
+
/// `verifier-r8-caps-recharter-signoff.md`. The SQLite FK-table integrity and
|
|
446
|
+
/// exact-nonce recall remain the GREEN half / subscription-E2E (§1.4).
|
|
447
|
+
#[test]
|
|
448
|
+
fn r8_copilot_fork_is_no_longer_capability_refused() {
|
|
449
|
+
// R8 uses the LIBRARY entry `fork_agent_with_transport` (the same entry the old
|
|
450
|
+
// refusal contract exercises), because the CLI `fork-agent` path reports
|
|
451
|
+
// ok:true for a copilot source (registration-only success) and never reaches
|
|
452
|
+
// the capability gate — a CLI-shaped R8 could never be RED. An in-process
|
|
453
|
+
// RecordingTransport stands the team up without a live copilot TUI (no
|
|
454
|
+
// ready-screen hang).
|
|
455
|
+
let env = HermeticTestEnv::enter("cf-r8");
|
|
456
|
+
let workspace = env.workspace("ws");
|
|
457
|
+
let team_dir = write_copilot_team_docs(&workspace);
|
|
458
|
+
// Session ABSENT at quick-start so it can create the team session; spawn_first
|
|
459
|
+
// flips session_present true, so the later fork sees a live session. (Starting
|
|
460
|
+
// with session_present=true causes a SessionConflict at quick-start.)
|
|
461
|
+
let transport = RecordingTransport::default();
|
|
462
|
+
quick_start_with_transport_in_workspace(
|
|
463
|
+
&workspace,
|
|
464
|
+
&team_dir,
|
|
465
|
+
Some(TEAM_NAME),
|
|
466
|
+
true,
|
|
467
|
+
Some("cfr8"),
|
|
468
|
+
&transport,
|
|
469
|
+
)
|
|
470
|
+
.expect("quick-start seeds the copilot team");
|
|
471
|
+
// Seed the source's complete session tuple so the fork reaches the capability
|
|
472
|
+
// gate (not a missing-session gate) — mirrors the old contract's seed.
|
|
473
|
+
seed_source_tuple_in_state(&workspace, SOURCE);
|
|
474
|
+
|
|
475
|
+
// The team KEY is the quick-start team_id ("cfr8"), not the display name.
|
|
476
|
+
let result = fork_agent_with_transport(
|
|
477
|
+
&workspace,
|
|
478
|
+
&AgentId::new(SOURCE),
|
|
479
|
+
&AgentId::new(NEW),
|
|
480
|
+
None,
|
|
481
|
+
false,
|
|
482
|
+
Some("cfr8"),
|
|
483
|
+
&transport,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Baseline: copilot fork is refused with a structured copilot/fork
|
|
487
|
+
// capability-unsupported error (copilot_provider_red.rs C-4-2/C-4-3). R8 pins
|
|
488
|
+
// that this blanket capability refusal is GONE post-recharter.
|
|
489
|
+
let text = format!("{result:?}").to_lowercase();
|
|
490
|
+
let capability_refused = result.is_err()
|
|
491
|
+
&& (text.contains("unsupported")
|
|
492
|
+
|| text.contains("capability")
|
|
493
|
+
|| (text.contains("copilot") && text.contains("fork")));
|
|
494
|
+
assert!(
|
|
495
|
+
!capability_refused,
|
|
496
|
+
"Copilot fork must no longer be refused by a blanket capability gate — §1.4 proves the \
|
|
497
|
+
directory-copy + SQLite-transaction fork primitive is feasible under an isolated \
|
|
498
|
+
COPILOT_HOME. Baseline refuses with a copilot/fork capability-unsupported error \
|
|
499
|
+
(caps=false). This re-charter is verifier-signed (verifier-r8-caps-recharter-signoff.md). \
|
|
500
|
+
result={result:?}"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// R7 permission-clamp GUARDRAIL — a fork's effective approval policy is derived
|
|
505
|
+
/// from the LEADER and never escalates above it (MUST-16). Naturally green at
|
|
506
|
+
/// baseline (the role model forces permission_mode=restricted and inherits
|
|
507
|
+
/// approval from the leader); its teeth engage after implement recompiles the
|
|
508
|
+
/// role via RoleSourceResolver — any raw clone of a compiled permission above the
|
|
509
|
+
/// leader trips it. The true escalation scenario (a source declaring capability
|
|
510
|
+
/// above the leader) is a real-machine concern.
|
|
511
|
+
#[test]
|
|
512
|
+
fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
|
|
513
|
+
// Claude source + success-path backing shim so the fork COMPLETES and a NEW
|
|
514
|
+
// row exists to inspect — no vacuous pass on a refused fork. The dangerous
|
|
515
|
+
// leader is established by the PINNED ancestry seam (see ANCESTRY_ENV note):
|
|
516
|
+
// `source=leader_process` is only correct when a dangerous ancestor is
|
|
517
|
+
// actually detected, so the fixture must supply one instead of borrowing
|
|
518
|
+
// the host's.
|
|
519
|
+
let case = Case::start("cf-r7");
|
|
520
|
+
let fork = case.fork(NEW);
|
|
521
|
+
assert_eq!(
|
|
522
|
+
fork.get("ok").and_then(Value::as_bool),
|
|
523
|
+
Some(true),
|
|
524
|
+
"R7 fixture precondition: the claude fork must complete so a NEW row exists to check the \
|
|
525
|
+
permission clamp (no vacuous guardrail). fork={fork}"
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
let rows = case.team_agent_rows();
|
|
529
|
+
let new_row = rows.get(NEW).unwrap_or_else(|| {
|
|
530
|
+
panic!(
|
|
531
|
+
"R7 fixture precondition: NEW fork row must exist to inspect the clamp; rows={rows:?}"
|
|
532
|
+
)
|
|
533
|
+
});
|
|
534
|
+
let policy = new_row.get("effective_approval_policy").unwrap_or_else(|| {
|
|
535
|
+
panic!("NEW fork row must carry an effective_approval_policy; row={new_row}")
|
|
536
|
+
});
|
|
537
|
+
let source = policy.get("source").and_then(Value::as_str);
|
|
538
|
+
let above = policy
|
|
539
|
+
.get("worker_capability_above_leader")
|
|
540
|
+
.and_then(Value::as_bool);
|
|
541
|
+
assert_eq!(
|
|
542
|
+
source,
|
|
543
|
+
Some("leader_process"),
|
|
544
|
+
"guardrail: a fork's effective approval policy must be derived from the leader \
|
|
545
|
+
(source=leader_process), not raw-cloned from the source; MUST-16. policy={policy}"
|
|
546
|
+
);
|
|
547
|
+
assert_ne!(
|
|
548
|
+
above,
|
|
549
|
+
Some(true),
|
|
550
|
+
"guardrail: a fork must never carry a capability above the leader \
|
|
551
|
+
(worker_capability_above_leader must not be true); MUST-16. policy={policy}"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/// R7b reverse positive control — with a pinned RESTRICTED leader ancestry the
|
|
556
|
+
/// fork's policy must resolve to `source=disabled` (fail-closed), never
|
|
557
|
+
/// `leader_process`, and still never escalate. This is the CI-truth face of the
|
|
558
|
+
/// clamp (a clean runner == no dangerous ancestor) and proves r7 does not pass
|
|
559
|
+
/// by constant behavior: the same fixture flips leader_process↔disabled purely
|
|
560
|
+
/// on the pinned seam. Mirrors the deterministic product contract
|
|
561
|
+
/// `lifecycle/tests/core.rs` (clean mocked ancestry → Disabled).
|
|
562
|
+
///
|
|
563
|
+
/// NOTE deliberately NOT asserted with the seam UNSET: unpinned, the policy is
|
|
564
|
+
/// a function of the host process tree (the exact non-hermeticity this
|
|
565
|
+
/// revision removes), so an unset-seam assertion would be red on any host
|
|
566
|
+
/// whose own leader runs dangerously.
|
|
567
|
+
#[test]
|
|
568
|
+
fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
|
|
569
|
+
let case = Case::start_with_ancestry("cf-r7b", RESTRICTED_LEADER_ARGV);
|
|
570
|
+
let fork = case.fork(NEW);
|
|
571
|
+
assert_eq!(
|
|
572
|
+
fork.get("ok").and_then(Value::as_bool),
|
|
573
|
+
Some(true),
|
|
574
|
+
"R7b fixture precondition: the claude fork must complete so a NEW row exists to check the \
|
|
575
|
+
policy (no vacuous control). fork={fork}"
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
let rows = case.team_agent_rows();
|
|
579
|
+
let new_row = rows.get(NEW).unwrap_or_else(|| {
|
|
580
|
+
panic!(
|
|
581
|
+
"R7b fixture precondition: NEW fork row must exist to inspect the policy; rows={rows:?}"
|
|
582
|
+
)
|
|
583
|
+
});
|
|
584
|
+
let policy = new_row.get("effective_approval_policy").unwrap_or_else(|| {
|
|
585
|
+
panic!("NEW fork row must carry an effective_approval_policy; row={new_row}")
|
|
586
|
+
});
|
|
587
|
+
assert_eq!(
|
|
588
|
+
policy.get("source").and_then(Value::as_str),
|
|
589
|
+
Some("disabled"),
|
|
590
|
+
"reverse control: a restricted/clean leader ancestry must resolve to source=disabled \
|
|
591
|
+
(fail-closed), never leader_process. policy={policy}"
|
|
592
|
+
);
|
|
593
|
+
assert_ne!(
|
|
594
|
+
policy
|
|
595
|
+
.get("worker_capability_above_leader")
|
|
596
|
+
.and_then(Value::as_bool),
|
|
597
|
+
Some(true),
|
|
598
|
+
"reverse control: a restricted leader must never yield a fork above the leader; MUST-16. \
|
|
599
|
+
policy={policy}"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Minimal in-process Transport for the R8 library-entry test (mirrors the
|
|
605
|
+
// RecordingTransport the existing copilot refusal contract uses). Kept local so
|
|
606
|
+
// R8 exercises the real fork lifecycle capability gate without a live copilot TUI.
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
#[derive(Clone, Debug)]
|
|
610
|
+
struct RecordedSpawn {
|
|
611
|
+
argv: Vec<String>,
|
|
612
|
+
env: BTreeMap<String, String>,
|
|
613
|
+
window: String,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#[derive(Default)]
|
|
617
|
+
struct RecordingTransport {
|
|
618
|
+
spawns: Mutex<Vec<RecordedSpawn>>,
|
|
619
|
+
session_present: Mutex<bool>,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
impl Transport for RecordingTransport {
|
|
623
|
+
fn kind(&self) -> BackendKind {
|
|
624
|
+
BackendKind::Tmux
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
fn spawn_first(
|
|
628
|
+
&self,
|
|
629
|
+
session: &SessionName,
|
|
630
|
+
window: &WindowName,
|
|
631
|
+
argv: &[String],
|
|
632
|
+
_cwd: &Path,
|
|
633
|
+
env: &BTreeMap<String, String>,
|
|
634
|
+
) -> Result<SpawnResult, TransportError> {
|
|
635
|
+
let mut spawns = self.spawns.lock().unwrap();
|
|
636
|
+
spawns.push(RecordedSpawn {
|
|
637
|
+
argv: argv.to_vec(),
|
|
638
|
+
env: env.clone(),
|
|
639
|
+
window: window.as_str().to_string(),
|
|
640
|
+
});
|
|
641
|
+
*self.session_present.lock().unwrap() = true;
|
|
642
|
+
Ok(SpawnResult {
|
|
643
|
+
pane_id: PaneId::new(format!("%{}", spawns.len())),
|
|
644
|
+
session: session.clone(),
|
|
645
|
+
window: window.clone(),
|
|
646
|
+
child_pid: Some(30_000 + spawns.len() as u32),
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fn spawn_into(
|
|
651
|
+
&self,
|
|
652
|
+
session: &SessionName,
|
|
653
|
+
window: &WindowName,
|
|
654
|
+
argv: &[String],
|
|
655
|
+
cwd: &Path,
|
|
656
|
+
env: &BTreeMap<String, String>,
|
|
657
|
+
) -> Result<SpawnResult, TransportError> {
|
|
658
|
+
self.spawn_first(session, window, argv, cwd, env)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
fn inject(
|
|
662
|
+
&self,
|
|
663
|
+
_target: &Target,
|
|
664
|
+
_payload: &InjectPayload,
|
|
665
|
+
_submit: Key,
|
|
666
|
+
_bracketed: bool,
|
|
667
|
+
) -> Result<InjectReport, TransportError> {
|
|
668
|
+
Ok(InjectReport {
|
|
669
|
+
stage_reached: InjectStage::Submit,
|
|
670
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
671
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
672
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
673
|
+
attempts: 1,
|
|
674
|
+
submit_diagnostics: None,
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
679
|
+
Ok(())
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
fn capture(
|
|
683
|
+
&self,
|
|
684
|
+
_target: &Target,
|
|
685
|
+
range: CaptureRange,
|
|
686
|
+
) -> Result<CapturedText, TransportError> {
|
|
687
|
+
Ok(CapturedText {
|
|
688
|
+
text: "Claude Code\n> \nOpenAI Codex\ncodex>\nCopilot".to_string(),
|
|
689
|
+
range,
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
694
|
+
match field {
|
|
695
|
+
PaneField::PaneWidth => Ok(Some("120".to_string())),
|
|
696
|
+
_ => Ok(None),
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
701
|
+
Ok(PaneLiveness::Live)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
705
|
+
Ok(Vec::new())
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
709
|
+
Ok(*self.session_present.lock().unwrap())
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
713
|
+
Ok(Vec::new())
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
fn set_session_env(
|
|
717
|
+
&self,
|
|
718
|
+
_session: &SessionName,
|
|
719
|
+
_key: &str,
|
|
720
|
+
_value: &str,
|
|
721
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
722
|
+
Ok(SetEnvOutcome::Applied)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
726
|
+
*self.session_present.lock().unwrap() = false;
|
|
727
|
+
Ok(())
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
731
|
+
Ok(())
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
735
|
+
Ok(AttachOutcome::Attached)
|
|
736
|
+
}
|
|
737
|
+
}
|