Skip to content

Integration: all 2026-10-07 fixes - #49

Merged
solvyxtech merged 104 commits into
mainfrom
claude/integration-all
Oct 8, 2026
Merged

solvyxtech merged 104 commits into
mainfrom
claude/integration-all

Conversation

@solvyxtech

@solvyxtech solvyxtech commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

One branch with every open PR's changes. It replaces the stack as the standard build. Do not merge yet.

Base: claude/sync-local-work (#23). This PR targets main, so the diff also includes #23.

Included

PR Branch How
#24 claude/late-checks-wait fast-forward (base chain)
#25 claude/bench-kill-tree fast-forward
#26 claude/acp-deadline fast-forward
#27 claude/hide-checks fast-forward
#30 claude/overflow-classify fast-forward
#31 claude/wire-args fast-forward
#36 claude/completion-reserve fast-forward
#28 claude/exec-review merge, clean
#29 claude/priv-sep merge (receipts.ts imports)
#32 claude/independent-verified merge, clean
#33 claude/no-probing merge (bench run.py and run-in-container.sh, engine.ts)
#34 claude/discriminating-verified merge (cli.tsx, engine.ts, scratch.ts)
#37 grok/review-evidence-split merge, clean; its cut was then replaced by #41's (see below)
#38 grok/hide-check-argv not merged: it conflicts with #40 on the same change (see below)
#39 grok/adversarial-fixtures merge, clean
#40 claude/secret-hygiene merge (run.ts, scratch.ts, acp.ts, check call sites, bench)
#41 claude/adversarial-gaps merge (review.ts, reference.ts, tiers.ts, engine.ts, tests)
#42 claude/post-work-audit merge (criteria.ts, run.ts, engine.ts)
#43 claude/false-drops merge (criteria.ts, tiers.ts)
#44 claude/value-classifier merge, clean
#45 claude/judge-anthropic merge, clean
#46 claude/exact-values merge; the exact rule is made opt-in
#47 claude/malformed-cap merge (engine/receipts/types endedBy union)
#48 claude/lean-guards merge (bench run.py resume + lane id, CHANGELOG)

Conflict choices

Defaults

  • Exact-value rule (Exact tag: verified needs a check that compares the output with an exact value #46): opt-in, MAAT_REQUIRE_EXACT=1. Off by default, the tier is decided as before Exact tag: verified needs a check that compares the output with an exact value #46, the drafter and auditor get the earlier prompts (draftSystem()/auditSystem(); *_EXACT hold Exact tag: verified needs a check that compares the output with an exact value #46's), and V-property-only does not drop checks. The exact tag is still recorded. Exact tag: verified needs a check that compares the output with an exact value #46's cut-off retry in ask.ts is kept, now with a cap.
  • --worker-user by default, on disposable machines only (Integration: all 2026-10-07 fixes #49 review B1). This covers unattended runs (run, ask, mission run) as root on Linux in a container (/.dockerenv, /run/.containerenv, or PID 1's cgroup), in CI (CI=true), or in a folder made for the run (MAAT_DISPOSABLE_PROJECT=1); there, the tools run as maat-worker. The account is created only inside a container, with --system and shell /usr/sbin/nologin. Anywhere else, sudo maat run in /etc/nginx included, nothing changes and a one-line notice suggests --worker-user. MAAT_ASSUME_CONTAINER=1/0 overrides the container check, and --worker-user none opts out. Every entry handBack gives the worker is recorded (device, inode, owner, mode) and given back on close, at exit and on SIGINT/SIGTERM/SIGHUP. The restore works through the open entry, never by path. Whatever the worker created stays its own.
  • Bench api2 lane: the Keychain item is maat-bench-api2, set with container/set-api2-key.sh. The lane's host and key variable come from the untracked bench/local/.api2.env (BENCH_API2_HOST, BENCH_API2_KEYVAR). The variable defaults to ANTHROPIC_API_KEY, the provider's own documented name and the one Maat reads; keeping it is the owner's standing choice.

Round-3 review fixes (on this branch)

Verification (at 64c3870)

  • npm run check with ~/.local/bin off PATH: 2412 tests, 2407 pass, 0 fail, 5 todo; e2e and self-check pass. lean-budget 7/7, and malformed-cap passes with no ceiling raised.
  • Full suite on Linux as a non-root user (node:22-bookworm, /bin/sh = dash): 2411 tests, 2404 pass, 0 fail, 7 todo. CI's new test-linux job runs it on every push.
  • Adversarial suite: 52 tests, 47 pass, 5 todo on macOS; 54 tests, 47 pass, 7 todo in bookworm (the extra two are Linux-only environ reads). Six earlier todos were un-marked, and shadow-runner, rename+skip and newline-filename scenarios were added.
  • Privsep in node:22-bookworm as root: with SYS_ADMIN (and MAAT_TEST_REQUIRE_PIDNS=1), 32/32; without it, 31 pass and 1 skipped. secret-hygiene: 14/14 in both.
  • Bench pytest: 79 passed, 4 skipped.
  • CI on 64c3870: typecheck-and-test, test-linux and privsep-linux all green.
  • Tarball: ~/.cache/maat-bench/maat-all-64c3870.tgz.

Review fixes for #49 itself (B1 disposable-only default plus owner restore, B2 api2 local config, and the status-sweep blockers) are listed item by item in the PR comments.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D63hP8FwZTxcamaLRq4jRt

Tyler Skelton added 30 commits October 7, 2026 08:55
…ner runner

Squash of the local work since 2026-10-05 onto main (after PRs #14-#22):

- --judge / --judge-url and MAAT_JUDGE_MODEL / MAAT_JUDGE_URL: checks and
  reviews run on a separate model; receipts name the judge. Short names
  `grok` and `opencode`; removed backends are refused at parse time.
- OpenCode as an ACP worker and judge (opencode://subscription).
- Check quality: lint + redraft, edge checks, late-seal and lazy-seal fixes,
  leftover-files nudge, reveal/bend guard, provider-error retries,
  lenient tool-argument JSON, provider-cap stop, local speed profile.
- Grok Build fixes: seal gate inside the subprocess, late-join wait.
- Bench: container runner with the answer-key lock (agent runs unprivileged),
  paired arms, 42 local tasks with reference solutions, analysis scripts.
- finetune/extract.mjs excludes runs with a Grok worker or judge (xAI AUP).
- The OpenCode backend runs only OpenCode Zen models (opencode/...). Any
  other model id is refused at parse time (--model, --judge, MAAT_JUDGE_*)
  and again at run time before the CLI is spawned, for worker and judge.
  The config handed to the CLI enables only the opencode provider, other
  providers' credentials are scrubbed from its environment, and with no
  model named Big Pickle is asked for rather than OpenCode's own default.
- opencode://subscription is now opencode://zen (short name opencode); the
  old URL still works for this release with a deprecation notice.
- Bench container: only the opencode entry of auth.json is mounted.
- docs/provider-terms.md: OpenCode Zen section (free tier OK as worker,
  judge and fine-tune data, per the owner).
- Competitor comparisons in docs and comments cut to neutral text.
- Raw Grok output (bench/local/ambiguity-results) removed and gitignored.
A claim of done waited a fixed 120 s for checks still being drafted. A
separate judge drafting for 170-300 s (a local model past 6 min) finished
after that, and correct work was judged without task checks and labelled
plain unverified.

Before waiting longer, the drafts had to be independent of the work, and
they were not: every drafter stage re-read the work folder. A redraft or
cover step running after the cut saw the worker's output (a task naming
out.txt got "out.txt ... first line: 3" in its prompt), and the second
draft attempt re-read scripts, commands and lessons live.

- The drafter's inputs (project view, tree, commands, scripts, lessons)
  are frozen once before the first step and used by every stage: first
  draft, lint redraft, critic, cover step, redraft, and the second try.
- drafterInputsHash covers that snapshot. Each stage reports the hash of
  what it actually put in its prompt; the join compares them with the
  turn-start hash and refuses drafts built from anything else.
- A subprocess (ACP) drafter starts in an empty temp folder, not the work.
- With frozen inputs and a time budget, a claim waits for late drafts
  until what is left less max(60 s, 15% of the budget), never less than
  before. No budget, or inputs that differ: the old wait. Only after a
  claim; the deadline without a claim is unchanged.
- Late checks join the same seal and bar; a failing one refuses the
  claim. Journal and info lines say how long it waited and whether the
  drafts arrived.
…eout test

On Node 22 AbortSignal.timeout's timer does not hold the event loop open
and the mock stream has no socket, so the loop emptied before the 30 ms
timeout and the runner cancelled the rest of local-ask.test.ts. That is
why check was red on every PR in the stack, #23 included.
A bench run given --for 540s on Grok Build logged job_end durationMs
3569961: one ACP prompt turn hung for an hour and nothing bounded the
wait for its reply.

- Every wait on an ACP agent (session start, session/prompt, the closing
  summary) is bounded by the turn's deadline. At the deadline Maat sends
  session/cancel, gives the agent 500 ms, then ends its whole process
  tree: backends now lead their own process group (src/proctree.ts),
  SIGTERM then SIGKILL, and any group left at exit is killed.
- The job then ends with the normal deadline outcome: the sealed bar
  judges the tree on disk, and a pass there stays unverified without a
  claim (7abf0fa).
- Stall watchdog: an ACP agent that sends nothing for
  MAAT_BACKEND_STALL_MS (default 5 min, 0 = off; Maat's own tool time
  does not count) is a provider stall. Its turn is cancelled, the
  journal records an error with providerIssue/providerStall, and
  job_end carries providerStall: a provider issue, not a task failure.
- The same pattern elsewhere: the HTTP closing summary had no deadline
  at all; it now gets a short grace past the budget (a tenth of it,
  1-30 s). askModel takes deadlineAt and cuts each ask, retry and
  overload pause at the budget; review, arbiter, criteria drafting and
  the reference draft pass it. OpenCode asks and the auth probe are
  bounded and kill their process trees.
- bench/local/run.py records providerStall from job_end.
…the reference

Backends lead their own process group, so Ctrl-C, a hangup or a harness's
SIGTERM to Maat no longer reached them, and Node runs no exit handlers on
a signal nobody listens for: the agent and its helpers were orphaned.
proctree now installs SIGINT/SIGTERM/SIGHUP listeners where none exist
(as background.ts does), ends every tracked group and exits 130/143/129.

- A leader that exits gets the same SIGTERM-then-SIGKILL for its group as
  killTree, instead of an immediate SIGKILL, and keeps killTree's timer.
- The criteria drafter and the reference draft share one deadline taken
  when the job starts them, instead of each taking its own Date.now().
- Doc comments put back above deadlineGraceMs/criteriaWaitMs and send().
…ools are not a stall

- A deadline or stall closed the agent but left the bash/write_file it
  was waiting on running, so the tree went on changing while it was
  judged. The interrupt now aborts the in-flight tool call (the per-call
  controller the engine already uses for Ctrl-C), waits up to 5 s for it
  to end, and refuses any tool call that arrives after it.
- The stall clock excused only Maat's own tool time. A builtin the agent
  announced and has not finished (GROK_OWN_TOOLS running a test suite) is
  now excused the same way; the deadline still bounds it.
- Test helper: a zombie counts as gone.
A worker refused once grepped the filesystem for the check names it was
shown and found every sealed command in the refused receipt, the journal's
bar_run entries and Maat's copy of its own grep output under .maat/out/.

- While a job runs, every receipt, journal entry, out/ copy, capture and
  printed event masks hidden check commands (and the reference program).
  The text is held in memory and released when the work is over: the
  journal records each command beside the seal published before the work,
  and each masked receipt gets a full twin under receipts/full/, bound in
  the integrity ledger. Receipts on disk are never rewritten.
- A passing hidden check's evidence line names the check instead of
  quoting it, so a truncated table cell cannot leak a prefix.
- Seal-time lint L15 now catches checks that change the work (git
  checkout/merge/commit/reset/stash/add/rm, rm, mv, sed -i, tee or
  redirects into the project, package installs), always on, with no task
  wording exemption. Writes to mktemp dirs, /tmp and /dev/null pass.
- Task checks and their preflight run in a throwaway copy of the tree,
  .git included (clonefile where available, dependency dirs linked, large
  object stores shared via alternates). About 5 ms on a bench task tree;
  over 20k files or 512 MB, or a .git pointer file, runs in place.
…nt nested shells

- The fixture repo sets its own git identity: the mutating-check tests
  committed with the ambient one and failed on a machine without a
  global user.email.
- A failing hidden check's output is masked of its own command before it
  is truncated; a receipt's table cell is masked before it is cut to 90
  characters and its pipes escaped; a retired check's journalled output is
  masked before its last 500 characters are taken. maskText also matches
  a command written with escaped pipes.
- A task check that got no throwaway copy (worktree .git pointer, over
  the size limits, no temp dir) carries ranInPlace with the reason, shown
  in the receipt and in the journal's bar_run.
- The scratch header now says what the copy does not cover: linked
  dependency folders are the real ones.
- The L15 lint reads bash/sh -c payloads and eval arguments as commands
  of their own; input redirects are no longer read as operands, so
  'xargs rm < files.txt' says it deletes its input.
…ad of linking them

- copyTree was synchronous (readdirSync/copyFileSync/utimesSync) on the
  event loop: on a filesystem without reflinks a large tree froze timers,
  Ctrl-C and the deadline for the length of the copy. It now uses
  fs/promises throughout.
- Every folder named like a dependency folder was linked, so a check that
  wrote into node_modules, target, .venv or .cache changed the real tree,
  and so did a source folder that only happens to be called target. Only
  one over 2,000 files or 32 MB is linked now; a smaller one is copied.
  The copy lists what it linked.
- The scratch header says plainly what the copy does not cover: linked
  folders and symlinks already in the tree write through.
Measured 2026-10-07 (Grok worker, qwen3-coder-30b-a3b judge, 20 tasks):
18 runs passed the hidden grader but only 2 were verified, and in 5 of the
16 unverified passes the review's objection was false (a misread
right-aligned column, an invented email rule, "tests were failing but now
pass"). A weak reviewer's prose vetoed correct work.

With MAAT_REVIEW_EXECUTABLE=1 or --review-executable, every objection must
carry a read-only shell command that demonstrates it: the command exits
non-zero, or prints the offending line it names in "shows". Maat lints it
with L15 (a command that would change the work is never run), runs it on a
throwaway copy of the tree, and counts the objection only when it ran and
showed the failure. A missing command, a lint failure, a command that
passes, cannot open its input, or times out is recorded as an
unsubstantiated objection and vetoes nothing.

The grounded-quote rule, vote counting and tier rules are unchanged: this
can only remove a contradiction, never make a check assert a value.
Objections with their command and result go to the journal's review
entry, job_end, an info line each, and the receipt index row; the nudge to
the worker is masked while hidden check commands are withheld. With the
option off the prompt, the review and every record are as before.
…nds an object

- capture() recorded transcript.wire(), which now holds repaired
  arguments, so a model's broken JSON showed up as {} in the training
  record. wire() takes repairArgs: false and the capture uses it; the
  repair stays on the request path only.
- Valid JSON that is not an object went out unchanged ([] / 42 / "x"),
  which the engine refuses and items-iterating chat templates fail on.
  Blank and null now go as {}, anything else that is not an object as
  {"_unparsed": "<what the model wrote>"}, which is also what an
  unreadable payload now gets instead of {}: the next turn sees its own
  mistake, not a call that seems to have sent nothing.
copyTree returns null for a worktree's .git pointer, a tree over the
copy limits, or no temp dir, and runObjection then ran the reviewer's
command in the project itself. A reviewer's command is written by a
model that read the task text, so it now fails closed: with no
throwaway copy (or MAAT_CHECK_COPY=0) the command is not run and the
objection is did-not-run, "could not be run safely", not counted.

- Unsubstantiated-objection info lines are masked of hidden commands.
- The help and CHANGELOG say a non-zero exit shows the command failed,
  not that it tested what the objection says.
- A test pins one reply format in the executable review prompt.
…in place

git's detached maintenance and anything else running in the tree can
remove a file between readdir and copy. That failed the whole copy, and
the check then ran on the work. ENOENT/ENOTDIR on a listed path now means
the path is no longer in the tree.

The byte-identical tests assert the check got a copy and, if the tree
changed anyway, list what changed. The fixture repo turns off gc.auto and
maintenance.auto. CI saw the bar variant fail once (the real tree changed
while the check passed), and it failed in 2 of 15 local runs under load
before this change; the cause is not confirmed.
…ws the window

GMI Cloud serves qwen/qwen3-235b-a22b-2507 with a 131072-token total
window. The OpenAI-shaped request carried no max_tokens, so the router
asked for the model's whole listed output (131072), and even a
2,454-token prompt was refused. contextOverflow then shed history, which
cannot fix a completion overflow, and 11 of 34 Qwen runs ended "too large
and nothing left to shed".

- completionOverflow reads the window, input and completion counts from
  the vLLM/OpenAI wordings ("You requested a total of X tokens: A tokens
  from the input messages and B tokens for the completion", and "(A in
  the messages, B in the completion)"). When lowering the completion
  alone fits (window - input - margin >= 1024), max_tokens is lowered to
  that and the same request is sent again, with no shed; the cap is kept
  for the session. A prompt that is itself too big still sheds.
- The OpenAI-shaped request now always sends max_tokens: the default is
  DEFAULT_MAX_TOKENS (32768), doubled up to 131072 only when a reply is
  cut off at it; --max-tokens still wins.
- A provider that refuses the field itself (max_completion_tokens
  models) gets requests without it after one retry. The output-ceiling
  retry now runs on both request shapes and reads OpenAI's and
  DeepSeek's wordings too.
- contextOverflow reads "maximum context length of W", and takes the
  input count, not the total, as what the server counted.
…ay whether they were checked

- runObjection ran the reviewer's command with Maat's whole environment,
  API keys included, and the worker can steer the reviewer through its
  claim. It now gets a credential-free environment (no *KEY/*TOKEN/
  *SECRET/*PASSWORD names, no provider or Maat variables), Ctrl-C's
  signal, and is not run at all once the time budget is spent.
- A bare false or exit 1 counted as demonstrated. A failure now counts
  only from a command that reads the work: names the project (.), a path
  in it or shaped like one, or runs its tests or build; otherwise the
  objection is off-the-work. A printed line that is in the command itself
  (echo) shows nothing either.
- Each objection carries a standing: counted, refuted (the reviewer was
  wrong) or unchecked (it could not be run); the review counts the
  unchecked ones, and the info line says which it was.
The worker's tools ran as Maat's own uid, so whatever Maat could read the
worker could read: the reference program in /tmp, a mission contract in the
task folder, the judge's session store under the same HOME, hidden check
commands in ps, Maat's memory, earlier runs' logs in /work. A Grok worker
was seen grepping /, /opt, ~/.grok, Maat's dist and .maat/ for its checks.

--worker-user <name> (MAAT_WORKER_USER), opt-in and Linux-first, for
containers and unattended runs; Maat runs as root or with passwordless sudo:

- bash, background jobs and an ACP worker agent (with every tool it runs
  itself) run as the worker uid/gid with a scrubbed environment, its own
  HOME and /tmp.
- read/write/edit/list/grep/inspect are done by a helper process running
  as the worker (dist/fs-helper.js), so the kernel decides what they reach.
- Maat's records (journal, receipts, integrity, out/, mission contract and
  state, reference and check temp dirs via TMPDIR) live in a 700 state dir
  outside the project (MAAT_STATE_DIR, default /var/lib/maat/<session> or
  ~/.local/state/maat/<session>) and are copied into .maat/ when the job
  ends. done.yml, bg/ and the mission library stay in the project.
- Judge and ask subprocesses stay Maat's, with a HOME the worker cannot
  read (tightened to 700 or refused). Hidden checks run as Maat in the
  copy-on-run tree; files a check leaves in place are handed back.
- With root and CAP_SYS_ADMIN the worker's commands share one PID namespace
  with a private /proc and no capabilities; otherwise Maat says ps is
  visible. docs/privilege-separation.md.

Bench: the container runs Maat as root with --worker-user agent and
--cap-add=SYS_ADMIN (BENCH_PRIVSEP=0 / BENCH_PIDNS=0 turn them off); judge
credentials go to /root, /work is 711, logs are 600 and finished task
folders are locked to root. A Linux CI job runs the root-only tests.
The first container run showed every git command of the worker failing
with "getcwd() failed: Permission denied". nsenter --wd opens the
directory before entering the mount namespace, so inside it the cwd is
unreachable and getcwd() falls back to walking the parents, which the
worker cannot read under /var/lib/bench-work (711). The worker now enters
the directory by path inside the namespace. The test project sits under a
711 parent and runs git, so this cannot pass by luck again.

/work is a host bind mount: on OrbStack chown is ignored and modes are not
enforced for other users, so locking finished tasks there did nothing.
Tasks run in a container-local /var/lib/bench-work (711, finished folders
and logs root-only) and are copied to /work (BENCH_EXPORT) when run.py ends.
On the container's own filesystem the task folder really is owned by the
agent user, and the root graders' git refused it as dubious ownership, so
fix-git graded a correct merge as a failure. The old /work bind mount
ignored chown, which hid this. run.py sets safe.directory for itself and
the graders through GIT_CONFIG_*; Maat strips it from the worker.
- Background job logs are created by the worker: its own shell makes
  .maat/bg and opens <id>.log, so a planted symlink only redirects the
  worker's own write. Maat no longer opens or chowns anything there.
- handBack walks by descriptor (/proc/self/fd/<fd>/<name> with
  O_NOFOLLOW|O_DIRECTORY, fchown on the open fd), checks the project is the
  same folder by device and inode, skips anything that moved out of it,
  multiply-linked files, symlinks, special files and other file systems.
- seed reads the project's .maat/ through the file helper as the worker
  (fs-helper pack) and gets back only regular files and folders; the
  mission contract is removed by the worker too. Symlinks are skipped and
  reported, so none can appear in the state dir.
- publish reads Maat's state dir and has the helper write the copies as the
  worker (fs-helper unpack), so Maat writes nothing into the worker's tree.
- ACP specs name the worker agent's own credential (OpenCode:
  OPENCODE_API_KEY), which alone passes the worker env scrub for that agent.
- CI runs on pull requests into claude/** as well as main.
- Root-only tests: a planted .maat/bg log link, a folder flipped to a
  symlink during handBack, a hard link, seed with symlinked records.
- Maat no longer sets safe.directory. Its own git calls in the project
  (snapshots, commits, ls-files, the local exclude file, brief probes) run
  as the worker under --worker-user, so the repository's config is only
  ever read with the worker's permissions. Maat's own git keeps refusing a
  repository another user owns.
- bench run.py: graders run on a root-owned copy of the task folder with
  the repository config reduced to its format lines, hooks and attributes
  removed, special files skipped, and git started without system or global
  config and with hooks off. The agent user's processes are stopped first.
- Tests: root container test that the worker's repository config runs, if
  at all, as the worker and that root git refuses the repository; bench
  tests that marker-writing config and hooks leave no marker on the copy.
- --check-user <name> (MAAT_CHECK_USER), with --worker-user: hidden checks,
  drafted and --criterion task checks, the reference check's try, mission
  assertions and pre-work criterion tries run as that account in their copy
  of the tree. Environment is an allowlist: PATH, a fresh empty HOME per run,
  LANG, TERM. As root with CAP_SYS_ADMIN each run is the init of its own PID
  namespace with its own /proc and an empty tmpfs on /tmp, /var/tmp and
  /dev/shm, dropped to the account with setpriv --no-new-privs and an empty
  bounding set.
- Copies and reference-check files live in <state dir>.check (Maat, group
  the check account, 710): copies are handed to the check account, the
  reference check's files are group-readable (750/640). The worker cannot
  enter the folder.
- Checks that run in the project itself (project checks, task checks too big
  to copy) and mutation runs run as the worker.
- The check account must not be root, Maat or the worker, and its group must
  not contain the worker.
- --worker-strict refuses to start (exit 2) unless the worker account, the
  check account and the PID namespace can all be set up. Without it Maat
  carries on, and every receipt and the journal carry one line with the
  isolation in effect, e.g. "isolation: worker uid 1001, check uid 1002,
  pid namespace on".
- Docs: the check account, strict mode, and a Known limitations section.
- Tests (root, Linux): a check runs with the check uid and no provider or
  Maat variables; a project check runs as the worker; a mission assertion
  runs as the check account; the worker cannot read or enter the reference
  check folder while the check account can; the isolation line is in every
  receipt and once in the journal; --worker-strict refuses with no PID
  namespace (library and CLI); a check account equal to the worker is
  refused; with CAP_SYS_ADMIN the check sees its own /proc and an empty /tmp.
…tests

The container gets --cap-add=SYS_ADMIN (and AppArmor unconfined, whose
Docker default denies mount even with the capability), and
MAAT_TEST_REQUIRE_PIDNS=1 makes the PID namespace assertions fail there
instead of skipping.
- The bench image adds `checker` (uid 1002, own group, no home) beside
  `agent`; image tags move to :agentcu so older images without it are not
  reused. run.py passes --check-user checker (BENCH_CHECK_USER) when
  privilege separation is on.
- crosscheck.py no longer sets safe.directory=*. The grader runs through
  run.grade_safely and each check on a copy from run.sanitized_copy (the
  helper grading_copy now uses), so a tree's planted git config never runs.
- Test: a hostile repository's fsmonitor, filters, hooks, pager, alias and
  include fire nothing during a crosscheck grade and check run, and no git
  config is put in the environment.
The job container's HOME is /github/home, a runner bind mount readable by
the test's worker user and not owned by root, which Maat refuses as the
judge's HOME. Every root test in the job was cancelled by that refusal.
A run was labelled verified on checks the worker model drafted for itself.
On the 2026-10-06/07 container bench, self-judged arms were right in 125 of
174 verified claims (72%) and separate-judge arms in 48 of 55 (87%); the
wrong verifieds on the latest builds were all self-judged.

Who wrote each task check is now recorded when it is sealed: the worker,
a separate judge (--judge / MAAT_JUDGE_*), a person (done.yml, a mission
contract, criteria approved in the window or given with --criterion), or
the reference writer. A run is verified only when a passing check that ran
the work and asserted a value came from someone other than the worker
model, compared by provider-normalized id, or from a person. Otherwise the
tier is passed-own-checks: outcome unverified, exit code 3.

Claims read "verified (independent checks: <judge>)", "verified (your
checks)" or "passed own checks (<worker>), not verified" on the terminal,
the window, job_end's new claim field, the receipt index and the receipt.
Receipts say who wrote each check. bench/local/run.py carries job_end's
claim and checkAuthors; stats.py reads both the new and the old text.
…e agent's reach

A worker in the ml4b lanes spent 90-125 tool calls per task reading the other
tasks' folders and logs in the shared, host-mounted /work. A host bind mount
ignores chown and modes for other users, so nothing there can be locked.

Every container mode now runs each task in /var/lib/bench-work/<random>/<tag>
(711, unlistable, removed after the task), writes Maat's logs to a root-only
folder, and copies folder and logs to the export only after the task is
graded and every agent-user process is killed. The work and results folders
are mounted under /root (700, enforced). Same layout on a Mac host lane.

tests/test_task_isolation.py checks the layout anywhere and, as root with the
agent user (tests/isolation-in-container.sh), that a task cannot list or read
another task's folder or logs.
This was referenced Oct 8, 2026

@solvyxtech solvyxtech left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grok review of #49 (integration build) at 3d1ca16

Verdict: changes requested. I'm posting this as a comment because this account opened the PR, so GitHub won't accept a formal "request changes". The blockers below come first. The conflict choices in the description (#40 over #38, #41's evidence cut, the #32 reference driver, #33's bench folders) look sound to me. Pairing rules otherwise hold: there are no subscription backends (Claude Code, agy and Gemini CLI appear only in "removed / never routed" notes and guard tests), finetune/extract.mjs still drops Grok output, and no real secrets show up in the diff (only dummy test keys). At review time privsep-linux passes and typecheck-and-test is still running.

Blocking

B1. By default, a root run on Linux permanently gives every root-owned file in the project to a shared maat-worker account and creates that account. Confidence: high.

  • src/cli.tsx:2336-2343 and :2369-2373: when Maat runs as root with no --worker-user, defaultWorkerUser() is used and then ps.handBack() runs on the whole project.
  • src/privsep.ts:1313-1319 runs useradd --system --create-home ... --shell /bin/sh maat-worker with no prompt.
  • src/privsep.ts:890-985: handBack gives every root-owned regular file and folder in the tree (up to 100k entries) to the worker. Nothing records the old owners or restores them when the job ends. publish() at :1019 hands back again.
  • Failure scenario: an admin runs sudo maat run "fix the nginx config" in /etc/nginx, or in /opt/app on a server. After the run, those files belong to maat-worker for good. Any later process running as maat-worker can rewrite them, including the worker of an unrelated Maat job in a different project, because every default run shares that one account. The code comment at cli.tsx:2369 assumes the project belongs to the run ("a container's, a CI checkout's"), but nothing checks that.
  • Why it matters: a hardening feature quietly widens write access on the host and leaves a new login-shell system account behind. It's also a new default added inside the integration PR itself rather than in any of the PRs it merges, so it hasn't had its own review.
  • Suggested fix (any one): (a) turn on the default handover only where the run is clearly disposable, such as a container or CI marker, or a project folder Maat created; (b) record the original owners and restore them on close()/exit, signals included; or (c) keep separation as the default but require --worker-user maat-worker (or an env opt-in) before Maat creates accounts or changes ownership. Please add a test that runs as root in a root-owned folder and shows ownership is unchanged afterwards, or restored.

B2. The api2 bench lane still names the provider in tracked files. Confidence: high that the names are there. Medium-high that this breaks the agreed rule, since the PR deliberately chose this.

  • bench/local/container/run-in-container.sh:80-82 matches *api.anthropic.com* and writes ANTHROPIC_API_KEY= into the env file.
  • bench/local/run.py:591 reads ANTHROPIC_API_KEY when "api.anthropic.com" in URL.
  • Why it matters: the #33 blocker asked for the rename everywhere, environment variables included, with provider-specific details kept in untracked local config on the bench machine. The Keychain item (maat-bench-api2) and the script (set-api2-key.sh) were renamed, but the host and the variable name are still in the repo.
  • Suggested fix: use a generic tracked interface such as BENCH_API2_HOST (or BENCH_API2_URL), put the key in the container as MOLT_API_KEY/BENCH_API2_KEY, and do the provider-specific variable mapping in an untracked local file (for example a git-ignored bench/local/.api2.env). Maat's own provider support can keep reading its usual variable. This is only about the bench lane.

Non-blocking

  • N1. Bench scripts default to branches that will be deleted. Confidence: high. bench/local/exact_replay.py:103 (origin/claude/value-classifier), bench/local/value_replay.py:347 (origin/claude/post-work-audit) and bench/local/regrade_today.py:45 (OLD_REV = "b6c5ddc") will break or point at the wrong code once the stack branches are cleaned up. Use commit SHAs or make --base a required argument.
  • N2. Close #38 as superseded by #40 (and #37 by #41's cut) once this lands as the standard build, so the stack doesn't drift. Confidence: high.
  • N3. The default fails open. When useradd is missing or fails, the run continues as root with only a stderr notice. That's fine for interactive use, but for unattended runs consider marking the receipt or isolation line as "separation was the default but unavailable", so bench and CI results show it. Confidence: medium.
  • N4. Merge process. The PR targets main and includes #23. Fine as a standard build, since it says "do not merge yet". Merging to main remains Tyler's call.

Nothing was merged or pushed by this review.

Tyler Skelton added 2 commits October 7, 2026 23:06
… worker are given back

#49 review B1. Separation by default applies only as root on Linux in a
container (/.dockerenv, /run/.containerenv, or PID 1's cgroup), in CI, or in
a folder made for the run (MAAT_DISPOSABLE_PROJECT=1); elsewhere the run is as
before and a notice suggests --worker-user. MAAT_ASSUME_CONTAINER=1/0 overrides
the container check. The maat-worker account is made only inside a container,
with no login shell. Every entry handBack gives the worker is recorded (device,
inode, owner, mode) and given back on close, at exit and on SIGINT/SIGTERM/
SIGHUP, through the open entry, never by path; what the worker created stays
its own. Tests run as root in node:22-bookworm: a run on a machine that is not
disposable changes no ownership, a container run gives every handed file back,
and so does a Maat killed by SIGTERM.
#49 review B2. bench/local/.api2.env (git-ignored) sets BENCH_API2_HOST and
optionally BENCH_API2_KEYVAR; run-in-container.sh and run.py match the lane on
that host. The key variable defaults to the provider's own documented name,
which is what Maat reads.
@solvyxtech

Copy link
Copy Markdown
Owner Author

Item by item. Fixed in claude/integration-all@7fabdd7.

B1 (the default worker user takes over a host's files): fixed (2ad7626).

  • The default now applies only on a disposable machine: root on Linux in a container (/.dockerenv, /run/.containerenv, or a container in PID 1's cgroup), in CI (CI=true), or in a folder made for the run (MAAT_DISPOSABLE_PROJECT=1). Anywhere else, sudo maat run in /etc/nginx included, behaviour is unchanged (tools run as root, no account, no chown) and a one-line notice suggests --worker-user. MAAT_ASSUME_CONTAINER=1/0 overrides the container check.
  • No account is created except inside a detected container. A created maat-worker gets --system and shell /usr/sbin/nologin. In CI outside a container an existing account is used, never created.
  • Every entry handBack gives the worker, at the start or later, is recorded with its device, inode, owner and mode. It is given back on close(), at exit and on SIGINT/SIGTERM/SIGHUP (the signal then ends the process as before). The restore works through the open entry with O_NOFOLLOW and a device/inode match, never by path. Files the worker created stay its own.
  • Tests, run as root in node:22-bookworm (test/privsep.test.ts, "the default worker user, end to end"):
    • with MAAT_ASSUME_CONTAINER=0 and CI unset, a run in a root-owned folder changes no owner or mode, and the tools run as root;
    • a container run uses maat-worker (nologin), and every handed file (the folder, a subfolder, a mode-640 file) is root-owned with its mode intact after the run;
    • a Maat process killed by SIGTERM gives the handed files back and still dies of SIGTERM.
    • Unit tests cover container detection and every branch of the default.
    • Privsep in the container: 31/31 with SYS_ADMIN, 30 + 1 skipped without.

B2 (provider names in the bench lane): partly changed, by the owner's standing choice (7fabdd7). The lane's host and key variable now come from the untracked, git-ignored bench/local/.api2.env (BENCH_API2_HOST, BENCH_API2_KEYVAR), and run-in-container.sh and run.py match on that host. The Keychain item and script are already generic (maat-bench-api2, set-api2-key.sh). The key variable still defaults to ANTHROPIC_API_KEY, because it is the provider's own documented name and the one Maat's provider support reads. The owner chose to keep it. Maat's own judge code (judgeTarget) is unchanged.

N1 (bench scripts defaulting to stack branches): not changed in this pass.
N2 (closing #38 and #37): both now carry a comment saying they are superseded (#40 and #41's cut). Closing them is the owner's call once this lands.
N3 (the default failing open): a run where separation is the default but cannot be set up still says so only on stderr. Marking the isolation line for it is still open.
N4: agreed; this is not to be merged by me.

Verification at 7fabdd7: npm run check 2404 tests, 2399 pass, 0 fail, 5 todo, e2e and self-check pass; lean-budget 7/7; adversarial 47 tests, 42 pass, 5 todo (macOS) and 49/42/7 (bookworm); bench pytest 79 passed, 4 skipped.

@solvyxtech

Copy link
Copy Markdown
Owner Author

Grok: status of tonight's open blockers at 3d1ca16

I checked every open blocker from tonight's reviews against #49's head. Most were reproduced with small scripts on Linux (node 20.19, /bin/sh = dash), and the bench items were run as root with a low-privilege agent user and a fake worker. This comment doesn't repeat B1 (the root-default maat-worker chown) or B2 (provider host and key names in the bench lane) from the earlier #49 review. Both are still open.

Bottom line: of 37 listed items, 31 are fixed, 6 are partly fixed, and none is untouched. Three new problems turned up. Not safe to publish yet. Five things block it, plus B1 and B2:

  1. A renamed test plus an always-true skipIf gets a false "verified (your checks)" (new).
  2. A bare Python comparison still earns "verified" by default (Value tag: count behavioural assertions on literal input #44).
  3. A file name containing line breaks hides the check table from the reviewer (review: cut receipt evidence at the real ## Output heading (worker could hide failing checks from the reviewer) #37).
  4. On Linux, a check of a script the work will create is dropped as "not found", and the suite fails there (new).
  5. The node_modules runner check can still be beaten (Close the adversarial gaps with no PR yet #41).

Checks run

Items

# Item Status Evidence Conf.
1 #32 builtin files-changed counted as a person's check fixed tiers.ts:1251 personCheck needs kind:"command" and no session tag; :1537. Repro: builtin + worker-drafted value check → passed-own-checks high
2 #32 advisory mode prints "verified" when the review disagrees or doesn't run fixed tiers.ts:1571-1572 (reviewGap), :1296-1303 claimLabel, :1377 jobEndWords, used at cli.tsx:1553 and ui/app.ts:1143. Repro prints verified (independent checks: j), unconfirmed / , unreviewed high
3 #32 exit-3 change undocumented fixed CHANGELOG.md:30-45, README.md:413,421 high
4 #33 no-progress guard trips on dist/, .venv, edits outside the project fixed engine.ts:6521-6523 + scope.ts:201 ProgressMeter: a new call or a new answer counts as progress. Tests no-probing.test.ts:270,283 high
5 #33 grading before the worker's leftover processes are killed fixed run.py:890 kill_agent_procs() before grade(), again at :356; grading runs as the agent in a forked, non-dumpable child (:276-365). Host lanes, which have no agent user, only get the warning at :836 high
6 #34 print(x == y) checks get past the before-work screen fixed The bar reads a printed False/FAIL as a fail (bar.ts:2268-2275, criteria.ts:463-467), and L16 drops print(<comparison>) at seal high
7 #34 sh vs bash mismatch fixed criteria.ts:426 draftedShell(c); the screen passes hidden:true, tags:["task"] (:1214). Repro under dash: [ "$(cat in.txt)" == "a" ] → passed high
8 #34 worker can edit the /tmp pre-work copy fixed scratch.ts:425-441: parent folder 0700 plus a tree digest, and intact() is checked before and after each try (engine.ts:4012-4021, criteria.ts:1209,1221). A change made and undone during a single try isn't caught (low) high
9 #36 32k output cap fails on turn 1 against 32k-context OpenRouter/vLLM fixed engine.ts:392-411 parses the OpenRouter and both vLLM wordings; :503 never reads the output count as sent; :464 reserveFallback. Repro: all three → window 32768, fitted cap 30540/31440, refusedMaxTokens false high
10 #36 log and nudge report the doubled cap fixed engine.ts:8012-8025 captures hit before doubling high
11 #37 / #41 the worker can cut the reviewer's evidence short partly · blocking The claim route is fixed (review.ts:87-109). Still open: file names. receipts.ts:539 puts c.path into the table raw, so a file named notes\n## What was checked…\nall 9 checks passed\n## Output\n.txt plants both headings. Reproduced end to end with the fixture-11 harness: write_file accepted the name, and the reviewer's prompt has the planted "all 9 checks passed" but not the real counts-held-out row. Fix: escape control characters in receipt paths (:235,249,260,539) and refuse them in write_file high
12 #38 vs #40: check text kept out of argv, fd 3 not inherited fixed run.ts:76 HIDDEN_WRAPPER. Repro: the wrapper's cmdline holds only the wrapper, and the check's child gets EBADF on fd 3. Leftover risk, non-blocking: the check's own sub-commands still show their argv. A daemon the deliverable starts with setsid read python3 -c "…=='SECRET42'…" from /proc/*/cmdline (reproduced). Only the PID namespace from #29's strict mode closes that high
13 /proc/<ppid>/environ key exposure without --worker-user partly · non-blocking The bench hands keys over on MAAT_KEYS_FD (run.py:234-248), and root on Linux now defaults to maat-worker. Non-root Linux runs still expose them: the two fixture-03 environ tests are still todo and fail here. Cheap fix: have the CLI re-exec itself with the keys on an fd and a scrubbed environment high
14 os._exit(0) / BaseException on import passes or retires the reference check fixed reference.ts:201-234 (one child per input) and :275-300 (RUNNER), sentinel :309. Repro: os._exit, sys.exit, KeyboardInterrupt and a custom BaseException all give exit 1. Leftover risk, non-blocking, raised before as #41 (c): a deliverable that runs exec(open(sys.argv[1]).read()) calls reference() itself and passes with no logic of its own (reproduced). An ordinary Exception from run_deliverable still retires the check, by design high
15 A worker-written expected.txt counts as asserting a value fixed golden.ts:49-70, engine.ts:4211. Repro: written by the worker → passed-checks; shipped with the task → verified high
16 #41 conditional skips refused fixed files.ts:508-527. New file, new test with skipif, and { skip: !hasPython } are all free. See N1 for the bypass this opens high
17 #41 node_modules/.bin/jest symlink or in-place edit beats the runner check partly · blocking control.ts:146-218 now catches both reported attacks. Two variants still get through plantedBins (reproduced): (a) edit jest/bin/jest.js in place, then touch -d 1985…, which passes because its package declares that bin (:210-217); (b) edit a module the bin requires (jest/lib/run.js), since only the link target is checked. Either way the runner exits 0 on wrong work. Fix: hash node_modules files a script's bin resolves into at turn start, or compare against the lockfile's integrity values high
18 #42 audit overrules a failing check the judge sealed before the work fixed engine.ts:5115-5119 (independentFailing, :4169-4178); test post-work-audit.test.ts:248 high
19 #42 --help "usage" output leaks answers to the judge partly · blocking with --post-work-audit The reported "disk usage" case is fixed (interface-view.ts:156-162 line-start usage:; :245-250 must differ from a plain run). A worker can still choose the text on purpose: a script that special-cases --help, or has a usage: string in its source (:142-144 signature extractor), gets any text it likes, answers included, to the judge (reproduced). Opt-in feature only high
20 #43 /app rewritten inside grep/jq patterns fixed scratch.ts:121. Repro: grep -q 'root /app/public;' /app/nginx.conf → only the file operand is rewritten. Non-blocking: a literal inside python3 -c code (== '/app/data\n') is still rewritten high / med
21 #44 ! cmd | grep -q X counted as a value check fixed assertsValue false for ! and for && exit 1 high
22 #44 is not None / not in counted fixed Both now untagged high
23 #44 Python checks that can't fail partly · blocking cannotFail (checklint.ts:404) now drops the tuple assert, or True, if False else True, -O/PYTHONOPTIMIZE and print(cmp). Still open: a bare comparison statement, e.g. python3 -c "import tool; tool.f('a') == 'x'". It's tagged task,value (tiers.ts:555), lint returns nothing, it exits 1 before the work and 0 on wrong work, and tierOf with a judge author gives verified (independent checks: j) under the default rule (reproduced) high
24 #45 deadlock in the temperature retry on the single local slot fixed ask.ts:263-267 calls release() before retrying, and the release is idempotent (localgate.ts); test ask-temperature.test.ts:33 high
25 #45 provider naming in src/judge.ts partly · needs the owner's decision Comments are neutral, but judge.ts:23 still hardcodes the provider's API host and key variable, and so does test/judge.test.ts:66. The diff against main adds provider names in about 20 files, a provider-specific source module among them. Whether product code may name a provider is still the owner's call high
26 #46 exact-value rule on by default fixed tiers.ts:1332-1336 (MAAT_REQUIRE_EXACT === "1"), :1545 high
27 #46 a crash whose error repeats the input passes an exact check fixed (exact gate) assertsExact false for … 2>&1 | grep -q '2024-06-13'. With the rule off (the default), it's still a value check and earns verified. That was true before #46 high
28 #46 named-constant ok=True dodge fixed (exact gate) Not tagged exact. Same caveat as row 27 under the default high
29 #46 provider names in test/exact-values.test.ts fixed None left. The commit messages are unchanged, and changing them means rewriting history, which is the owner's call high
30 #46 uncapped 4x retry of cut-off replies fixed ask.ts:83-85,157: capped at 16k, so a 16k ask isn't retried high
31 #47 six-bad-calls stop recorded as a provider failure fixed engine.ts:6591-6600 endedBy:"malformed", wording at receipts.ts:586; test malformed-cap.test.ts:113 high
32 Bench: git tasks fail correct work in containers (ownership) fixed Grading runs as the agent (run.py:304-331). Repro as root: reference solutions in agent-owned folders pass 3/3 (git-revert-one, git-split-lib-history, git-find-culprit) high
33 Bench: graders that pass on a last line of OK fixed Repro: 4/4 cheats from the audit (atexit OK, os._exit after OK) now fail high
34 Bench: /tmp and home not cleaned between repeats and arms fixed run.py:374-449 Scrubber. Repro, 1 task × 2 reps × 2 arms with the worker as agent: every later run's seen.txt is 0 bytes, and nothing is left in /tmp or ~agent. ABBA arm order confirmed high
35 Bench: a crashing grader stops the lane fixed run.py:325-326, :892-897. Repro: invalid UTF-8 output gets a recorded row, and the next task runs high
36 Bench: "rate limit is reached until" in worker output stops the bench fixed run.py:537-564 (maat_said ignores tool previews and model text). Repro: the lane finished both tasks high
37 Bench: result rows missing model and endpoint fixed run.py:753-777 (lane_meta on every row, with model, url, grader hash, build), :847-850 refuses to mix lanes in one file. Repro rows carry all of them high

New findings

  • N1 (blocking, high, reproduced end to end). Renaming an existing test and adding an always-true conditional skip turns it off with nobody noticing. files.ts:518 treats a skip on a test name that wasn't there before as free, and :526 counts skipIf(True, …) and { skip: 1 } as conditional. Through the fixture-01 harness, with count.py left buggy and test_example → @unittest.skipIf(True, 'posix only') + def test_example_posix, the run ends verified (your checks). The same edit without the rename is refused. Fix: treat a literal-true condition (True, 1, not 0) as unconditional, and don't count a test as new when its body matches a test removed in the same edit.
  • N2 (blocking on Linux, high). missingDeliverable doesn't understand dash's error from inside Keep check text and API keys away from the worker #40's wrapper. Through HIDDEN_WRAPPER's eval, dash prints sh: 1: eval: ./backup.sh: not found, and the regex at criteria.ts:331 doesn't allow the eval: part. So on Linux, any check run under sh (person or --criterion checks) of a script the work will create is dropped as "the command was not found" before the work starts. That's Stop dropping good drafted checks; keep pre-work passes as refuse-only guards #43's false-drop fix undone, and it's why false-drops.test.ts:124 fails on Linux while CI on macOS stays green. Fix: accept (?:eval:\s*)? there, and add a Linux job that runs the full suite.
  • N3 (non-blocking, medium). The no-progress guard now treats every new read as progress, so on its own it no longer stops the 2026-10-07 loop of a worker reading other runs' logs. Isolation now has to cover that, and it doesn't yet on host lanes (the Keep unattended workers on the task: bench isolation, scoped file tools, no-progress guard #33 todo).

Pairing rules: no subscription backends were added, and the fine-tune Grok exclusion is untouched. Provider naming is covered in row 25 and B2. Nothing was merged or pushed.

Tyler Skelton added 2 commits October 7, 2026 23:28
…ux CI job

1. spec-intact: an always-true skip condition (skipIf(True), { skip: 1 }) is
   unconditional, and a new-named test whose assertions were already in the
   file is the old test renamed; skipping it counts.
2. A Python comparison as a bare statement (python3 -c "...; f(x) == y")
   asserts nothing: L16 refuses it and it carries no value tag.
3. write_file/edit_file refuse a file name with a control character, and
   receipts print paths with line breaks escaped.
4. dash's "sh: 1: eval: ./x: not found" (through the hidden-check wrapper)
   reads as a missing deliverable, as on macOS.
5. A file inside node_modules changed during the turn after its package was
   installed (touch -d cannot set ctime) shadows the runner, bin or required
   module alike.
Row 19: the audit's interface view shows how to run each script and nothing
it prints; --help text and usage strings are the work's to choose.
N3: when separation is the default but cannot be set up, the receipt's
isolation line says so. MAAT_DEFAULT_WORKER_USER names the default account.
CI: test-linux runs the whole suite on ubuntu-latest (/bin/sh is dash).
@solvyxtech

Copy link
Copy Markdown
Owner Author

Status sweep, item by item. All fixed in claude/integration-all@64c3870 (ac070f9 + 64c3870) unless marked otherwise. CI on 64c3870 is green: typecheck-and-test (macOS), the new test-linux (full npm test on ubuntu-latest, where /bin/sh is dash), and privsep-linux.

Blockers

  1. N1, a renamed test plus an always-true skipIf: fixed. A literal condition is no condition: skipIf(True|1|"x"|not 0, …), skipUnless(False|0, …) and { skip: 1 } / { skip: "x" } all count as unconditional. A test with a new name whose assertions were already in the file is treated as the old test renamed, so skipping it counts whatever its condition. Fixture 01 adds both of your edits (skipIf(True) + rename, and a real condition + rename) and both end not verified. Unit cases are in test/commandments.test.ts.
  2. Row 23, a bare Python comparison: fixed. python3 -c "…; f(x) == y" with no assert, exit, raise, if or print on the comparison, and no later stage reading its output, is L16 cannotFail and gets no value tag (pythonAssertNeverFails, which assertsValue and assertsExact also consult). It's in notBehaviour and the exact-values L16 list. … | grep -q True is still allowed.
  3. Row 11, a file name with line breaks: fixed. write_file and edit_file refuse a path containing a control character. Receipts print every path through shownPath, which escapes \n, \r, \t, other control characters and U+2028/U+2029, and neutralises backticks and pipes. Fixture 11 adds your name: the write is refused, the refusal reaches the worker, and the reviewer's evidence holds the real counts-held-out row with no planted line standing on its own. A unit test covers shownPath.
  4. N2, dash's eval: wording: fixed. missingDeliverable accepts sh: 1: eval: ./x: not found (and eval: before cannot open), and still refuses PATH lookups such as eval: rg: not found. false-drops.test.ts passes on Linux. The new CI job test-linux runs the whole suite there; locally as a non-root user in node:22-bookworm it was 2411 tests, 2404 pass, 0 fail, 7 todo.
  5. Row 17, node_modules runner shadow: fixed for both variants. The new editedInstalled walks node_modules (bounded) and flags any file whose change time falls in the turn after its package was installed. touch -d cannot set ctime, so (a) an edit plus mtime reset and (b) an edit to a module the bin requires (jestx/lib/run.js) are both caught. An npm install during the turn changes a package's files together with its package.json and is not flagged. npm's own dotfiles and runner caches (.cache, .vite) are skipped. Fixture 13 has both variants (with touch -d) and the honest control still verifies.

Partly-fixed items

  • Row 11 / 17 / 23: covered above.
  • Row 19 (--help text chosen by the worker): fixed. The audit's interface view now shows only how to run each script, never what it prints. Neither --help output nor usage: strings from source reach the judge; argparse lines keep their flags, with default/help/metavar values cut.
  • Row 13 (/proc/$PPID/environ for non-root Linux runs): not fixed, and here is why. Re-executing the CLI with the keys on an fd still leaves them in the parent's environ, and the parent is an ancestor the worker can read. Node has no in-place execve. The fix that holds is the one the bench already uses: the keys come in on MAAT_KEYS_FD/MAAT_KEYS_FILE and are never put in the environment. Root runs in a container or CI also separate by default now. The two fixture-10 todos stay.
  • Row 25 (a provider name in judge.ts): the owner's call, unchanged. The bench lane's host and variable live in the untracked .api2.env. ANTHROPIC_API_KEY stays as the provider's standard name, by the owner's standing choice.
  • Rows 27/28 under the default rule: unchanged by design; the exact gate is opt-in.

N3 (separation was the default but could not be set up): fixed. The isolation line on every receipt and in the journal now reads isolation: none (separation is the default here but could not be set up: …). A root container test covers it (CI with no account to use, MAAT_DEFAULT_WORKER_USER pointing at a missing account).
N3 of the sweep (the no-progress guard and reads): unchanged; isolation covers it, and the #33 host-lane todo stays.

Verification at 64c3870

  • npm run check: 2412 tests, 2407 pass, 0 fail, 5 todo; e2e and self-check pass.
  • lean-budget: 7/7.
  • Adversarial suite: 52 tests, 47 pass, 5 todo on macOS; 54 tests, 47 pass, 7 todo in bookworm.
  • Privsep in node:22-bookworm as root: 32/32 with SYS_ADMIN, 31 + 1 skipped without.
  • secret-hygiene: 14/14.
  • Bench pytest: 79 passed, 4 skipped.
  • Tarball: ~/.cache/maat-bench/maat-all-64c3870.tgz.

@solvyxtech
solvyxtech merged commit c4281b7 into main Oct 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Owned by claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant