10 Commits
Author SHA1 Message Date
Kevin Codex 31d645ac25 contracts: the lock's --update refuses a path with a space in it
From the CodeRabbit review of #90. --update wrote such a path into contracts/deployed.lock, and the check then
refused the line it had just written: an entry is "kind hash  path" with no space in the path, which is also the
only shape the guard reads. The path is refused before anything is written.
2026-10-12 20:14:49 +08:00
Kevin Codex dccfff4b8e ci: pr-guard: each rule's own text says what the rule covers
From the CodeRabbit review of #90: the "contracts" finding explained itself as "new contract sources", while it
is given for every change under contracts/ that is not blocked, edits and deletions too. Read the other rules'
texts against the code as well: contract-locked names the lock's own guards, submodule says "removed", binary
names the image and font types it accepts, case-collision covers two new paths, dangerous-code mentions the
environment and key stores, dot-file names its exceptions.
2026-10-12 20:04:08 +08:00
Kevin Codex ba892f740d ci: pr-guard: a large file's findings past the limit are counted; CONTRIBUTING names .keep
From the CodeRabbit review of #90.

- A lockfile or a file over 2 MB is read for credentials and hidden characters, and at most 200 findings are
  listed per file. Past that the report now says how many it left out, as it already did for every other file.
- The new dot-file rule leaves .gitignore, .gitkeep and .keep alone. CONTRIBUTING named only the first two.
2026-10-12 19:54:21 +08:00
Kevin Codex 906d754a28 ci: pr-guard: close the gaps an audit of the merged guard found
A full read of the guard for changes that get no finding, or a lighter one than the rule promises. Each item was
reproduced before it was fixed, and has a test.

Refused for everyone:
- criss-cross (new): a branch with more than one common ancestor with main. Its diff showed nothing while merging
  it took a merged fix back out (a "git merge -s ours" of an old side branch). No merge on main has that shape.
- odd-path: any character outside ASCII in a file name. A Mac folds the long s onto s, so "contract<long s>/src/..."
  and ".v<long s>code/tasks.json" landed on the real files at checkout and matched no rule.
- install-script: binding.gyp. npm builds it at install time with no script in package.json.
- lockfile: an entry's name and version have to be a name and a version. A version that walks the path gave one
  package another package's tarball.
- unread (new): a file the guard cannot read (over 25 MB, past 1500 files, an error) was "unscanned", which an
  owner's own pull request passes. A lockfile and a text file over 2 MB are still read for credentials and hidden
  characters.
- hidden-unicode: a variation selector away from an emoji, and a carriage return inside a line (a compiler ends a
  "//" comment there).
- case-collision: two new names that differ only by case. A file renamed only in its case is no longer one.
- dev-machine: CLAUDE.local.md, AGENTS.override.md, .devcontainer.json, *.code-workspace, .clinerules/, .zed/,
  .roo/, .kiro/ and more of their kind.

For an owner to read:
- dot-file (new): any other new file or folder whose name begins with a dot (.coderabbit.yaml, .eslintrc.js). A
  .gitignore or .gitkeep is left alone.
- A file under pages/ is a page whatever it is called, so "x.test.tsx" there is not exempt as a test. LICENSES.ts is
  code, LICENSE-MIT is prose.
- auth-removed knows the app's own names (validate..., check..., admit..., assert...), and a check that is
  commented out counts as removed.
- new-env: process.env?.NAME, and a helper called with the name (intEnv("NAME", 5), vm.envOr("NAME", ...)).
- contract-rules: owner, paused, feeBps, setProtocolFee, written without a library.

Also: a finding never repeats a credential (a git dependency's token was quoted in the job log); the lock script
refuses a line that is not an entry in the one shape the guard reads; CONTRIBUTING says what "contracts" and
"binary" cover and lists the three new rules.

161 tests (146 before). Every merged pull request on main and all eight open ones keep their outcome.
2026-10-12 18:12:43 +08:00
Kevin Codex f2a5c065ba ci: pr-guard: hosts finished at run time, checks lost with a deleted file, lock paths as git spells them
From the third CodeRabbit review of #88, and from reviewing those fixes.

- new-host: a scheme with no host after it ("https://" + target) had no finding. It is "(computed)" now, as a
  template placeholder already was. So are an empty host ("https://@" + x), an address that ends its string and
  has more joined on ("https://known.org" + ".elsewhere.io"), and "//" + host. Also read: a host behind escaped
  slashes, an address with no scheme, a wildcard host. The base branch's hosts are read with the same rule.
  Not seen, on purpose: a scheme that is itself split ("https:" + "//" + host). A string that is only "https:" is
  how the app compares protocols, in eleven places.
- dangerous-code: the same things spelled another way. Function() without new, eval reached indirectly, a timer
  given its code as a string, createElement("script"), and the environment taken as one object
  ({ KEY } = process.env, env: process.env).
- auth-removed: a deleted file takes its checks with it, and that was never reported, since a deleted file is not
  read. Now it is, unless the same check turns up again in what the pull request adds (a move).
- check-deployed-sources.sh: --update refuses a file git does not track, writes each path as git spells it from
  the root (the guard compares the lock's paths letter for letter, so "./contracts/..." would have dropped the
  file's protection), and removes its temporary file when it refuses. The check says so when a locked file is
  present but untracked.
- CONTRIBUTING: the check turns green after the approval and a re-run of the job, as the job summary says.

146 tests. All 65 merged pull requests on main keep their outcome.
2026-10-12 15:53:46 +08:00
Kevin Codex 4c69fa33db docs: CONTRIBUTING lists the guard's outcomes rule by rule, and a test keeps it that way
The two lists were prose written beside the rules, so they drifted: the review found the lock's check scripts
missing from "blocked" and .gitattributes missing from "needs approval". Each point now opens with the names of
the rules it describes, the same names the check prints, and workflows.test.mjs fails when a rule is missing from
the guide or sits under the wrong outcome.
2026-10-12 14:56:14 +08:00
Kevin Codex 22879a0061 ci: pr-guard: the XHTML namespace is matched by its end; CONTRIBUTING lists the rules as enforced
- CodeQL read `includes("http://www.w3.org/1999/xhtml")` as an incomplete check of a URL. It is a search of an SVG's
  text for the one namespace under which HTML works inside it. The end of the address is matched now, which says so
  and finds at least as much.
- CONTRIBUTING.md said every binary that is not an image or font is blocked. Audio, video and PDF files go to a
  maintainer instead, as the guard has always had it. The two lists now also name package manager configuration,
  case-colliding names, removed security checks, the license and security policy and unreadable content, and point
  to the RULES table as the full list.
2026-10-12 14:40:11 +08:00
Kevin Codex 7140291a24 ci: pr-guard: SVGs are read whole, served files need an owner, deleted dependency files are findings
From the second CodeRabbit review of #88.

- SVG: the check ran on single added lines, so a handler on a line of its own, a script element under a namespace
  prefix, or "javascript&#58;" got no finding at all. The whole file is now read, after undoing character
  references. Also refused: a default attribute from a document type, an animation that sets a handler, HTML inside
  the image. An SVG that cannot be read in full is blocked instead of set aside as "unscanned".
- Files the site serves: app/public is read the other way round. Everything in it needs an owner except what cannot
  act (images, fonts, plain text), because a browser runs script in anything served as HTML or XML and the list of
  such file types is long (.xht, .xsl, .rss and more had the gap the review found for .svg). Every SVG, XML or HTML
  file under app/ counts, and the app manifest, whatever the letter case of the name.
- Deleting package.json or a lockfile is a "dependencies" finding. It used to pass with nothing reported.

142 tests. All 65 merged pull requests on main keep their outcome; the one that added the brand logos gains
"needs an owner" notes for its SVGs.
2026-10-12 14:32:30 +08:00
Kevin Codex 094d19229b ci: pr-guard review fixes: who pushed the head, off-registry specs, safe reports
Fixes from the CodeRabbit and CodeQL review of #88, and from a second read of the result.

- An owner's own pull request turns review findings into notices only when an owner pushed the head commit. The
  event's sender is that pusher only for a push event whose commit is still the head; for opened and reopened the
  branch's activity log is asked, and when GitHub cannot say, nobody pushed it. Before, reopening a pull request
  after someone else had pushed to it counted as the owner's push.
- Dependency specs: the rule now lists what a registry spec may hold instead of every way to leave the registry.
  Local paths (../../x, ./x, ~/x, /x, .), tarball names, scp-style git remotes and unknown protocols were only
  "review" before.
- A manifest or lockfile the guard cannot check (too large to read, nested too deep, past the file limit) is
  blocked. It used to be "unscanned", which one approval lets through. Both are read whole whatever their size.
- Reports: finding details are code spans in the job summary, so a dependency or script name cannot write a link or
  HTML into it. A line of the job log no longer starts with a file name, which the runner could read as a workflow
  command.
- The exit code is set, not forced: a JSON report sent into a pipe was cut at 8 KB on a Mac.
- check-deployed-sources.sh: a library with edited or added files, with a state git cannot read, or with files but
  no repository is drift. The lock's last line counts without its newline, and --update works on an empty lock.
- Workflows: every checkout drops its credentials, the guard job says what its two read permissions are for, and
  the tests pin the guard workflow line for line.
- Not changed: a review does not start the guard. pull_request_review runs the pull request's copy of the
  workflow, so it cannot be trusted to judge, and re-running the trusted job takes a write permission that no
  workflow here has. The workflow header and CONTRIBUTING.md say so, and say when to re-run by hand.

136 tests (113 before), on Node 20, 22 and 24 and in a Linux container.
2026-10-12 12:53:52 +08:00
Kevin Codex 7265d164b4 ci: pr-guard, a security gate for pull requests; actions and Foundry pinned
This is a public repository for a wallet-connected app, and until now a pull request could edit the workflow that
judged it (ci.yml runs on pull_request, from the pull request's own copy).

pr-guard runs on pull_request_target, so main's copy of the workflow and of .github/scripts/pr-guard.mjs judges every
pull request. It has a read-only token and no secrets, and it never checks out, installs or runs anything from the
pull request: the head commit is fetched as git objects and only read. The script uses Node built-ins only.

Blocked for everyone, owners included (merging takes an admin bypass): a change to any path in
contracts/deployed.lock or under contracts/lib, anything under .github, submodule changes, symlinks, lockfile entries
that are not the package's own registry tarball with a sha512 hash, new install-time scripts, dependencies from git,
URLs, folders or aliases, credentials and key files, invisible or direction-changing Unicode, binaries that are not
real images or fonts, scripted SVGs, editor, agent and git-hook configuration, package manager configuration, and
paths that collide by letter case.

Needs an owner's approval of the exact head commit (a notice on an owner's own pull request): new files under
contracts, dependency and npm script changes, on-chain addresses, wallet calls, hosts and environment variables the
repository has not used, code execution and raw HTML, removed signature, nonce or rate-limit checks, security headers
and served static files, build, deploy and migration files, the license and security policy, and content that cannot
be read by eye. A push after the approval needs a new one.

ci.yml: actions pinned to the commits their tags point to today (checkout v4.4.0, setup-node v4.4.0,
foundry-toolchain v1.9.1) and Foundry to v1.5.1, which is what "stable" resolves to; the contracts job runs the lock
check; a guard job runs the guard's 113 tests on the image the guard itself uses; a deps job verifies npm registry
signatures and audits production dependencies.

The guard takes effect once this is on main and "pr-guard" is a required status check in the branch rules.
2026-10-11 12:26:01 +08:00