Skip to content

Review levels: the security pages, their citations test, the adversarial suite and the Dockerfile are locked as engine guards (#858) [PROTECTED: owner merges] - #885

Merged
jayden1711 merged 6 commits into
mainfrom
issue-858-lock-guard-docs
Oct 21, 2026
Merged

jayden1711 merged 6 commits into
mainfrom
issue-858-lock-guard-docs

Conversation

@jayden1711

@jayden1711 jayden1711 commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Intent: lock the engine's public security claims (docs/guardrails.md, docs/security*.md, SECURITY.md), the test that keeps their citations true (test/docs-guardrails.test.ts) and the adversarial suite (test/adversarial/**) as engine guards, the way #853 locked deploy/**: L3, self-modification and never auto-merged, in the engine's own repo, whatever a narrowed review.yaml says. The same PR closes the #853 gaps the owner asked for: the engine's container image (Dockerfile*, dockerfile*, Containerfile*) and its root deployment files (a bare deploy, compose.*, docker-compose.*) are locked in code. Five of those globs match no file yet, so each has a NOT_YET_FILES entry in test/merge-policy.test.ts, added under the owner's decision on this PR (option 2: additions only, each paired with a new lock). A new test keeps every NOT_YET_FILES entry paired with its lock. Nothing loosens (measured: the 216-row before/after table below, and the managed-app rows for the five new globs).

Risk class: protected. The change touches src/review.ts (the review levels), .worklane/review.yaml and test/merge-policy.test.ts, all in the owner's critical set.

Closes #858

Digest lines

The NOT_YET_FILES change, exactly as committed (test/merge-policy.test.ts, line 106; head 1687d568). The two existing entries are unchanged and in place; the five new ones are appended. Because the table is one line, the line itself shows as changed in the diff (measured: git diff --cached, -1 +1):

-const NOT_YET_FILES = [`${BRAND.configDir}/experiments.yaml`, 'deploy/**'];
+const NOT_YET_FILES = [`${BRAND.configDir}/experiments.yaml`, 'deploy/**', 'deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*'];

Each added entry and its paired lock in src/review.ts SELF_MODIFICATION_PARTS, exactly as committed:

NOT_YET_FILES entry Paired lock line in src/review.ts
'dockerfile*' { what: "the engine's container image", globs: ['Dockerfile*', 'dockerfile*', 'Containerfile*'] },
'Containerfile*' { what: "the engine's container image", globs: ['Dockerfile*', 'dockerfile*', 'Containerfile*'] },
'deploy' { what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },
'compose.*' { what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },
'docker-compose.*' { what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },

The same five globs also join ENGINE_ROOT_FILES, so they stand aside in a managed app's repo:

const ENGINE_ROOT_FILES = ['SECURITY.md', 'Dockerfile*', 'dockerfile*', 'Containerfile*', 'deploy', 'compose.*', 'docker-compose.*'];

What changed

  • src/review.ts SELF_MODIFICATION_PARTS. Every glob that was there stays.
    • New part, "the public security claims": docs/guardrails.md, docs/security*.md, SECURITY.md.
    • New part, "the engine's container image": Dockerfile*, dockerfile*, Containerfile*.
    • New part, "the engine's deployment files": deploy, compose.*, docker-compose.*. It sits next to "the machine setup" (deploy/**).
    • "The tests of the harness controls" gains test/docs-guardrails.test.ts, test/adversarial/**, test/engine-guard-locks.test.ts and test/not-yet-files-locks.test.ts. The last two are the tests that hold these locks, and each locks itself.
  • src/review.ts engineSourceGlob: a new exact list, ENGINE_ROOT_FILES (above). These root-level globs stand aside in a managed app's repo, like everything under ENGINE_TREE. The match is exact (includes), so no existing glob changes scope. Measured: test/review.test.ts › "the globs that hold in every repo are exactly …" passes, and its exact list is unchanged.
  • test/merge-policy.test.ts: the five NOT_YET_FILES entries above. Nothing else in that file changed (measured: git diff origin/main -- test/merge-policy.test.ts on this branch shows that one line only).
  • test/not-yet-files-locks.test.ts (new, round 3):
    • It reads NOT_YET_FILES from test/merge-policy.test.ts with the weakening hold's own tableLines. It fails if any entry is not exactly one of SELF_MODIFICATION_PARTS's globs, so an exception can't arrive without its lock, or outlive it.
    • It fails closed on a table it can't read plainly: missing, empty, declared twice, a spread or a name, or any interpolation other than ${BRAND.configDir}. Fixture checks in the same file cover each of these.
    • It is a test that reads test source, so it is a backstop. The by-design protection is the never-grow hold above.
  • .worklane/review.yaml engine-guards: new lines only. They cover every path above, including the five root globs. An engine that predates the code lock reads this file from main.
  • test/engine-guard-locks.test.ts (new) has three tests:
    1. Each path is L3, self-modification, auto === false and floor auto === false, with the plain "changes X: path" text. This holds under this repo's review.yaml, the example's, and one that drops every category, with engine scope given or left out. Round 3 adds dockerfile, Containerfile, Containerfile.dev, deploy, compose.yaml, compose.yml, compose.override.yaml, docker-compose.yml, docker-compose.prod.yml and the new pairing test.
    2. A managed app's same-named files keep exactly main's level and categories.
    3. This repo's review.yaml holds each path and the root deployment files under engine-guards.
  • Adversarial suite:
    • Eight new cases in cases/locked-paths.json. Six are from round 1: the security pages docs-only, the citations test, the suite's own runner, cases and todo, a rename of SECURITY.md to an unlocked name, a deleted case file, and the Dockerfile running as root. Round 3 adds container-image-files-added (a root dockerfile and Containerfile) and deployment-files-added (a bare deploy, compose.yaml, docker-compose.yml).
    • The Tests: a public adversarial suite of hostile PRs the merge gate must hold, as data (#707 part 1) #855 todo "this suite's own runner and fixtures are self-modification …" and the round-2 todo for the five root globs are both replaced by those cases.
  • changelog.d/885-lock-security-pages-and-suite.md: the second note now says the root deployment files are locked in code.

How it was verified

  • Round 3, fail-without for the pairing (measured, in this worktree, before the locks went in). I added the new test and the five NOT_YET_FILES entries, with no lock in src/review.ts yet. Then npm run build and node --import ./dist/test/isolate.js --test dist/test/not-yet-files-locks.test.js dist/test/merge-policy.test.js:
    • every NOT_YET_FILES entry … is a self-modification lock glob fails with + [ 'deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*' ] - [].
    • The dead-glob test in merge-policy.test.ts passed in that same run. That is the gap: an entry with no lock went unseen.
  • Round 3, one broken pair, scratch copy (measured). I used Write to make a scratch copy of the built test: imports pointed at this worktree's dist/, logic unchanged. It reads a scratch copy of the NOT_YET_FILES line in which 'Containerfile*' became 'containerfile*'. node --test on it: 1 test, 0 pass, 1 fail, with + [ 'containerfile*' ] - []. No repo file was edited for this.
  • Round 3, adversarial fail-without, method (b) (measured). I added a fresh worktree at main a5df2254 and wrote the two new cases there, verbatim, as a new file test/adversarial/cases/zz-885.json. --test-name-pattern="885" dist/test/adversarial/adversarial.test.js: 2 run, 2 fail, both "merged with the evaluator fooled".
  • Round 3, managed-app rows (measured). A scratch script calls the built computeLevel and selfModificationPart on main a5df2254 and on head. In a managed app's scope under the example review.yaml, all nine new paths are L1 app-non-money on both. In engine scope they go from L1 app-non-money (no part) to L3 app-non-money,self-modification.
  • Round 3, on head (measured, after merging main a5df2254, with temp HOME, state and TMPDIR). I named each file: not-yet-files-locks, merge-policy, review, engine-guard-locks, adversarial/adversarial, docs-guardrails, changelog and brand. Result: 218 tests, 206 pass, 0 fail, 12 todo. changelog-fragments and changelog-fragments-selfcheck: 12 pass, 0 fail.
  • npm run typecheck: clean (measured). npm run check:denylist: denylist: 1375 files clean (measured). npm run check:dco -- origin/main HEAD: DCO: all commits signed off (measured). The push gate printed 2 commit(s) and 1 tip(s) clean (measured).
  • Round 1, fail-without, method (b) (measured, main f9422c4e). engine-guard-locks: 3 tests, 1 pass, 2 fail. The six round-1 adversarial cases: 6 run, 6 fail.
  • Round 1, before and after, as review: lock the engine's deployment manifests (deploy/**) as self-modification, before the first one exists [PROTECTED: owner merges] #853 did (measured). 36 paths × 3 configs × 2 scopes gave 216 rows; 70 differ, and every one is tighter. No row under the example or the emptied config in managed-app scope changed. Excerpt:
    - docs/guardrails.md | loosened | engine | L1 | - | app-non-money | auto=true | floor=true
    + docs/guardrails.md | loosened | engine | L3 | the public security claims | app-non-money,self-modification | auto=false | floor=false
    - Dockerfile | loosened | engine | L1 | - | app-non-money | auto=true | floor=true
    + Dockerfile | loosened | engine | L3 | the engine's container image | app-non-money,self-modification | auto=false | floor=false
    
  • Not run locally: test/coordinator.test.ts › "self-modification: a harness PR touching only one of the harness's own controls …", and test/change-files-renames.test.ts, which runs a script with the real PATH. CI runs both. Inferred: the new globs pass the coordinator flow the same way deploy/** does, since none is a config-folder or workflow path that is refused before a PR.

For the owner

Follow-ups

Adversarial pass

Attack Result
Add a NOT_YET_FILES entry with no lock (round 3) Fails not-yet-files-locks. Measured: before the locks, 5 unpaired.
Drop a lock from src/review.ts and keep its NOT_YET_FILES entry Fails not-yet-files-locks; the entry is reported unpaired. Measured on a fixture: unpaired(['deploy', 'compose.*'], ['compose.*']) gives ['deploy'].
Typo one entry so it pairs with nothing (containerfile*) Fails. Measured: the scratch copy run, 1 fail.
Hide an entry from the reader (a spread, a variable, another interpolation, a second declaration) Throws, so the test fails closed. Measured: the fixture checks in the second test.
Grow the table by a .push on a later line Waits: the weakening hold's table-used rule catches it. Measured by the final check with weakeningReasons (comment linked under "For the owner"). Only the pairing test misses it, because it reads the declaration alone.
Weaken the pairing test itself Locked: it is in "the tests of the harness controls" and in review.yaml engine-guards. Measured: the test/not-yet-files-locks.test.ts rows in engine-guard-locks.
Add a root compose.yaml, deploy, dockerfile or Containerfile (round 3) Waits: high-risk: self-modification. Measured: two new cases, which pass on head and fail on main.
A managed app's own compose file, Containerfile or deploy Unchanged from main. Measured: managed-app rows, and test 2 of engine-guard-locks.
Neighbours of the new globs (sub/Containerfile, apps/x/compose.yaml, apps/x/deploy, deploy.md, deployment.yaml, Deploy, compose, docker-compose, composer.json) Not locked. Measured: an assertion in engine-guard-locks.
Rename SECURITY.md to an unlocked name and edit it in the same PR Waits. Measured: security-page-renamed-away.
Weaken the suite's runner (test.skip), drop a case and empty todo.json together Waits, naming all three files. Measured: adversarial-suite-edit.
Drop every category from review.yaml, with nothing at L1–L3 and no waits Still L3 and auto=false, floor auto=false. Measured: the first test in engine-guard-locks.
Case variants (docs/Security.md, Deploy/compose.yaml) Not locked: globs are case-sensitive in src/guardrails/glob.ts. Inferred. dockerfile* is now locked as its own glob (measured). Other case variants are still open.

What this can never do

  • It can never lower any path's level or turn any auto from false to true, in any scope or config. Measured: the 216 rows (round 1) and the nine new paths (round 3), all tighter or unchanged.
  • It can never change a managed app's locks under that app's own review.yaml. Measured: example and emptied rows in managed-app scope, and review.test.ts's every-repo list.
  • It can never take a glob out of SELF_MODIFICATION_PARTS, LOCKED_CATEGORIES or ALWAYS_L3, or an entry out of NOT_YET_FILES. Measured from the diff: those tables only gain entries.

Compromised component

  • Agent user / worker:
    • Before: it could rewrite the guardrails page, the security write-up or SECURITY.md, weaken the adversarial suite or the citations test, or add a root compose file, deploy, dockerfile or Containerfile. With a fooled evaluator, that merged by itself.
    • After: each waits for the owner, whatever review.yaml says.
    • Adding a NOT_YET_FILES entry is held by the weakening hold. Removing a lock it pairs with fails the pairing test, and src/review.ts is itself locked.
  • Coordinator / evaluator: they can't lower the level. computeLevel and the merge floor are code, and a fully fooled evaluator still gets "waits" (measured by the adversarial stub).
  • Dashboard: it doesn't touch review levels. Nothing changes.
  • Other instances: they read the same code and review.yaml from main. Nothing new is exposed.
  • Hub viewer: read-only. Nothing changes.
  • Someone with push access: they could merge around any merge-policy lock, as before. GitHub's ruleset, not this code, holds them. This PR doesn't change that.
  • A managed app's repo: unchanged. Its SECURITY.md, Dockerfile, Containerfile, compose files and deploy keep its own categories.

Package size

Measured: npm pack --dry-run --json of main a5df2254 against head 1687d568, both built with tsc (no web build), compared file by file.

  • Tarball: 1,462,728 to 1,463,465 bytes (+737 B).
  • Unpacked: 4,903,731 to 4,906,198 bytes (+2,467 B).
  • Files: 489 to 489.
  • Only one file changed: dist/src/review.js, 37,480 to 39,947 bytes (new globs, parts and comments).
  • Inferred: well inside PACKAGE_BUDGET (2,050,000 / 6,400,000), so no budget change.

Release notes

changelog.d/885-lock-security-pages-and-suite.md, under "Guardrail changes: what each one does". Measured: test/changelog.test.ts and test/changelog-fragments*.test.ts pass.

New dependencies

None.

…te and the Dockerfile are locked

A docs-only change to docs/guardrails.md, docs/security*.md or SECURITY.md, and a tests-only change to
test/docs-guardrails.test.ts or anything under test/adversarial/, was L0 and could merge by itself on a PASS
(measured: L0, auto=true under this repo's review.yaml and the example's). The root Dockerfile waited only where
a review.yaml kept deploy-config, and Dockerfile.<name> not at all.

Each is now self-modification in the engine's repo (src/review.ts SELF_MODIFICATION_PARTS): L3 and never
auto-merged, whatever review.yaml says. The new parts are "the public security claims" and "the engine's
container image"; the tests join "the tests of the harness controls", with the new test that holds these locks.
SECURITY.md and Dockerfile* are named in ENGINE_ROOT_FILES, so like the rest of the engine's own files they
stand aside in a managed app's repo, whose same-named files keep their categories (measured: unchanged).

The engine's own review.yaml lists all of them under engine-guards (an engine that predates the lock reads it
from main), and also a bare deploy path, the root compose files, dockerfile* and Containerfile*. Those last
ones are not locked in code yet: a glob that matches no file needs an entry in test/merge-policy.test.ts
NOT_YET_FILES, the owner's file. The adversarial suite gets six cases for the new locks (an edit, a rename
away and a deletion among them), its "this suite locks itself" todo becomes them, and a todo for the code lock
on deploy and the compose files takes its place.

Test (fails without): test/engine-guard-locks.test.ts (2 of 3 fail on main; the third, a managed app's files
unchanged, passes on main by design), and the six new cases in test/adversarial/cases/locked-paths.json (6 of 6
fail on main).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…e lock

The guardrail change from #858 (this PR, #885) gets its own fragment under "Guardrail changes: what each one
does", so the release says what now waits for the owner and that a managed app's same-named files are unchanged.

Test (fails without): none, release notes only; test/changelog.test.ts and test/changelog-fragments*.test.ts pass.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 1/2)

Reviewed head d829d008 (base and current main f9422c4e; the head already contains main). Protected PR: the owner merges. Includes the independent principle-9 review of ENGINE_ROOT_FILES.

The code is sound and every lock it adds holds. The one required change is to the PR body. It is a text correction (principle 5); no code change is required.

Acceptance (#858)

Criterion Result Evidence
Each path is L3, self-modification and auto === false under this repo's review.yaml, the example's, and one that drops every category met measured: test/engine-guard-locks.test.ts passes on head. My own script also calls the built computeLevel, selfModificationPart, mergeDecision and waitCategoriesOf. Every locked path is L3 with self-modification under all three configs in engine scope, auto=false, and floor auto=false.
A managed app's same-named paths are unchanged met measured: in managed-app scope, every row under the example and the emptied config is identical on main and head. The only managed-app rows that change are under this repo's own review.yaml (engine-guards added), which a managed app never reads.
#855's todo "this suite locks itself" becomes a passing case met measured: adversarial-suite-edit (runner test.skip, a case file narrowed, todo emptied) and adversarial-case-file-deleted pass on head and fail on main. inferred: controls.json, the remaining fixture, has no adversarial case. It is covered by engine-guard-locks.test.ts, which has it in both locked and present.
By-design protection, not a lint met inferred from code: the protection is SELF_MODIFICATION_PARTS (runtime classification plus the merge floor) and the locked .worklane/review.yaml. No source-reading lint is added.

1. Additions only (before/after)

measured. I used my own path set, so this is not the builder's exact 36 paths. It covers 53 paths × 3 configs (own, example, every category emptied with nothing at L1 to L3 or waiting) × 2 scopes, which is 318 rows. 68 differ, and all 68 are tighter. None is looser or mixed. A row counts as looser if the level drops, auto or the floor goes from false to true, a category is lost, or a part is lost.

  • Changed rows: the 13 locked paths, for example docs/guardrails.md, which goes from L0 or L1 with auto=true to L3 with auto=false and floor auto=false. Also the review.yaml-only paths (deploy, compose.*, docker-compose.*, dockerfile, Containerfile), which go from L1 with auto=true to L3 with engine-guards and auto=false under this repo's review.yaml only.
  • Unchanged: sub/Dockerfile, apps/x/compose.yaml, apps/x/deploy/compose.yaml, Deploy/compose.yaml, docs/guide.md, docs/securityx.txt, docs/design/security.md, README.md, test/adversarial.test.ts, composer.json and deployment.yaml. The case variants are unchanged too (see 3).
  • The property the builder claims (70 of 216 rows changed, none looser) reproduces. The exact count does not, because the path lists differ.
  • measured from the diff: SELF_MODIFICATION_PARTS, LOCKED_CATEGORIES and ALWAYS_L3 only gain entries. The four lines removed from src/review.ts are three doc-comment lines and the engineSourceGlob line, which is rewritten to add || ENGINE_ROOT_FILES.includes(glob).

2. ENGINE_ROOT_FILES (principle 9)

  • Scope is right. measured: without the exception, a managed app's own SECURITY.md and Dockerfile* would gain the engine lock. With it, the managed-app rows under the example and emptied configs are byte-identical to main. That is engine-guards: lock the guardrails docs, their test and test/adversarial/** (Lane A; follow-up to #852, #855) #858's "a managed app's same-named files are unchanged".
  • It cannot let anything of the engine's stand aside:
    • inferred from code: managed-app scope comes only from root's file, and readLockScope returns engine for the engine's own repo (src/managed-apps.ts:164-167) and on any error.
    • measured (grep of src/): the match is an exact includes. No existing locked glob, and no GUARD_FILES entry that merge-policy-weakening.ts ENGINE_ONLY sees, equals SECURITY.md or Dockerfile*. Only the two new globs stand aside.
    • measured: test/review.test.ts "the globs that hold in every repo are exactly …" passes on head with its list unchanged.
  • The body names the check it exempts and why that check's finding was wrong (measured: "ENGINE_ROOT_FILES … principle 9" paragraph). Relative to main, nothing loosens. The exception only narrows a new lock to the engine's scope.

3. What isn't covered: case variants and nested files

  • measured: on head and on main alike, docs/Security.md, docs/SECURITY.md, Security.md, docs/Guardrails.md and DOCKERFILE are L0 or L1 with auto=true under every config. sub/Dockerfile and Deploy/compose.yaml are L1 with auto=true. No route that main had closed is reopened.
  • Do these change what the locks protect? No.
    • measured: grep of src/, scripts/, .github/, web/ and package.json finds no reader of any security or guardrails page. Only src/review.ts names them.
    • measured: test/docs-guardrails.test.ts:16-21 reads the exact lowercase paths.
    • inferred: on a case-sensitive checkout (Linux, the only platform CI covers), a variant is a separate file. It does not change the content of a locked file or what the citations test checks.
  • Residual routes, none of them a regression, for the owner to decide on:
    • (a) measured: README.md is L0 with auto=true, and it is the only place that links to the three pages (README.md:61, :94). One PR could add docs/Security.md and repoint the README links to it, and it would merge by itself. Readers would be sent to unlocked claims, though the locked pages stay intact.
    • (b) measured: .github/SECURITY.md is L3 with auto=false under this repo's review.yaml (ci-config, engine), but L1 with auto=true under the emptied config, so it has no code lock. guess: GitHub prefers .github/SECURITY.md over the root one for the repo's Security tab. That would make it the displayed policy. It belongs with the .github/** floor work in Adversarial suite: one pure gate function shared with merge-flow; name the remaining floor gaps (follow-up to #855) #860 part 2.
    • (c) inferred, untested platform: on a case-insensitive filesystem (macOS), docs/Security.md beside docs/security.md collides on checkout.
  • The builder lists the case variants and nested files as open, and they are outside engine-guards: lock the guardrails docs, their test and test/adversarial/** (Lane A; follow-up to #852, #855) #858's path list. So they are notes, not required changes.

4. The six new cases and the replaced todo

  • measured: locked-paths.json is +56/-0, so no case was removed or narrowed. In todo.json, one line is swapped. The removed gap ("this suite's own runner and fixtures are self-modification …") is covered by:
    • adversarial-suite-edit: the runner, a case file and todo.json, edited together.
    • adversarial-case-file-deleted: a case file deleted.
    • engine-guard-locks.test.ts: controls.json, and a case file that doesn't exist yet (cases/new-group.json).
  • measured: the todo count on head is 13, the same as before the swap. Nothing was weakened (rule 10).
  • Each case runs under all three configs in engine scope (schema defaults, adversarial.test.ts:90-91).

5. The deploy / compose code-lock gap

  • measured: I applied merge-policy.test.ts:112's own check (engineFiles.some(f => globToRegExp(glob).test(f))) to head's git ls-files. deploy, compose.* and docker-compose.* match no file, so the dead-glob test would fail without NOT_YET_FILES entries. dockerfile* and Containerfile* also match no file, so they would need entries too. See required change 1.
  • measured: in this repo, review.yaml alone holds them. Under this repo's review.yaml, deploy, compose.yaml, compose.yml, compose.override.yaml, docker-compose.yml, docker-compose.prod.yml, dockerfile and Containerfile are L3 with engine-guards and auto=false, but floor auto=true.
    • Under the example or emptied config they are L1 with auto=true.
    • inferred: in this repo that holds by design for now, because .worklane/review.yaml is itself locked ("the review levels"). Narrowing it needs an owner merge.

6. Fail-without (method b)

  • Setup (measured): I made a fresh worktree at main f9422c4e and wrote two new files into it with Write; no existing file was edited.
    • test/engine-guard-locks.test.ts, byte-identical to head (cmp printed nothing).
    • The six cases verbatim as the new file test/adversarial/cases/zz-858.json (group locked paths).
  • Then I ran npm run build through the npm wrapper, with a temp HOME.
  • node --import ./dist/test/isolate.js --test dist/test/engine-guard-locks.test.js: 3 tests, 1 pass, 2 fail.
    • "regression: a change to the security pages …" fails with docs/guardrails.md + null - 'the public security claims'.
    • "this repo's review.yaml holds each of them …" fails with docs/guardrails.md: only docs/tests, 2 lines 'L0' !== 'L3'.
    • The pass is the managed-app test, which holds on main by design.
  • node --import ./dist/test/isolate.js --test "--test-name-pattern=#858" dist/test/adversarial/adversarial.test.js (pattern quoted, explicit file): 6 tests, 6 fail.
    • Five fail with … under engine review.yaml, engine scope: merged with the evaluator fooled (true !== false).
    • container-image-edit fails with no reason with "high-risk: self-modification (Dockerfile)" in ["high-risk: deploy-config (Dockerfile)"].
  • On head (measured): review, merge-policy, adversarial/adversarial, docs-guardrails, engine-guard-locks, changelog, changelog-fragments, changelog-fragments-selfcheck and brand, each file named explicitly, gave 223 tests, 210 pass, 0 fail, 13 todo.
  • npm run typecheck: clean (measured).
  • npm run check:denylist: denylist: 1322 files clean (measured). That is one more than the body's 1321: the changelog fragment landed after that run.

7. Size

measured: npm pack --ignore-scripts of main f9422c4e gives 1,401,437 B and head gives 1,401,940 B, so +503 B. dist/src/review.js goes from 35,583 to 37,089 B (+1,506). Both match the body.

Protection, hygiene, CI

  • Protection (measured):
    • Protected paths touched: src/review.ts and .worklane/review.yaml. The PR is marked protected: title, **Risk class:** protected, the needs-owner label, and the "What this can never do", "Compromised component" and "Adversarial pass" sections.
    • The "can never do" claims hold against the 318-row comparison and the diff.
    • Compromised component covers each actor.
  • Hygiene:
    • Closes #858 is present (measured).
    • Both non-merge commits are signed off with no attribution trailers (measured). The merge commit has no sign-off, and the dco check passed.
    • No new dependencies and no .claude/ change (measured: file list). No product name in the src/ diff (measured: brand test passes).
    • Body claims are labelled.
  • Scope: Dockerfile* and the review.yaml-only deployment lines go beyond engine-guards: lock the guardrails docs, their test and test/adversarial/** (Lane A; follow-up to #852, #855) #858's path list. They were briefed as review: lock the engine's deployment manifests (deploy/**) as self-modification, before the first one exists [PROTECTED: owner merges] #853 follow-ups and are all tightening (measured above).
  • CI (measured, gh pr checks 885): every check passes, including all 8 test shards, dco, denylist, push-gate, agentshield and new-deps. The slow systemd jobs were skipped. An earlier new-deps run was cancelled and superseded by a pass.
  • Rule A: none of the new tests reads real state (inferred from code: they read only the repo and the preloaded isolate's temp dirs).

Required changes

  1. Correct the PR body's owner instructions (principle 5).
    • The problem: the intro says the code lock for "a bare deploy path, the root compose files, dockerfile* and Containerfile*" is "set out under 'For the owner' below". The For-the-owner section, its NOT_YET_FILES snippet and the new todo.json gap cover only deploy, compose.* and docker-compose.*.
    • measured: dockerfile* and Containerfile* match no file either, so a code lock for them needs NOT_YET_FILES entries too.
    • The fix: either add 'dockerfile*', 'Containerfile*' to the snippet's NOT_YET_FILES, the suggested part's globs and ENGINE_ROOT_FILES, or say plainly that those two stay review.yaml-only.
    • Recommended, not required: widen the todo.json gap line to match.

Notes for the owner (not blocking)

  • Residual routes 3(a) README link repointing and 3(b) .github/SECURITY.md without a code lock are not regressions. Whether "the public security claims" part should also cover README.md's security links or .github/SECURITY.md is a scope decision for you.
  • The fail-without base worktree is left in place: .worktrees/eval-885-base holds the two files I wrote, so a plain worktree remove refuses it.

…review.yaml alone holds

Round 1 measured that dockerfile* and Containerfile* match no file either, so, like a bare deploy and the
compose files, their code lock needs NOT_YET_FILES entries in test/merge-policy.test.ts, the owner's file.
The todo line now names all five, so the suite's list of open gaps matches what is built.

Test (fails without): none, a todo line's wording; test/adversarial/adversarial.test.ts passes with 13 todo.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 1, required change 1: done in the body and in one commit, 9c1cf850.

  • The body's intro, the "For the owner" section and the "Compromised component" section now name all five review.yaml-only globs: deploy, compose.*, docker-compose.*, dockerfile* and Containerfile*. The owner step gives the exact NOT_YET_FILES line with all five, the two src/review.ts parts and the ENGINE_ROOT_FILES line.
  • The permission rule for editing test/merge-policy.test.ts was refused, so those globs aren't added to code in this PR. The PR doesn't touch that file (measured: gh pr diff 885 --name-only).
  • test/adversarial/todo.json: the gap line now names all five (the change you recommended). The todo count stays 13.
  • Measured on 9c1cf850: engine-guard-locks, adversarial/adversarial, merge-policy and review, each file named explicitly, gave 173 tests, 160 pass, 0 fail, 13 todo.
  • The denylist count in the body is now labelled with the commit it was measured at (1321 at c84eb225; your 1322 on d829d008 includes the fragment).

…docs

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…le and Containerfile are locked in code (#885)

The five root globs this PR held only in the engine's review.yaml (deploy, compose.*, docker-compose.*, dockerfile*,
Containerfile*) are now self-modification in src/review.ts: dockerfile* and Containerfile* join the container image
part, and a new part, the engine's deployment files, holds deploy, compose.* and docker-compose.*. All five match no
file yet, so each gets its NOT_YET_FILES entry in test/merge-policy.test.ts (owner decision on #885, option 2:
additions only, each paired with its lock). They join ENGINE_ROOT_FILES, so a managed app's own files of these names
keep their categories (measured: same level and categories as main under the example review.yaml).

A new test, test/not-yet-files-locks.test.ts, reads NOT_YET_FILES the way the weakening hold does (tableLines) and
fails on any entry that is not one of SELF_MODIFICATION_PARTS's globs, so an exception can't outlive its lock or
arrive without one. It locks itself under the tests of the harness controls. The adversarial todo for this gap is
replaced by two cases.

Test (fails without): test/not-yet-files-locks.test.ts fails with the five entries added and no locks
('deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*' unpaired); the two new adversarial
cases fail on main a5df225 ("merged with the evaluator fooled").

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS (round 3, final check), head 1687d568

Critical set (review levels, test/merge-policy.test.ts NOT_YET_FILES): owner merges. Code, tests and locks pass. There are two body corrections below (required, body only, no code change and no re-review). Both are false statements about how the weakening hold reports this change. Neither one changes what the PR does.

1. The NOT_YET_FILES line (test/merge-policy.test.ts:106)

  • Measured: git diff origin/main...HEAD changes this one line in the file (+1 -1). The two existing entries (${BRAND.configDir}/experiments.yaml, 'deploy/**') are unchanged and still first, in their old order. Five entries are added at the end: 'deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*'.
  • Measured: each new entry equals a glob in SELF_MODIFICATION_PARTS, exactly (src/review.ts:340, :344). The new pairing test passes on head.
  • Dockerfile* is left out on purpose, so it is not a gap. Measured: git ls-files lists Dockerfile. The glob matches a real file, so the dead-glob test checks it like any other glob. If it were in NOT_YET_FILES, that test's "matches a file now: take it out" assertion would fail (inferred, from test/merge-policy.test.ts:111).

2. The pairing test (test/not-yet-files-locks.test.ts)

Measured. I made my own scratch copy of the built test. The logic is byte-identical to dist/test/not-yet-files-locks.test.js; only the imports point at my eval worktree's dist. I ran it once per fixture:

Fixture Result
head test/merge-policy.test.ts ok
broken pair ('Containerfile*' changed to 'containerfile*', the builder's check) fail: + [ 'containerfile*' ] - []
table renamed (not declared) fail: NOT_YET_FILES is not declared
...EXTRA spread fail: holds more than plain strings: ...EXTRA
declaration never closes fail: line 3 is at its indent but doesn't close it
head table plus NOT_YET_FILES.push('evil/**'); on a later line ok (the gap, see 3)

It fails closed on a table it can't read (measured). The in-file fixture test also passes on head (measured).

3. The builder's claimed .push gap: covered by the weakening hold

  • Measured: NOT_YET_FILES is registered never-grow (src/merge-policy-weakening.ts:55, CHECK_TABLES).
  • Measured: I called weakeningReasons on head's test/merge-policy.test.ts with one added line, NOT_YET_FILES.push('evil/**');. Result: design: may weaken a check: changes a check's or guard's table outside its own list (test/merge-policy.test.ts: "NOT_YET_FILES: NOT_YET_FILES.push('evil/**');"). This happens with scope left out and with scope engine. An index assignment (NOT_YET_FILES[NOT_YET_FILES.length] = ...) is held the same way.
  • The gap is not real at merge time. The pairing test alone misses it, but the by-design hold (design.md:377) catches it.

4. ENGINE_ROOT_FILES is not in CHECK_TABLES

  • Shrinking it is not the risk. A shrink makes more locks hold in a managed app's repo, which is a tightening. The loosening is growth: every glob added there stops holding in a managed app's repo. It also stops holding in the weakening hold, because ENGINE_ONLY is engineSourceGlob (src/merge-policy-weakening.ts:313).
  • Measured: growing it on its own line gives guard-lines, because src/review.ts is in GUARD_FILES. A ENGINE_ROOT_FILES.push(...) on a new line gives no hold reason. Any change to src/review.ts is still L3 self-modification, so it waits for the owner anyway. Nothing merges unseen; only the hold's reason wouldn't name the table.
  • Measured: none of its seven globs existed in src/ on main, so nothing that main locked stands aside now.
  • Recommendation (not required here): once Merge policy: a check table or lint test registers itself where it's declared; only removing or loosening one waits (#869) [PROTECTED: owner merges] #886 lands, register both ENGINE_ROOT_FILES and ENGINE_TREE (same exposure, unregistered on main too) as never-grow with a // check-table: never-grow marker in src/review.ts. That belongs in a follow-up, not in this PR.

5. Managed-app rows: nothing that was locked opens

Measured with a scratch script against the built computeLevel and mergeDecision:

  • Inputs: main a5df2254 and head; every path in git ls-files plus 16 new paths (1,396 paths); configs base own, head own and example; scopes unset, engine and managed-app.
  • 12,564 rows: 0 looser (level, a lost category, or auto turning true). 174 are tighter, all in engine scope (29 per config and scope). 0 rows changed in managed-app scope.
  • The review.yaml change alone (head code, base own against head own): 0 looser.
  • The nine new paths: L1 app-non-money before and after in app scope. In engine scope, L1 app-non-money becomes L3 app-non-money,self-modification.
  • engine-guard-locks on head: 3/3 pass. Fail-without, method (b): the file written fresh into a main a5df2254 worktree (sha256 identical to head's) gives 3 tests, 1 pass, 2 fail. Test 2 passes on main by design.
  • Adversarial: all 8 new cases, written as a new file cases/zz-eval-885.json into the main worktree, give 8 run, 8 fail. Seven fail with "merged with the evaluator fooled". container-image-edit fails with no reason with "high-risk: self-modification (Dockerfile)" in ["high-risk: deploy-config (Dockerfile)"]. On head they pass.

6. Scope beyond the brief: no weakening

Measured: weakeningOf on the whole diff (origin/main..HEAD, engine scope) returns four reasons. Each one holds the PR for the owner as intended:

  • unreadable on the NOT_YET_FILES declaring line;
  • test-lines on the removed todo.json line;
  • guard-lines on the reflowed doc comment in src/review.ts;
  • marker on the test.skip inside the adversarial-suite-edit case's attack text.

The scope items:

  • Lock on the new test, and the engine-guard-locks additions: tighter only.
  • Todo removed: it is replaced by the adversarial-suite-edit and adversarial-case-file-deleted cases. Both fail on main and pass on head (measured), so the gap is closed, not hidden.
  • review.yaml: +17 -0, additions only (measured).
  • Fragment: notes only.

7. Other checks

Check Result
npm run typecheck clean (measured)
npm run check:denylist denylist: 1375 files clean (measured)
Named tests on head not-yet-files-locks, merge-policy, engine-guard-locks, adversarial, review: 180 tests, 168 pass, 0 fail, 12 todo. docs-guardrails, changelog, brand: 38/38 (measured)
Size tarball 1,462,728 → 1,463,465 (+737); unpacked 4,903,731 → 4,906,198 (+2,467); 489 files both. Only dist/src/review.js changed, 37,480 → 39,947. Matches the body (measured). Inside PACKAGE_BUDGET (2,050,000 / 6,400,000)
git merge-tree with origin/main a5df2254 clean (measured)
git merge-tree with #886 head f03b59df conflict in test/merge-policy.test.ts (measured). #886 adds // check-table: never-grow on the line above NOT_YET_FILES, and this PR rewrites that line. src/review.ts auto-merges. Whichever lands second keeps the marker above this PR's line. The pairing test's imports (tableLines, withoutComments) are still exported on #886's head (measured), but its header comment "(CHECK_TABLES)" goes stale (inferred)
Commits 4 non-merge commits, all Signed-off-by, no attribution trailers (measured)
CI at 1687d568 green: 20 pass, 0 fail, 7 skipping (the real-systemd jobs, skipped by the workflow). All 8 test shards, denylist, dco, push-gate, scan and scope pass. Measured with gh pr checks 885; nothing rerun
Protected marking title [PROTECTED: owner merges], **Risk class:** protected, needs-owner label, real "What this can never do" and "Compromised component" sections (measured)
Real state the new tests read only repo files. No PATH, host tool or state access (measured, by grep)

Body corrections (required, body only)

  1. "For the owner": "The weakening hold (Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820) will flag test/merge-policy.test.ts NOT_YET_FILES (table grown)" is wrong. Measured: the hold reports can't read a change to check it ... (test/merge-policy.test.ts: "NOT_YET_FILES: the line that declares it is removed or changed"), not table-grown. It still holds (fail closed). Because the table is one line, any change to it shows as unreadable and the added entries are not named, which is why the digest has to show the line.
  2. Adversarial pass, .push row: "tableLines, and so the weakening hold, reads only the declaration, so such an entry would be hidden from both" is wrong for the hold. Measured: the hold's table-used rule holds it (section 3 above). Only the pairing test misses it.

@jayden1711
jayden1711 merged commit 62ecc97 into main Oct 21, 2026
28 of 29 checks passed
@jayden1711
jayden1711 deleted the issue-858-lock-guard-docs branch October 21, 2026 03:15
jayden1711 added a commit that referenced this pull request Oct 21, 2026
… onto marks

Main added four check tables and one lint test to the central lists in the batch (#883, #884). Each now carries its
mark where it is declared, with the same kind, and the conflict in src/merge-policy-weakening.ts keeps the central
lists to what can't carry a mark:
- test/child-process.test.ts SPECIFIERS, BINDINGS and WORKER_SPECIFIERS: `// check-table: never-shrink`, and the file
  `// lint-test` (#883);
- scripts/bench/pins.ts SELECTION_RULES: `// check-table: never-grow` (#884).
#885's new NOT_YET_FILES value keeps its never-grow mark (the conflict in test/merge-policy.test.ts).

Also registered, as both #886 reviewers recommended: src/review.ts ENGINE_TREE and ENGINE_ROOT_FILES,
`// check-table: never-grow`. A glob under them stands aside in a managed app's repo (lockedCategories, holds), so
growing either drops locks there; shrinking only applies them in more places.

REGISTERED_BEFORE_MARKS now holds main's 75 registrations at 41f6dd4 (53 tables, 22 lint tests).

Test (fails without): test/merge-policy.test.ts "every check registered before the marks is still registered" and
"... still held by the hold itself", over the 75. A scratch probe on in-memory copies shows SELECTION_RULES, BINDINGS,
ENGINE_TREE and ENGINE_ROOT_FILES each give a hold reason naming them with their mark and none without it.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
jayden1711 added a commit that referenced this pull request Oct 21, 2026
…#858/#885's locks beside the vacuity tests'

Three conflicts. merge-flow.ts and vacuity.ts: spawnSync and execFileSync now come from #846's
os/child-process.js (no module but it imports node:child_process for a value). review.ts: main's
lock entries (#858, #885) kept beside the six vacuity test files. On the merged tree: typecheck and
build clean; the vacuity suites with merge-policy, review, engine-guard-locks, not-yet-files-locks,
child-process, docs-guardrails and first-task-progress: 167 tests, 0 fail.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine-guards: lock the guardrails docs, their test and test/adversarial/** (Lane A; follow-up to #852, #855)

1 participant