Skip to content

wiki: GET /api/version + npm run deploy refuses + narrower agent wrangler rules - #54

Merged
Deicyde merged 9 commits into
mainfrom
claude/worker-version-stamp
Oct 17, 2026
Merged

Deicyde merged 9 commits into
mainfrom
claude/worker-version-stamp

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Problem

  1. The live Worker can't be tied to a release without credentials. There is no version endpoint: on 2026-10-13, GET /api/version, /api/health, /version and /healthz all returned 404. Only wrangler deployments status, which needs Cloudflare credentials, shows which Worker version is live.
  2. npm run deploy is a bare wrangler deploy. It uploads the Worker built from local wiki/src together with the whole local wiki/public, which replaces the Brain selector and the entire asset tree. It runs outside the promoter, so there is no journal and no canary. Since Docs: record the Brain activation and correct project status #53, the docs say production deploys go only through site/ops/brain-promote-release.sh, but only the docs enforce that.
  3. The agent allowlist pre-approves production mutations. .claude/settings.json allowed npx wrangler *, which covers deploy, versions deploy/upload, rollback and secret put. It also pre-approved an env-prefixed bare npx wrangler deploy.

Change

GET /api/version (public, Cache-Control: no-store, no D1, registered after registerSecurityHeaders so the security headers wrap it):

{"ok":true,
 "worker":{"version_id":"…","version_tag":"…","uploaded_at":"…"},
 "brain_release":{"release_id":"sha256:…","manifest_sha256":"…","git_commit":"<authority commit>"}}
  • worker comes from Cloudflare's version_metadata binding, CF_VERSION_METADATA in wiki/wrangler.jsonc. The promoter copies that file verbatim, so the binding needs no dashboard setup.
  • brain_release is the release the asset selector names, plus authority.git_commit from its release.json. It is null when no release resolves, and the route never 503s.
  • The promoter tags each deploy brain-<release hex12>-<attempt> (site/ops/brain_promote_release.py). That tag is evidence of a promoter deploy, not proof: it is a free-form --tag that anyone with deploy credentials could set. For a promoter deploy, the Worker source is git_commit's, because the promoter only deploys a checkout whose release-affecting paths match the release's authority commit.
  • The path is under /api, so RESERVED already covers it.

npm run deploy refuses. It prints npm run deploy is disabled: production deploys go only through site/ops/brain-promote-release.sh (docs/BRAIN-RELEASE-RUNBOOK.md) to stderr and exits 1. The promoter never calls npm run deploy, so the promotion path is unaffected.

Docs (AGENTS.md, CLAUDE.md, wiki/README.md, docs/BRAIN-RELEASE-RUNBOOK.md, docs/BRAIN-ACTIVATION-2026-10-04.md). Where #53 said "never plain npm run deploy / wrangler deploy", the docs now say:

  • npm run deploy refuses.
  • Never run wrangler deploy directly.
  • The only emergency lever is wrangler rollback to a recorded version, per the runbook's "Wrangler rollback" section.
  • A forward hotfix path, if one is ever wanted, would be a journaled promoter mode, not a hand-run deploy.

AGENTS.md and CLAUDE.md stay identical past line 1. wiki/README.md's Routes table also gets a GET /api/version row: the fields, when each is null, and the evidence-not-proof caveat on brain- tags.

.claude/settings.json (agent permission rules):

  • Removed the allow entry that pre-approved an env-prefixed bare npx wrangler deploy.
  • Replaced Bash(npx wrangler *) with read-only and local rules:
    • deployments list|status and versions list|view, each with and without --no-install, since the runbook uses the --no-install form.
    • tail and whoami.
    • d1 execute wikilean --local, for the backup restore drill.
  • New ask rules: Bash(npx wrangler * --remote*) and its --no-install twin, so the --local prefix rule can never pre-approve a command that reaches the remote database.
  • New deny rules for deploy, versions deploy, versions upload, rollback, secret put|delete|bulk, delete (the Worker), triggers deploy, d1 delete and d1 time-travel restore (both act only on the remote database, take no --remote flag, and confirm themselves when there's no TTY), d1 migrations apply with --remote before or after the database name, and the global --install-skills flag. Each is listed with and without --no-install (the deploy-class verbs also in bare form, d5d2a55a). Rollback stays a human action, as the runbook already requires.
  • wrangler dev is not pre-approved: --remote/-r runs it "with access to production resources", and a prefix rule can't exclude that. npm run dev was never allowlisted.
  • Remote d1 execute and kv key put now prompt instead of running unasked.
  • None of this affects the promoter, which runs wrangler as its own subprocess.

Dropped from the original PR, and why

  • The guarded manual deploy (wiki/scripts/deploy.ts, wiki/test/deploy-guard.test.ts). It contradicted Docs: record the Brain activation and correct project status #53's promoter-only rule, and a passing guard still ran a real deploy from any HEAD with no journal or canary. Review also found argument forms that skipped the checks and still deployed. The guard checked only the selector and release.json, not the artifacts or index trees it uploads. "Don't deploy during an open promotion attempt" was a comment, not a check. Deleting the script closes all of these.
  • git_sha / git_dirty and WIKILEAN_GIT_SHA. Only the deleted script set the stamp, and the promoter passes no --var, so these fields would always be null. For a promoter deploy, brain_release.git_commit identifies the source.

There is no break-glass deploy path. If the promoter itself is broken in an incident, the runbook's wrangler rollback is the lever. A forward hotfix would need a new journaled promoter mode, which would be a separate decision.

How tested

  • npx tsc --noEmit -p . passes.
  • npx vitest run: 40 files and 932 tests pass. test/version.test.ts covers metadata plus a resolved release, the null fallbacks with no binding and no release, and no-store on both.
  • An ad-hoc request to /api/version returned HSTS, X-Frame-Options: DENY, frame-ancestors 'none' and Cache-Control: no-store.
  • npm run deploy, npm run deploy --dry-run, npm_config_dry_run=true npm run deploy and npm run deploy -- --dry-run all print the refusal and exit 1. With extra -- arguments, npm appends them after exit 1, so the shell also prints a harmless exit: too many arguments.
  • site/ops/test_brain_release_authority.py (7 tests), test_brain_nightly.py (23) and test_brain_promote_release.py (59) pass.
  • .claude/settings.json parses as JSON. Replaying 20 representative wrangler commands through Claude Code's wildcard matcher (its escaping, * handling and deny > ask > allow order) gives the intended result for each: d1 delete, d1 time-travel restore and d1 migrations apply --remote in either flag order are refused, remote d1 execute prompts, and the --local, whoami and deployments list forms stay allowed.
  • Merged main 4593c994. The only conflict was the route registration in wiki/src/index.ts, where both registerCompanionRoutes and registerVersionRoutes are kept; this PR's own changes are unchanged by the merge, and CI passed on it.

Release impact

  • Release-affecting: wiki/src, wiki/wrangler.jsonc and wiki/package.json. This ships only through release B and an approved promotion; nothing deploys on merge. There is no D1 migration, no secret, and no dashboard step.
  • Release-neutral: .claude/settings.json, AGENTS.md (since Release policy: AGENTS.md and CONTRIBUTING.md are release-neutral #68), CLAUDE.md, wiki/README.md, docs/ and wiki/test/. The settings change takes effect for agent sessions as soon as this merges.
  • Ordering:
    • Merge this before release B is cut. After a release-affecting merge, B can't be promoted from main.
    • Don't merge a wiki/wrangler.jsonc change while a promotion attempt sits between --execute and final_state. None is open: the 2026-10-04 attempt was reconciled on 2026-10-08.

What Jack must do

Nothing to configure. After release B is promoted, run curl https://wikilean.jackmccarthy.org/api/version. It should show a worker.version_tag of brain-<B hex12>-<attempt> and a brain_release.git_commit equal to B's authority commit.

🤖 Generated with Claude Code

The live Worker could not be tied to a commit without Wrangler access, and
'npm run deploy' (plain wrangler deploy of ./public) would silently replace
the production Brain release selector and asset tree.

- wrangler.jsonc binds version_metadata (CF_VERSION_METADATA).
- GET /api/version returns the Worker Version id/tag/upload time, the git
  SHA stamped by npm run deploy (WIKILEAN_GIT_SHA, '<sha>[-dirty]'), and the
  selected Brain release with its authority git commit. Promoter deploys
  carry no stamp; their Worker source is the release's authority commit.
- npm run deploy now runs scripts/deploy.ts: it refuses unless the local
  public/assets/brain/current.json names the same release and manifest as
  the live selector, then runs wrangler deploy --var WIKILEAN_GIT_SHA:<sha>.
  --dry-run skips the selector check. The Brain promoter calls wrangler
  directly and never passes through this script.
…arg allowlist

- The upload replaces the live selector, so the guard now requires every
  field to match (audited_at and previous_* rollback pointers included), and
  the refusal names the build-public flags that reproduce the live selector.
- The local release directory must hold the manifest the selector names
  (sha256 of releases/<manifest_sha256>/release.json).
- Real deploys accept only --message/--tag; --assets, --config, --env,
  positionals etc. are refused. --var goes last (wrangler's --var is a greedy
  array). Ambiguous --dry-run forms and npm-consumed --dry-run are refused.
- The -dirty stamp only considers wiki/src and the Worker config files;
  fetch and spawn failures are printed; main-module detection resolves
  symlinks.
- /api/version reports the exact manifest_sha256 alongside release_id.
@Deicyde

Deicyde commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner Author

Superseded by the review of 0d492c1d (merge conflict resolved): #54 (comment)

Posted by the WikiLean PR swarm (lead); updated 2026-10-17.

@Deicyde

Deicyde commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

Status review (2026-10-14): ❓ Needs an owner decision

The /api/version half is solid and could merge on its own. It uses no D1, every Brain failure comes back as null, the route sits under the reserved /api prefix, and the promoter is compatible with the new version_metadata binding.

Decision: is a guarded manual deploy a sanctioned path?
#53 adds "production deploys ONLY via the exact promoter … never plain npm run deploy". This PR turns npm run deploy into a guarded deploy that does reach production when the selector and manifest match. Pick one:

  • (a) Break-glass: require an explicit override flag, check for an open promotion attempt (the header says not to deploy during one, but nothing enforces it), and update Docs: record the Brain activation and correct project status #53's wording in AGENTS/CLAUDE.md, wiki/README.md, BRAIN-RELEASE-RUNBOOK, BRAIN-ACTIVATION and ROADMAP.
  • (b) Dry-run only: deploy.ts refuses real deploys.

(The 2026-10-04 attempt was reconciled on 2026-10-08, so nothing is blocked today.)

Nits

  • checkManifest hashes only release.json. It doesn't check the artifacts that manifest lists, even though the manifest already carries their sha256 and byte counts. A public/ tree with missing or stale shards passes the guard and then serves 503s on /api/brain/*.
  • The stamp is git rev-parse HEAD, with no check that HEAD is pushed or on origin/main. A local-only commit can be stamped as clean.
  • After merge, wiki/README.md:141 still calls npm run deploy wrangler deploy — production, and Docs: record the Brain activation and correct project status #53's new text (AGENTS/CLAUDE.md, wiki/README.md, BRAIN-RELEASE-RUNBOOK, BRAIN-ACTIVATION) calls it plain wrangler deploy. None of these mention the guard.

Conflicts: #47 conflicts in wiki/src/index.ts, on adjacent imports. Keep both; the security headers are registered first, so they wrap /api/version (verified).

To ship: this is release-affecting (wiki/src, wrangler.jsonc, package.json, scripts), so it needs the next Brain release and promotion. Afterwards, GET /api/version should report the promoter's version_tag and the release's authority commit.


🤖 Automated status pass by Claude Code over all 18 open PRs (#46–#63), posted for the repo owner. An independent verifier re-checked each finding. All 18 heads were also merged and tested together in a scratch tree; the only test failure that comes from combining them is #47 × #55 (three other pairs had mechanical text conflicts). Reviewed head: 7dfb81b5.

Deicyde and others added 3 commits October 14, 2026 14:00
Resolves the wiki/src/index.ts import conflict with #47 by keeping both
imports; registerVersionRoutes stays after registerSecurityHeaders (and
registerRepoRoutes), so the security headers wrap /api/version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…row agent wrangler rules

Production deploys go only through the Brain release promoter (#53), so a
guarded manual `npm run deploy` contradicted that policy, and its guard
could be bypassed and checked only two files of the tree it uploads.

- `npm run deploy` now prints where deploys go and exits 1.
  scripts/deploy.ts and its test are gone.
- GET /api/version drops git_sha/git_dirty and WIKILEAN_GIT_SHA. Only the
  deleted script set the stamp, so it would always be null. The route keeps
  the Worker Version metadata and the Brain release id, manifest digest and
  authority commit. Comments call the promoter's brain-<hex12>-<attempt>
  tag evidence of a promoter deploy, not proof.
- AGENTS.md, CLAUDE.md, wiki/README.md, the release runbook and the
  activation record now say `npm run deploy` refuses and never to run
  `wrangler deploy` directly. They name `wrangler rollback` as the only
  emergency lever, and say a forward hotfix path would be a journaled
  promoter mode.
- .claude/settings.json: drop the pre-approved env-prefixed bare
  `npx wrangler deploy`. Replace `npx wrangler *` with read-only
  deployments/versions/tail/whoami rules and local-only `d1 execute
  --local`, with an ask rule on any `--remote`. Deny deploy, versions
  deploy/upload, rollback and secret put/delete/bulk for agent sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Deicyde Deicyde changed the title wiki: GET /api/version + guarded npm run deploy wiki: GET /api/version + npm run deploy refuses + narrower agent wrangler rules Oct 15, 2026
…te migrations, --install-skills

The deny list covered deploy, version uploads, rollback and secrets. It
now also denies `wrangler delete` (deletes the Worker), `triggers deploy`
(routes and cron triggers), `d1 migrations apply --remote` (AGENTS.md says
never to use it: remote tracking is out of sync) and the global
`--install-skills` flag, which writes agent configuration from any
otherwise read-only command. Bare forms without arguments are listed too.
All of these already prompted instead of running; now they are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve wiki/src/index.ts by keeping both route registrations: registerCompanionRoutes (from #64 on main) and registerVersionRoutes (this PR). The routes don't overlap, and both come after registerSecurityHeaders.
@Deicyde

Deicyde commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

Conflict resolved; ready to merge after two small fixes (reviewed at 0d492c1d, which merges main 4593c994 into d5d2a55a; CI green on it: https://github.com/Deicyde/WikiLean/actions/runs/39064969152). The only conflict was the route registration in wiki/src/index.ts, resolved by keeping both registerCompanionRoutes(app); and registerVersionRoutes(app);. This PR's own changes are byte-identical to d5d2a55a, and nothing main gained since the branch point conflicts with them. Still required, as in the last review:

  • a GET /api/version row after /api/auth/* in the wiki/README.md Routes table;
  • in .claude/settings.json, deny rules for npx wrangler d1 delete * and d1 time-travel restore * (both act on the remote database with no --remote flag, and they confirm themselves when there's no TTY), an apply *--remote* migrations pattern, and the npx --no-install ask twin.

One body fix: since #68, AGENTS.md is release-neutral, so the release-affecting files are wiki/src, wiki/wrangler.jsonc and wiki/package.json. The maintainer decisions stand: strip the hotfix-path clause or record it as ROADMAP item 8, and a settings follow-up PR after this one. Merge with #50 and #63 before release B.

Posted by the WikiLean PR swarm (lead).

Fields, when each is null, the always-200 no-store response, and that a brain- version tag is evidence of a promoter deploy, not proof. The row sits after /api/auth/* so it doesn't collide with #50's /api/runs row at the end of the table.
…ote-first migrations

wrangler d1 delete and d1 time-travel restore act only on the remote database and take no --remote flag, so the --remote ask rule never saw them, and without a TTY wrangler's confirmation falls back to yes. Deny both, with --no-install twins. The migrations deny now matches 'apply --remote <db>' as well as 'apply <db> --remote', and the --remote ask rule gets its --no-install twin.
@Deicyde
Deicyde merged commit cf496c7 into main Oct 17, 2026
4 checks passed
@Deicyde
Deicyde deleted the claude/worker-version-stamp branch October 17, 2026 20:21
Deicyde added a commit that referenced this pull request Oct 17, 2026
wiki: GET /api/version + npm run deploy refuses + narrower agent wrangler rules
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant