build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) #178

Merged
clawbot merged 1 commits from fix/issue-170-assert-debug-off into next 2026-08-10 16:15:23 +02:00
Collaborator

Closes #170.

Bases on next. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed as f7f141a and this branch was rebased onto next carrying only its own guard commit.)

The gap

Deleting the __BUILD_DEBUG__ define from build.js leaves make check green while every emitted bundle ships with the debug branch live. tests/ loads src/shared/constants.js outside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there.

The check

  • build.js passes metafile: true and writes dist/constants-bundles.txt: every emitted JS output whose input set includes src/shared/constants.js. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against.
  • src/shared/constants.js exports BUILD_DEBUG_MARKER, derived from DEBUG itself, which the bundler constant-folds to autistmask-build-debug=off or =on. If DEBUG is not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap.
  • script/verify-build cross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker.
  • Wired into make build and make build-debug (the latter asserting the inverse), so script/cibuild covers it. Deliberately not in make check, which would otherwise depend on dist/ existing.

Verification

make check, make build, make build-debug and script/cibuild green on the consolidated tree, with the four bundles verified off. Falsification re-run after the rebase: with the define deleted, make build exits 2 with "carries both debug markers" while make check still exits 0 — the guard is the only thing that catches it.

Diagnostic wording that overstates the consequence is tracked separately in #180.

Closes #170. Bases on `next`. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed as `f7f141a` and this branch was rebased onto `next` carrying only its own guard commit.) ## The gap Deleting the `__BUILD_DEBUG__` define from `build.js` leaves `make check` green while every emitted bundle ships with the debug branch live. `tests/` loads `src/shared/constants.js` outside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there. ## The check - `build.js` passes `metafile: true` and writes `dist/constants-bundles.txt`: every emitted JS output whose input set includes `src/shared/constants.js`. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against. - `src/shared/constants.js` exports `BUILD_DEBUG_MARKER`, derived from `DEBUG` itself, which the bundler constant-folds to `autistmask-build-debug=off` or `=on`. If `DEBUG` is not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap. - `script/verify-build` cross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker. - Wired into `make build` and `make build-debug` (the latter asserting the inverse), so `script/cibuild` covers it. Deliberately not in `make check`, which would otherwise depend on `dist/` existing. ## Verification `make check`, `make build`, `make build-debug` and `script/cibuild` green on the consolidated tree, with the four bundles verified `off`. Falsification re-run after the rebase: with the define deleted, `make build` exits 2 with "carries both debug markers" while `make check` still exits 0 — the guard is the only thing that catches it. Diagnostic wording that overstates the consequence is tracked separately in https://git.eeqj.de/sneak/AutistMask/issues/180.
clawbot added the needs-review label 2026-08-09 07:08:16 +02:00
clawbot self-assigned this 2026-08-09 07:08:17 +02:00
Author
Collaborator

Summary for review

Base is PR #169's branch, not main. Deliberate, and the one place I went
against the issue's notes. main has no __BUILD_DEBUG__ define, so a guard
branched there could not be shown to trigger — there would be no define to
delete. Merge after #169; retarget to main then. Full reasoning at the top
of the PR body.

What to look at

The interesting decision is that the check reads two facts from two
independent sources
, rather than inferring one from the other:

  • which bundles to inspect comes from esbuild's metafile, written by
    build.js to dist/constants-bundles.txt;
  • each bundle's DEBUG state comes from BUILD_DEBUG_MARKER, a new
    constant in src/shared/constants.js derived from DEBUG and folded by the
    bundler to one of two string literals.

Collapsing these into one signal is the silent-pass failure: a marker-less
bundle would then be indistinguishable from content/index.js, which
legitimately has no marker, and the check reads green forever. Worth
attacking that reasoning if you think it is wrong.

The marker replaces the DEBUG:!1 substring match the issue mentioned,
because a string literal is stable across esbuild versions and !1 is not.
Its useful property is that when DEBUG is not resolvable at build time the
fold does not happen and both literals land in the bundle — which is
exactly the shape of the regression being guarded, so the ambiguity is the
detector rather than a hole in it.

Where it runs

Build path only. make build and make build-debug both invoke
script/verify-build, and Dockerfile:17 runs a bare make build, so CI
covers it. Not in script/check, on purpose: that would make check
depend on dist/ existing and pull a build into its time budget, and the
"skip if dist/ is absent" workaround is precisely the silently-green
behaviour the issue forbids. Per the steer, the build path wins and I am
saying so out loud.

Verification

  • make check green: 5 suites, 57 tests, prettier clean.
  • make build green, 4 bundles verified off; make clean && make build green
    from an empty dist/.
  • make build-debug and AUTISTMASK_DEBUG=1 make build green, 4 bundles
    verified on.
  • script/cibuild (real docker build ., so make check then make build)
    green end to end.

Both required failure modes, actually triggered

Full transcripts are in the PR body. In short:

  • (a) define deletedmake check exits 0 on that same tree, and
    make build exits 2 with carries both debug markers, so the build-time DEBUG value was never resolved and the debug branch is still live. Define
    restored before committing; it is at build.js:95.
  • (b) unrecognizable bundle — a bundle overwritten with content carrying
    neither marker fails with carries no debug marker, so its DEBUG state cannot be determined [...] Refusing to report success.

I also triggered four further guards: debug artifacts against a release
verification, empty manifest, missing manifest, and a manifest gone
stale/short (caught by the cross-check that no unlisted bundle may carry a
marker — this is what stops the check trusting whatever the manifest happens
to say).

Things a reviewer might reasonably push on

  • build.js grew a local bundle() helper, which is why the four esbuild
    call sites collapse in the diff. Bundler options are unchanged apart from
    metafile: true. The motive is that a future entry point cannot silently
    skip metafile collection.
  • The two new jest tests pin source-level invariants only. They cannot see a
    compiled bundle and are not offered as a substitute for the artifact check.
  • The marker adds a short string to every shipped bundle that contains
    constants.js.
## Summary for review **Base is PR #169's branch, not `main`.** Deliberate, and the one place I went against the issue's notes. `main` has no `__BUILD_DEBUG__` define, so a guard branched there could not be shown to trigger — there would be no define to delete. Merge after #169; retarget to `main` then. Full reasoning at the top of the PR body. ### What to look at The interesting decision is that the check reads **two facts from two independent sources**, rather than inferring one from the other: - **which bundles to inspect** comes from esbuild's metafile, written by `build.js` to `dist/constants-bundles.txt`; - **each bundle's DEBUG state** comes from `BUILD_DEBUG_MARKER`, a new constant in `src/shared/constants.js` derived from `DEBUG` and folded by the bundler to one of two string literals. Collapsing these into one signal is the silent-pass failure: a marker-less bundle would then be indistinguishable from `content/index.js`, which legitimately has no marker, and the check reads green forever. Worth attacking that reasoning if you think it is wrong. The marker replaces the `DEBUG:!1` substring match the issue mentioned, because a string literal is stable across esbuild versions and `!1` is not. Its useful property is that when `DEBUG` is *not* resolvable at build time the fold does not happen and **both** literals land in the bundle — which is exactly the shape of the regression being guarded, so the ambiguity is the detector rather than a hole in it. ### Where it runs Build path only. `make build` and `make build-debug` both invoke `script/verify-build`, and `Dockerfile:17` runs a bare `make build`, so CI covers it. **Not** in `script/check`, on purpose: that would make `check` depend on `dist/` existing and pull a build into its time budget, and the "skip if `dist/` is absent" workaround is precisely the silently-green behaviour the issue forbids. Per the steer, the build path wins and I am saying so out loud. ### Verification - `make check` green: 5 suites, 57 tests, prettier clean. - `make build` green, 4 bundles verified off; `make clean && make build` green from an empty `dist/`. - `make build-debug` and `AUTISTMASK_DEBUG=1 make build` green, 4 bundles verified on. - `script/cibuild` (real `docker build .`, so `make check` then `make build`) green end to end. ### Both required failure modes, actually triggered Full transcripts are in the PR body. In short: - **(a) define deleted** — `make check` exits `0` on that same tree, and `make build` exits `2` with `carries both debug markers, so the build-time DEBUG value was never resolved and the debug branch is still live`. Define restored before committing; it is at `build.js:95`. - **(b) unrecognizable bundle** — a bundle overwritten with content carrying neither marker fails with `carries no debug marker, so its DEBUG state cannot be determined [...] Refusing to report success`. I also triggered four further guards: debug artifacts against a release verification, empty manifest, missing manifest, and a manifest gone stale/short (caught by the cross-check that no unlisted bundle may carry a marker — this is what stops the check trusting whatever the manifest happens to say). ### Things a reviewer might reasonably push on - `build.js` grew a local `bundle()` helper, which is why the four esbuild call sites collapse in the diff. Bundler options are unchanged apart from `metafile: true`. The motive is that a future entry point cannot silently skip metafile collection. - The two new jest tests pin source-level invariants only. They cannot see a compiled bundle and are not offered as a substitute for the artifact check. - The marker adds a short string to every shipped bundle that contains `constants.js`.
Author
Collaborator

Review: PR #178 — VERDICT: PASS

Reviewed at head b5f3da2 against base acb5885 (PR #169). Independent
checkout; nothing modified or committed (working tree verified clean at the
end of every experiment below).


1. Independent reproduction of the two required failure modes

I did not take the author's transcripts on trust. Both were reproduced from
scratch in an isolated worktree.

(a) Deleting the __BUILD_DEBUG__ define fails the build, while make check stays green

Removed __BUILD_DEBUG__: JSON.stringify(debugBuild), from the define map at
build.js:95, then make clean && make build:

Bundles containing src/shared/constants.js: 4 (listed in dist/constants-bundles.txt)
Build complete: dist/chrome/ and dist/firefox/
Verifying emitted bundles (expecting autistmask-build-debug=off)...
verify-build: FAIL: dist/chrome/src/background/index.js carries both debug markers, ...
make: *** [Makefile:40: build] Error 1
build exit=2

Same tree, same commit, immediately after:

make check exit=0

Confirmed: make check cannot catch this and make build now does. Define
restored via git checkout --; build.js:95 intact.

(b) An unrecognizable bundle fails rather than passes

Clean release build, then printf 'var reminified = 1;\n' > dist/chrome/src/popup/index.js:

verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
    determined. ... Refusing to report success.
exit=1

Confirmed.


2. Additional failure modes the author did not demonstrate

Eight further attacks constructed against a known-good release dist/. All
hard-fail; none produced a silent pass.

Attack Result
Manifest lists a file that does not exist FAIL (lists ..., which does not exist), exit 1
Bundle unreadable (chmod 000) FAIL (exit 1) — see finding N2 on the message
Bundle truncated mid-marker FAIL, exit 1
Stale leftover debug artifact at an unlisted path (index-old.js) FAIL via the unlisted-bundle cross-check, exit 1
Listed bundle replaced by a symlink to the debug build FAIL (mode mismatch), exit 1 — -f and grep both follow the link
BUILD_DEBUG_MARKER deleted from src/shared/constants.js, then rebuilt make build FAILS (no marker); make test also fails 2 tests
AUTISTMASK_DEBUG=true make build (non-literal 1) release build, verified off — matches build.js:48 exactly
script/verify-build invoked from an unrelated cwd (/tmp) correct, exit 0 — repo root resolution works

I also re-ran the author's own guards (empty manifest, missing manifest, debug
artifacts against a release verification) and all reproduced exactly as
claimed.

Attempt to construct a passing-but-wrong scenario

I could not find one. The two-source design closes it: the manifest comes from
esbuild's metafile and is independent of the marker, so a bundle losing its
marker is still listed and still demanded to carry one; and the
check_unlisted_bundles cross-check means an under-generated or stale manifest
cannot buy silence, because any unlisted dist/**/*.js carrying a marker is
itself a failure. Renaming or removing constants.js empties the manifest,
which the -s test rejects. main() never reaches its success echo without
count > 0 and every counted bundle matching expected. has_marker failing
for any reason (unreadable, absent, truncated) resolves to "neither", which is
a hard failure, not an absence of evidence. Neither loop runs in a subshell, so
fail's exit 1 is always effective — verified empirically, not just by
reading.

The author's claim that there is no path to exit 0 without positively
identifying the expected marker in at least one bundle holds up.


3. Two-source design

Sound, and the reasoning is correct. Collapsing the two facts would make a
marker-less bundle indistinguishable from dist/chrome/src/content/index.js,
which legitimately carries no marker (confirmed: 8 emitted .js files, 4 in
the manifest, and check_unlisted_bundles passes on a good build, i.e. the
other four genuinely have no marker). Manifest staleness is handled from three
directions: pre-cleared at build.js:117 before anything is emitted, written
last at build.js:196 so a build that dies leaves none, and cross-checked
against the emitted tree at script/verify-build:75-90. A manifest left over
from a debug build cannot survive a release build, since the release build
deletes it and rewrites it.

4. BUILD_DEBUG_MARKER risk

No new risk found. It is derived from DEBUG on one line
(src/shared/constants.js:21-23), read nowhere at runtime, and its only effect
on the artifact is a 25-byte string literal. It cannot alter behaviour because
nothing branches on it. It cannot become the thing that goes wrong silently: I
deleted it and both the artifact check and the jest suite failed. DEBUG
itself and the typeof guard at constants.js:8 are byte-identical to #169.

5. Scope, conventions, non-weakening

  • script/verify-build is #!/bin/sh, set -eu, repo root via
    pwd -P, main "$@" at the bottom — matches the other twelve. /bin/sh on
    this host is dash and the script executes under it, so POSIX compliance is
    demonstrated, not asserted. No local, no [[, no arrays, no +=.
  • build.js diff: the bundle() helper preserves bundle, format,
    platform, target, minify and define verbatim; the only added option is
    metafile: true. Justified, and it does stop a future entry point skipping
    metafile collection.
  • Nothing from #169 is weakened: isDebugBuild(), the define, and the
    constants.js DEBUG line are unchanged.
  • make build-debug succeeds and asserts the inverse (verified).
  • Tests are not vacuous — verified by mutation: breaking the marker in
    constants.js fails 2 of the 2 new tests.

6. Base branch

The deviation from note 6 was the right call and is disclosed in the PR body,
the PR comment, and TODO.md. main has no __BUILD_DEBUG__ to delete, so
demonstration (a) — the whole point of the issue — would have been unrunnable.
Merge ordering is unmistakable. Verified: b5f3da2 sits directly on acb5885
(linear), and git merge-tree against current main reports zero
conflicts
for both this branch and #169's, so the retarget is clean.

Merge order: #169 to main first, then retarget #178 to main and merge.

7. Policy

  • No Claude/Anthropic references anywhere in the diff, commit message, or PR
    body. No attribution trailers.
  • Commit b5f3da2 subject ends with (closes #170); the PR title does too, so
    the squash subject carries it either way.
  • RULES.md unmodified. TODO.md is a single four-line insert at the top of
    Completed Steps; stale Status/Next Step untouched, per note 6.
  • README Entrypoints documents script/verify-build, plus a Debug Builds note.
  • make check green (5 suites, 57 tests, prettier clean), so make fmt is
    clean.
  • No scope creep. The two out-of-scope items (#152, #166) are correctly left
    alone.

8. Verification runs (make targets and script/ only)

  • make check — green
  • make build — green, 4 bundles verified off
  • make build-debug — green, 4 bundles verified on
  • make verify-build standalone — green on a release dist, fails on a debug one
  • make clean && make build — green from an empty dist/
  • script/cibuild (real docker build .) — green; verify-build observed
    running inside the image build, so the CI property holds
  • CI status on b5f3da2: success (check / check (push), 40s)
  • Mergeable: yes, no conflicts

Non-blocking findings

N1. script/verify-build:55-57 — the both-markers error message overstates
the defect, and the same overstatement is in the commit body and PR body.

The message says the missing define means "the debug branch is still live". I
checked the actual emitted output. With the define removed, the bundle contains:

var cB=typeof __BUILD_DEBUG__<"u"?__BUILD_DEBUG__:!1,gk=cB?"autistmask-build-debug=on":"autistmask-build-debug=off"

In extension context __BUILD_DEBUG__ is an undeclared global, so typeof
yields "undefined" and DEBUG evaluates false at runtime. The artifact is
therefore not drainable; what is actually broken is that the build no longer
resolves DEBUG at build time at all, so the artifact's mode is decided by an
undeclared global rather than by the build, and AUTISTMASK_DEBUG=1 silently
stops working. That is still worth failing on, and the message's remedy ("check
that build.js still defines __BUILD_DEBUG__") is exactly right — but the
severity claim is wrong. The commit body's "silently restoring the
drainable-wallet vulnerability in every shipped artifact" inherits the same
claim from issue #170's text, so this is not the author's invention, but it does
write a false security assertion into repo history. Acceptable wording: "the
build-time DEBUG value was never resolved, so this build no longer honours
AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time."
Worth a follow-up issue rather than a rework of this PR.

N2. script/verify-build:382>/dev/null in has_marker conflates "grep
could not read the file" with "no match".

Reproduced with chmod 000 on a listed bundle: the script correctly hard-fails,
but reports "carries no debug marker ... the emitted output changed shape",
which sends the reader hunting for an esbuild change instead of a permission
problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O
error as its own condition. Outcome is already fail-safe, so this is diagnostics
quality only.

N3. build.js:35 and script/verify-build:76 both restrict to *.js.

Consistent, so nothing is missed today (all four esbuild outfiles are .js).
But it is the one output shape that could escape the guard entirely: a future
bundle emitted as .mjs would be excluded from the manifest and from the
unlisted-bundle cross-check, so it would carry no marker requirement and raise
no error. Worth a comment noting the coupling, or widening the cross-check to
all regular files under dist/.

N4. script/verify-build:48-52 and :77 share the unscoped global _file.

sh has no local, and read_marker and check_unlisted_bundles currently
never nest, so this is safe today. It is a latent trap if anyone later calls
read_marker from the cross-check loop.

N5. script/verify-build:118 and :77read -r strips leading/trailing
IFS whitespace and cannot represent a path containing a newline.

Purely theoretical given the fixed output names, noted only because interior
spaces are handled correctly while leading/trailing ones are not.

N6. AUTISTMASK_DEBUG=1 make build produces a debug artifact that the guard
reports as "verified".

Reproduced. This is a consistency check against the requested mode, not an
unconditional "release artifacts are never debug" assertion. Issue #170
explicitly requires AUTISTMASK_DEBUG=1 make build to succeed and assert the
inverse, so this is per spec and not a defect; and script/cibuild is
docker build ., which does not inherit host environment, so CI is unaffected.
Recorded so the residual is on the record: the mode is announced loudly twice
(Build mode: DEBUG (INSECURE ...) and expecting autistmask-build-debug=on),
so it is not silent.

N7. PR title is 86 characters. The repo's default merge style is squash, so
that becomes the landing commit subject. No policy rule mandates 72, and the
commit's own subject is well within it. Cosmetic.


None of N1-N7 is a correctness hole, none can cause a silent pass, and none
weakens the guarantee the issue asked for. Every item in the definition of done
is met and independently verified.

PASS. Merge after #169, then retarget to main.

## Review: PR #178 — VERDICT: PASS Reviewed at head `b5f3da2` against base `acb5885` (PR #169). Independent checkout; nothing modified or committed (working tree verified clean at the end of every experiment below). --- ## 1. Independent reproduction of the two required failure modes I did not take the author's transcripts on trust. Both were reproduced from scratch in an isolated worktree. ### (a) Deleting the `__BUILD_DEBUG__` define fails the build, while `make check` stays green Removed `__BUILD_DEBUG__: JSON.stringify(debugBuild),` from the `define` map at `build.js:95`, then `make clean && make build`: ``` Bundles containing src/shared/constants.js: 4 (listed in dist/constants-bundles.txt) Build complete: dist/chrome/ and dist/firefox/ Verifying emitted bundles (expecting autistmask-build-debug=off)... verify-build: FAIL: dist/chrome/src/background/index.js carries both debug markers, ... make: *** [Makefile:40: build] Error 1 build exit=2 ``` Same tree, same commit, immediately after: ``` make check exit=0 ``` Confirmed: `make check` cannot catch this and `make build` now does. Define restored via `git checkout --`; `build.js:95` intact. ### (b) An unrecognizable bundle fails rather than passes Clean release build, then `printf 'var reminified = 1;\n' > dist/chrome/src/popup/index.js`: ``` verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be determined. ... Refusing to report success. exit=1 ``` Confirmed. --- ## 2. Additional failure modes the author did not demonstrate Eight further attacks constructed against a known-good release `dist/`. All hard-fail; none produced a silent pass. | Attack | Result | | --- | --- | | Manifest lists a file that does not exist | FAIL (`lists ..., which does not exist`), exit 1 | | Bundle unreadable (`chmod 000`) | FAIL (exit 1) — see finding N2 on the message | | Bundle truncated mid-marker | FAIL, exit 1 | | Stale leftover debug artifact at an unlisted path (`index-old.js`) | FAIL via the unlisted-bundle cross-check, exit 1 | | Listed bundle replaced by a **symlink** to the debug build | FAIL (mode mismatch), exit 1 — `-f` and `grep` both follow the link | | `BUILD_DEBUG_MARKER` deleted from `src/shared/constants.js`, then rebuilt | `make build` FAILS (no marker); `make test` also fails 2 tests | | `AUTISTMASK_DEBUG=true make build` (non-literal `1`) | release build, verified `off` — matches `build.js:48` exactly | | `script/verify-build` invoked from an unrelated cwd (`/tmp`) | correct, exit 0 — repo root resolution works | I also re-ran the author's own guards (empty manifest, missing manifest, debug artifacts against a release verification) and all reproduced exactly as claimed. ### Attempt to construct a passing-but-wrong scenario I could not find one. The two-source design closes it: the manifest comes from esbuild's metafile and is independent of the marker, so a bundle losing its marker is still listed and still demanded to carry one; and the `check_unlisted_bundles` cross-check means an under-generated or stale manifest cannot buy silence, because any unlisted `dist/**/*.js` carrying a marker is itself a failure. Renaming or removing `constants.js` empties the manifest, which the `-s` test rejects. `main()` never reaches its success `echo` without `count > 0` and every counted bundle matching `expected`. `has_marker` failing for any reason (unreadable, absent, truncated) resolves to "neither", which is a hard failure, not an absence of evidence. Neither loop runs in a subshell, so `fail`'s `exit 1` is always effective — verified empirically, not just by reading. The author's claim that there is no path to exit 0 without positively identifying the expected marker in at least one bundle holds up. --- ## 3. Two-source design Sound, and the reasoning is correct. Collapsing the two facts would make a marker-less bundle indistinguishable from `dist/chrome/src/content/index.js`, which legitimately carries no marker (confirmed: 8 emitted `.js` files, 4 in the manifest, and `check_unlisted_bundles` passes on a good build, i.e. the other four genuinely have no marker). Manifest staleness is handled from three directions: pre-cleared at `build.js:117` before anything is emitted, written last at `build.js:196` so a build that dies leaves none, and cross-checked against the emitted tree at `script/verify-build:75-90`. A manifest left over from a debug build cannot survive a release build, since the release build deletes it and rewrites it. ## 4. `BUILD_DEBUG_MARKER` risk No new risk found. It is derived from `DEBUG` on one line (`src/shared/constants.js:21-23`), read nowhere at runtime, and its only effect on the artifact is a 25-byte string literal. It cannot alter behaviour because nothing branches on it. It cannot become the thing that goes wrong silently: I deleted it and both the artifact check and the jest suite failed. `DEBUG` itself and the `typeof` guard at `constants.js:8` are byte-identical to #169. ## 5. Scope, conventions, non-weakening - `script/verify-build` is `#!/bin/sh`, `set -eu`, repo root via `pwd -P`, `main "$@"` at the bottom — matches the other twelve. `/bin/sh` on this host is `dash` and the script executes under it, so POSIX compliance is demonstrated, not asserted. No `local`, no `[[`, no arrays, no `+=`. - `build.js` diff: the `bundle()` helper preserves `bundle`, `format`, `platform`, `target`, `minify` and `define` verbatim; the only added option is `metafile: true`. Justified, and it does stop a future entry point skipping metafile collection. - Nothing from #169 is weakened: `isDebugBuild()`, the define, and the `constants.js` DEBUG line are unchanged. - `make build-debug` succeeds and asserts the inverse (verified). - Tests are not vacuous — verified by mutation: breaking the marker in `constants.js` fails 2 of the 2 new tests. ## 6. Base branch The deviation from note 6 was the right call and is disclosed in the PR body, the PR comment, and `TODO.md`. `main` has no `__BUILD_DEBUG__` to delete, so demonstration (a) — the whole point of the issue — would have been unrunnable. Merge ordering is unmistakable. Verified: `b5f3da2` sits directly on `acb5885` (linear), and `git merge-tree` against current `main` reports **zero conflicts** for both this branch and #169's, so the retarget is clean. **Merge order: #169 to `main` first, then retarget #178 to `main` and merge.** ## 7. Policy - No Claude/Anthropic references anywhere in the diff, commit message, or PR body. No attribution trailers. - Commit `b5f3da2` subject ends with ` (closes #170)`; the PR title does too, so the squash subject carries it either way. - `RULES.md` unmodified. `TODO.md` is a single four-line insert at the top of Completed Steps; stale Status/Next Step untouched, per note 6. - README Entrypoints documents `script/verify-build`, plus a Debug Builds note. - `make check` green (5 suites, 57 tests, prettier clean), so `make fmt` is clean. - No scope creep. The two out-of-scope items (#152, #166) are correctly left alone. ## 8. Verification runs (make targets and `script/` only) - `make check` — green - `make build` — green, 4 bundles verified `off` - `make build-debug` — green, 4 bundles verified `on` - `make verify-build` standalone — green on a release dist, fails on a debug one - `make clean && make build` — green from an empty `dist/` - `script/cibuild` (real `docker build .`) — green; `verify-build` observed running inside the image build, so the CI property holds - CI status on `b5f3da2`: **success** (`check / check (push)`, 40s) - Mergeable: yes, no conflicts --- ## Non-blocking findings **N1. `script/verify-build:55-57` — the both-markers error message overstates the defect, and the same overstatement is in the commit body and PR body.** The message says the missing define means "the debug branch is still live". I checked the actual emitted output. With the define removed, the bundle contains: ``` var cB=typeof __BUILD_DEBUG__<"u"?__BUILD_DEBUG__:!1,gk=cB?"autistmask-build-debug=on":"autistmask-build-debug=off" ``` In extension context `__BUILD_DEBUG__` is an undeclared global, so `typeof` yields `"undefined"` and `DEBUG` evaluates **false** at runtime. The artifact is therefore not drainable; what is actually broken is that the build no longer resolves `DEBUG` at build time at all, so the artifact's mode is decided by an undeclared global rather than by the build, and `AUTISTMASK_DEBUG=1` silently stops working. That is still worth failing on, and the message's remedy ("check that build.js still defines `__BUILD_DEBUG__`") is exactly right — but the severity claim is wrong. The commit body's "silently restoring the drainable-wallet vulnerability in every shipped artifact" inherits the same claim from issue #170's text, so this is not the author's invention, but it does write a false security assertion into repo history. Acceptable wording: "the build-time DEBUG value was never resolved, so this build no longer honours AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time." Worth a follow-up issue rather than a rework of this PR. **N2. `script/verify-build:38` — `2>/dev/null` in `has_marker` conflates "grep could not read the file" with "no match".** Reproduced with `chmod 000` on a listed bundle: the script correctly hard-fails, but reports "carries no debug marker ... the emitted output changed shape", which sends the reader hunting for an esbuild change instead of a permission problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O error as its own condition. Outcome is already fail-safe, so this is diagnostics quality only. **N3. `build.js:35` and `script/verify-build:76` both restrict to `*.js`.** Consistent, so nothing is missed today (all four esbuild outfiles are `.js`). But it is the one output shape that could escape the guard entirely: a future bundle emitted as `.mjs` would be excluded from the manifest *and* from the unlisted-bundle cross-check, so it would carry no marker requirement and raise no error. Worth a comment noting the coupling, or widening the cross-check to all regular files under `dist/`. **N4. `script/verify-build:48-52` and `:77` share the unscoped global `_file`.** `sh` has no `local`, and `read_marker` and `check_unlisted_bundles` currently never nest, so this is safe today. It is a latent trap if anyone later calls `read_marker` from the cross-check loop. **N5. `script/verify-build:118` and `:77` — `read -r` strips leading/trailing IFS whitespace and cannot represent a path containing a newline.** Purely theoretical given the fixed output names, noted only because interior spaces are handled correctly while leading/trailing ones are not. **N6. `AUTISTMASK_DEBUG=1 make build` produces a debug artifact that the guard reports as "verified".** Reproduced. This is a consistency check against the requested mode, not an unconditional "release artifacts are never debug" assertion. Issue #170 explicitly requires `AUTISTMASK_DEBUG=1 make build` to succeed and assert the inverse, so this is per spec and not a defect; and `script/cibuild` is `docker build .`, which does not inherit host environment, so CI is unaffected. Recorded so the residual is on the record: the mode is announced loudly twice (`Build mode: DEBUG (INSECURE ...)` and `expecting autistmask-build-debug=on`), so it is not silent. **N7. PR title is 86 characters.** The repo's default merge style is squash, so that becomes the landing commit subject. No policy rule mandates 72, and the commit's own subject is well within it. Cosmetic. --- None of N1-N7 is a correctness hole, none can cause a silent pass, and none weakens the guarantee the issue asked for. Every item in the definition of done is met and independently verified. **PASS.** Merge after #169, then retarget to `main`.
Author
Collaborator

Manager note (the review verdict is in its own comment above).

Independent adversarial review passed with no blocking findings. The reviewer
reproduced both demonstrated failure modes itself rather than trusting the
author's transcripts, and then constructed eight further attacks the author
never tried
— manifest listing a nonexistent file, unreadable bundle via
chmod 000, a bundle truncated mid-marker, a stale debug artifact at an
unlisted path, a listed bundle replaced by a symlink to the debug build,
BUILD_DEBUG_MARKER deleted from constants.js and rebuilt,
AUTISTMASK_DEBUG=true, and running the script from an unrelated cwd. All
hard-failed. None silently passed.

It also verified the structural point that makes the design work: neither loop
runs in a subshell, so fail's exit 1 always takes effect — checked
empirically, not by reading. And check_unlisted_bundles stops a short or
stale manifest from buying silence, which was the hole I was most worried
about when I asked for two independent signals.

Marking merge-ready and assigning to @sneak.

Correction to this issue's own framing — my error

The reviewer inspected what the emitted bundle actually contains when the
define is deleted:

var cB = typeof __BUILD_DEBUG__ < "u" ? __BUILD_DEBUG__ : !1, 

In extension context that global is undeclared, so DEBUG evaluates to
false at runtime and the artifact is not drainable.

So the claim I wrote into issue #170 — and repeated in the manager comment
dispatching it, and which the author then inherited into the commit body —
that deleting the define "silently restores the drainable-wallet vulnerability
in every shipped artifact" is overstated and wrong. I did not verify the
compiled output before asserting it; I reasoned from the source and assumed the
fallback would go the unsafe way. It goes the safe way.

The real defect the guard catches is still worth catching: DEBUG would no
longer be resolved at build time
, leaving an unresolved ternary against an
undeclared global. That is a broken build contract and it means the release/
debug distinction has silently stopped being enforced at all — including, on a
future refactor, in whichever direction the fallback then happens to go. But it
is a correctness and build-integrity failure, not a live funds-loss one, and
this PR should not land carrying a scarier justification than the facts
support.

Nothing about the PR changes as a result; the guard is correct and worth having
on exactly the same terms. Only the wording is wrong, in three places
(script/verify-build:55-57, the commit body, and issue #170), and that is
folded into the follow-up below rather than being churned into a rework.

Merge ordering — important

Base is fix/issue-149-debug-build-flag, not main. That deviation from
the issue's note 6 was correct: main has no define to delete, so the central
demonstration would have been unrunnable. It is disclosed in the PR body, the
PR comment, and TODO.md.

Merge #169 to main first, then retarget this PR to main and merge.
git merge-tree shows zero conflicts against current main for both branches.

Non-blocking findings

Filed together as #180 against 1.0.0 — all three are in the new script and are
better fixed in one pass than churned here:

  • Reword the script/verify-build:55-57 diagnostic per the correction above.
  • script/verify-build:382>/dev/null conflates "grep could not read the
    file" with "no match", so an unreadable bundle fails safe but is
    misdiagnosed.
  • build.js:35 and script/verify-build:76 both filter to *.js, so a future
    non-.js bundle would escape both the manifest and the cross-check.

Not acting on: the shared unscoped _file (latent, safe today), whitespace in
paths via read -r, and AUTISTMASK_DEBUG=1 make build blessing a debug
artifact — the last is explicitly required by #170 and Docker CI does not
inherit host env.

Manager note (the review verdict is in its own comment above). Independent adversarial review passed with no blocking findings. The reviewer reproduced both demonstrated failure modes itself rather than trusting the author's transcripts, and then constructed **eight further attacks the author never tried** — manifest listing a nonexistent file, unreadable bundle via `chmod 000`, a bundle truncated mid-marker, a stale debug artifact at an unlisted path, a listed bundle replaced by a **symlink to the debug build**, `BUILD_DEBUG_MARKER` deleted from `constants.js` and rebuilt, `AUTISTMASK_DEBUG=true`, and running the script from an unrelated cwd. All hard-failed. None silently passed. It also verified the structural point that makes the design work: neither loop runs in a subshell, so `fail`'s `exit 1` always takes effect — checked empirically, not by reading. And `check_unlisted_bundles` stops a short or stale manifest from buying silence, which was the hole I was most worried about when I asked for two independent signals. Marking `merge-ready` and assigning to @sneak. ## Correction to this issue's own framing — my error The reviewer inspected what the emitted bundle actually contains when the define is deleted: ```js var cB = typeof __BUILD_DEBUG__ < "u" ? __BUILD_DEBUG__ : !1, … ``` In extension context that global is undeclared, so `DEBUG` evaluates to **false** at runtime and the artifact is **not** drainable. So the claim I wrote into issue #170 — and repeated in the manager comment dispatching it, and which the author then inherited into the commit body — that deleting the define "silently restores the drainable-wallet vulnerability in every shipped artifact" is **overstated and wrong**. I did not verify the compiled output before asserting it; I reasoned from the source and assumed the fallback would go the unsafe way. It goes the safe way. The real defect the guard catches is still worth catching: **`DEBUG` would no longer be resolved at build time**, leaving an unresolved ternary against an undeclared global. That is a broken build contract and it means the release/ debug distinction has silently stopped being enforced at all — including, on a future refactor, in whichever direction the fallback then happens to go. But it is a correctness and build-integrity failure, not a live funds-loss one, and this PR should not land carrying a scarier justification than the facts support. Nothing about the PR changes as a result; the guard is correct and worth having on exactly the same terms. Only the wording is wrong, in three places (`script/verify-build:55-57`, the commit body, and issue #170), and that is folded into the follow-up below rather than being churned into a rework. ## Merge ordering — important Base is `fix/issue-149-debug-build-flag`, **not** `main`. That deviation from the issue's note 6 was correct: `main` has no define to delete, so the central demonstration would have been unrunnable. It is disclosed in the PR body, the PR comment, and `TODO.md`. **Merge #169 to `main` first, then retarget this PR to `main` and merge.** `git merge-tree` shows zero conflicts against current `main` for both branches. ## Non-blocking findings Filed together as #180 against 1.0.0 — all three are in the new script and are better fixed in one pass than churned here: - Reword the `script/verify-build:55-57` diagnostic per the correction above. - `script/verify-build:38` — `2>/dev/null` conflates "grep could not read the file" with "no match", so an unreadable bundle fails safe but is misdiagnosed. - `build.js:35` and `script/verify-build:76` both filter to `*.js`, so a future non-`.js` bundle would escape both the manifest and the cross-check. Not acting on: the shared unscoped `_file` (latent, safe today), whitespace in paths via `read -r`, and `AUTISTMASK_DEBUG=1 make build` blessing a debug artifact — the last is explicitly required by #170 and Docker CI does not inherit host env.
clawbot added merge-ready and removed needs-review labels 2026-08-09 07:20:38 +02:00
clawbot removed their assignment 2026-08-09 07:20:39 +02:00
sneak was assigned by clawbot 2026-08-09 07:20:39 +02:00
clawbot added this to the 1.0.0 milestone 2026-08-09 07:20:39 +02:00
sneak changed target branch from fix/issue-149-debug-build-flag to main 2026-08-09 16:19:12 +02:00
clawbot added needs-rebase and removed merge-ready labels 2026-08-09 17:02:01 +02:00
sneak was unassigned by clawbot 2026-08-09 17:02:01 +02:00
clawbot self-assigned this 2026-08-09 17:02:02 +02:00
clawbot changed title from build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) to WIP: build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) 2026-08-10 14:40:04 +02:00
clawbot changed title from WIP: build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) to build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) 2026-08-10 15:47:08 +02:00
clawbot changed target branch from main to next 2026-08-10 15:47:08 +02:00
clawbot added 2 commits 2026-08-10 15:47:08 +02:00
security: make DEBUG a build-time flag defaulting to off (closes #149)
All checks were successful
check / check (push) Successful in 29s
acb58856c4
DEBUG was hardcoded to true in src/shared/constants.js, so every wallet
created from a build of main received the publicly committed test recovery
phrase and was instantly drainable. There was no way to produce a non-debug
build at all: the real entropy path in generateMnemonic() was dead code in
every artifact.

DEBUG is now a build-time constant injected by esbuild's define in build.js,
alongside the existing __BUILD_* defines, and read by constants.js with the
same typeof guard buildInfo.js uses. It is false unless the build was run
with AUTISTMASK_DEBUG=1 — an exact match, so an unset, empty or mistyped
value fails safe towards a release build. The build prints which mode it
used, and make build-debug is a shim for the debug case.

What DEBUG does when enabled is unchanged: the red banner plus the hardcoded
test phrase, no new conditionals. Mnemonic generation deliberately keeps
reading the compile-time constant rather than isDebug() from log.js, which
also ORs in the runtime debugMode flag the settings toggle drives; routing it
through isDebug() would let a user of a release build re-enable the known
test phrase for real wallets. That is now recorded at the call site, in the
README DEBUG Mode Policy, and covered by a regression test.

New tests/wallet.test.js covers both build modes: with the flag off, two
successive generateMnemonic() calls differ, both validate as BIP-39 phrases,
both are 12 words, neither is DEBUG_MNEMONIC, and the result still derives a
usable HD wallet — including with the runtime toggle forced on. With the flag
on, DEBUG is true and the test phrase is returned, so the debug path stays
proven rather than silently removed.

Verified with make check (55 tests, lint, fmt-check all green), and with
make build and make build-debug: all four bundles across dist/chrome and
dist/firefox export DEBUG:!1 in a release build and DEBUG:!0 in a debug
build, and AUTISTMASK_DEBUG=true likewise yields DEBUG:!1.
build: assert DEBUG is off in every emitted bundle (closes #170)
All checks were successful
check / check (push) Successful in 40s
b5f3da2388
PR #169 made DEBUG a build-time flag defaulting off, but nothing guarded
the wiring. The tests load src/shared/constants.js outside a bundle and
take the jest fallback branch, so deleting the __BUILD_DEBUG__ define
from build.js left all tests passing and make check green while silently
restoring the drainable-wallet vulnerability in every shipped artifact.
The property only exists in the emitted output, so it is now asserted
against the emitted output.

script/verify-build reads two independent facts per bundle. Which
bundles must be inspected comes from esbuild's metafile: build.js writes
dist/constants-bundles.txt naming every emitted JS output whose input
set includes constants.js, so the set is derived from the real
dependency graph rather than a hardcoded count or filenames. What each
bundle's DEBUG state is comes from BUILD_DEBUG_MARKER, a new constant
derived from DEBUG itself that the bundler folds to exactly one of two
string literals. Deriving the bundle set from the marker would be the
silent-pass hole: a bundle with no marker would be indistinguishable
from content/index.js, which legitimately contains none.

The marker is a plain string rather than a match on minified `DEBUG:!1`,
because minifier output is not a contract across esbuild versions. When
DEBUG is not known at build time the fold cannot happen and both
literals survive, which is exactly the shape of the regression this
guards against. Every way of failing to determine a bundle's state is a
hard failure: missing manifest, empty manifest, a listed file that does
not exist, both markers, neither marker, the wrong marker, or a bundle
carrying a marker while absent from the manifest. There is no path on
which the script exits 0 without positively identifying the expected
marker in at least one bundle.

It runs on the build path only. make build and make build-debug both
invoke it, the latter asserting the inverse, and Dockerfile:17 runs a
bare make build, so CI fails on a release build with a live debug
branch. It is deliberately not in script/check: that would make check
depend on dist/ existing and pull a full build into its time budget, and
the obvious workaround -- skip when dist/ is absent -- is precisely the
silently-green behaviour this exists to prevent.
clawbot force-pushed fix/issue-170-assert-debug-off from b5f3da2388 to 38596b4c79 2026-08-10 15:56:48 +02:00 Compare
Author
Collaborator

Rebase review at 38596b4PASS: nothing lost or duplicated in the conflict resolution (.PHONY keeps test-e2e, all 14 script/ entrypoints documented once, single BUILD_DEBUG_MARKER/__BUILD_DEBUG__ definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted at build.js:95 -> make build exits 2, make check still exits 0; define restored); make check (6 suites / 117 tests), make build, make build-debug, script/cibuild (layers RUN make check / RUN make build executed, not CACHED) and make test-e2e (4/4, the one path CI does not cover) all green; commit subject ends (closes #170), no attribution trailers; CI success on the head commit; fast-forwards onto next.

Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets fix/issue-149-debug-build-flag and "must be merged after" #169, which has already landed as f7f141a, and that it should later be retargeted to main (base is now next). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.

Rebase review at `38596b4` — **PASS**: nothing lost or duplicated in the conflict resolution (`.PHONY` keeps `test-e2e`, all 14 `script/` entrypoints documented once, single `BUILD_DEBUG_MARKER`/`__BUILD_DEBUG__` definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted at `build.js:95` -> `make build` exits 2, `make check` still exits 0; define restored); `make check` (6 suites / 117 tests), `make build`, `make build-debug`, `script/cibuild` (layers `RUN make check` / `RUN make build` executed, not `CACHED`) and `make test-e2e` (4/4, the one path CI does not cover) all green; commit subject ends ` (closes #170)`, no attribution trailers; CI success on the head commit; fast-forwards onto `next`. Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets `fix/issue-149-debug-build-flag` and "must be merged after" https://git.eeqj.de/sneak/AutistMask/pulls/169, which has already landed as `f7f141a`, and that it should later be retargeted to `main` (base is now `next`). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.
clawbot force-pushed fix/issue-170-assert-debug-off from 38596b4c79 to 627c0c158e 2026-08-10 16:14:45 +02:00 Compare
clawbot merged commit e9fa8bec47 into next 2026-08-10 16:15:23 +02:00
clawbot deleted branch fix/issue-170-assert-debug-off 2026-08-10 16:15:23 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#178