Repository navigation
review: lock the engine's deployment manifests (deploy/**) as self-modification, before the first one exists [PROTECTED: owner merges] - #853
Conversation
…dification, before the first one exists Deployment manifests (a compose file, a chart, a module) set the engine's isolation and exposure: the user it runs as, what it mounts, which ports it publishes. Until now only review.yaml's deploy-config category held deploy/**, so a review.yaml without it left a deploy/compose.yaml change at L1, free to auto-merge (measured on main at 3e193c7). deploy/** joins "the machine setup" part in SELF_MODIFICATION_PARTS (always L3, in the merge policy's floor), and the engine's own review.yaml lists it under engine, for an engine that predates the lock (it reads the file from main). deploy/ joins ENGINE_TREE, so like src/ and scripts/ the lock stands aside where root names a managed app: an app's own deploy/ stays its deploy-config, as before. No file exists under deploy/ yet, so the dead-glob check takes deploy/** in NOT_YET_FILES; that check now matches the entry as a glob, so the first deploy file fails it until the entry is taken out. Test (fails without): test/merge-policy.test.ts, "a PR touching only deploy/compose.yaml is L3 ...". Against the unchanged src/review.ts and review.yaml it fails at "this repo" (no self-modification category); 1 of 2 selected tests pass. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…es, so deploy/** runs through the flow
The coordinator's self-modification test walks every locked glob through a real PR and took a glob with no file
as an exact path. deploy/** has no file yet, so it got no path and the test crashed ("Cannot read properties of
undefined (reading 'startsWith')", measured with fullsuite.sh). It now writes a file the glob matches
(deploy/new-file), and asserts that it does, so deploy/** is checked end to end: the PR waits for the owner and
isn't merged. An exact path (experiments.yaml) is unchanged.
Test (fails without): test/coordinator.test.ts, "self-modification: a harness PR touching only one of the
harness's own controls ...": failed in the full suite at the previous commit (3855 of 3856 run pass), passes here.
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: PASS (with notes)Reviewed head Acceptance (owner task, no issue; #703's "new deploy files are protected from the first PR")
Fail-without (method b)measured: fresh worktree at main 2ce7726, the PR's The exception (principle 9)
coordinator.test.tsmeasured from the diff: -2 lines (a comment, the ProtectionProtected paths touched: Mergesmeasured: Builder's named gaps (all pre-existing, identical on main and head, none reopened)
Notes (non-blocking)
CI: shard 4/4 (node 24), scope, dco, scan, desktop, installed-engine-config, new-deps passed; the other test shards, push-gate, denylist and agentshield were pending when I looked (measured, |
Intent: Lock the engine's deployment manifests (
deploy/**) as self-modification now, before any exist, so that nodeploy/**PR (#703's compose, chart and module) can merge below the owner. On main, the firstdeploy/PR already waits, because it adds a new top-level module: "design: new top-level module (deploy/)", L1, auto=false, even under a loosened review.yaml (measured by the reviewer: #853 (comment)). What this PR closes is every laterdeploy/PR. On main, oncedeploy/exists, such a PR is L1 with auto=true under a review.yaml withoutdeploy-config(measured, below). This PR also adds the missing code-locked L3. Deployment manifests set the engine's isolation and exposure: the user it runs as, what it mounts, which ports it publishes.deploy/**joins "the machine setup" part insrc/review.tsSELF_MODIFICATION_PARTS(always L3, in the merge policy's floor), and the engine's own.worklane/review.yamllists it underengine.Risk class: protected. It changes
src/review.ts(engine, and itself locked as "the review levels"),.worklane/review.yaml(engine, locked) andtest/merge-policy.test.ts(a locked test of the harness controls).Owner task, 2026-10-19 (no issue). Related: #703 (its acceptance criterion "New deploy files are protected from the first PR", done here ahead of it).
What changed
src/review.ts:deploy/**in "the machine setup", with a comment saying why.deploy/added toENGINE_TREE, so this lock, like every lock onsrc/,scripts/,test/anddocs/, stands aside only where root names a repo as a managed app (Self-modification locks: cover the tool guard's whole import tree, and apply only in the engine's own repo #665). An app's owndeploy/stays itsdeploy-config, exactly as on main..worklane/review.yaml:deploy/**inengine, so an engine that predates the code lock (it reads review.yaml from main) holds it too.test/merge-policy.test.ts: the new regression test (below).deploy/**added toNOT_YET_FILES(see "Exception added to a check").test/coordinator.test.ts: the end-to-end self-modification test now gives a locked glob that has no file yet a file it matches, and asserts that the file matches. Before, it crashed ondeploy/**.docs/separate-users.md: the managed-apps row now namesdeploy/among the engine-source locks that stand aside.CHANGELOG.md: one bullet under### Guardrail changes: what each one doesin## v0.5.3. When the branch was cut, main still read## v0.5.3 (2026-10-19)(measured:grep -n '^## ' CHANGELOG.mdat 3e193c7), so the bullet went in that section, per the release rule. Main has since become## v0.5.3 (unreleased)(measured at 2ce7726), and this branch merges with it cleanly (measured:git merge-tree --write-tree HEAD origin/main), so the bullet lands in the unreleased section. It sits above the first generated heading, so the release keeps it once the section is undated (test/changelog.test.tspasses, measured).How it was verified
node measure.mjs, a scratch script calling the builtcomputeLevel,mergeDecision,selfModificationPartondeploy/compose.yaml):SELF_MODIFICATION_PARTS(src/review.ts:325onward) has nodeploy/glob, and.worklane/review.yaml:7enginehas nodeploy/**.selfModificationPart('deploy/compose.yaml')=null.L3, categoriesdeploy-config, app-non-money, auto-mergefalse. It was held, but only by the built-indeploy-configcategory that review.yaml lists at L3, which a review.yaml can drop.deploy-configandengineand lists nothing at L3 or waiting:L1, auto-mergetrue. My script passed no new top-level module, which is the case for everydeploy/PR after the first. The first one also gets the design wait for a new top-level module and does not auto-merge on main (measured by the reviewer, comment linked above).L3,deploy-config, engine, self-modification, autofalse. The loosened review.yaml:L3,self-modification, autofalse. Part:the machine setup. Managed-app scope:null(unchanged).src/review.tsandreview.yaml:node --import ./dist/test/isolate.js --test --test-name-pattern "deploy/compose.yaml|self-modification: every glob" dist/test/merge-policy.test.jsgave 1 pass, 1 fail:this repo/+ undefined - ['deploy/compose.yaml'](no self-modification category). With the change: pass..github/release-signersone, for this repo's review.yaml, the example's, and one that dropsdeploy-config,engine, every L3 and every wait:L3,self-modification: [deploy/compose.yaml],mergeDecision.auto === false, the reasonhigh-risk: self-modification (deploy/compose.yaml), the whychanges the machine setup: deploy/compose.yaml, and the floor with no wait categories passed at all. Then: this repo'senginecategory holds it; and in managed-app scope there is no engine lock and itsdeploy-configkeeps it at L3.test/merge-policy.test.ts,test/review.test.ts,test/changelog.test.ts,test/brand.test.ts: 54/54 pass (measured).npm run typecheck: clean (measured).npm run check:denylist:1319 files clean(measured).fullsuite.sh, excludes mcp-* and plugin-fetch) at the first commit (measured): 3913 tests, 3855 pass, 57 skipped, 1 fail. The failure wastest/coordinator.test.ts"self-modification: a harness PR touching only one of the harness's own controls ...". That test walks every locked glob through a real PR and treated a glob with no file as an exact path, sodeploy/**got no path (Cannot read properties of undefined (reading 'startsWith')). The second commit gives such a glob a file it matches (deploy/new-file) and asserts the match, sodeploy/**now goes through the coordinator flow end to end. That test passes (measured: run alone by name, 1/1). I did not rerun the full suite after that one-test fix; CI runs it.git diff origin/main...FETCH_HEAD -- src/review.ts(issue-785) has hunks at@@ -334,8and@@ -345,10. This change is at line 333 and inENGINE_TREE(~393), outside both.git merge-tree --write-tree HEAD FETCH_HEADmerges clean (tree ee6caff6).Exception added to a check (decision guide 9)
test/merge-policy.test.ts"self-modification: every glob names real engine files (none dead)".deploy/**is added toNOT_YET_FILES, which exempts it.deploy/**is owned by Deployment: the engine in your own cloud account (Helm), on your own servers (Terraform or OpenTofu, or docker compose on one VM), or on one machine (one command plus doctor); re-runs never loosen; time-to-first-merged-PR measured every run (provisional: blocked on the product architecture doc) #703 (Lane C), so this PR can't add a placeholder file there.engineFiles.includes(glob), which a glob never satisfies. It now usesengineFiles.some((f) => globToRegExp(glob).test(f)). So the first commit that adds any file underdeploy/fails this test until the entry comes out, and the glob is then checked like the others (inferred from the code; not exercised, since adding a deploy file is Deployment: the engine in your own cloud account (Helm), on your own servers (Terraform or OpenTofu, or docker compose on one VM), or on one machine (one command plus doctor); re-runs never loosen; time-to-first-merged-PR measured every run (provisional: blocked on the product architecture doc) #703's). For the existing exact-path entry (experiments.yaml) the check is the same as before (inferred:globToRegExpof a literal path matches only that path).Adversarial pass
Each attack run through the built
computeLevel/selfModificationPartwith the engine's own review.yaml (measured:attack.mjsin the scratchpad), unless labelled.deploy-config, noengine, empty L3 and waits): still L3 and waits (measured, the new test).deploy/helm/templates/deployment.yamlanddeploy/terraform/main.tf: L3, self-modification, "the machine setup" (measured).deploy/: counts under both paths since Review levels: a renamed file counts under its old and its new path, in changeFiles and every path reader [PROTECTED: owner merges] #688 (inferred, not run here).test/merge-policy.test.tsis itself locked ("the tests of the harness controls") (inferred fromSELF_MODIFICATION_PARTS).deploy/compose.yamlhad no engine lock (selfModificationPart(…, 'managed-app')=null, measured on 3e193c7), and it still has none. Nothing closed is reopened (measured). The existing "globs that hold in every repo" test is unchanged and passes (measured).deploy: L1, partnull(measured). A first PR could adddeployas a symlink to an unlocked folder before any realdeploy/exists. A new top-level file is not a "new top-level module" (src/coordinator/merge-flow.ts:509counts only paths with a/), so it could auto-merge (inferred).deploy/: rootcompose.yamlanddocker-compose.yml: L1, partnull(measured). Deployment: the engine in your own cloud account (Helm), on your own servers (Terraform or OpenTofu, or docker compose on one VM), or on one machine (one command plus doctor); re-runs never loosen; time-to-first-merged-PR measured every run (provisional: blocked on the product architecture doc) #703 names acompose.yaml.Dockerfileis L3 bydeploy-configonly (measured), not locked.Deploy/compose.yaml(case variant): L1, partnull(measured). It is a new top-level directory, so the merge policy holds it as a design-level change (inferred frommerge-flow.ts:509,merge-policy.ts:222).deploy/; and a follow-up locks the root compose names and a baredeploypath for the engine scope only (not every repo, so a managed app's own compose file isn't labelled the engine's machine setup). That's the owner's call on scope.What this can never do
engine, anddeploy/toENGINE_TREE(measured: the before/after above;git diff origin/main -- src/review.ts .worklane/review.yamlchanges three lines, each adding one entry to a list, and removes no glob or category).deploy/as the engine's:deploy/is inENGINE_TREE(measured: managed-app partnull).Compromised component
deploy/**. It is now L3 and in the floor, so it waits for the owner whatever review.yaml says (measured). It could still put a manifest outsidedeploy/(Adversarial pass 7).deploy/, stand aside for that app's repo, never the engine's (src/managed-apps.tsrefuses the engine's repo). Same loosening as review: lock the tool guard's whole import tree; engine-source locks stand aside only where root names a managed app [PROTECTED: owner merges] #691, now coveringdeploy/too (inferred fromlockedCategories).src/review.tsor review.yaml directly. That's the ruleset's job, unchanged.Package size
Measured with
npm pack --dry-run --json(through the npm wrapper), main at 3e193c7 against this branch, each built in its own worktree:dist/src/review.js35,197 → 35,583 (+386, the new glob and its comment). Nothing else in the package changed.New dependencies
None.