Repository navigation
Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] - #789
Conversation
… or App, the engine's own pinned built in The root updater installed a commit once every required check name had a green run, from any source: another App installed on the engine repo, or a workflow run posting a check run onto main's commit through the API, could make a commit installable. evaluateChecks now takes each required check only from its pinned source: updates.json required_check_sources (root's), else ENGINE_CHECK_SOURCES, the updater's own fixed list of the engine's checks and the workflow file that runs each (measured on a batched main commit), else the App the repo's branch protection or ruleset requires; a check nothing pins refuses the commit. A workflow pin needs GitHub Actions to have posted it from that file's run, in the repo, for that very commit. Every check on the commit must still be green, whatever its source. The default checks are pinned without a manual step on any machine. realDeps turns it on; tests' stand-in deps without it judge by name as before, and a test pins that the installed updater always has it. Test (fails without): test/update-check-sources.test.ts (a spoofed same-name check from another workflow, commit, repo or App never makes main's commit or a release's commit installable; an unpinned check is refused) and test/update-checks.test.ts (each default check pinned to the file that posts it). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: CHANGES NEEDED (round 1/2)Reviewed head What it must do (from #727, for the updater)
Fail-without (measured)
|
…nned names as before; unread rules wait Review round 1 and the owner's choice for existing installs (option c, 2026-10-19): - A workflow pin also needs the check run to be one of its workflow run's own jobs (its page /<repo>/actions/runs/<run>/job/<id>, the run id kept from actions/runs), so a check run made through the API into that suite doesn't count. - A required check nothing pins is judged by name, exactly as before, so a machine with an --add-check name keeps updating. An invalid updates.json pin still refuses the commit. - A rules read that GitHub didn't answer (a rate limit, say) waits instead of judging by name or being remembered as a refusal for an hour. Test (fails without): test/update-check-sources.test.ts (a check run in the pinned suite that isn't one of its jobs; an unpinned check judged as before; unread rules wait; another App's failing run still refuses). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 1: changes at dcf0577Each claim is labelled measured, inferred or guess.
Verification (measured):
|
…ost, over https From an adversarial pass against the owner's decision guide: a check run's page with the right path on another host, or over http, passed the job-page check. Now it must be the workflow run's own page (its html_url, kept from actions/runs) plus /job/<id>, over https on that host. Also tested: turning updates off works with an invalid pin in updates.json and reads nothing from GitHub. Test (fails without): test/update-check-sources.test.ts "the right path on another host" and "the right page over http". Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Fix from an adversarial pass (head a59a426)
|
Evaluator verdict: CHANGES NEEDED (round 2)I reviewed head Round-1 required change and the owner's decision
Adversarial checks (measured)
Fail-without (measured)
Size, hygiene, protection, CI
Required changes
A genuinely new trade-off for the ownerNone for this PR alone. The question of whether a pinned name should be blocked by any same-name non-pass, not only the latest run, is raised on #788 and applies here too. |
…claim says latest run per name
Review round 2:
- ruleApps built a {}, so a check the branch's rules pin under the name __proto__ lost its pin and
was judged by name. Its map now has no prototype.
- The claim that every check on the commit must be green is corrected: the latest run of every name
must be, as before, so a pin never turns a check that isn't a pass, as judged by name, into a pass.
Test (fails without): test/update-check-sources.test.ts "a check the branch's rules pin under the
name __proto__ keeps its pin".
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 3: changes at 703db4e
Verification (measured):
|
Evaluator verdict: CHANGES NEEDED (round 3)I reviewed head Both round-2 changes are met. One change is required, from a new owner decision (relayed by the orchestrator, 2026-10-19). Round-2 required changes
Re-run of round 2's fixtures (measured, this head)
Fail-without (measured)
Rule 10, hygiene, protection, CI, merge (measured)
Required change
For the owner (inferred)Existing machines: on main, |
…ource's non-pass refuses The owner's rule (2026-10-19): for a pinned required check, only the latest run from its pinned source on the commit counts, by the runs' own order (the later workflow run, then the later check run, as a re-run attempt is), so a later run of that source supersedes its earlier or cancelled ones; a run for another commit never does. Every run of its name from any other source must be a pass too. An unpinned check (an --add-check name nothing pins) is judged by name, exactly as before. Test (fails without): test/update-check-sources.test.ts (cancelled-then-passed and failed-then-passed install; passed then cancelled in the later run refuses; another App's failure refuses; another commit's run never supersedes; a re-run attempt counts as the newer; unpinned unchanged). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 4: changes at 71b2f2cThe owner's rule (2026-10-19) for a pinned required check:
Tests (measured):
Fail-without on 703db4e (measured): After (measured):
Wording is updated in the body, CHANGELOG, |
Evaluator verdict: CHANGES NEEDED (round 4)I reviewed head Where it stands: the owner's rule is in place and matches #788's. Three gaps remain:
The refined rule (measured,
|
| Case | Verdict |
|---|---|
| Owner 1: cancelled, then passed, same pinned workflow | installs |
| Owner 2: failed, then passed, same pinned workflow | installs |
| Owner 3: pinned pass + another App's same-name failure (workflow pin; App pin) | refused (from another source ended failure) |
| Passed, then a later own run failed | refused |
| A later run of the pinned file for another commit: its pass over the commit's failure; its failure under its pass | refused; refused |
| Re-run attempt: pass over fail; fail over pass | installs; refused |
Foreign App's newer same-name pass over the pinned failure (also with a forged /actions/runs/100/job/999 page) |
refused |
API-made Actions check in the pinned suite (/runs/<id> page), newer pass |
refused |
| Another workflow file's newer pass over the pinned file's failure (workflow pin) | refused |
The rules pin scan to the Actions App (15368): evil.yml's later pass over gitleaks.yml's failure |
installs |
| Run 100 re-run (newest check run) fails after run 200 passed | installs |
- Order independence: every case gave one verdict across all list orders.
- The supersede comes only from the pinned source (inferred,
scripts/machine/worklane-update.cjs:456-466): an App pin needsr.app.id, and a workflow pin needs the Actions slug, the suite's run, that path, repo and sha, and the job page. The exception is the Actions-App pin (change 1). - Statuses (measured,
grep -n "statuses\|/status"): no match in the file. The updater reads no commit statuses, as on main. So a same-name status that isn't success never refuses a pinned check.
Fail-without (measured)
- Method: a scratch worktree at
703db4e3, withtest/update-check-sources.test.tsfrom71b2f2cd, thennpx tscandnode --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js. - Result:
# tests 13 # pass 11 # fail 2, matching the builder: the owner-rule test (cancelled-then-passed refused) and the re-run-attempt test. - On the head:
- every compiled test except
mcp-*andplugin-fetch:# tests 3523 # pass 3466 # fail 0 # skipped 57; npm run typecheck: exit 0;npm run check:denylist:denylist: 1280 files clean.
- every compiled test except
Usual checks (measured)
- Rule 10: this round removes one assertion line. It now also accepts the new message
from another source ended failure, and the test still assertschosen === false. Nothing is weakened. - DCO:
node scripts/check-dco.mjs origin/main 71b2f2cdgivesDCO: all commits signed off. - Attribution: no Co-Authored-By or "Generated with" in the commits or the body.
- Protection: the title has
[PROTECTED: owner merges], the body has**Risk class:** protected, the PR carriesneeds-owner, and it has real "What this can never do" and "Compromised component" sections. One line is false (change 3). - Adversarial pass: present, with round-4 rows.
- Rule A: the tests use a stand-in
fetchJsonand temp paths (inferred). - CI on
71b2f2cd(gh pr checks 789): 24 pass, 2 skipping, 0 failing. - Merges (
git merge-tree --write-tree): clean with origin/main178c44e6, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794.
Required changes
- An Actions-App pin must not supersede across workflow files (principle 1; the owner's rule says "that workflow"). This is the same as Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's change 2.
- Measured: with the branch's rules pinning
scanto App 15368,gitleaks.ymlfailed (run 100) and another workflow file's later run passed (run 101) gives installs. - Fix: under an App pin, take the latest run per workflow file; every group's latest must pass.
- Test: that case, failing on
71b2f2cd.
- Measured: with the branch's rules pinning
- Statuses: the owner's rule is "any non-pass of the same name from any other source blocks".
- Read the commit's statuses for pinned required names (
GET /repos/{o}/{r}/commits/{sha}/status; no answer means wait, never a pass), and refuse on a same-name status that isn'tsuccess. Add a test. - The cost is one more unauthenticated GET per commit judged (inferred). If you think the owner must weigh that call, say so in the body and narrow the claims. Do not leave the gap unstated.
- Read the commit's statuses for pinned required names (
- Correct "What this can never do: Make a commit installable that wasn't before" (principle 5).
- Measured (
vs-main.mjs, main's updater piped in fromgit show origin/main:…): run 100 was cancelled at 10:00:05, after run 200's job started (10:00:01). Main refuses:required check "scan" ended cancelled. This head installs it. - That is the owner's case 1, so the behaviour is right but the claim is false.
- Say instead: a pinned source's later run supersedes its own earlier failed or cancelled runs, which the by-name latest could refuse. That is the owner's decision, and the only way it is looser.
- Measured (
For the owner (a genuinely new trade-off; not a required change)
Which run counts as "latest"? The specified order is workflow run, then check run.
- The case: run 200 passed, then the older run 100 was re-run and its newest attempt failed. The commit installs (measured).
- Fail-closed alternative: also require the newest check run of that source to pass.
…uses, statuses read (#727) - A pinned source's own runs are grouped by workflow file (one group for an App that isn't Actions; a pin to GitHub Actions' App reads the workflow runs too). In every group both the latest run and the newest check run must pass (ownVerdict), so one file never supersedes another and a re-run of an older run that failed after a newer run passed refuses the commit. - The commit's combined status is read (one more GET per commit judged): a same-name status that isn't success refuses a pinned check, pending waits, and no answer waits. - The owner's failed-then-passed fixture gets realistic check run ids (the later run's job has the higher id); its assertion is unchanged. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…atuses (#727) The two fixtures that spread realDeps (test/update-redact.test.ts and scripts/ci/updater-drain-root.mjs) answered every non-compare read with check runs only, so the new combined-status read got no answer and the commit waited. They now answer an empty statuses list too, and keep testing the same path with pinning on. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 5 at db5ad78 (two signed-off commits on 71b2f2c, no rewrite).
Call counts (measured,
That is one more unauthenticated GET per commit judged. At the 10-minute timer with a green tip, check reads go from 12 to 18 an hour, against GitHub's 60 an hour per IP (inferred). Fail-without (measured): the new test file on 71b2f2c gave Fixtures that spread After (measured, at db5ad78): The body has a new Adversarial pass, round 5 section. |
Evaluator verdict: CHANGES NEEDED (round 5)I reviewed head Where it stands: the rule, the tightening and the status read are right, and every round-4 change is met. Two small things are left:
The read cost itself is for the owner. The owner's rule and the tightening (measured,
|
| Case | Verdict |
|---|---|
| Owner 1 / 2: cancelled or failed, then passed (workflow pin; App pin; Actions-App pin from the rules) | installs |
| Owner 3: pinned pass + another App's same-name failure (workflow, App and Actions-App pins) | refused (from another source ended failure) |
Actions-App pin: gitleaks.yml failed, or failed in the newest run, and evil.yml passed |
refused |
Actions-App pin: gitleaks.yml still running, evil.yml passed |
waits |
Actions-App pin: an Actions run whose workflow run wasn't read, passed, beside a failed gitleaks.yml |
refused |
| Re-run of the older run fails after the newer run passed (workflow pin; Actions-App pin) | refused |
| Re-run of the older run passes; a normal re-run of the latest run passes (older run cancelled); a re-run attempt of the latest run passes (workflow and Actions-App pins) | installs |
| Round 4's set: a later own run failed or running; another commit's run; a foreign App's newer pass (with a forged job page); an API-made run in the suite; another file under a workflow pin | refused / waits, as round 4 |
Status failure / error / pending / success of the pinned name; failure of another name |
refused / refused / waits / installs / installs |
App pin: the App fails scan, then passes another check later |
refused |
| App pin: the App later passes the same name over its own failure | installs (the owner's rule for an App source; see the owner note) |
- No loss of function from the newest-check-run rule: a normal passing re-run installs (table).
- Real data: I checked 120 commits of this repo with a workflow run twice or more (read-only
gh api,realids.mjs), giving 439 (file, name) pairs. The newest check run was never outside the latest run (measured; see Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's verdict).
- Real data: I checked 120 commits of this repo with a workflow run twice or more (read-only
Statuses (measured, rule-789.mjs, chooseCommit with a stand-in GitHub, scan pinned to gitleaks.yml and passing)
| Status answer | Result |
|---|---|
null (any HTTP error, e.g. a 403 rate limit: realDeps.fetchJson returns null when !r.ok) |
waits: GitHub gave no answer for its commit statuses |
no statuses array; statuses not an array |
waits |
| the read throws (network error) | chooseCommit throws ECONNRESET, and nothing is installed. runOnce records the error and rethrows (inferred, worklane-update.cjs:1485-1490) |
page 1 full, total_count 150, page 2 no answer |
waits |
page 1 full, total_count 101, scan failing on page 2 |
refused |
page 1 full (100), no total_count, scan failing on page 2 |
installs (page 2 never asked) |
an entry with no context beside a failing scan; state FAILURE; state missing |
refused (fail closed) |
| empty | installs |
GitHub's combined-status answer carries total_count (inferred, from the API's shape), so the full-page case needs a malformed answer. The same check in #803's reader was made to fail closed in round 3.
Unauthenticated reads (measured per run, inferred per hour)
Method: calls-789.mjs, chooseCommit with pinChecks: true and the 15 ENGINE_CHECK_SOURCES names, over a stand-in GitHub. The updater source for each revision is piped in from git show.
| Per run | main 178c44e6 |
round 4 71b2f2cd |
this head |
|---|---|---|---|
| tip green, 58 check runs | 1 | 2 | 3 |
| tip green, 140 check runs (2 pages) | 2 | 3 | 4 |
| 20 commits, all red, first sight (default lookback 20) | 20 | 40 | 60 |
The updater's other unauthenticated calls, read from the code (inferred):
compareonce per run whenever main's tip isn't the installed commit (:1560);- the branch and rules reads (2) only when a required name has no
updates.jsonorENGINE_CHECK_SOURCESpin (:496-500); - the release channel adds no REST read before the check-runs read (
ls-remoteand the tag fetch are git, not REST). - Nothing else on the machine calls the REST API without a token. Every
src/GitHub client sends one (inferred,grep). - With the tip installed, a run makes no REST read (
:1528).
Per hour, at one run every 10 minutes (OnUnitActiveSec=10min):
- A machine waiting on a green new tip (not quiet, say): main 12, round 4 18, this head 24. Each run is the compare plus the check runs, plus the workflow runs and the status here.
- The body's "12 to 18 an hour" counts the check reads only. Adding the compare, it is 18 to 24.
- First sight of a red or idle streak: up to 60 + 1 reads in one run at the default lookback (20), against GitHub's 60 an hour per IP.
- Main's batched CI leaves most commits below the tip with no checks, so they are idle (inferred).
- At
max_lookback: 100, up to 300 + 1.
- Past the limit: each read answers 403, so
fetchJsonreturnsnulland the commit waits (fail closed, measured above). The memo keeps each red or idle verdict for an hour. - So: it's safe (never a wrong install), but an update can be delayed by up to an hour while the budget refills (inferred).
Fail-without (measured)
- Method: a scratch worktree at
71b2f2cd, withtest/update-check-sources.test.tsfromdb5ad787, thennpm run buildandnode --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js. - Result:
# tests 16 # pass 13 # fail 3, matching the builder:an Actions-App pin never lets one workflow file supersede another(ci.yml failed, evil.yml passed later);"latest" fails closed;a pinned check's same-name commit status that isn't success refuses it.
- On the head:
- every compiled test except
mcp-*andplugin-fetch:# tests 3526 # pass 3469 # fail 0 # skipped 57; npm run typecheck: exit 0;npm run check:denylist:denylist: 1280 files clean.
- every compiled test except
Usual checks (measured)
- Rule 10:
- The owner's failed-then-passed fixture got explicit ids (the later run's job is newer), and its assertion is unchanged. With list-order ids it would be a failed re-run of the older run, which now refuses. So that is not a weakening.
- The two
realDepsfixtures gainedstatuses: [].
- DCO:
node scripts/check-dco.mjs origin/main db5ad787:DCO: all commits signed off. - Attribution: none in the commits or the body.
- Protection:
[PROTECTED: owner merges],**Risk class:** protected,needs-owner, and real "What this can never do" and "Compromised component" sections. Their lines match the code (measured above), except the read count (change 2). - Adversarial pass: present, with round-5 rows.
- Rule A: the tests use a stand-in
fetchJsonand temp paths (inferred). - CI on
db5ad787(gh pr checks 789): 24 pass, 2 skipping, 0 failing. - Merges (
git merge-tree --write-tree):- clean with origin/main
178c44e6, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794; - with Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's head
456d4682: a conflict inCHANGELOG.mdonly, in two hunks. It is mechanical:- take Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's side, which renumbers the neighbouring entries (Updater: keep the root updater itself in step with the engine it installs, so updater fixes need no root rerun of updates.sh #760→Updater: replace itself from a verified release tag after a healthy install, never with an older one [PROTECTED: owner merges] #761, among others);
- insert this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 entries: the machine step "The same rerun … installs the updater that judges each required check by its source" with "No new sudoers rules", and the guardrail entry "The engine updater counts a pinned required check only from its source".
- clean with origin/main
Required changes
- The status reader must not take a full page without
total_countas the end (principle 1).- In
statusesOf, keep reading while a page has 100 entries, and returnnullpastCHECK_PAGES_MAX. - Add a test: page 1 full with no
total_count,scanfailing on page 2, must refuse. It installs ondb5ad787(measured).
- In
- Correct the read-count claims (principle 5).
- Say that a run also makes the compare call, so a waiting machine goes from 18 to 24 reads an hour (main: 12).
- Say that the first sight of a 20-commit red or idle streak can take 61 reads in one run (main: 21). Past GitHub's 60 an hour, reads get no answer and the update waits.
- Keep "inferred" on the hourly figures.
For the owner (a new cost; not a required change)
- The cost: every commit judged costs 3 unauthenticated reads instead of main's 1 (measured). A run can reach the 60-an-hour limit on its own at the default lookback, and then installs wait for up to an hour (inferred). It never installs wrongly.
- A cheap mitigation the builder could add if you want it: skip the workflow-run and status reads for a commit where some pinned required name has no check run at all. Such a commit is missing or idle under any pin. Below a batched tip that is most commits, so the burst would drop to about 1 read per commit (inferred).
- App pins: as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's verdict, under a pin to an App that isn't Actions, that App's later run of the same name supersedes its earlier failure (measured: installs). A third-party CI App that runs PR-supplied pipelines could use that. The fail-closed alternative gives up the owner's cases 1 and 2 for that App.
…ins fail closed (#727) - Every pager (check runs, workflow runs, the combined status) reads the next page while a page is full, whatever total_count says; a missing or wrong total_count never makes a full page the last. Workflow runs and statuses still full past CHECK_PAGES_MAX pages: no answer (wait). - The read cost: the workflow runs and statuses aren't read for a commit its check runs already rule out (a pinned required check with no passing run at all, or a check judged by name that ended red or is still running); the verdict then comes from the check runs alone. - A pin to an App that isn't Actions fails closed: every one of that App's check runs of the name must pass. An Actions run is told by its App id, as the engine tells it. The deciding run is the same whatever the list order. - CHANGELOG states the measured read counts and that one run can still exceed GitHub's 60 an hour, for the owner to weigh. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 6 at 457e89b: one signed-off commit on db5ad78, no rewrite.
Fail-without on db5ad78 (measured): After (measured):
The body has a new Adversarial pass, round 6 section on the pagers. |
Evaluator verdict: CHANGES NEEDED (round 6)I reviewed head Where it stands:
The owner's rule and the tightenings (measured,
|
| Case | Verdict |
|---|---|
| Owner 1 / 2 (workflow pin; Actions-App pin from the rules; App pin with the re-run in the same suite) | installs |
| Owner 1 / 2 under an App pin, across two suites | refused (ended cancelled / ended failure) |
| App pin: suite 62 still running | waits |
| Owner 3 under each pin kind | refused (from another source ended failure) |
Actions-App pin: evil.yml after a failed, or newest-failed, gitleaks.yml; an Actions run with an unread workflow run beside a failed gitleaks.yml |
refused |
Actions-App pin: evil.yml passed, gitleaks.yml running |
waits |
| Re-run of the older run fails after the newer run passed (workflow pin; Actions-App pin) | refused |
| Re-run of the older run passes; a normal re-run of the latest run; a re-run attempt of the latest run | installs |
| Round 4's set: another head, a foreign App's newer pass (also with a forged job page), another file | refused |
| Round 4's set: an API-made run in the suite | waits (missing) |
Statuses failure / error / pending / success / another name's failure |
refused / refused / waits / installs / installs |
App pin: two pipelines of the App post scan into one suite, A failed and B passed |
installs (informational; see the owner note and change 3) |
The read-skipping (measured, skip-789.mjs)
Method: 20,000 random scenarios, each compared two ways.
- The scenarios:
- required checks from
scan,dco(bothENGINE_CHECK_SOURCESpins),customanddesktop(retired); updates.jsonpins (an App, a workflow, Actions) and branch rules naming 15368, 1111, -1 or none;- 0 to 6 runs from Actions (five workflow runs, some for another commit, some API-made), from App 1111 (two suites) and from App 2222, in every state and conclusion;
- 0 to 2 statuses.
- required checks from
- E_skip is the real
chooseCommit(one candidate, just landed), with its skips. - E_full is
evaluateChecksover the same runs, with pins fromforShagiven a list that never skips, so the workflow runs and statuses are always read.
Result:
- E_skip installs where E_full doesn't: 0.
- E_full installs where E_skip doesn't: 0.
- The same verdict kind: 16,311.
- Refused vs waits differs in 3,689, and neither side installs in any of them.
- In those, the skip reports a different blocking check first. For example,
dcofailed from another source whilescanhasn't run from its source: E_skip refuses ondco, E_full waits onscan. - Main would also refuse
dcothere (inferred, by name).
- In those, the skip reports a different blocking check first. For example,
Why no skip can hide a failure (inferred, code):
- each skip (
hopeless,red,running) returns a non-install, and the full evaluation would block on the same check (no passing run at all; a by-name latest run that is red or still running); - so no read that's skipped can be the one that turns an install into a refusal.
- A refused verdict can move
chooseCommiton to an older green commit sooner than a wait would. It never installs a commit that isn't green.
Pagers and the status API (measured, rule-789.mjs, chooseCommit)
| Answer | Result |
|---|---|
status null (any HTTP error, e.g. a 403 rate limit), or no statuses array |
waits |
| the status read throws | chooseCommit throws; nothing installed |
status page 1 full with no total_count, or total_count 100 or 50 (wrong), and a failing scan on page 2 |
refused |
status page 2 null |
waits |
| 10 full status pages | waits |
check runs page 1 full with total_count 100 (wrong) or none, and a foreign failing scan on page 2 |
refused |
check runs page 2 null |
waits |
workflow runs page 1 full with no total_count, and the pinned run on page 2 |
installs (page 2 read) |
workflow runs page 2 null |
waits |
check runs: the pinned pass on page 1, a foreign failing scan on page 10 |
refused |
check runs: the pinned pass on page 1, a foreign failing scan on page 11 (past CHECK_PAGES_MAX) |
installs (change 1) |
Read counts (measured, calls-789.mjs, chooseCommit with the 15 engine checks)
The compare call before chooseCommit adds 1 per run (inferred, code).
Per run (reads in chooseCommit) |
main 178c44e6 |
round 5 db5ad787 |
this head |
|---|---|---|---|
| tip green, 58 check runs | 1 | 3 | 3 |
| tip green, 140 check runs | 2 | 4 | 4 |
| 20 commits red on their check runs, first sight | 20 | 60 | 20 |
| 20 commits green by name, but a pinned file's later run failed, first sight | 1 (installs) | 60 | 60 |
tip green, plus one unpinned --add-check name that hasn't run |
1 | 5 | 5 |
- With the compare, that gives 4 per run waiting (main 2), 21 at first sight of a red streak (main 21) and 61 in the worst case (main 21). The body's table matches.
- With an unpinned
--add-checkname, a run also reads the branch rules (2 more). The body says so in prose ("only when some required check has no pin").
Fail-without (measured)
- Method: a scratch worktree at
db5ad787, withtest/update-check-sources.test.tsfrom457e89b5. - Result:
# tests 19 # pass 16 # fail 3, the builder's three: the pagers, the read cost, and the non-Actions App pin (actual: 'installs'). - On the head:
- every compiled test except
mcp-*andplugin-fetch:# tests 3529 # pass 3472 # fail 0 # skipped 57; npm run typecheck: exit 0;npm run check:denylist:denylist: 1280 files clean.
- every compiled test except
Usual checks (measured)
- Rule 10: nothing removed or weakened. The diff adds tests only, plus the
CHANGELOG.mdtext. - DCO:
node scripts/check-dco.mjs origin/main 457e89b5:DCO: all commits signed off. - Attribution: none in the commits or the body.
- Protection:
[PROTECTED: owner merges],**Risk class:** protected,needs-owner, and real "What this can never do" and "Compromised component" sections. - Adversarial pass: present, with round-6 rows.
- Rule A: stand-in
fetchJson, temp paths (inferred). - CI on
457e89b5(gh pr checks 789): 24 pass, 2 skipping, 0 failing. - Merges (
git merge-tree --write-tree):- clean with origin/main
178c44e6, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794; - with Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's
456d4682:CHANGELOG.mdonly, two hunks. It is mechanical: take Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's side and insert this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 entries.
- clean with origin/main
Required changes
- The check-run pager must not judge a truncated list while any required check is pinned (principle 1).
- What happens:
checkRunsOfreturns the firstCHECK_PAGES_MAXpages when there are more (worklane-update.cjs:378-386). - Measured: a foreign App's failing
scanon page 11 installs (on page 10 it refuses). - Why it matters now: under the owner's rule, any other-source non-pass blocks a pinned check, so an unread page can hold the failure. By name, only the latest run counted, so main didn't depend on the older pages.
- Cost (inferred): GitHub lists check runs newest first (measured on main's tip: ids descending). Past 1000 runs, the push CI's own required runs are already unread today, so such a commit already waits or idles.
- Fix: return
null(the commit waits) past the cap whenpinningis on, aspagesOfdoes for the other two. - Correct the body's "Check runs keep main's documented newest-pages behaviour; a required check among the older pages reads as not run, so it isn't installed". That covers the required check itself, not another source's failure.
- Test: one that installs on
457e89b5.
- What happens:
- Correct "A workflow pin needs the check run posted by GitHub Actions (
app.slug)" (principle 5). Since this round it isapp.id15368 (isActions). - Correct the non-Actions App claim (principle 5), as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788.
- The claims: "fails closed" and "What is left is a re-run in the same check suite".
- Say instead: any later same-name run that App puts in the same suite replaces its earlier one, a second pipeline as well as a re-run (measured above: installs).
For the owner (not required changes)
- The read cost is unchanged from the body's table (measured above). One run can still make 61 reads, past GitHub's 60 an hour, and then waits up to an hour. It never installs wrongly.
- Non-Actions App pins within one suite: the same trade-off as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's verdict.
…ged (#727) checkRunsOf still full past CHECK_PAGES_MAX returned the pages read, so a failing run of another source on page 11 went unread and the commit installed. While pins are judged that is now no answer (the commit waits), as the workflow-run and status pagers already do; without pins the newest pages are kept, as before. The CHANGELOG's non-Actions App wording says a later same-name run the App puts in the same suite replaces the earlier one, a second pipeline as well as a re-run. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 10: the fix is 5f746c2, and origin/main 6e0d7d8 is merged in as 8a417d1.
Fail-without (measured): I wrote the new tests first and ran them against the unchanged code: After (measured):
Package size: a delta of 0. Nothing this PR changes is packed. |
Evaluator verdict: PASS (round 10)I reviewed head
Both round-9 findings are fixed. No pinned or transient case changed. Four named gaps are left; each mirrors the engine, and none is a regression against main. 1.
|
| Case | 0b6e7a9 | 8a417d1 |
|---|---|---|
| Pin on page 1 (control) | waits | waits |
| Page 1 holds 100 other rules, the pin is on page 2 | installs | waits (pages 1 and 2 read, per_page=100) |
| Page 1 holds 30 other rules, the pin is on "page 2" | installs | installs, 1 page read |
- The 30-rule row isn't a gap: with
per_page=100, a page of 30 is the last one, so GitHub would put no rule on a page 2 (inferred). My stand-in served one anyway.
New edge cases (edges.mjs; ci and lint pass from App 2222 only):
| Case | Verdict | Pages read |
|---|---|---|
Pin ci→4242 on page 1 |
waits | 1 |
Pin ci→4242 on page 2 behind a full page 1 |
waits | 1,2 |
Pin ci→2222 (its real App) on page 2 |
installs: the page-2 pin is used, not a blanket wait | 1,2 |
| 9 full pages, then exactly 100 on page 10 with the pin | waits (cap) | 1–10 |
| 10 full pages, none pinning, page 11 empty (exactly 1,000 rules) | waits (cap) | 1–10 |
| 9 full pages, then a short page 10 with the pin | waits | 1–10 |
Page 1 full, no Link header, pin on page 2 |
waits: it pages on fullness, not Link |
1,2 |
Page 1 short (50), Link says rel="next", pin on page 2 |
installs: page 2 unread | 1 |
Page 1 full, Link says next, page 2 empty |
installs | 1,2 |
The same pin (ci→4242) on page 1 and page 2 |
waits | 1,2 |
The same pin (ci→2222) on page 1 and page 2 |
installs | 1,2 |
ci→4242 on page 1, a contradicting ci→5555 on page 2 |
installs (by name) | 1,2 |
| The same contradiction, both on page 1 | installs (by name) | 1 |
ci→2222 on page 1, a contradicting ci→4242 on page 2 |
installs (by name) | 1,2 |
Decision (a) on page 1:
| Page 1 answer | Install | Verdict |
|---|---|---|
permission 403, 404, or plain-text Forbidden 403 |
bare | installs |
| 500, rate-limited 403, or plain-text secondary-limit 403 | bare | waits |
| permission 403 or 404 | lint pinned in updates.json |
waits |
- Page 2 refused for permission, a 404, or an object instead of a list: waits, for a bare install too. Decision (a) applies to page 1 only.
- Against the engine (
checkAppsandgithubCheckAppsat Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #7889b9f846c; inferred, code): each edge case behaves the same way.- Both page on fullness at 100 a page.
- Both stop past 10 pages: the engine throws, the updater waits.
- Both drop a check that two rules pin to different Apps.
3. Decision (a) and no loosening (measured, matrix.mjs, 320 cases)
- The same 4 configs × 10 branch outcomes × 8 rules outcomes as round 9, against the owner's rule (3) written out on its own: 0 wrong.
- Against
0b6e7a9d: 0 verdicts changed, so 0 pinned or transient cases changed.- Round 9 had already measured 8 changes against
12148c19, all bare installs with only permission or 404 refusals. Those stand.
- Round 9 had already measured 8 changes against
- My first run of this matrix at
8a417d13showed 57 changes. That was my harness: its stand-in matched only the unpaged rules URL, so?per_page=100fell through to its default 404. I fixed it to match the paged URL too, and reran. The 0 above is from the fixed harness.
4. Fail-without, by the new method (measured)
The setup:
- a fresh worktree at
0b6e7a9d; - the head's
test/update-check-sources.test.tswritten in with Write and Edit only, nogit checkout; git hash-objectof the written file:df719979…, equal to8a417d13:test/update-check-sources.test.ts;- built, then run with
node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.
Result: # tests 24 # pass 16 # fail 8.
- Two failures show the bugs:
- decision (a):
bare, a rate-limited 403 whose body is plain text,expected: 'waits' actual: 'installs'; - the paging test. Its first failing line is the
per_page=100assertion (expected: true actual: false).
- decision (a):
- Six failures (tests 3, 4, 8, 9, 14, 18) come from the stand-ins now answering only
?per_page=100. The old code asks the unpaged URL, getsnull, and waits (GitHub gave no answer for the branch's rules). They are not bug demonstrations. - The body's
# tests 24 # pass 22 # fail 2is from the tests as first written, before those stand-ins changed (inferred from its wording). The committed file gives 16/8 on0b6e7a9d. The two meaningful failures are the same in both.
The page-2 expected value changed from "refused" to "waits". That is legitimate:
- On
0b6e7a9dthe test never reached that assertion. Theper_pageassertion fails first (measured, above). - "waits" is exactly the verdict a page-1 pin gets: both wait (measured, edge table).
- The direct before/after for that property is my
rulespage.mjs: installs on0b6e7a9d, waits on8a417d13(measured). - A page-2 pin to the real App installs, so the assertion isn't vacuous.
After, at 8a417d13: update-check-sources, update-checks, update-redact and package-files gave # tests 44 # pass 44 # fail 0.
5. Stand-in audit, rule 10 (measured, git show 5f746c2c)
Three stand-ins changed, and none is loosened:
gitHub(): answers?per_page=100instead of the unpaged URL. A page 2 would getnull(unread, waits), which fails closed.- The "rules were asked" assertion: now asserts the paged URL. That is exactly as strict.
- The round-8 fixture: answers the paged URL.
Elsewhere in the file:
- The decision-(a) test kept all 16 original rows: 4 refusals × answered or refused × bare or pinned, with the same expected values.
- It moved into
viaRealDeps, which serves the same answers for any page. - It gained 3 plain-text rows (12 assertions).
- No assertion was removed.
6. The merge, the package and the rest (measured)
8a417d13merges6e0d7d81(Test runs: each run in a user namespace of its own; slirp4netns as the test user; a run can't join Docker's namespaces (#545) [PROTECTED: owner merges] #780):- The remerge diff is 0 lines: a clean automatic merge.
5f746c2c..8a417d13lists exactly the files main changed.- The head's diff against main is 7 files:
CHANGELOG.md,docs/separate-users.md,scripts/ci/updater-drain-root.mjs,scripts/machine/worklane-update.cjsand three tests. test/package-files.test.tsis main's file.
- CHANGELOG:
- 0 of main's bullets are missing, and both Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets are present.
- The one PR-side bullet that differs is main's own Test runs: each run in a user namespace of its own; slirp4netns as the test user; a run can't join Docker's namespaces (#545) [PROTECTED: owner merges] #780 edit of a line both sides share (
load it again, once (#686, #778)). The head carries main's new version.
- Package:
- 0
src/files differ from main. npm pack --dry-run --json: none of the 7 changed paths is in the pack.- So the pack is main's (inferred from those two measured facts). The body says the same, labelled the same way. The budget is untouched.
- 0
- The broad suite (
fullsuite.sh, scratch HOME):# tests 3861 # pass 3803 # fail 1 # skipped 57.- The one failure is
dashboard-speed'sevent cache: every view read incrementally…, theread ECONNRESETthat round 9 measured failing on maind5674d04too. That is this machine, not this PR. - This matches the body's "1 failure".
- The one failure is
npm run typecheck: exit 0. Denylist:denylist: 1310 files clean.- DCO:
DCO: all commits signed off, over6e0d7d81..8a417d13and over0b6e7a9d..8a417d13. Attribution lines: 0, in the commits and in the body. - Body:
- an "Adversarial pass, bced758 (decision (a)) and round 10" section, which names what was missed and what was tried;
- corrections for both round-9 findings;
- Package size;
- protected markings, with "What this can never do" and "Compromised component" present;
Part of #727.
- CI on
8a417d13(gh pr checks 789): every job passes, including all four real-systemd jobs andClaude sign-in probe. Onlybootstrapwas skipped. - Overlap (
git merge-tree --write-tree):- clean with main;
- conflicts with Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788
9b9f846cand doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803ad0022d5inCHANGELOG.mdonly, as before.
Named gaps (not required here; each mirrors the engine, and none is a regression against main)
- Contradicting pins become "by name". Two rules pinning one check to different Apps drop the pin, on one page or across pages, and that check is judged by name, so it installs. Both
ruleAppshere and the engine'sgithubCheckAppsdo this on purpose ("no single source passes both").- It is not new this round, and main judges every check by name anyway.
- Under principle 1, failing closed would wait instead. That change belongs to both PRs together, so it fits a follow-up issue for the owner.
- Paging follows fullness, not
Link. A short page whoseLinkheader says there is another page ends the read. Whether GitHub ever does that withper_page=100is a guess; its documented behaviour is full pages until the last. - 1,000 or more branch rules: a bare install waits for good, where main installs. This fails closed, and it is a guess that it matters.
- The body's fail-without numbers are from the tests as first written (22/2). The committed file gives 16/8 on
0b6e7a9d, as explained above. The meaningful failures match.
Before the owner merges (procedural)
- CHANGELOG: whichever of Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 and Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789 merges second needs origin/main merged in first, keeping both Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets (inferred: mechanical).
- Merge order: Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, then doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803, then Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, then Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794 last, once the installed engine includes Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 (inferred).
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
v0.5.3 is released (main 71316eb). The merge auto-placed this PR's two bullets in its released notes; they move, unchanged, to a new v0.5.4 (unreleased) section above it, ended by an empty generated heading so the release keeps them. Main's v0.5.3 text is as main has it. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> # Conflicts: # CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> # Conflicts: # CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> # Conflicts: # CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…ins.md Main's CHANGELOG.md holds released sections only (#870). The two bullets this PR added are the fragment's, byte for byte, under the same headings; CHANGELOG.md is main's exactly. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
The combined check of 2026-10-20 (main f9422c4, clean dist with build:web) measured the queue in loop order: #881, #788, #803, #789, #839, #844, #834 and #878 merged give a 1,931,687-byte tarball, under 1,950,000; adding #769 gives 1,951,238, over by 1,238 (the full queue is 1,960,374). Per the owner's practice the raise goes on the PR that crosses, at the new size plus 5%, rounded up to 10,000: 1,951,238 * 1.05 = 2,048,800, so 2,050,000. The unpacked budget stays at 6,400,000. Part of #726. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Intent: the root updater (
scripts/machine/worklane-update.cjs,evaluateChecks) counts a pinned required check only from its source: the workflow file or GitHub App pinned for its name.ownVerdict). So a later run of a file supersedes that file's earlier failed or cancelled runs, one file never supersedes another, and a re-run of an older run that failed after a newer run passed refuses;updates.json,ENGINE_CHECK_SOURCESor what GitHub's rules name), a branch or rules read GitHub refuses for permission (a 403 that isn't a rate limit), or answers 404, means "no App named". So a bare-names install installs as on main.realDepsgainsfetchRead, which tells a permission refusal from a transient one as the engine'sclassifyGitHubErrordoes: it takes the body as JSON when it parses, else as text, as the engine'sreqdoes (round 10; measured: 0 of 704 cases differ from the engine).x-ratelimit-remaining: 0, was read as a permission refusal, and a bare install installed where the engine waits (the reviewer, measured: 10 of 924 cases; my grid, 9 of 704 on 0b6e7a9).checkAppsdoes (round 10): 100 a page, the next page while one is full, up to 10. A later page refused, not a list, or past the cap leaves the rules unread, so it waits. Decision (a) applies to page 1 only.per_page, so with page 1 full a ruleset's pin on page 2 went unread, and a bare install installed (the reviewer, measured). Deps without it (tests' stand-ins) read every refusal as transient.Risk class: protected.
scripts/machine/is a root helper;scripts/ci/updater-drain-root.mjsruns as root in CI. The owner merges.Part of #727 (#788 closes it).
How it works
/etc/worklane/updates.jsonrequired_check_sources:{"<name>": {"workflow": ".github/workflows/<file>"}}or{"<name>": {"app_id": N}}. It is root's, and the machine helper writes onlyenabledandchannel. An invalid entry refuses the commit (invalid pin for "x") and never falls back to the name.ENGINE_CHECK_SOURCES: the updater's own fixed list of the engine's CI checks and the workflow file that runs each. It is never read from the commit judged.GET /repos/{o}/{r}/branches/{b}) or ruleset (GET /repos/{o}/{r}/rules/branches/{b}) requires. These are read once per run, and only when some required check has no pin from 1 or 2. If GitHub doesn't answer them (a rate limit, say), the commit waits (noAnswer). It is neither judged by name nor remembered as a refusal.app.id.app.id15368 (isActions). Correction: through 457e89b this saidapp.slug, which round 6 replaced. Its check suite's workflow run (GET /repos/{o}/{r}/actions/runs?head_sha=, one call per commit judged) must have thatpath, the repo the updater follows andhead_shaequal to the commit.html_urlmust be/<repo>/actions/runs/<that run's id>/job/<its own id>(isJobOfRun). The run id is kept from the sameactions/runsanswer, so this needs no extra call.ENGINE_CHECK_SOURCES), the updater reads the commit's combined status (GET /repos/{o}/{r}/commits/{sha}/status?per_page=100, the latest status per context);successrefuses a pinned required check, andpendingwaits;GitHub gave no answer for its commit statuses).total_countsays, so a missing or wrongtotal_countnever makes a full page the last. That covers the check runs, the workflow runs (paged since round 6) and the statuses (pagesOf).total_countwas taken as the last. The reviewer measured a failing status on page 2 installing.checkRunsOfkeys oncheckPins'pinned, which is true when some required check is pinned. Unread rules count as pinned, and the commit waits anyway.total_countsays, and past the cap it keeps the newest pages.forShathen reads nothing and takes no shortcut.pinChecksis always on, a bare-name config waited past 10 pages where main installs (the reviewer, measured).scanof another source on page 11 installed (the reviewer, measured).hopeless,red,runninginforSha). That happens when:chooseCommitand the release channel's check of the release's main commit.realDepssetspinChecks: true, and a test pins that. Stand-in deps in older tests don't set it and judge by name, as before. Two fixtures that spreadrealDeps(test/update-redact.test.tsandscripts/ci/updater-drain-root.mjs) pin theirtestcheck to the App that posts it. In round 5 they also answer the status read with an emptystatuseslist. Without it, the status read got no answer and the commit waited: the broad suite at 1b515c3 failed the twoupdate-redacttests (measured). Fixed in db5ad78.Existing machines (owner rule: nothing reopens a route)
UPDATE_DEFAULT_CHECKSis inENGINE_CHECK_SOURCES, pinned to the workflow file that posts it on main. I measured the mapping on batched main commit 720a807 by joining its check runs to its workflow runs bycheck_suite_id; the reviewer's run ofchooseCommiton that commit's real answers gaveCHOSEN.test/update-checks.test.ts) keeps the list in step with the workflows.--add-checkthat isn't the engine's is judged by name, as before, until root pins it (measured: the "judged by name, exactly as before" test compares with and without pins).updates.shrerun this release already asks for, or the release channel's self-update (Updater: keep the root updater itself in step with the engine it installs, so updater fixes need no root rerun of updates.sh #760) after that.Package size (the owner's practice, 2026-10-19, as revised late that day)
This PR doesn't change
PACKAGE_BUDGET, and its package-size delta is 0 / 0.npm pack --dry-run --jsonafter a cleannpm ci,npm run buildandnpm run build:webin throwaway worktrees, since removed): this branch at 0b6e7a9 packs exactly as main, 1,902,158 / 5,934,411, with the same 512 files.git diff --name-only origin/main HEADlistsCHANGELOG.md,docs/separate-users.md,scripts/ci/updater-drain-root.mjs,scripts/machine/worklane-update.cjsand three tests. None of them is inpackage.json'sfiles, so the pack is main's (measured: the list; the pack itself is inferred from it).How it was verified
Round 10 (measured):
# tests 24 # pass 16 # fail 8(the reviewer, measured).expected: 'waits' actual: 'installs') and the paging test (no rules URL asked forper_page=100).?per_page=100), which the unchanged code doesn't ask for.# tests 24 # pass 22 # fail 2. That came from an earlier version of the test file: written before the fix, so before the stand-ins moved to the paged URL, and before the page-2 pin's expected verdict changed from "refused" to "waits".update-check-sources,update-checks,engine-updateandupdate-redactgave# tests 75 # pass 74 # fail 0 # skipped 1.test/update-check-sources.test.tsnow answer the paged URL (?per_page=100); they answered/rules/branches/mainbefore. No assertion was loosened.classify-cmp.mjs, a scratch script):realDeps.fetchReadagainstclassifyGitHubError, ascheckAppsreads it, over 704 cases. The cases are 11 statuses × 16 bodies (JSON, text, no message, a JSON string) × 4 header sets. 5f746c2: 0 differ. 0b6e7a9: 9 differ, all a 403 with rate-limit text in a plain-text or JSON-string body.Changelog fragments (Release notes: one changelog.d file per change, moved into the dated section by the changelog step [PROTECTED: owner merges] #870) (measured): origin/main ec3fd42 is merged in as cb50dd5, signed off, with no rebase.
CHANGELOG.mdwas the only conflict. I took main's side (-X theirs), sogit diff origin/main -- CHANGELOG.mdis empty.CHANGELOG.mdhas the samegit patch-idbefore and after (d9ff458d).changelog.d/789-updater-pins.md(f3b6a29, 7 lines), byte for byte under### After installand### Guardrail changes: what each one does(grep -Fx).changelog,changelog-fragments,changelog-fragments-selfcheckandupdate-check-sourcesgave 63 of 63, and typecheck is clean.origin/main 71316eb (v0.5.3 released, Release v0.5.3: changelog and version [PROTECTED: owner merges] #828) merged in (8e899bc; a signed-off merge, no rewrite, no conflicts). Git put this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets into the released v0.5.3 notes, so 56d2191 moves them, unchanged, to a new
## v0.5.4 (unreleased)section: one under "After install", one under "Guardrail changes: what each one does", ended by an empty### Guardrail changes.git diff origin/main -- CHANGELOG.mdshows only those added lines, andchangelog,package-filesandupdate-check-sourcesgave 41 of 41 (measured). When this and Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 meet inCHANGELOG.md, both PRs' Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets stay.origin/main 6e0d7d8 merged in (8a417d1; a signed-off merge, no rewrite, no conflicts).
cl-check.mjs: no v0.5.3 bullet dropped (ours 47, theirs 48, merged 50), and the trailing heading is present. Typecheck is clean, anddenylist: 1310 files clean.The owner's decision (a) (measured):
chooseCommitthroughrealDeps' own reads over a stand-in GitHub (globalThis.fetch), withciandlintpassing from App 2222. A bare install and one withcipinned inupdates.jsonare each run under a permission 403, a 404, a 500 and a rate-limited 403. Each case runs twice: once with the rules read answered, once with it refused the same way.test/update-check-sources.test.tsfrom bced758, in a throwaway worktree (since removed):# tests 23 # pass 22 # fail 1. The failure is the new test's first bare row:expected: 'installs' actual: 'waits'.update-check-sourcesgave# tests 23 # pass 23 # fail 0.origin/main d5674d0 merged in (0b6e7a9; a signed-off merge, no rewrite, no conflicts).
cl-check.mjs: no v0.5.3 bullet was dropped (ours 42, theirs 45, merged 47), and the trailing heading is present.denylist: 1309 files clean.Round 1 fail-without (measured):
test/update-check-sources.test.tschecked out from dcf0577 (npx tsc: 0 errors). Run asnode --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js, with a scratch HOME and state dirs and a fakeclaude. The worktree was removed with--force.# tests 9 # pass 6 # fail 3:a same-name check from another workflow, ...: casea check run in the pinned suite that is not one of its jobs,true !== false(the commit was chosen);a required check nothing pins ... judged by name, exactly as before: it was refused instead (chosen: false);the branch's rules unread (a rate limit, say): ... waits: no wait (actual: '').Adversarial-pass fail-without (measured): the new test file against dcf0577 gave
# tests 10 # pass 9 # fail 1. Thethe right path on another hostcase:true !== false, the commit was chosen.Round 5 fail-without (measured).
test/update-check-sources.test.tsfrom 1b515c3. It was removed with--force.# tests 16 # pass 13 # fail 3:ci.ymlfailed andevil.ymlpassed later:actual: 'installs';actual: 'installs';actual: 'installs'.installs) is unchanged. With ids following list order, that fixture was a failed re-run of the older run, which now refuses.Round 8 fail-without (measured).
test/update-check-sources.test.tsfrom a8211ed. It was removed with--force.# tests 22 # pass 20 # fail 2:customwith 11 full pages wasn't chosen (it waited);The owner's status decision in the updater: with a required check pinned,
realDeps.fetchJsongivesnullfor any HTTP error. So a refused status read, a page-1 403 or 404 included, gives no answer and the commit waits (measured, test). With none pinned, statuses aren't read, as on main.Round 7 fail-without (measured): a scratch worktree at 457e89b, with
test/update-check-sources.test.tsfrom 5520710, gave# tests 20 # pass 19 # fail 1. The failure: past page 10,actual: 'installs'. The worktree was removed with--force.Round 6 fail-without (measured).
test/update-check-sources.test.tsfrom 457e89b. It was removed with--force.# tests 19 # pass 16 # fail 3:chosenwas the commit);/actions/runswas read for a commit missing a pinned check;The updater's unauthenticated REST reads, per run (measured).
scratchpad/work/issue-727/calls2.cjsruns the wholerun()on the main channel, compare included, at each revision. The file is loaded fromgit show;workis the working tree. It uses the 15UPDATE_DEFAULT_CHECKS, pins on asrealDepsruns them, a stand-in GitHub, and a busy machine.scanpassed ingitleaks.yml's first run and failed in its later run (worst case)OnUnitActiveSec=10min,scripts/setup/updates.sh:184)::1528, the reviewer).Round 4 fail-without (measured): the new test file against 703db4e gave
# tests 13 # pass 11 # fail 2:installs, gotrequired check "test (ubuntu-latest, 22)" ended cancelled;expected: true actual: false).Round 3 fail-without (measured): the new test file against a59a426 gave
# tests 11 # pass 10 # fail 1.a check the branch's rules pin under the name __proto__ keeps its pinfailed withexpected: false actual: true.Round 0 fail-without (measured, unchanged): against origin/main's updater,
# tests 16 # pass 9 # fail 7. Among them, a spoofed workflow's commit was chosen for install, and the release channel installed on a spoofedscan.After (measured): I ran every compiled test except
mcp-*andplugin-fetch, with a scratch HOME:# tests 3532 # pass 3475 # fail 0 # skipped 57(at a8211ed). At b3dae4a one run failedqueue.test(a queued run surviving its session's kill), which this PR doesn't touch; run alone it passed 3 times out of 3 (measured).npm run typecheckis clean,npm run check:denylistgivesdenylist: 1280 files clean, andnode scripts/check-dco.mjs origin/main HEADgivesDCO: all commits signed off.Guess, not measured: a check run made through the API into an Actions suite has a page other than its run's job (
/<repo>/runs/<id>). The job-page check fails closed either way.Overlap (measured with
git merge-tree):CHANGELOG.mdconflicts with Dashboard policy changes go through root's helper (set-policy), never a loosening (#726 part 2) [PROTECTED: owner merges] #770, which already conflicts with main there.Adversarial pass (decision guide)
Fixtures and stand-ins only (
test/update-check-sources.test.ts)./runs/<id>, or another run's job)hasn't run on this commit from its pinned source)http:the right path on another host: true !== false); now refusedhtml_urlplus/job/<id>, over https on that hostended failure) on both heads: the all-green loop over the latest run of every name__proto__expected: false actual: true); now refusedObject.create(null)inruleApps)from another source ended failure), by the owner's ruleended cancelled)--add-checkname with an older failure from another App--add-checknameno answer for the branch's rules)updates.jsonpininvalid pin for "scan"), never falls back to the namejunkinupdates.jsonrunreturnsdisabledand makes no GitHub readenabledandchannel(scripts/machine/worklane-machine.cjs:463,465);updates.jsonpins are root'slintto 15368),ci.ymlfailed and anevil.ymladded in the commit passed later"lint" ended failure).ci.ymlstill running waits; the added file failing refuses; both passing installsownVerdictper file; the workflow runs read for an Actions-App pin)failure,error,pending), or no answer for statusesfailure/errorrefuse,pendingand no answer wait,successand another name's status installrealDepsgained a pin, so they keep testing the same path with pinning on. In round 5 one fixture got realistic check run ids, with its assertion unchanged (above)Adversarial pass, bced758 (decision (a)) and round 10
reqreads the body. The reviewer found 10 of 924 cases where the updater installed and the engine waits.fetch, as every other read does, sochooseCommitthrows and nothing is installed.Adversarial pass, round 5: what else could make a pinned check pass
These are the ways a pinned check could pass from something other than its pinned workflow file's own latest run on the commit. Each is checked at 1b515c3.
head_shamust be the commit. A later run of the file for another commit never supersedes, and its failure refuses (round 4 tests).isJobOfRun), so it is another source (round 1 tests);Adversarial pass, round 6: every pager, a full page with a missing or wrong
total_countEach pager in the updater was checked (measured, tests):
checkRunsOf): a full page whosetotal_countsays 100, with a failing check on page 2, is refused. It used to stop at page 1, and so does main.pagesOf, new): a full page of 100 workflow runs is followed by page 2.pagesOf): a full page with nototal_count, or withtotal_count: 100, and the pinned name's failure on page 2, is refused. On db5ad78 it installed.Check runs keep main's documented newest-pages behaviour; a required check among the older pages reads as not run, so it isn't installed.That covered the required check itself, not another source's failure on an unread page. Round 7: check runs wait too while pins are judged (below).Round 9: origin/main merged in, and every read whose error could look like an answer
CHANGELOG.mdonly, resolved mechanically: main's side, with this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets. One went in After install, beside the updater rerun (Updater: replace itself from a verified release tag after a healthy install, never with an older one [PROTECTED: owner merges] #761). The other went under "Guardrail changes: what each one does", above the trailing### Guardrail changesheading.scripts/lib/changelog.mjssectionForin memory). This side's Intake: GitHub-sourced bug reports get the checked contract (repro: true); coordinator and checkDraft agree on which done_when block counts #643 is main's intake: a GitHub-sourced report is worked from its checked draft; one done_when block per body [PROTECTED: owner merges] #671, and main's own sudoers line replaces this side's "No new sudoers rules".changelog,update-check-sourcesandengine-updatepassed on the merge.realDeps.fetchJsongivesnullfor any HTTP error, a 403 or 404 included.nullfor the check runs, the workflow runs or the statuses is no answer, and the commit waits.nullfor the branch or its rules isrulesUnread, and it waits. Since bced758: except a permission 403 or a 404 while no required check is pinned, which is "no App named" (the owner's decision (a)).Adversarial pass, round 8: a config with only bare names, against main
Method (measured,
scratchpad/work/issue-727/bare8-789.cjs):chooseCommitwithrequired_checks: [custom], noupdates.jsonpin, and branch rules naming no App, withpinCheckson as on every real install.total_countright, missing or wrong;custompassed, failed, running, absent, or only on page 11; and nothing else, another check red on page 1, or no answer for page 2.Results:
Two kinds of difference on 5520710, both fixed:
custompassed with 10 full pages waited, where main installs.redshortcut: it ran with nothing pinned, so a bare config whosecustomwas running or absent was refused on another check's failure, where main waits oncustom.Left, justified: with any required name not pinned in
updates.jsonorENGINE_CHECK_SOURCES, each run reads the branch protection and rules (2 GETs, more while the rules' pages are full, since round 10). That is how a pin from GitHub's rules is found, and it has been in this PR since round 0. If they get no answer, the commit waits (rulesUnread). Main reads neither.Inferred: on a real install the engine's own default checks are pinned by
ENGINE_CHECK_SOURCES, so a bare-names-only config is one whoserequired_checkswere all replaced with names that aren't the engine's.Adversarial pass, round 7: every GitHub read on the updater's path
A truncated or refused read can only end as wait (no install now; the tip is asked again next run) or refused. It never ends as an install.
total_countcheckRunsOf)pinChecks, true on every install)fetchJsongivesnull: no answerchooseCommitthrows: nothing installedpagesOf)pagesOf)per_page)rulesUnread: waits; a permission 403 or 404 on the branch or rules page 1, with no required check pinned, is "no App named" (the owner's decision (a), bced758; page 1 only since round 10)hopeless,red,running)Measured:
check runs still full past CHECK_PAGES_MAX pages wait…, which installs on 457e89b);total_count(round 6 tests).What this can never do
ownVerdictand its tests).updates.jsonis root's.Compromised component
required_check_sources; the machine helper writes onlyenabledandchannel. No new reach.New dependencies
None.