Skip to content

fix(assemble): an apostrophe glued to a ${ or the closing '' (#244) - #304

Merged
lionello merged 1 commit into
masterfrom
fix/244-assembler-quote-escaping
Aug 20, 2026
Merged

fix(assemble): an apostrophe glued to a ${ or the closing '' (#244)#304
lionello merged 1 commit into
masterfrom
fix/244-assembler-quote-escaping

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

Motivation

bin/assemble-module.py's nix_escape() escaped in two passes — ''''', then ${''${. Each rule is right alone and wrong in combination: the escape for ${ starts with '', and Nix lexes a run of apostrophes greedily, three at a time. So an ordinary shell line

echo "key '${LOCAL_WEBHOOK_SPAWN_KEY:-event}' is unusable" >&2

reached the generated module as ' + ''${…, which Nix reads as an escaped '' plus a live antiquotation. nix run .#assemble succeeded; every later nix build failed with error: undefined variable 'event' pointing into modules/agent-box.nix, with nothing wrong in the source file. With a ${FOO} (no :-default) there is no undefined variable and the payload is silently mangled instead — that half ships. Found in #243, which worked around it by rewording the line. Fixes #244.

Real Nix showed the same flaw at the other end of a payload: text ending in an odd number of apostrophes glues onto the host's closing '' and the string never terminates.

What changed

  • bin/assemble-module.py — one pass over '+\$\{|'+|\$\{, so a quote run and the ${ behind it are escaped together. Pairs become '''; an odd trailing apostrophe becomes ''\', which leaves the following ''${ (or the host's closing '') unambiguous. The result is now splice-anywhere safe.
  • tests/test-assemble-module.py (new) — decodes escaped text the way Nix's IND_STRING lexer rules do (raising on a live ${, an unterminated string, or trailing text), and round-trips every string up to 5 characters over ' $ { } \ a — 9331 of them — through nix_escape and back. Plus explicit expected escapes, the exact fix(sessions): let a session name be as long as a dispatch can mint #243 line, and two resolve()-level tests for @@include: vs @@include-verbatim:. One test re-runs the corpus through the old two-pass escaping and asserts it fails, so a decoder that accepted everything would not pass silently.
  • flake.nix — new eval-level check assemble-module-escaping (both systems), plus a CI step. module-generated-up-to-date cannot catch an escaping bug: it regenerates the file with the same assembler, so the check and the bug agree on the wrong bytes.
  • modules/src/webhook-spawn.sh — the fix(sessions): let a session name be as long as a dispatch can mint #243 workaround (key printed unquoted, with a comment pointing at assemble-module.py: a single quote directly before ${ escapes wrong — silent corruption or a broken generated module #244) is replaced by the natural wording. This is also the regression test with real Nix in it: the committed module now carries ''\'''${LOCAL_WEBHOOK_SPAWN_KEY:-event}', so module-single-file, vm-closure and golden-snapshot all parse it. The golden fixture shows the rendered script back with the apostrophes in place.

The model is validated against real Nix

The committed test uses a Python model of the lexer, because the check runs in a build sandbox with no nix. The model was checked against the real thing on this box — all 9331 escaped strings evaluate back to their source through nix-instantiate --eval --json:

$ python3 tests/test-assemble-module.py --nix
all 9331 escaped strings round-trip through real Nix

--nix is kept in the file so anyone can redo that.

User-visible and security effects

None on a deployed box beyond one diagnostic's wording. agent-box-webhook-spawn now says

agent-box-webhook-spawn: key '<key>' does not fit a session name (max 150 characters)

The escaping change is a no-op for every payload in the tree today (no modules/src/* file contains the pattern — one could not, the tree would not build), so modules/agent-box.nix changes only by that reworded line.

Checks run

On this aarch64-linux box (all exit 0):

  • assemble-module-escaping — 7 tests OK
  • python3 tests/test-assemble-module.py --nix — 9331/9331 through real Nix
  • module-generated-up-to-date, module-single-file, multi-user, download-route, webhook-route
  • golden-snapshot — failed with exactly the one expected diagnostic diff, regenerated with nix run .#update-golden, green after

VM tests and vm-closure are x86_64-linux-only; CI covers them.

nix_escape() applied its two rules as two passes: `''`→`'''`, then
`${`→`''${`. Both are right alone, and wrong together — the escape for
`${` starts with `''`, and Nix lexes a run of apostrophes greedily three
at a time. So an ordinary shell line

    echo "key '${LOCAL_WEBHOOK_SPAWN_KEY:-event}' is unusable" >&2

came out as `'` + `''${`, which Nix reads as an escaped `''` plus a LIVE
antiquotation: `nix run .#assemble` succeeded and every later `nix build`
failed with `undefined variable 'event'` pointing into the generated
module, with nothing wrong in the source file. With a `${FOO}` (no
default) the payload is silently mangled instead — that half ships.

Escape the run and the `${` behind it in one pass: pairs become `'''`,
and an odd trailing apostrophe becomes `''\'`, which leaves the `''${`
unambiguous. Real Nix has the same problem at the other end of the
payload — a text ending in an odd apostrophe run glues onto the host's
closing `''` and the string never terminates — so that case is escaped
too, which makes the result splice-anywhere safe.

tests/test-assemble-module.py decodes the escaped text the way Nix's
IND_STRING rules do and round-trips every string up to 5 characters over
`' $ { } \ a` (9331 of them) through it; the model was validated by
running the same corpus through `nix-instantiate --eval --json`, which
`--nix` re-runs on demand. module-generated-up-to-date cannot catch an
escaping bug on its own: it regenerates the file with the same
assembler, so the check and the bug agree on the wrong bytes.

modules/src/webhook-spawn.sh gets its natural wording back (the #243
workaround printed the key unquoted), so the generated module now
carries the escaped form and every eval check parses it for real.

Closes #244

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
@lionello
lionello merged commit d64c67e into master Aug 20, 2026
1 check passed
@lionello
lionello deleted the fix/244-assembler-quote-escaping branch August 20, 2026 00:20
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Agent-Box Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

assemble-module.py: a single quote directly before ${ escapes wrong — silent corruption or a broken generated module

2 participants