mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
1cebfee94d
## Measure one skill: filesystem-sealed probe dispatch and the first current ledger row Provenance: the instrument-panel roadmap (`docs/plans/2026-08-26-instrument-panel-roadmap.md`) closed out with zero skills measured because control-arm reps read `skills/<skill>/SKILL.md` from the checkout and the operator's skill roots mid-run. Its named successor was filesystem-sealed dispatch. This PR builds the seal, then uses it, and the instrument caught one more leak on the way. **The seal.** Codex's own seatbelt does not nest inside an outer `sandbox-exec` profile, so a rep now runs under an outer profile with Codex's sandbox bypassed for exactly that reason: `file-read*` denied on the checkout and every skill root (`~/.agents`, `~/.claude/skills`, `~/.gemini/skills`, `~/.codex/skills`), `file-write*` denied everywhere except the rep's workspace, a scratch HOME, and the temp roots. HOME and CODEX_HOME are scratch (auth symlinked). Dispatch refuses without `sandbox-exec` unless `PROBE_SEAL=none` is set, which marks the run coverage-ineligible. The seal is recorded in the capture contract (`agentops-skill-probe-capture.v3`, a `seal` block), coverage eligibility requires `seal.mode == seatbelt`, and `verify-scorecard` re-checks that the denied roots include the repository and the four skill roots. Existing 08-26 sets become `legacy-unsealed` and ineligible, which changes nothing they were counting. **The leak the seal did not cover.** The first sealed capture showed the xhigh control-2 rep listing the shared live workspace, finding `treatment-1.prompt`, and reading it; the treatment prompt embeds the skill. So every rep now gets its own empty workspace, prompts reach a rep on stdin only, raw JSONL and stderr live in a harness-private dispatch directory the rep can only stat, and a `sibling-prompt-read` trap degrades any rep whose successful command names a prompt, contract, seal, or stage file or whose output lists a sibling's artifacts. Replaying the contaminated set under the new trap turns its REGRESSIVE into INERT with the contaminated rep degraded; that set is superseded and not committed. **The second pass, after the judges read the first.** A Fable validator passed the first sealed capture with four findings and a Codex read failed it with seven, all real: the whole per-user temp tree stayed readable (hundreds of stale probe directories held prompt files with the canonical skill bytes), sibling checkouts and the real `~/.codex` sessions were readable because seatbelt matches the traversed path and the skill roots are symlinks into the main checkout, the dispatch directory was writable so a rep could rename an earlier prompt into its workspace, the contract trusted asserted seal metadata (a hand-made Linux record was coverage-eligible), the only real-Darwin test nested seatbelt and proved nothing, and the operator's `config.toml` was symlinked in whole, so every rep started the operator's MCP servers. The seal is now one run directory removed on exit (`home/ ws/ tmp/ dispatch/`, reset per rep); reads are denied under the real HOME, the real TMPDIR, `/tmp`, the checkout, the git common directory, and each skill root's resolved target; `dispatch/` is write-denied with metadata-only reads; `file-link` and `file-clone` are denied on every denied root; the rep's config is rebuilt from top-level scalars only and `auth.json` is copied, not linked. The contract binds platform, mechanism, wrapper, the denied and allowed roots, the rep environment, and the sanitization, and coverage refuses any record that omits them; the Darwin test performs its reads inside the seal. One recorded hole remains by necessity: the codex launcher under `~/.local/bin` and `~/dev/dotfiles/bin` is re-allowed by literal path, and coverage refuses any allowed path inside the checkout. **The third pass, after the judges read the second.** Both judges failed the hardened tip again, and the headline was network: bypassing codex's own sandbox to nest the outer seal had dropped every inner deny, not only the filesystem ones, so a rep could fetch the canonical skill from GitHub while the disk was sealed. A rep now reaches the network only through a harness-owned CONNECT proxy (`scripts/lib/probe-connect-proxy.py`) with a host allowlist discovered from real runs (`chatgpt.com`, `ab.chatgpt.com`, the rotating `.oaiusercontent.com` suffix, and the API-key hosts), behind a seatbelt `network*` deny that admits only the proxy; every attempt is logged, a refused one degrades the rep, and web search is disabled in the rep's config. The profile is now rendered from the bound seal block by one function, so `verify-scorecard` reconstructs it and requires the digest to match; the recorded launcher chain is the only allowed read under a denied root; the wrap invokes `/usr/bin/sandbox-exec` by absolute path; the real CODEX_HOME and the Darwin cache directory are denied and bound; every non-stdio descriptor is closed and `/dev` writes narrowed to four devices; each rep runs in its own process group that must be empty before the next reset; one minimal config is generated per run, bound by text and digest, and re-parsed after each rep; one guarded trap covers the run root and any unpublished stage; and the coverage gate prints an eligibility row for every ledger row that names a scorecard. What stays open is stated in the README: allow-default still governs what the profile does not deny. **The fourth pass, after the judges read the third.** Both judges failed the third tip on properties the record carried but the verifier did not check. GNU `timeout` sets its own process group and ran outside the seal, so the per-rep reap was vacuous (four sleeping survivors after the test passed); the seal is now the outermost process, timeout runs inside it with `--foreground`, its absolute path is bound, and the reap counts survivors before signalling. The verifier now pins, not merely records: the egress host set (a module constant), port 443 only, no unix sockets, a loopback proxy, the generated config text equal to the renderer's output, the real CODEX_HOME and cache root inside the denied roots, writable roots under the run root, the four device paths, the environment allowlist, the launcher chain's digest against the producer identity, and it refuses unknown record fields. The proxy logs an attempt before dialing, captures the rep at accept time, refuses names that resolve to private or loopback ranges, and its source joins the evaluator hash set; the per-rep egress log is published with the fixture set. Withdrawn ledger rows get their own eligibility reason. The README lists which seal fields the verifier pins and which it only records; the cross-capture aggregate is recounted to 4 of 14. Still open and disclosed: Mach IPC and Apple Events beyond the filesystem and network seals, exact-byte environment sealing beyond the allowlist, and cleanup ownership by inode. **The fifth pass, after the judges read the fourth.** Both judges found the seal, proxy, reap ordering, transcripts, and scorecards sound and failed the tip on claims the tree did not hold: the egress log the docs called published was caught by the repository's `*.log` ignore rule, a `setsid()` child escaped the process-group reap, and one ledger sentence said no rep ran a command when a control rep had run `rg` over its empty workspace. The log is now tracked and required, strictly parsed, with both allowed and refused counts recomputed per rep; the reap is fatal to the capture when it cannot prove emptiness and an `lsof` sweep of the run root catches a session escape; the rep launches through a real `env -i` boundary; the timeout budget, the launcher chain's adjacency, every required root in both read and link denies, the evaluator match, and the trust-table value are pinned by the verifier; and every sentence in the ledger, README, and runbook names the check that enforces it. The remaining gap is stated: a child that both escapes the session and holds nothing under the run root is not detected. **The sixth pass, after CI read the fifth.** The first CI run of this PR failed on Linux: the launcher-chain pin walked the capturing Mac's filesystem, so on the runner both sets read as unsealed and the coverage headline fell to zero. A pin that holds on one host is not a pin. The chain is now bound as structure in the seal record (each link's path, kind, symlink target, and the final file's digest, with the invoked path as the head), the verifier checks that record on any host, and it cross-checks the live filesystem only where the head exists, reporting which happened. A gate-level test runs the coverage gate twice, once with the chain made absent, and requires byte-identical output. **The measurement.** `premortem-plan-shape-t2` (two seeded structural defects, band [1,4]) captured seven times on 2026-09-03, once under each seal; the first six sets were deleted with their scorecards when a judge or CI broke that seal. The rows that count are the seventh capture: low INERT (control 0/2, treatment 0/2) and xhigh INERT (control 1/2, treatment 1/2); no rep ran a command, no egress was refused, both per-rep egress logs are tracked and verified, and both scorecards verify with every pinned field, the launcher chain checked as a record and cross-checked on the capturing host. The superseded captures read, at low, BEHAVIORAL 1/1, INERT, INERT, BEHAVIORAL 1/2, INERT, BEHAVIORAL 1/2 and, at xhigh, INERT, BEHAVIORAL 2/2, INERT, INERT, INERT (control 1/2), INERT. The reversals across captures are recorded as an unresolved N=2 observation, not variance; across all seven the treatment arm put the marks in band in 7 of 26 usable reps and the control arm in 2 of 28, and the ledger marks that aggregate as disclosure, not a row. Headroom is SEPARATED at both levels. This is the first current, manifest-backed ledger row; it records response-shape behavior change, never quality uplift, and what it records today is INERT at both levels. **Evidence on the tip.** Probe, exec-lib, metadata, coverage, headroom, and adapter bats suites green with the Darwin seal, survivor, session-escape, and environment-boundary tests running; `verify-scorecard` on both scorecards; `check-skill-probe-coverage.sh` reports measured 1 of 12 with the 2026-08-26 row labelled `verdict-withdrawn`; regen check clean; full gate green with a HEAD-built binary; CI's bats command green; Go bar, lint, security, and the gate-tightening ratchet green. **Judgment, stated plainly.** The caller elected Fable 5.1 as the binding validator. Its fifth-round verdict on the pre-CI tip was PASS with no verdict-driving findings; its read of the portability delta failed on one test-only point (the coverage test had dropped the `measured` pin that CI tripped on) which this tip restores. The cross-family Codex reads of the same tips are FAIL. They closed the network seal, the environment boundary, the fatal reap, the data roots, the config binding, and the documentation, and they keep these open, recorded here as the next pass rather than hidden: the evaluator identity check still accepts the pre-proxy four-file shape for legacy sets; an inherited absolute `CODEX_EXEC_TIMEOUT_BIN` is trusted without a probe; a refusal logged under a rep label outside the expected set is not rejected; an empty top-level `[projects]` table passes the drift check; the `lsof` sweep treats a failing `lsof` as a clean result; and the launcher chain, now bound as a record that any host can check, is corroborated against the filesystem only on the capturing host, so a record whose head does not exist there is taken on its word (the docs say so; Codex wants the live walk required at capture and the invoked head bound into the producer identity). Fable also noted a test that mis-resolves when TMPDIR sits under `/private/tmp`, a `2026-09-04` date in one refusal message, and that the low control replies used a `S2: FINDING <reason>` shape the discriminator scores as "plan not walked" rather than by band (outcome-invariant). Each of those changes a harness or verifier file and would orphan the two rows again, so they are filed as the follow-up instead of an eighth capture today. Across six judging rounds the open set shrank only from round four on; the convergence law's stop signal was reached at round three and the run continued on the caller's instruction to land the instrument. The two ledger rows are honest about what they measure, and the seal they ran under is the one this tree can verify on any host.
1773 lines
75 KiB
Bash
1773 lines
75 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
bats_require_minimum_version 1.5.0
|
|
|
|
setup() {
|
|
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
|
HARNESS="$REPO_ROOT/scripts/probe-skill.sh"
|
|
META_TOOL="$REPO_ROOT/scripts/lib/probe-fixture-metadata.py"
|
|
PREAMBLE="$REPO_ROOT/scripts/lib/preamble.sh"
|
|
DISPATCH_HELPER="$REPO_ROOT/scripts/lib/codex-exec.sh"
|
|
PROBES="$BATS_TEST_TMPDIR/probes"
|
|
SKILLS="$BATS_TEST_TMPDIR/skills"
|
|
PROBE_DIR="$PROBES/demo"
|
|
mkdir -p "$PROBE_DIR" "$SKILLS/demo-skill"
|
|
export SKILL_PROBES_DIR="$PROBES"
|
|
export SKILL_PROBE_SKILLS_DIR="$SKILLS"
|
|
|
|
cat > "$PROBE_DIR/probe.json" <<'JSON'
|
|
{"id":"demo","skill":"demo-skill","reps":2,"discriminator":"discriminator.sh","treatment_source":"injected-prelude"}
|
|
JSON
|
|
cat > "$SKILLS/demo-skill/SKILL.md" <<'MD'
|
|
---
|
|
name: demo-skill
|
|
description: Fixture skill.
|
|
---
|
|
# Demo skill
|
|
|
|
CANONICAL_ACTION
|
|
MD
|
|
printf 'QUESTION\n' > "$PROBE_DIR/question.md"
|
|
printf 'PRELUDE_ACTION\n' > "$PROBE_DIR/treatment-prelude.md"
|
|
cat > "$PROBE_DIR/discriminator.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if grep -q '^INFRA$' "$1"; then exit 2; fi
|
|
grep -q '^ACTION$' "$1"
|
|
SH
|
|
chmod +x "$PROBE_DIR/discriminator.sh"
|
|
|
|
# Filesystem seal (move 2): live dispatch fails closed without a seatbelt.
|
|
# Linux CI has no sandbox-exec, so every live test runs with a pass-through
|
|
# stand-in that honors the exact `sandbox-exec -p <profile> cmd...` shape
|
|
# the harness builds and simply runs cmd. The darwin-only seal test drops
|
|
# it from PATH and uses the real /usr/bin/sandbox-exec.
|
|
SEATBELT_STUB_DIR="$BATS_TEST_TMPDIR/seatbelt-stub"
|
|
mkdir -p "$SEATBELT_STUB_DIR"
|
|
cat > "$SEATBELT_STUB_DIR/sandbox-exec" <<'SH'
|
|
#!/usr/bin/env bash
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-p|-f|-n|-D) shift 2;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
exec "$@"
|
|
SH
|
|
chmod +x "$SEATBELT_STUB_DIR/sandbox-exec"
|
|
export PATH="$SEATBELT_STUB_DIR:$PATH"
|
|
# The harness takes /usr/bin/sandbox-exec by absolute path, so a PATH stub
|
|
# no longer shadows it (that was the point of the fix). Tests that are not
|
|
# ABOUT the seal ask for the pass-through through the documented seam; the
|
|
# seal tests unset it and get the real kernel sandbox.
|
|
export PROBE_SEAL_SANDBOX_EXEC="$SEATBELT_STUB_DIR/sandbox-exec"
|
|
|
|
RUNTIME_STUB="$BATS_TEST_TMPDIR/codex-runtime-identity"
|
|
cat > "$RUNTIME_STUB" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then
|
|
printf 'codex-cli probe-test\n'
|
|
exit 0
|
|
fi
|
|
printf 'runtime identity stub is not a live producer\n' >&2
|
|
exit 70
|
|
SH
|
|
chmod +x "$RUNTIME_STUB"
|
|
}
|
|
|
|
write_transcript() {
|
|
local directory="$1" name="$2" model="$3" effort="$4" body="$5"
|
|
python3 - "$directory" "$name" "$body" <<'PY'
|
|
import json, pathlib, sys
|
|
|
|
directory = pathlib.Path(sys.argv[1])
|
|
name = sys.argv[2]
|
|
body = sys.argv[3]
|
|
arm, rep_text = name.rsplit("-", 1)
|
|
rep = int(rep_text)
|
|
contract = json.loads((directory / "capture-contract.json").read_text())
|
|
prompt = contract["prompts"][0 if arm == "control" else 1]
|
|
position = next(
|
|
item["position"]
|
|
for item in contract["schedule"]
|
|
if item["arm"] == arm and item["rep"] == rep
|
|
)
|
|
events = [
|
|
{
|
|
"type": "agentops.probe-input.v1",
|
|
"arm": arm,
|
|
"rep": rep,
|
|
"position": position,
|
|
"prompt": prompt,
|
|
},
|
|
{"type": "thread.started", "thread_id": f"thread-{directory.name}-{name}"},
|
|
{"type": "turn.started"},
|
|
{
|
|
"type": "item.completed",
|
|
"item": {"id": f"item-{name}", "type": "agent_message", "text": body},
|
|
},
|
|
{
|
|
"type": "turn.completed",
|
|
"usage": {"input_tokens": 1, "cached_input_tokens": 0, "output_tokens": 1},
|
|
},
|
|
]
|
|
(directory / f"{name}.txt").write_text(
|
|
"".join(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n" for event in events)
|
|
)
|
|
PY
|
|
}
|
|
|
|
write_legacy_transcript() {
|
|
local directory="$1" name="$2" model="$3" effort="$4" body="$5"
|
|
{
|
|
printf 'OpenAI Codex fixture\n'
|
|
printf '%s\n' '--------'
|
|
printf 'workdir: /fixture\n'
|
|
printf 'model: %s\n' "$model"
|
|
printf 'reasoning effort: %s\n' "$effort"
|
|
printf '%s\n' '--------'
|
|
printf 'user\nQUESTION\n'
|
|
printf 'codex\n'
|
|
printf '%s\n' "$body"
|
|
printf 'tokens used: 1\n'
|
|
} > "$directory/$name.txt"
|
|
}
|
|
|
|
make_fixture_set() {
|
|
local name="$1" model="$2" effort="$3" control_body="$4" treatment_body="$5"
|
|
local directory="$PROBE_DIR/$name" rep
|
|
mkdir -p "$directory"
|
|
python3 "$META_TOOL" snapshot \
|
|
--fixture-dir "$directory" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo \
|
|
--requested-model "$model" \
|
|
--requested-effort "$effort" \
|
|
--producer-override-bin "$RUNTIME_STUB" >/dev/null
|
|
for rep in 1 2; do
|
|
write_transcript "$directory" "control-$rep" "$model" "$effort" "$control_body"
|
|
write_transcript "$directory" "treatment-$rep" "$model" "$effort" "$treatment_body"
|
|
done
|
|
python3 "$META_TOOL" create \
|
|
--fixture-dir "$directory" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--harness "$HARNESS" \
|
|
--preamble "$PREAMBLE" \
|
|
--dispatch-helper "$DISPATCH_HELPER" \
|
|
--probe demo \
|
|
--reps 2 \
|
|
--requested-model "$model" \
|
|
--requested-effort "$effort" >/dev/null
|
|
}
|
|
|
|
json_field() {
|
|
python3 -c '
|
|
import json, sys
|
|
value = json.loads(sys.argv[1])
|
|
for part in sys.argv[2].split("."):
|
|
value = value[int(part)] if isinstance(value, list) else value[part]
|
|
if value is None:
|
|
print("null")
|
|
elif isinstance(value, bool):
|
|
print("true" if value else "false")
|
|
else:
|
|
print(value)
|
|
' "$1" "$2"
|
|
}
|
|
|
|
@test "git attributes preserve transcript bytes and normalize all hashed text inputs" {
|
|
run git -C "$REPO_ROOT" check-attr text whitespace -- \
|
|
evals/skill-probes/example/fixtures/control-1.txt \
|
|
evals/skill-probes/example/fixtures-xhigh-2026-08-04/treatment-2.txt
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(printf '%s\n' "$output" | grep -c ': text: unset$')" -eq 2 ]
|
|
[ "$(printf '%s\n' "$output" | grep -c ': whitespace: unset$')" -eq 2 ]
|
|
|
|
run git -C "$REPO_ROOT" check-attr text eol -- \
|
|
evals/skill-probes/example/probe.json \
|
|
evals/skill-probes/example/question.md \
|
|
evals/skill-probes/example/treatment-prelude.md \
|
|
evals/skill-probes/example/fixtures-v3/capture-contract.json \
|
|
evals/skill-probes/example/discriminator.sh \
|
|
skills/example/SKILL.md \
|
|
scripts/probe-skill.sh \
|
|
scripts/lib/preamble.sh \
|
|
scripts/lib/codex-exec.sh \
|
|
scripts/lib/probe-fixture-metadata.py
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(printf '%s\n' "$output" | grep -c ': text: set$')" -eq 10 ]
|
|
[ "$(printf '%s\n' "$output" | grep -c ': eol: lf$')" -eq 10 ]
|
|
}
|
|
|
|
@test "replay takes producer provenance and binding from verified fixture metadata" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" schema)" = "agentops-skill-probe.v3" ]
|
|
[ "$(json_field "$output" producer.model)" = "observed-model" ]
|
|
[ "$(json_field "$output" producer.effort)" = "low" ]
|
|
[ "$(json_field "$output" producer.identity.source)" = "test-override" ]
|
|
[ "$(json_field "$output" producer.identity.coverage_eligible)" = "false" ]
|
|
[ "$(json_field "$output" producer.threads.0.path)" = "control-1.txt" ]
|
|
[ "$(json_field "$output" fixture_set.name)" = "fixtures" ]
|
|
[[ "$(json_field "$output" fixture_set.binding_sha256)" == sha256:* ]]
|
|
[ "$(json_field "$output" evaluator_matches_capture)" = "true" ]
|
|
[ "$(json_field "$output" treatment_source)" = "injected-prelude" ]
|
|
[[ "$(json_field "$output" honesty)" == *"NOT full-skill activation"* ]]
|
|
[ "$(json_field "$output" capture_evaluator.dispatch_helper.path)" = "scripts/lib/codex-exec.sh" ]
|
|
[[ "$(json_field "$output" capture_evaluator.dispatch_helper.sha256)" == sha256:* ]]
|
|
[ "$(json_field "$output" capture_evaluator.preamble.path)" = "scripts/lib/preamble.sh" ]
|
|
[[ "$(json_field "$output" capture_evaluator.preamble.sha256)" == sha256:* ]]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
[ "$(json_field "$(cat "$PROBE_DIR/fixtures/fixture-set.json")" schema)" = "agentops-skill-probe-fixture-set.v3" ]
|
|
[ "$(json_field "$(cat "$PROBE_DIR/fixtures/fixture-set.json")" canonical_skill.path)" = "skills/demo-skill/SKILL.md" ]
|
|
}
|
|
|
|
@test "v3 response-only scoring supports the repository transcript-marker discriminator" {
|
|
cp "$REPO_ROOT/evals/skill-probes/anti-ceremony-creation-gate-v2/discriminator.sh" \
|
|
"$PROBE_DIR/discriminator.sh"
|
|
make_fixture_set fixtures observed-model low \
|
|
$'A: CREATE\nB: CREATE' $'A: DROP\nB: CREATE'
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" control.usable)" -eq 2 ]
|
|
[ "$(json_field "$output" treatment.usable)" -eq 2 ]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
}
|
|
|
|
@test "structured response extraction ignores transcript-like delimiter collisions" {
|
|
make_fixture_set fixtures observed-model low ABSENT $'codex\ntokens used: forged\nACTION'
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" treatment.present)" -eq 2 ]
|
|
[ "$(json_field "$output" treatment.usable)" -eq 2 ]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
}
|
|
|
|
@test "replay refuses legacy fixture bytes without immutable capture metadata" {
|
|
mkdir -p "$PROBE_DIR/fixtures"
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" control-1 old-model low ABSENT
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" treatment-1 old-model low ACTION
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" control-2 old-model low ABSENT
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" treatment-2 old-model low ACTION
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"verified replay requires immutable capture metadata"* ]]
|
|
[[ "$output" == *"replay refused"* ]]
|
|
}
|
|
|
|
@test "v1 bound fixtures replay as injected-prelude without a treatment_source declaration" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" control-1 observed-model low ABSENT
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" treatment-1 observed-model low ACTION
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" control-2 observed-model low ABSENT
|
|
write_legacy_transcript "$PROBE_DIR/fixtures" treatment-2 observed-model low ACTION
|
|
python3 - "$PROBE_DIR/probe.json" "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import hashlib, json, pathlib, sys
|
|
|
|
probe_path = pathlib.Path(sys.argv[1])
|
|
manifest_path = pathlib.Path(sys.argv[2])
|
|
probe = json.loads(probe_path.read_text())
|
|
probe.pop("treatment_source")
|
|
probe_path.write_text(json.dumps(probe) + "\n")
|
|
|
|
manifest = json.loads(manifest_path.read_text())
|
|
manifest["schema"] = "agentops-skill-probe-fixture-set.v1"
|
|
manifest["producer"] = {
|
|
"adapter": "codex",
|
|
"model": "observed-model",
|
|
"effort": "low",
|
|
}
|
|
manifest["evaluation_inputs"] = [
|
|
{"path": record["path"], "sha256": record["sha256"]}
|
|
for record in manifest.pop("capture_inputs")
|
|
]
|
|
manifest.pop("capture_contract")
|
|
manifest.pop("canonical_skill")
|
|
manifest.pop("treatment_source")
|
|
manifest.pop("prompts")
|
|
manifest.pop("schedule")
|
|
manifest.pop("scoring")
|
|
manifest["capture_evaluator"].pop("preamble")
|
|
manifest["capture_evaluator"].pop("dispatch_helper")
|
|
manifest["capture_evaluator"].pop("network_proxy")
|
|
for record in manifest["transcripts"]:
|
|
transcript_path = manifest_path.parent / record["path"]
|
|
record["sha256"] = "sha256:" + hashlib.sha256(transcript_path.read_bytes()).hexdigest()
|
|
for record in manifest["evaluation_inputs"]:
|
|
if record["path"] == "probe.json":
|
|
record["sha256"] = "sha256:" + hashlib.sha256(probe_path.read_bytes()).hexdigest()
|
|
payload = {key: value for key, value in manifest.items() if key != "binding_sha256"}
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
manifest["binding_sha256"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
manifest_path.write_text(json.dumps(manifest) + "\n")
|
|
manifest_path.with_name("capture-contract.json").unlink()
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" treatment_source)" = "injected-prelude" ]
|
|
[[ "$(json_field "$output" honesty)" == *"NOT full-skill activation"* ]]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
}
|
|
|
|
@test "replay rejects transcript tampering before discrimination" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
printf 'ACTION\n' >> "$PROBE_DIR/fixtures/control-1.txt"
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"transcript digest mismatch for control-1.txt"* ]]
|
|
}
|
|
|
|
@test "replay rejects producer-config tampering through the fixture-set binding" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
python3 - "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["requested_producer"]["model"] = "relabeled-model"
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"requested_producer disagrees with bound producer request"* ]]
|
|
}
|
|
|
|
@test "fixture creation rejects prompt-event tampering before manifest binding" {
|
|
local directory="$PROBE_DIR/fixtures"
|
|
local rep
|
|
mkdir -p "$directory"
|
|
python3 "$META_TOOL" snapshot \
|
|
--fixture-dir "$directory" --probe-dir "$PROBE_DIR" --skills-dir "$SKILLS" \
|
|
--probe demo --requested-model observed-model --requested-effort low \
|
|
--producer-override-bin "$RUNTIME_STUB" >/dev/null
|
|
for rep in 1 2; do
|
|
write_transcript "$directory" "control-$rep" observed-model low ABSENT
|
|
write_transcript "$directory" "treatment-$rep" observed-model low ACTION
|
|
done
|
|
python3 - "$directory/control-1.txt" <<'PY'
|
|
import base64, json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
events = [json.loads(line) for line in path.read_text().splitlines()]
|
|
events[0]["prompt"]["content_base64"] = base64.b64encode(b"TAMPERED\n").decode()
|
|
path.write_text("".join(json.dumps(event) + "\n" for event in events))
|
|
PY
|
|
|
|
run python3 "$META_TOOL" create \
|
|
--fixture-dir "$directory" --probe-dir "$PROBE_DIR" --skills-dir "$SKILLS" \
|
|
--harness "$HARNESS" --preamble "$PREAMBLE" \
|
|
--dispatch-helper "$DISPATCH_HELPER" --probe demo --reps 2 \
|
|
--requested-model observed-model --requested-effort low
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"probe input event does not match bound control-1 prompt"* ]]
|
|
[ ! -e "$directory/fixture-set.json" ]
|
|
}
|
|
|
|
@test "fixture creation refuses retroactive capture-contract creation" {
|
|
local directory="$PROBE_DIR/fixtures"
|
|
mkdir -p "$directory"
|
|
write_legacy_transcript "$directory" control-1 observed-model low ABSENT
|
|
write_legacy_transcript "$directory" treatment-1 observed-model low ACTION
|
|
write_legacy_transcript "$directory" control-2 observed-model low ABSENT
|
|
write_legacy_transcript "$directory" treatment-2 observed-model low ACTION
|
|
|
|
run python3 "$META_TOOL" create \
|
|
--fixture-dir "$directory" --probe-dir "$PROBE_DIR" --skills-dir "$SKILLS" \
|
|
--harness "$HARNESS" --preamble "$PREAMBLE" \
|
|
--dispatch-helper "$DISPATCH_HELPER" --probe demo --reps 2 \
|
|
--requested-model observed-model --requested-effort low
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"requires a pre-existing capture contract written before execution"* ]]
|
|
[ ! -e "$directory/capture-contract.json" ]
|
|
[ ! -e "$directory/fixture-set.json" ]
|
|
}
|
|
|
|
@test "v3 replay scores captured discriminator bytes after current probe drift" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
printf '# changed scoring input\n' >> "$PROBE_DIR/discriminator.sh"
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
}
|
|
|
|
@test "v3 replay remains self-contained after current canonical skill drift" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
printf '\nchanged canonical guidance\n' >> "$SKILLS/demo-skill/SKILL.md"
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
}
|
|
|
|
@test "capture rejects a canonical skill whose declared identity mismatches probe.json" {
|
|
sed -i.bak 's/name: demo-skill/name: unrelated-skill/' "$SKILLS/demo-skill/SKILL.md"
|
|
rm -f "$SKILLS/demo-skill/SKILL.md.bak"
|
|
mkdir -p "$PROBE_DIR/fixtures"
|
|
|
|
run python3 "$META_TOOL" snapshot \
|
|
--fixture-dir "$PROBE_DIR/fixtures" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo \
|
|
--requested-model observed-model \
|
|
--requested-effort low \
|
|
--producer-override-bin "$RUNTIME_STUB"
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"canonical skill identity mismatch"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures/capture-contract.json" ]
|
|
}
|
|
|
|
@test "replay flags are constraints and mismatches fail rather than relabel fixtures" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
|
|
run bash "$HARNESS" --probe demo --replay --model another-model
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"does not match bound fixture producer request"* ]]
|
|
|
|
run bash "$HARNESS" --probe demo --replay --effort xhigh
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"does not match bound fixture producer request"* ]]
|
|
|
|
run env PROBE_MODEL=ambient-relabel SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --replay
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"does not match bound fixture producer request"* ]]
|
|
}
|
|
|
|
@test "a zero-usable control arm cannot produce a false BEHAVIORAL verdict" {
|
|
make_fixture_set fixtures observed-model low INFRA ACTION
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" control.usable)" = "0" ]
|
|
[ "$(json_field "$output" treatment.usable)" = "2" ]
|
|
[ "$(json_field "$output" verdict)" = "UNMEASURED" ]
|
|
}
|
|
|
|
@test "both zero-usable arms yield UNMEASURED with null rates" {
|
|
make_fixture_set fixtures observed-model low INFRA INFRA
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" control.rate)" = "null" ]
|
|
[ "$(json_field "$output" treatment.rate)" = "null" ]
|
|
[ "$(json_field "$output" verdict)" = "UNMEASURED" ]
|
|
}
|
|
|
|
@test "named alternate fixture sets are independently selectable and verified" {
|
|
make_fixture_set fixtures default-model low ABSENT ACTION
|
|
make_fixture_set fixtures-xhigh-2026-08-04 alternate-model xhigh ABSENT ABSENT
|
|
|
|
run bash "$HARNESS" --probe demo --replay \
|
|
--fixtures fixtures-xhigh-2026-08-04 \
|
|
--model alternate-model \
|
|
--effort xhigh
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" fixture_set.name)" = "fixtures-xhigh-2026-08-04" ]
|
|
[ "$(json_field "$output" producer.model)" = "alternate-model" ]
|
|
[ "$(json_field "$output" producer.effort)" = "xhigh" ]
|
|
[ "$(json_field "$output" verdict)" = "INERT" ]
|
|
}
|
|
|
|
@test "producer strings are JSON serialized without shell interpolation" {
|
|
make_fixture_set fixtures 'gpt-"quoted"' low ABSENT ACTION
|
|
|
|
run bash "$HARNESS" --probe demo --replay --model 'gpt-"quoted"'
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" producer.model)" = 'gpt-"quoted"' ]
|
|
}
|
|
|
|
@test "replay discloses when the current evaluator differs from capture" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
python3 - "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import hashlib, json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["capture_evaluator"]["harness"]["sha256"] = "sha256:" + "0" * 64
|
|
payload = {key: item for key, item in value.items() if key != "binding_sha256"}
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
value["binding_sha256"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" evaluator_matches_capture)" = "false" ]
|
|
[ "$(json_field "$output" capture_evaluator.harness.sha256)" = "sha256:0000000000000000000000000000000000000000000000000000000000000000" ]
|
|
[[ "$(json_field "$output" evaluator.harness.sha256)" == sha256:* ]]
|
|
}
|
|
|
|
@test "v3 replay requires the dispatch helper in capture evaluator identity" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
python3 - "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import hashlib, json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["capture_evaluator"].pop("dispatch_helper")
|
|
payload = {key: item for key, item in value.items() if key != "binding_sha256"}
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
value["binding_sha256"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"capture_evaluator must contain exactly"* ]]
|
|
[[ "$output" == *"dispatch_helper"* ]]
|
|
}
|
|
|
|
@test "v3 replay requires the sourced preamble in capture evaluator identity" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
python3 - "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import hashlib, json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["capture_evaluator"].pop("preamble")
|
|
payload = {key: item for key, item in value.items() if key != "binding_sha256"}
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
value["binding_sha256"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"capture_evaluator must contain exactly"* ]]
|
|
[[ "$output" == *"preamble"* ]]
|
|
}
|
|
|
|
@test "replay discloses dispatch helper drift from a bound v3 capture" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
python3 - "$PROBE_DIR/fixtures/fixture-set.json" <<'PY'
|
|
import hashlib, json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["capture_evaluator"]["dispatch_helper"]["sha256"] = "sha256:" + "0" * 64
|
|
payload = {key: item for key, item in value.items() if key != "binding_sha256"}
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
value["binding_sha256"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
|
|
run bash "$HARNESS" --probe demo --replay
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" evaluator_matches_capture)" = "false" ]
|
|
[ "$(json_field "$output" capture_evaluator.dispatch_helper.sha256)" = "sha256:0000000000000000000000000000000000000000000000000000000000000000" ]
|
|
[[ "$(json_field "$output" evaluator.dispatch_helper.sha256)" == sha256:* ]]
|
|
}
|
|
|
|
@test "live capture refuses to overwrite an existing immutable fixture set" {
|
|
make_fixture_set fixtures stale-model low ABSENT ACTION
|
|
cp "$PROBE_DIR/fixtures/fixture-set.json" "$BATS_TEST_TMPDIR/manifest.before"
|
|
cp "$PROBE_DIR/fixtures/treatment-1.txt" "$BATS_TEST_TMPDIR/treatment.before"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN=definitely-missing-probe-producer \
|
|
SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --live --reps 2
|
|
|
|
[ "$status" -eq 2 ]
|
|
[ -z "$output" ]
|
|
[[ "$stderr" == *"refusing to overwrite immutable fixture set"* ]]
|
|
[[ "$stderr" == *"choose a new --fixtures name"* ]]
|
|
cmp "$BATS_TEST_TMPDIR/manifest.before" "$PROBE_DIR/fixtures/fixture-set.json"
|
|
cmp "$BATS_TEST_TMPDIR/treatment.before" "$PROBE_DIR/fixtures/treatment-1.txt"
|
|
}
|
|
|
|
@test "publish refuses a fixture destination that appears during dispatch" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-publish-race"
|
|
local count="$BATS_TEST_TMPDIR/publish-race-count"
|
|
local target="$PROBE_DIR/fixtures-raced"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli race-stub\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
if [[ "$n" -eq 4 ]]; then
|
|
mkdir "$PROBE_RACE_TARGET"
|
|
printf 'external-writer\n' > "$PROBE_RACE_TARGET/sentinel"
|
|
fi
|
|
prompt="$(cat)"
|
|
if [[ "$prompt" == *PRELUDE_ACTION* ]]; then response=ACTION; else response=ABSENT; fi
|
|
printf '{"type":"thread.started","thread_id":"race-%s"}\n' "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"%s"}}\n' "$n" "$response"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
PROBE_STUB_COUNT="$count" \
|
|
PROBE_RACE_TARGET="$target" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-raced
|
|
|
|
[ "$status" -eq 1 ]
|
|
[ "$(cat "$target/sentinel")" = "external-writer" ]
|
|
[ ! -e "$target/fixture-set.json" ]
|
|
[ "$(find "$target" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" -eq 1 ]
|
|
[[ "$stderr" == *"refusing to replace existing immutable fixture set"* ]]
|
|
[[ "$stderr" == *"failed to publish fixture set"* ]]
|
|
}
|
|
|
|
@test "publisher rejects an externally mutated hidden-stage transcript without exposing target" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local stage="$PROBE_DIR/.fixtures-publish-stage"
|
|
local target="$PROBE_DIR/fixtures-publish-mutated"
|
|
mv "$PROBE_DIR/fixtures" "$stage"
|
|
|
|
printf 'MUTATED_BEFORE_PUBLISH\n' >> "$stage/treatment-1.txt"
|
|
|
|
run python3 "$META_TOOL" publish \
|
|
--stage-dir "$stage" \
|
|
--target-dir "$target" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
local publish_status="$status"
|
|
[ "$publish_status" -eq 2 ]
|
|
[[ "$output" == *"transcript digest mismatch"* ]]
|
|
[ ! -e "$target" ]
|
|
[ -f "$stage/fixture-set.json" ]
|
|
}
|
|
|
|
@test "publisher never exposes or deletes an externally replaced hidden-stage transcript" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local stage="$PROBE_DIR/.fixtures-replaced-transcript-stage"
|
|
local target="$PROBE_DIR/fixtures-replaced-transcript"
|
|
local replacement="$BATS_TEST_TMPDIR/replacement-transcript"
|
|
mv "$PROBE_DIR/fixtures" "$stage"
|
|
printf 'replacement-owner\n' > "$replacement"
|
|
|
|
mv "$replacement" "$stage/control-1.txt"
|
|
|
|
run python3 "$META_TOOL" publish \
|
|
--stage-dir "$stage" \
|
|
--target-dir "$target" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
local publish_status="$status"
|
|
[ "$publish_status" -eq 2 ]
|
|
[[ "$output" == *"transcript digest mismatch"* ]]
|
|
[ ! -e "$target" ]
|
|
[ "$(cat "$stage/control-1.txt")" = "replacement-owner" ]
|
|
[ -f "$stage/fixture-set.json" ]
|
|
}
|
|
|
|
@test "atomic publisher preserves an externally owned destination" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local stage="$PROBE_DIR/.fixtures-raced-stage"
|
|
local target="$PROBE_DIR/fixtures-raced-at-rename"
|
|
mv "$PROBE_DIR/fixtures" "$stage"
|
|
|
|
mkdir "$target"
|
|
printf 'external-writer\n' > "$target/sentinel"
|
|
|
|
run python3 "$META_TOOL" publish \
|
|
--stage-dir "$stage" \
|
|
--target-dir "$target" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
local publish_status="$status"
|
|
[ "$publish_status" -eq 2 ]
|
|
[[ "$output" == *"refusing to replace existing immutable fixture set"* ]]
|
|
[ "$(cat "$target/sentinel")" = "external-writer" ]
|
|
[ ! -e "$target/fixture-set.json" ]
|
|
[ "$(find "$target" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" -eq 1 ]
|
|
[ -f "$stage/fixture-set.json" ]
|
|
}
|
|
|
|
@test "publisher rejects a bound evaluator hash that does not match the repository file" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local stage="$PROBE_DIR/.fixtures-bad-evaluator-stage"
|
|
local target="$PROBE_DIR/fixtures-bad-evaluator"
|
|
mv "$PROBE_DIR/fixtures" "$stage"
|
|
python3 - "$stage/fixture-set.json" <<'PY'
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
manifest = json.load(open(path, encoding="utf-8"))
|
|
digest = manifest["capture_evaluator"]["harness"]["sha256"]
|
|
manifest["capture_evaluator"]["harness"]["sha256"] = (
|
|
digest[:-1] + ("0" if digest[-1] != "0" else "1")
|
|
)
|
|
payload = dict(manifest)
|
|
payload.pop("binding_sha256")
|
|
manifest["binding_sha256"] = "sha256:" + hashlib.sha256(
|
|
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
).hexdigest()
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
json.dump(manifest, handle, sort_keys=True, separators=(",", ":"))
|
|
handle.write("\n")
|
|
PY
|
|
|
|
run python3 "$META_TOOL" publish \
|
|
--stage-dir "$stage" \
|
|
--target-dir "$target" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"capture evaluator hashes do not match the exact repo-local evaluator files"* ]]
|
|
[ ! -e "$target" ]
|
|
[ -f "$stage/fixture-set.json" ]
|
|
}
|
|
|
|
@test "publisher rejects current probe inputs that differ from the capture" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local stage="$PROBE_DIR/.fixtures-input-race-stage"
|
|
local target="$PROBE_DIR/fixtures-publish-input-mutated"
|
|
mv "$PROBE_DIR/fixtures" "$stage"
|
|
|
|
printf 'MUTATED_BEFORE_PUBLISH\n' >> "$PROBE_DIR/question.md"
|
|
|
|
run python3 "$META_TOOL" publish \
|
|
--stage-dir "$stage" \
|
|
--target-dir "$target" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
local publish_status="$status"
|
|
[ "$publish_status" -eq 2 ]
|
|
[[ "$output" == *"current probe inputs differ from the self-contained capture"* ]]
|
|
[ ! -e "$target" ]
|
|
[ -f "$stage/fixture-set.json" ]
|
|
}
|
|
|
|
@test "failed live dispatch does not publish or score any prior transcript" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-failing"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli failing-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
printf 'producer failure\n' >&2
|
|
exit 77
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-failed
|
|
|
|
# An incomplete live dispatch is a FAILED run: nonzero, no scorecard, no
|
|
# fixture set, and no capture stage left in the probe directory.
|
|
[ "$status" -ne 0 ]
|
|
[ -z "$output" ]
|
|
[ ! -e "$PROBE_DIR/fixtures-failed" ]
|
|
[ -z "$(find "$PROBE_DIR" -mindepth 1 -maxdepth 1 -name '.fixtures-failed.capture.*')" ]
|
|
[[ "$stderr" == *"producer failure"* ]]
|
|
[[ "$stderr" == *"incomplete live run; fixture set not published"* ]]
|
|
}
|
|
|
|
@test "a partial live run stays UNMEASURED even when each arm has one usable rep" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-partial"
|
|
local count="$BATS_TEST_TMPDIR/partial-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli partial-stub\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
if [[ "$n" -gt 2 ]]; then printf 'producer failure\n' >&2; exit 77; fi
|
|
prompt="$(cat)"
|
|
if [[ "$prompt" == *PRELUDE_ACTION* ]]; then response=ACTION; else response=ABSENT; fi
|
|
printf '{"type":"thread.started","thread_id":"partial-%s"}\n' "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"%s"}}\n' "$n" "$response"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" PROBE_STUB_COUNT="$count" \
|
|
SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-partial
|
|
|
|
# Half a capture is not a null result: it exits nonzero and writes nothing,
|
|
# so a caller cannot read a partial dispatch as an honest measurement.
|
|
[ "$status" -ne 0 ]
|
|
[ -z "$output" ]
|
|
[ ! -e "$PROBE_DIR/fixtures-partial" ]
|
|
[ -z "$(find "$PROBE_DIR" -mindepth 1 -maxdepth 1 -name '.fixtures-partial.capture.*')" ]
|
|
[[ "$stderr" == *"incomplete live run"* ]]
|
|
}
|
|
|
|
@test "multi-rep live capture fails closed when canonical input mutates during dispatch" {
|
|
python3 - "$PROBE_DIR/probe.json" <<'PY'
|
|
import json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["treatment_source"] = "canonical-skill"
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
local producer="$BATS_TEST_TMPDIR/codex-mutating"
|
|
local count="$BATS_TEST_TMPDIR/mutating-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli mutating-stub\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
prompt="$(cat)"
|
|
if [[ "$n" -eq 1 ]]; then printf '\nMUTATED_DURING_CAPTURE\n' >> "$PROBE_MUTATE_FILE"; fi
|
|
if [[ "$prompt" == *CANONICAL_ACTION* ]]; then response=ACTION; else response=ABSENT; fi
|
|
printf '{"type":"thread.started","thread_id":"mutating-%s"}\n' "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"%s"}}\n' "$n" "$response"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
PROBE_STUB_COUNT="$count" \
|
|
PROBE_MUTATE_FILE="$SKILLS/demo-skill/SKILL.md" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-mutated
|
|
|
|
[ "$status" -eq 2 ]
|
|
[[ "$stderr" == *"live capture inputs changed during dispatch"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-mutated" ]
|
|
}
|
|
|
|
@test "scorecard output refuses to overwrite existing evidence" {
|
|
make_fixture_set fixtures observed-model low ABSENT ACTION
|
|
local scorecard="$BATS_TEST_TMPDIR/scorecard.json"
|
|
printf 'sentinel\n' > "$scorecard"
|
|
|
|
run --separate-stderr bash "$HARNESS" --probe demo --replay --output "$scorecard"
|
|
|
|
[ "$status" -eq 2 ]
|
|
[ "$(cat "$scorecard")" = "sentinel" ]
|
|
[[ "$stderr" == *"refusing to overwrite immutable scorecard output"* ]]
|
|
}
|
|
|
|
@test "successful live capture publishes transcripts and manifest as one verified set" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-success"
|
|
local count="$BATS_TEST_TMPDIR/success-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli success-stub\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
prompt="$(cat)"
|
|
if [[ "$prompt" == *PRELUDE_ACTION* ]]; then response=ACTION; else response=ABSENT; fi
|
|
printf '{"type":"thread.started","thread_id":"success-%s"}\n' "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"%s"}}\n' "$n" "$response"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" PROBE_STUB_COUNT="$count" \
|
|
SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --live --capture --reps 2 \
|
|
--model requested-live --effort high
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ -f "$PROBE_DIR/fixtures/fixture-set.json" ]
|
|
[ ! -e "$PROBE_DIR/fixtures/.capture-inputs" ]
|
|
[ "$(json_field "$output" producer.model)" = "requested-live" ]
|
|
[ "$(json_field "$output" producer.identity.source)" = "test-override" ]
|
|
[ "$(json_field "$output" producer.identity.coverage_eligible)" = "false" ]
|
|
[ "$(json_field "$output" requested_producer.model)" = "requested-live" ]
|
|
[[ "$(json_field "$output" fixture_set.binding_sha256)" == sha256:* ]]
|
|
[ "$(json_field "$output" evaluator_matches_capture)" = "true" ]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
run python3 "$META_TOOL" verify \
|
|
--fixture-dir "$PROBE_DIR/fixtures" \
|
|
--probe-dir "$PROBE_DIR" \
|
|
--skills-dir "$SKILLS" \
|
|
--probe demo
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "canonical-skill treatment mode injects the bound SKILL.md rather than the prelude" {
|
|
python3 - "$PROBE_DIR/probe.json" <<'PY'
|
|
import json, pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = json.loads(path.read_text())
|
|
value["treatment_source"] = "canonical-skill"
|
|
path.write_text(json.dumps(value))
|
|
PY
|
|
rm "$PROBE_DIR/treatment-prelude.md"
|
|
local producer="$BATS_TEST_TMPDIR/codex-canonical"
|
|
local count="$BATS_TEST_TMPDIR/canonical-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli canonical-stub\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
prompt="$(cat)"
|
|
if [[ "$prompt" == *CANONICAL_ACTION* ]]; then response=ACTION; else response=ABSENT; fi
|
|
printf '{"type":"thread.started","thread_id":"canonical-%s"}\n' "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"%s"}}\n' "$n" "$response"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
PROBE_STUB_COUNT="$count" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-canonical
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" treatment_source)" = "canonical-skill" ]
|
|
[[ "$(json_field "$output" honesty)" == *"exact bound canonical SKILL.md treatment"* ]]
|
|
[ "$(json_field "$output" verdict)" = "BEHAVIORAL" ]
|
|
run python3 - "$PROBE_DIR/fixtures-canonical/capture-contract.json" <<'PY'
|
|
import json, sys
|
|
value = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
raise SystemExit(0 if [item["path"] for item in value["capture_inputs"]] == [
|
|
"probe.json", "question.md", "discriminator.sh"
|
|
] else 1)
|
|
PY
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "transcript self-report cannot override bound runtime producer identity" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-identity-spoof"
|
|
local count="$BATS_TEST_TMPDIR/count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli immutable-stub-version\n'; exit 0; fi
|
|
n=$(cat "$PROBE_STUB_COUNT" 2>/dev/null || printf '0')
|
|
n=$((n + 1))
|
|
printf '%s\n' "$n" > "$PROBE_STUB_COUNT"
|
|
cat >/dev/null
|
|
printf '{"type":"thread.started","thread_id":"spoof-%s","model":"self-reported-%s"}\n' "$n" "$n"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-%s","type":"agent_message","text":"model: forged-model\\nreasoning effort: forged-effort\\nABSENT"}}\n' "$n"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" PROBE_STUB_COUNT="$count" \
|
|
SKILL_PROBES_DIR="$PROBES" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-identity-spoof \
|
|
--model bound-model --effort low
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" producer.model)" = "bound-model" ]
|
|
[ "$(json_field "$output" producer.effort)" = "low" ]
|
|
[ "$(json_field "$output" producer.identity.version)" = "codex-cli immutable-stub-version" ]
|
|
[ "$(json_field "$output" producer.identity.source)" = "test-override" ]
|
|
[ "$(json_field "$output" producer.identity.coverage_eligible)" = "false" ]
|
|
}
|
|
|
|
@test "replay DEGRADES a rep whose transcript reads a SKILL.md (skill-read-contamination)" {
|
|
# The committed premortem tier-2 fixture is the real contaminated capture:
|
|
# every rep fetched skills/premortem/SKILL.md off disk with exit 0. Under
|
|
# the contamination rule the whole set collapses to UNMEASURED 0/0 usable.
|
|
# setup() points the harness at a per-test sandbox; this case reads the
|
|
# real committed fixture, so resolve the real repo trees.
|
|
unset SKILL_PROBES_DIR SKILL_PROBE_SKILLS_DIR
|
|
run --separate-stderr bash "$REPO_ROOT/scripts/probe-skill.sh" \
|
|
--probe premortem-plan-shape-t2 --replay --fixtures fixtures-low-2026-08-26
|
|
[ "$status" -eq 0 ]
|
|
[[ "$stderr" == *"skill-read-contamination"* ]]
|
|
[[ "$output" == *'"verdict": "UNMEASURED"'* ]]
|
|
}
|
|
|
|
# transcript_message DIR NAME -> the agent_message text of one captured transcript.
|
|
transcript_message() {
|
|
python3 - "$1/$2.txt" <<'PY'
|
|
import json, sys
|
|
for line in open(sys.argv[1], encoding="utf-8"):
|
|
event = json.loads(line)
|
|
item = event.get("item") or {}
|
|
if event.get("type") == "item.completed" and item.get("type") == "agent_message":
|
|
print(item["text"])
|
|
PY
|
|
}
|
|
|
|
# path_without_seatbelt -> a PATH with no executable sandbox-exec anywhere on it:
|
|
# drops the pass-through stub dir and replaces any real dir that carries one
|
|
# with a symlink farm of that dir minus sandbox-exec.
|
|
path_without_seatbelt() {
|
|
local dir out="" farm entry
|
|
while IFS= read -r dir; do
|
|
[[ -n "$dir" && "$dir" != "$SEATBELT_STUB_DIR" ]] || continue
|
|
if [[ -x "$dir/sandbox-exec" ]]; then
|
|
farm="$BATS_TEST_TMPDIR/no-seatbelt$(printf '%s' "$dir" | tr '/' '_')"
|
|
mkdir -p "$farm"
|
|
for entry in "$dir"/*; do
|
|
[[ "$(basename "$entry")" != "sandbox-exec" ]] || continue
|
|
ln -s "$entry" "$farm/$(basename "$entry")"
|
|
done
|
|
dir="$farm"
|
|
fi
|
|
out="${out:+$out:}$dir"
|
|
done < <(printf '%s\n' "${PATH//:/$'\n'}")
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
@test "sealed dispatch gives the rep a scratch HOME and CODEX_HOME with no operator skill roots" {
|
|
local fake_codex_home="$BATS_TEST_TMPDIR/real-codex-home"
|
|
mkdir -p "$fake_codex_home/skills"
|
|
printf '{"token":"fixture"}\n' > "$fake_codex_home/auth.json"
|
|
# The operator config the rep must NOT inherit. It is not filtered any more,
|
|
# it is not read at all: the rep runs a GENERATED config.
|
|
cat > "$fake_codex_home/config.toml" <<'TOML'
|
|
model = "operator-model"
|
|
web_search = "live"
|
|
notify = ["/Users/operator/hook", "turn-ended"]
|
|
|
|
[mcp_servers.smart-connections]
|
|
command = "npx"
|
|
args = ["-y", "smart-connections-mcp"]
|
|
|
|
[projects."/somewhere/else"]
|
|
trust_level = "trusted"
|
|
TOML
|
|
local producer="$BATS_TEST_TMPDIR/codex-seal-env"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli seal-env-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
leak=no-leak
|
|
if test -e "$HOME/.agents/skills"; then leak=LEAK; fi
|
|
auth=no-auth
|
|
if test -f "${CODEX_HOME:-/nonexistent}/auth.json" && ! test -L "${CODEX_HOME}/auth.json"; then
|
|
auth="copy:$(cat "$CODEX_HOME/auth.json" | tr -d '\n\"{} ')"
|
|
fi
|
|
config=no-config
|
|
if test -f "${CODEX_HOME:-/nonexistent}/config.toml"; then
|
|
config="tables:$(grep -c '^\[' "$CODEX_HOME/config.toml" || true)"
|
|
config="$config:websearch:$(grep -c 'web_search = "disabled"' "$CODEX_HOME/config.toml" || true)"
|
|
config="$config:operator:$(grep -c 'operator-model\|notify\|mcp_servers' "$CODEX_HOME/config.toml" || true)"
|
|
fi
|
|
names="$(env | sed 's/=.*//' | sort | tr '\n' ' ')"
|
|
text="home=$HOME|codex_home=${CODEX_HOME:-unset}|tmpdir=${TMPDIR:-unset}|$leak|auth=$auth|config=$config|env=$names|cwd=$PWD"
|
|
printf '{"type":"thread.started","thread_id":"seal-env-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-seal","type":"agent_message","text":"%s"}}\n' "$text"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" CODEX_HOME="$fake_codex_home" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-seal
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" seal.seal_mode)" = "seatbelt" ]
|
|
[ "$(json_field "$output" seal.coverage_eligible)" = "true" ]
|
|
[ "$(json_field "$output" verdict)" = "INERT" ]
|
|
local text rep_home
|
|
text="$(transcript_message "$PROBE_DIR/fixtures-seal" control-1)"
|
|
rep_home="${text#home=}"; rep_home="${rep_home%%|*}"
|
|
[ -n "$rep_home" ]
|
|
[ "$rep_home" != "$HOME" ]
|
|
[[ "$rep_home" != "$REPO_ROOT"/* ]]
|
|
[[ "$text" == *"|codex_home=$rep_home/.codex|"* ]]
|
|
[[ "$text" == *"|no-leak|"* ]]
|
|
[[ "$text" != *LEAK* ]]
|
|
# auth.json is COPIED, not symlinked: the real home is read-denied, so a
|
|
# symlink into it cannot resolve inside the seal.
|
|
[[ "$text" == *"|auth=copy:token:fixture|"* ]]
|
|
# The generated config carries no table, turns web search off, and holds
|
|
# nothing of the operator's file.
|
|
[[ "$text" == *"|config=tables:0:websearch:1:operator:0|"* ]]
|
|
# The rep's environment is exactly what the seal discloses.
|
|
local names
|
|
names="${text#*|env=}"; names="${names%%|*}"
|
|
local declared
|
|
declared="$(python3 -c '
|
|
import json, sys
|
|
print(" ".join(sorted(json.loads(sys.argv[1])["seal"]["env_allowlist"])))
|
|
' "$output")"
|
|
local name
|
|
for name in $names; do
|
|
[[ " $declared " == *" $name "* ]] || {
|
|
echo "rep saw undeclared variable: $name" >&2
|
|
false
|
|
}
|
|
done
|
|
[[ "$text" != *"|cwd=$REPO_ROOT"* ]]
|
|
[ "$(json_field "$output" seal.rep_env.HOME)" = "$rep_home" ]
|
|
[ "$(json_field "$output" seal.rep_env.CODEX_HOME)" = "$rep_home/.codex" ]
|
|
[[ "$text" == *"|tmpdir=$(json_field "$output" seal.rep_env.TMPDIR)|"* ]]
|
|
[ "$(json_field "$output" seal.auth_copied)" = "true" ]
|
|
[[ "$(json_field "$output" seal.config_sanitized)" == *web_search* ]]
|
|
[ "$(json_field "$output" seal.platform)" = "$(uname -s)" ]
|
|
[ "$(json_field "$output" seal.mechanism)" = "sandbox-exec" ]
|
|
# The network seal is recorded, and the whole real CODEX_HOME is denied,
|
|
# not only its skills directory.
|
|
[ "$(json_field "$output" seal.network.mode)" = "proxy-allowlist" ]
|
|
[[ "$(json_field "$output" seal.network.proxy)" == 127.0.0.1:* ]]
|
|
[[ "$(json_field "$output" seal.network.hosts)" == *chatgpt.com* ]]
|
|
[ "$(json_field "$output" seal.real_codex_home)" = "$fake_codex_home" ]
|
|
[[ "$(json_field "$output" seal.denied_read_roots)" == *"$fake_codex_home"* ]]
|
|
[[ "$(json_field "$output" seal.launcher_chain)" == *"$producer"* ]]
|
|
[[ "$(json_field "$output" seal.config_sha256)" == sha256:* ]]
|
|
# The bound text is the exact file bytes, trailing newline included, so
|
|
# the digest the verifier recomputes from config_text is the recorded one.
|
|
python3 -c '
|
|
import hashlib, json, sys
|
|
seal = json.loads(sys.argv[1])["seal"]
|
|
digest = "sha256:" + hashlib.sha256(seal["config_text"].encode("utf-8")).hexdigest()
|
|
assert digest == seal["config_sha256"], (digest, seal["config_sha256"])
|
|
assert seal["config_text"].endswith("\n"), "config_text lost its trailing newline"
|
|
' "$output"
|
|
[[ "$(json_field "$output" seal.dev_write_paths)" == *"/dev/null"* ]]
|
|
[[ "$(json_field "$output" seal.denied_read_roots)" == *"$HOME/.agents"* ]]
|
|
[[ "$(json_field "$output" seal.denied_link_roots)" == *"$(json_field "$output" seal.dispatch_root)"* ]]
|
|
[[ "$(json_field "$output" seal.profile_sha256)" == sha256:* ]]
|
|
# The run directory is gone with the harness process.
|
|
[ ! -e "$(json_field "$output" seal.run_root)" ]
|
|
}
|
|
|
|
@test "seatbelt profile denies the rep every byte the seal claims to deny" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "seatbelt (sandbox-exec) is macOS-only"
|
|
# The REAL kernel sandbox, not the pass-through seam.
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
local checkout_skill main_skill cache_dir custom_codex_home
|
|
checkout_skill="$(ls "$REPO_ROOT"/skills/*/SKILL.md | head -n 1)"
|
|
[ -s "$checkout_skill" ]
|
|
# The MAIN checkout this linked worktree shares: same canonical bytes under
|
|
# a path the checkout deny alone does not reach (Codex SEAL-ALT-002).
|
|
main_skill="$(dirname "$(git -C "$REPO_ROOT" rev-parse --path-format=absolute --git-common-dir)")/skills/$(basename "$(dirname "$checkout_skill")")/SKILL.md"
|
|
[ -e "$main_skill" ] || main_skill="$checkout_skill"
|
|
cache_dir="$(getconf DARWIN_USER_CACHE_DIR)"
|
|
# A CODEX_HOME outside HOME: only its /skills subdirectory was denied before,
|
|
# so its sessions and rollouts (which carry canonical text) stayed readable.
|
|
custom_codex_home="$BATS_TEST_TMPDIR/outside-codex-home"
|
|
mkdir -p "$custom_codex_home/sessions"
|
|
printf '{"token":"fixture"}\n' > "$custom_codex_home/auth.json"
|
|
printf 'CANONICAL_ACTION rollout\n' > "$custom_codex_home/sessions/rollout.jsonl"
|
|
|
|
# The producer runs INSIDE the seal and probes it itself: nesting another
|
|
# sandbox-exec here would prove nothing, because seatbelt does not nest.
|
|
local producer="$BATS_TEST_TMPDIR/codex-seal-read"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli seal-read-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
run="$(dirname "$(dirname "$PROBE_SEAL_PROFILE_FILE")")"
|
|
text=""
|
|
probe() {
|
|
local label="$1" rc=0
|
|
shift
|
|
"$@" >/dev/null 2>&1 || rc=$?
|
|
text+="$label=$rc|"
|
|
}
|
|
probe checkout cat "$PROBE_SEAL_CHECKOUT_TARGET"
|
|
probe main cat "$PROBE_SEAL_MAIN_TARGET"
|
|
probe skills cat "$PROBE_SEAL_SKILLS_TARGET"
|
|
probe home cat "$PROBE_SEAL_HOME_TARGET"
|
|
probe codexhome cat "$PROBE_SEAL_CODEX_HOME_TARGET"
|
|
probe cachedir ls "$PROBE_SEAL_CACHE_TARGET"
|
|
probe tmproot ls "$PROBE_SEAL_TMPDIR"
|
|
probe dispatchls ls "$run/dispatch"
|
|
probe dispatchcat cat "$run/dispatch/control-1.prompt"
|
|
probe dispatchstat stat -f %z "$run/dispatch/control-1.prompt"
|
|
probe proxylog cat "$run/dispatch/network.log"
|
|
probe steal mv "$run/dispatch/control-1.prompt" ./stolen
|
|
probe hardlink ln "$PROBE_SEAL_CHECKOUT_TARGET" ./linked
|
|
probe clone cp -c "$PROBE_SEAL_CHECKOUT_TARGET" ./cloned
|
|
ln -s "$PROBE_SEAL_CHECKOUT_TARGET" ./symlinked 2>/dev/null
|
|
probe symlink cat ./symlinked
|
|
probe write bash -c 'printf ok > ./written'
|
|
probe readback cat ./written
|
|
probe profile cat "$PROBE_SEAL_PROFILE_FILE"
|
|
# No direct egress: --noproxy keeps this off the harness proxy, so it is the
|
|
# kernel that refuses it and no CONNECT is attempted. The proxy's own refusal is
|
|
# a separate test, because it DEGRADES the rep by design.
|
|
probe curldirect /usr/bin/curl -sS -m 6 --noproxy '*' -o /dev/null https://raw.githubusercontent.com/
|
|
# Descriptors: only stdio is inherited, and writing to fd 9 fails.
|
|
# No harness descriptor is usable: 9 was the open transcript sink, and 3..8
|
|
# covered the dispatch handles.
|
|
usable=""
|
|
for fd in 3 4 5 6 7 8 9; do
|
|
if bash -c "printf x >&$fd" 2>/dev/null; then usable="$usable$fd,"; fi
|
|
done
|
|
text+="usablefds=${usable:-none}|"
|
|
printf '{"type":"thread.started","thread_id":"seal-read-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-seal","type":"agent_message","text":"%s"}}\n' "$text"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
CODEX_HOME="$custom_codex_home" \
|
|
PROBE_SEAL_CHECKOUT_TARGET="$checkout_skill" \
|
|
PROBE_SEAL_MAIN_TARGET="$main_skill" \
|
|
PROBE_SEAL_SKILLS_TARGET="$SKILLS/demo-skill/SKILL.md" \
|
|
PROBE_SEAL_HOME_TARGET="$HOME/.codex/config.toml" \
|
|
PROBE_SEAL_CODEX_HOME_TARGET="$custom_codex_home/sessions/rollout.jsonl" \
|
|
PROBE_SEAL_CACHE_TARGET="$cache_dir" \
|
|
PROBE_SEAL_TMPDIR="${TMPDIR:-/tmp}" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-seal-read
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" seal.sandbox_exec)" = "/usr/bin/sandbox-exec" ]
|
|
[ "$(json_field "$output" seal.network.mode)" = "proxy-allowlist" ]
|
|
local text
|
|
text="$(transcript_message "$PROBE_DIR/fixtures-seal-read" treatment-2)"
|
|
# Denied: every root the seal names (the checkout, the shared git root, the
|
|
# skills dir, the operator home, a CODEX_HOME outside it, the Darwin cache
|
|
# dir and the temp root), the dispatch contents and listing, both laundering
|
|
# paths, and both network paths.
|
|
local label
|
|
for label in checkout main skills home codexhome cachedir tmproot dispatchls \
|
|
dispatchcat proxylog steal hardlink clone symlink curldirect; do
|
|
[[ "$text" == *"|$label=0|"* ]] && {
|
|
echo "seal allowed $label: $text" >&2
|
|
false
|
|
}
|
|
[[ "$text" == *"$label="[1-9]* ]]
|
|
done
|
|
# Allowed: metadata on dispatch (node stats its stdio at startup), writing
|
|
# and reading back inside the workspace, and the profile itself.
|
|
[[ "$text" == *"|dispatchstat=0|"* ]]
|
|
[[ "$text" == *"|write=0|"* ]]
|
|
[[ "$text" == *"|readback=0|"* ]]
|
|
[[ "$text" == *"|profile=0|"* ]]
|
|
# Only stdio reached the producer: FD 9 was the open transcript sink and the
|
|
# dispatch handles sat just above it.
|
|
[[ "$text" == *"|usablefds=none|"* ]]
|
|
# Nothing this rep did reached the proxy at all.
|
|
[[ "$stderr" != *"network-egress"* ]]
|
|
}
|
|
|
|
@test "a rep that reaches for a host off the allowlist is refused and DEGRADED" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "the network seal needs the real seatbelt"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# Fable M2V-01: `curl https://raw.githubusercontent.com/.../skills/premortem/SKILL.md`
|
|
# inside the profile returned the canonical bytes, because the outer profile
|
|
# was `(allow default)` and codex's own sandbox is bypassed inside it. The
|
|
# rep's only egress is now the harness proxy, and the proxy allows only the
|
|
# bound hosts.
|
|
local producer="$BATS_TEST_TMPDIR/codex-egress"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli egress-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
/usr/bin/curl -sS -m 8 -o /dev/null \
|
|
https://raw.githubusercontent.com/boshu2/agentops/main/skills/premortem/SKILL.md \
|
|
>/dev/null 2>&1 || true
|
|
printf '{"type":"thread.started","thread_id":"egress-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-egress","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-egress
|
|
|
|
# Refused egress degrades the rep, so the capture is incomplete: nonzero,
|
|
# no scorecard, no fixture set.
|
|
[ "$status" -ne 0 ]
|
|
[ -z "$output" ]
|
|
[[ "$stderr" == *"network-egress"* ]]
|
|
[[ "$stderr" == *"raw.githubusercontent.com:443"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-egress" ]
|
|
[ -z "$(find "$PROBE_DIR" -mindepth 1 -maxdepth 1 -name '.fixtures-egress.capture.*')" ]
|
|
}
|
|
|
|
@test "a forked child is reaped and a rep that edits its config is DEGRADED" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "process-group reaping is exercised against the real seal"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# A producer that forks a child outliving the turn: the survivor would see
|
|
# the NEXT rep's workspace, which is the whole point of resetting one. It
|
|
# records its own pid so the test can prove the child is actually gone; the
|
|
# earlier version of this test passed while four `/bin/sleep 45` processes
|
|
# were still running with ppid 1, because GNU timeout had setpgid'd itself
|
|
# and the reap was signalling a group the rep was never in.
|
|
local producer="$BATS_TEST_TMPDIR/codex-survivor"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli survivor-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
/bin/sleep 45 &
|
|
printf 'SURVIVOR_PID=%s\n' "$!" >&2
|
|
printf 'trust_level = "trusted"\n' >> "$CODEX_HOME/config.toml"
|
|
printf '{"type":"thread.started","thread_id":"survivor-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-survivor","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-survivor
|
|
|
|
[ "$status" -ne 0 ]
|
|
[ -z "$output" ]
|
|
# The rep is degraded for BOTH reasons the harness can see.
|
|
[[ "$stderr" == *"config-mutated"* ]]
|
|
[[ "$stderr" == *"rep-survivor"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-survivor" ]
|
|
|
|
# Nothing the producer forked is still running.
|
|
local pids pid
|
|
pids="$(printf '%s\n' "$stderr" | sed -n 's/.*SURVIVOR_PID=\([0-9][0-9]*\).*/\1/p')"
|
|
[ -n "$pids" ]
|
|
for pid in $pids; do
|
|
! kill -0 "$pid" 2>/dev/null
|
|
[ -z "$(ps -o pid= -p "$pid" 2>/dev/null)" ]
|
|
done
|
|
}
|
|
|
|
@test "a child that ignores TERM still leaves the rep DEGRADED and dies" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "process-group reaping is exercised against the real seal"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# TERM alone is not a reap: a child that traps it has to be escalated to
|
|
# KILL, and the rep is degraded either way because it outlived its rep.
|
|
local producer="$BATS_TEST_TMPDIR/codex-stubborn"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli stubborn-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
bash -c 'trap "" TERM; sleep 45' &
|
|
printf 'SURVIVOR_PID=%s\n' "$!" >&2
|
|
printf '{"type":"thread.started","thread_id":"stubborn-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-stubborn","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-stubborn
|
|
|
|
[ "$status" -ne 0 ]
|
|
[[ "$stderr" == *"rep-survivor"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-stubborn" ]
|
|
# Nothing the producer forked is still running.
|
|
local pids pid
|
|
pids="$(printf '%s\n' "$stderr" | sed -n 's/.*SURVIVOR_PID=\([0-9][0-9]*\).*/\1/p')"
|
|
[ -n "$pids" ]
|
|
for pid in $pids; do
|
|
! kill -0 "$pid" 2>/dev/null
|
|
[ -z "$(ps -o pid= -p "$pid" 2>/dev/null)" ]
|
|
done
|
|
}
|
|
|
|
@test "PROBE_SEAL=none runs unsealed, prints the ineligible notice, and records seal_mode=none" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-seal-none"
|
|
local seal_copy="$BATS_TEST_TMPDIR/seal-none.json"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli seal-none-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
if [[ ! -e "$PROBE_STUB_SEAL_COPY" ]]; then
|
|
cp "$SKILL_PROBES_DIR"/demo/.fixtures-none.capture.*/seal.json "$PROBE_STUB_SEAL_COPY"
|
|
fi
|
|
printf '{"type":"thread.started","thread_id":"seal-none-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-seal","type":"agent_message","text":"home=%s"}}\n' "$HOME"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env PROBE_SEAL=none CODEX_EXEC_BIN="$producer" \
|
|
PROBE_STUB_SEAL_COPY="$seal_copy" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-none
|
|
|
|
[ "$status" -eq 0 ]
|
|
[[ "$stderr" == *"seal: none (coverage-ineligible)"* ]]
|
|
[ -f "$seal_copy" ]
|
|
[ "$(json_field "$(cat "$seal_copy")" seal_mode)" = "none" ]
|
|
[ "$(json_field "$(cat "$seal_copy")" coverage_eligible)" = "false" ]
|
|
[ "$(json_field "$output" seal.seal_mode)" = "none" ]
|
|
[ "$(json_field "$output" seal.coverage_eligible)" = "false" ]
|
|
[ "$(transcript_message "$PROBE_DIR/fixtures-none" control-1)" = "home=$HOME" ]
|
|
}
|
|
|
|
@test "live dispatch refuses to run unsealed when no seatbelt binary is usable" {
|
|
local producer="$BATS_TEST_TMPDIR/codex-never-run"
|
|
local count="$BATS_TEST_TMPDIR/never-run-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli never-run-stub\n'; exit 0; fi
|
|
printf 'invoked\n' >> "$PROBE_STUB_COUNT"
|
|
cat >/dev/null
|
|
exit 70
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
# (a) an unusable explicit binary refuses rather than falling back to PATH.
|
|
run --separate-stderr env PROBE_SEAL_SANDBOX_EXEC="$BATS_TEST_TMPDIR/absent-sandbox-exec" \
|
|
CODEX_EXEC_BIN="$producer" PROBE_STUB_COUNT="$count" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-unsealed
|
|
[ "$status" -eq 2 ]
|
|
[ -z "$output" ]
|
|
[[ "$stderr" == *"PROBE_SEAL_SANDBOX_EXEC is not executable"* ]]
|
|
[ ! -e "$count" ]
|
|
[ ! -e "$PROBE_DIR/fixtures-unsealed" ]
|
|
[ -z "$(find "$PROBE_DIR" -mindepth 1 -maxdepth 1 -name '.fixtures-unsealed.capture.*')" ]
|
|
|
|
}
|
|
|
|
@test "live dispatch refuses when no seatbelt binary exists at all" {
|
|
[[ -x /usr/bin/sandbox-exec ]] \
|
|
&& skip "the system seatbelt is present, so PATH cannot hide it"
|
|
local producer="$BATS_TEST_TMPDIR/codex-never-run"
|
|
local count="$BATS_TEST_TMPDIR/never-run-count"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli never-run-stub\n'; exit 0; fi
|
|
printf 'invoked\n' >> "$PROBE_STUB_COUNT"
|
|
cat >/dev/null
|
|
exit 70
|
|
SH
|
|
chmod +x "$producer"
|
|
local unsealed_path
|
|
unsealed_path="$(path_without_seatbelt)"
|
|
run --separate-stderr env -u PROBE_SEAL_SANDBOX_EXEC PATH="$unsealed_path" \
|
|
CODEX_EXEC_BIN="$producer" PROBE_STUB_COUNT="$count" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-unsealed2
|
|
[ "$status" -eq 2 ]
|
|
[[ "$stderr" == *"sandbox-exec"* ]]
|
|
[[ "$stderr" == *"PROBE_SEAL=none"* ]]
|
|
[ ! -e "$count" ]
|
|
}
|
|
|
|
@test "the seal runs the system seatbelt, not a sandbox-exec earlier on PATH" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "the PATH-shadow hazard is macOS-only"
|
|
# A stub named sandbox-exec sits FIRST on PATH and records that it ran. The
|
|
# harness must take /usr/bin/sandbox-exec by absolute path instead: before
|
|
# this fix the wrap invoked the bare name while the record claimed the
|
|
# system binary, so a shadowing stub would have silently disabled the seal.
|
|
local marker="$BATS_TEST_TMPDIR/shadow-marker"
|
|
local shadow="$BATS_TEST_TMPDIR/shadow-bin"
|
|
mkdir -p "$shadow"
|
|
cat > "$shadow/sandbox-exec" <<'SH'
|
|
#!/usr/bin/env bash
|
|
printf 'shadow ran\n' >> "$PROBE_STUB_SHADOW_MARKER"
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in -p|-f|-n|-D) shift 2;; *) break;; esac
|
|
done
|
|
exec "$@"
|
|
SH
|
|
chmod +x "$shadow/sandbox-exec"
|
|
|
|
local producer="$BATS_TEST_TMPDIR/codex-shadow"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli shadow-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
printf '{"type":"thread.started","thread_id":"shadow-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-shadow","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env -u PROBE_SEAL_SANDBOX_EXEC PATH="$shadow:$PATH" \
|
|
PROBE_STUB_SHADOW_MARKER="$marker" CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-shadow
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" seal.sandbox_exec)" = "/usr/bin/sandbox-exec" ]
|
|
[ "$(python3 -c '
|
|
import json, sys
|
|
print(json.loads(sys.argv[1])["seal"]["wrap"][0])
|
|
' "$output")" = "/usr/bin/sandbox-exec" ]
|
|
[ ! -e "$marker" ]
|
|
}
|
|
|
|
# probe_input_field DIR NAME FIELD -> one field of the transcript's bound
|
|
# probe-input event (the first JSONL line).
|
|
probe_input_field() {
|
|
python3 - "$1/$2.txt" "$3" <<'PY'
|
|
import json, sys
|
|
event = json.loads(open(sys.argv[1], encoding="utf-8").readline())
|
|
print(event.get(sys.argv[2], "MISSING"))
|
|
PY
|
|
}
|
|
|
|
# workspace_covered SCORECARD PATH -> succeeds when PATH (literal or realpath)
|
|
# sits under one of the scorecard's seal.writable_roots.
|
|
workspace_covered() {
|
|
python3 - "$1" "$2" <<'PY'
|
|
import json, os, sys
|
|
roots = json.loads(sys.argv[1])["seal"]["writable_roots"]
|
|
candidates = {sys.argv[2], os.path.realpath(sys.argv[2])}
|
|
covered = any(
|
|
path == root or path.startswith(root.rstrip("/") + "/")
|
|
for path in candidates
|
|
for root in roots
|
|
)
|
|
raise SystemExit(0 if covered else 1)
|
|
PY
|
|
}
|
|
|
|
@test "each live rep starts from an emptied workspace and receives the prompt on stdin only" {
|
|
# 2026-09-03 defect: every rep's prompt file was written into the ONE shared
|
|
# live workspace, so a control rep ran `rg --files`, saw treatment-1.prompt,
|
|
# read it, and held the canonical SKILL.md bytes. The prompt is delivered on
|
|
# stdin; nothing a rep can list from its cwd may carry it or any sibling.
|
|
# The second pass made the workspace ONE path, reset between reps, so the
|
|
# seatbelt profile is constant and the contract binds one profile digest.
|
|
local producer="$BATS_TEST_TMPDIR/codex-workspace"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli workspace-stub\n'; exit 0; fi
|
|
prompt="$(cat)"
|
|
if [[ "$prompt" == *QUESTION* ]]; then stdin=stdin-prompt; else stdin=no-stdin-prompt; fi
|
|
listing="$(ls -A1 . | tr '\n' ',')"
|
|
sibling=none
|
|
for candidate in ./*.prompt ./*.jsonl ./*.stderr ./*.txt ./*.json; do
|
|
[[ -e "$candidate" ]] || continue
|
|
sibling="read:${candidate#./}:$(wc -c < "$candidate" | tr -d ' ')"
|
|
break
|
|
done
|
|
carried=none
|
|
[[ -e ./carried-over ]] && carried=LEFTOVER
|
|
[[ -e "$HOME/.codex/carried-over" ]] && carried="$carried,HOME-LEFTOVER"
|
|
printf 'rep\n' > ./carried-over
|
|
printf 'rep\n' > "$HOME/.codex/carried-over"
|
|
text="cwd=$PWD|entries=${listing:-empty}|$stdin|carried=$carried|sibling=$sibling"
|
|
printf '{"type":"thread.started","thread_id":"workspace-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"item-workspace","type":"agent_message","text":"%s"}}\n' "$text"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-workspace
|
|
|
|
[ "$status" -eq 0 ]
|
|
[ "$(json_field "$output" seal.seal_mode)" = "seatbelt" ]
|
|
local name text cwd first=""
|
|
for name in control-1 treatment-1 control-2 treatment-2; do
|
|
text="$(transcript_message "$PROBE_DIR/fixtures-workspace" "$name")"
|
|
cwd="${text#cwd=}"; cwd="${cwd%%|*}"
|
|
[ -n "$cwd" ]
|
|
[[ "$cwd" != "$REPO_ROOT"/* ]]
|
|
[[ "$cwd" != "$PROBE_DIR"/* ]]
|
|
# (a) the prompt arrives on stdin; the cwd holds no prompt file and no
|
|
# other rep's artifacts.
|
|
[[ "$text" == *"|entries=empty|"* ]]
|
|
[[ "$text" == *"|stdin-prompt|"* ]]
|
|
[[ "$text" == *"|sibling=none" ]]
|
|
# (b) nothing the previous rep wrote in the workspace or the scratch
|
|
# CODEX_HOME survived the reset.
|
|
[[ "$text" == *"|carried=none|"* ]]
|
|
# (c) one path for the whole capture, so the profile stays constant.
|
|
[ -z "$first" ] || [ "$cwd" = "$first" ]
|
|
first="$cwd"
|
|
# (d) the seal's writable roots cover the workspace, and the bound
|
|
# probe-input event records where the rep ran and that it was reset.
|
|
workspace_covered "$output" "$cwd"
|
|
[ "$(probe_input_field "$PROBE_DIR/fixtures-workspace" "$name" workspace)" = "$cwd" ]
|
|
[ "$(probe_input_field "$PROBE_DIR/fixtures-workspace" "$name" workspace_reset)" = "True" ]
|
|
done
|
|
[ "$(json_field "$output" seal.workspace_root)" = "$first" ]
|
|
[ "$(json_field "$output" verdict)" = "INERT" ]
|
|
run python3 "$META_TOOL" verify \
|
|
--fixture-dir "$PROBE_DIR/fixtures-workspace" \
|
|
--probe-dir "$PROBE_DIR" --skills-dir "$SKILLS" --probe demo
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "a group that will not die returns the fatal code, not a degraded rep" {
|
|
# The reap has three outcomes and only two used to matter. A process that
|
|
# outlived KILL returned the same "something survived" as one that died to
|
|
# TERM, so the schedule continued past a group nobody could prove empty.
|
|
# The two reap functions are sliced out of the SHIPPED harness, so this
|
|
# tests the bytes that run, without adding a seam to the harness for it.
|
|
local slice="$BATS_TEST_TMPDIR/reap.sh"
|
|
sed -n '/^rep_group_members() {/,/^}/p;/^reap_rep_group() {/,/^}/p' \
|
|
"$HARNESS" > "$slice"
|
|
[ -s "$slice" ]
|
|
grep -q 'reap_rep_group()' "$slice"
|
|
|
|
local psdir="$BATS_TEST_TMPDIR/ps-immortal"
|
|
mkdir -p "$psdir"
|
|
# Always reports one member of the queried group, whatever it is: the
|
|
# observable shape of a process that survives TERM and KILL.
|
|
cat > "$psdir/ps" <<'SH'
|
|
#!/usr/bin/env bash
|
|
printf '%s 4242\n' "$PROBE_STUB_PGID"
|
|
SH
|
|
chmod +x "$psdir/ps"
|
|
|
|
run env PATH="$psdir:$PATH" PROBE_STUB_PGID=31337 bash -c '
|
|
set -uo pipefail
|
|
. "'"$slice"'"
|
|
reap_rep_group 31337
|
|
echo "rc=$?"
|
|
'
|
|
[ "$status" -eq 0 ]
|
|
# 2 is the fatal code the dispatcher turns into an aborted capture; 1 would
|
|
# only degrade the rep.
|
|
[[ "$output" == *"rc=2"* ]]
|
|
|
|
# And the caller treats 2 as fatal: the dispatcher exits before the reset.
|
|
grep -q 'could not prove the .* process group empty; capture aborted' "$HARNESS"
|
|
}
|
|
|
|
@test "an unreadable process table aborts the capture rather than reading as empty" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "process-group reaping is exercised against the real seal"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# Piping a failed `ps` into awk printed 0, so "could not look" and "nothing
|
|
# there" were the same answer.
|
|
local psdir="$BATS_TEST_TMPDIR/ps-broken"
|
|
mkdir -p "$psdir"
|
|
printf '#!/usr/bin/env bash\nexit 1\n' > "$psdir/ps"
|
|
chmod +x "$psdir/ps"
|
|
local producer="$BATS_TEST_TMPDIR/codex-psfail"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli psfail-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
printf '{"type":"thread.started","thread_id":"psfail-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"i","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env PATH="$psdir:$PATH" CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-psfail
|
|
|
|
[ "$status" -ne 0 ]
|
|
[[ "$stderr" == *"capture aborted"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-psfail" ]
|
|
}
|
|
|
|
@test "a child that calls setsid is caught by the run-root residue check" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "session escape is exercised against the real seal"
|
|
command -v lsof >/dev/null 2>&1 || skip "lsof is required for the residue check"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# A setsid() child leaves the rep's process group entirely, so the group
|
|
# reap cannot see it: four such survivors once outlived a capture with only
|
|
# `config-mutated` firing. Anything still holding the run root is the same
|
|
# defect by another route.
|
|
local producer="$BATS_TEST_TMPDIR/codex-setsid"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli setsid-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
python3 -c '
|
|
import os, sys, time
|
|
if os.fork() == 0:
|
|
os.setsid()
|
|
os.chdir(sys.argv[1])
|
|
sys.stderr.write("SETSID_PID=%d\n" % os.getpid())
|
|
sys.stderr.flush()
|
|
time.sleep(45)
|
|
' "$PWD" &
|
|
sleep 0.5
|
|
printf '{"type":"thread.started","thread_id":"setsid-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"i","type":"agent_message","text":"ABSENT"}}\n'
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-setsid
|
|
|
|
[ "$status" -ne 0 ]
|
|
[[ "$stderr" == *"rep-survivor"* ]]
|
|
[[ "$stderr" == *"session escape"* ]]
|
|
[ ! -e "$PROBE_DIR/fixtures-setsid" ]
|
|
|
|
local pids pid
|
|
pids="$(printf '%s\n' "$stderr" | sed -n 's/.*SETSID_PID=\([0-9][0-9]*\).*/\1/p')"
|
|
[ -n "$pids" ]
|
|
for pid in $pids; do
|
|
[ -z "$(ps -o pid= -p "$pid" 2>/dev/null)" ]
|
|
done
|
|
}
|
|
|
|
@test "the rep's environment is exactly the seal's env_allowlist" {
|
|
[[ "$(uname -s)" == "Darwin" && -x /usr/bin/sandbox-exec ]] \
|
|
|| skip "the env boundary is exercised against the real seal"
|
|
unset PROBE_SEAL_SANDBOX_EXEC
|
|
export PATH="${PATH#"$SEATBELT_STUB_DIR:"}"
|
|
# Emptying the environment from inside the dispatch subshell could not clear
|
|
# bash's readonly exports (SHELLOPTS, BASHOPTS, UID, EUID, PPID): they
|
|
# reached the producer undeclared. `env -i` is the boundary now.
|
|
export SHELLOPTS BASHOPTS 2>/dev/null || true
|
|
local producer="$BATS_TEST_TMPDIR/codex-env"
|
|
cat > "$producer" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${1:-}" == "--version" ]]; then printf 'codex-cli env-stub\n'; exit 0; fi
|
|
cat >/dev/null
|
|
names="$(env | sed 's/=.*//' | sort -u | tr '\n' ' ')"
|
|
printf '{"type":"thread.started","thread_id":"env-%s"}\n' "$$"
|
|
printf '{"type":"turn.started"}\n'
|
|
printf '{"type":"item.completed","item":{"id":"i","type":"agent_message","text":"env=%s"}}\n' "$names"
|
|
printf '{"type":"turn.completed","usage":{"input_tokens":1,"cached_input_tokens":0,"output_tokens":1}}\n'
|
|
SH
|
|
chmod +x "$producer"
|
|
|
|
run --separate-stderr env CODEX_EXEC_BIN="$producer" \
|
|
bash "$HARNESS" --probe demo --live --reps 2 --fixtures fixtures-env
|
|
|
|
[ "$status" -eq 0 ]
|
|
local text names declared
|
|
text="$(transcript_message "$PROBE_DIR/fixtures-env" control-1)"
|
|
names="${text#env=}"
|
|
declared="$(python3 -c '
|
|
import json, sys
|
|
print(" ".join(sorted(json.loads(sys.argv[1])["seal"]["env_allowlist"])))
|
|
' "$output")"
|
|
# Set equality: nothing the rep sees is undeclared, and nothing declared is
|
|
# missing.
|
|
run python3 -c '
|
|
import sys
|
|
seen = set(sys.argv[1].split())
|
|
declared = set(sys.argv[2].split())
|
|
extra = sorted(seen - declared)
|
|
missing = sorted(declared - seen)
|
|
print("extra:", extra, "missing:", missing)
|
|
raise SystemExit(0 if not extra and not missing else 1)
|
|
' "$names" "$declared"
|
|
[ "$status" -eq 0 ]
|
|
# The readonly exports that used to survive are gone.
|
|
[[ "$names" != *SHELLOPTS* ]]
|
|
[[ "$names" != *BASHOPTS* ]]
|
|
}
|