Skip to content

fix(runtime): close two inline-script write paths plan mode waved through - #65

Merged
mangit955 merged 2 commits into
mainfrom
fix/plan-mode-inline-script-writes
Aug 30, 2026
Merged

fix(runtime): close two inline-script write paths plan mode waved through#65
mangit955 merged 2 commits into
mainfrom
fix/plan-mode-inline-script-writes

Conversation

@mangit955

Copy link
Copy Markdown
Owner

Closes #63.

Plan mode is gated twice and both are load-bearing: the first gate withholds the
writing tools, the second refuses a write that arrives anyway. The second exists
because run_terminal has to stay available for inspection. Two shapes answered
classifyInvocation(args).writes === false and were waved through.

What changed

runtime/toolEffects.ts only.

  1. INLINE_WRITE was Python- and JS-shaped. Perl writes open(OUT, ">f")
    a redirect where the pattern wanted w or a — so the mode class is now
    ['"][wa>]. Ruby's File.write is named too.
  2. A shell-out inside an inline script was invisible. CODE_SUBPROCESS
    already grades exactly this on the REPL path; classifyCommand now runs the
    same test against the raw command when INLINE_SCRIPT matches. That pattern
    gained bare system( and qx( for Perl and Ruby.

Both run before stripQuoted, because the script lives inside those quotes.

Newly refused

perl -e 'open(OUT, ">input.tex"); ...', ruby -e 'File.write(...)', and any
-c/-e script calling system(...), qx(...), subprocess, os.popen,
child_process, execSync, spawnSync or Bun's shell. Nothing is newly
permitted.

Two narrowings I made reading the diff back

Fail-closed is the invariant, but a rule that only refuses gets widened until it
refuses everything. Each has a fixture:

  • system( uses (?<![.\w]) rather than \b, with os.system listed
    separately. A plain \bsystem\s*\( also matches platform.system() — a
    read-only call common in inspection code — which would have refused ordinary
    plan-mode reads.
  • Backticks are deliberately absent from the subprocess list, even though
    they run a program in Perl and Ruby: node -e 'console.log(`w ${x}`)' is a
    template literal.

Shell rules are still not applied to interpreter source. Negative fixtures pin a
comparison, a right shift and a read-mode open.

Evidence

Fixtures are verbatim from jobs/tb2-post-1.1/overfull-hbox__Jk3CkEc — call 30,
which rewrote input.tex through open(OUT, ">input.tex") and ran pdflatex
through system(...), and call 27, its read-only sibling that must still pass.

Replaying that trial through the old and new classifier: 4 of 58 calls were
flagged as writes before, 12 after. The eight new ones are calls 22, 30, 31,
33, 35, 37, 38, 39 — the perl scripts that edited the file.

Each fix proved by mutation, with the mutation confirmed applied and the revert
diffed back to clean:

mutation tests red
drop > from the mode class 3
drop File.write 1
drop CODE_SUBPROCESS from the inline path 5
widen the system lookbehind away 2

bun run verify --all on this branch: 4 gates passed. bun test: 1914
pass, 0 fail across 116 files
. Reverse-order sweep green on the branch this was
developed on.

Not checked: nothing ran against a live provider or an actual perl process — this
is classifier behaviour only.

…ough

Plan mode's second gate asks `classifyInvocation(args).writes`, and two shapes
answered false. Both reach disk through `run_terminal`, which the first gate has
to keep available for inspection.

`INLINE_WRITE` was Python- and JS-shaped. Perl's idiom is `open(OUT, ">f")` — a
redirect where the pattern wanted `w` or `a` — so `>` now sits in that class, and
Ruby's `File.write` is named. And `classifyCommand` never looked for a shell-out
inside an inline script, though `CODE_SUBPROCESS` already grades exactly that on
the REPL path; the inline path now runs the same test against the raw command.

Newly refused while planning: `perl -e 'open(OUT, ">input.tex"); ...'`,
`ruby -e 'File.write(...)'`, and any `-c`/`-e` script that calls `system(...)`,
`qx(...)`, `subprocess`, `os.popen`, `child_process`, `execSync`, `spawnSync`
or Bun's shell. Nothing is newly permitted. Both tests run against the raw
command, before `stripQuoted`, because the script lives inside those quotes.

Two narrowings, each with a fixture, because a rule that only refuses will be
widened until it refuses everything:

- `system(` is matched bare for Perl and Ruby but only where nothing precedes
  it, with `os.system` listed separately. A plain `\bsystem\s*\(` also matches
  `platform.system()`, a read-only call common in inspection code.
- A backtick is not on the subprocess list even though it runs a program in both
  languages: `node -e 'console.log(`w ${x}`)'` is a template literal.

Shell rules are still not applied to interpreter source. `segmentsOf` splits on
`;` and `|`, and the redirect test matches `value >> 16`; the negative fixtures
pin a comparison, a right shift and a read-mode open.

The fixtures are the commands verbatim from
jobs/tb2-post-1.1/overfull-hbox__Jk3CkEc — call 30, which rewrote input.tex and
ran pdflatex, and call 27, its read-only sibling that must still pass. Replaying
that trial, 4 of 58 calls classified as writes before and 12 after; the eight new
ones are the perl scripts that edited the file.

Widening `writes` also makes `hasUnverifiedEdits()` stricter, which is correct.

Closes #63
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
woop-code Ready Ready Preview Aug 30, 2026 7:51am

Two false positives in the pattern the previous commit widened, both found by
review, both verified before changing anything. Each refuses an ordinary
plan-mode read, which is the failure the narrowings in that commit exist to
avoid.

`qx` no longer takes a slash delimiter. `qx`, `qy`, `qz` and `qw` are the
standard names for a quaternion's components, so `norm = qx / qw` and
`qx/n, qy/n` are division — and `codeShellsOut` graded both as a subshell,
which refuses the arithmetic outright while planning. Only `qx(` and `qx{`
are matched now, which no arithmetic produces. The cost is Perl's `qx/cmd/`,
left out for the same reason a backtick already was: its delimiter cannot be
told from an operator in the languages that share this pattern, and the repl
path this pattern also serves runs Python and JavaScript, where `qx//` never
means a subshell.

The bare `system(` lookbehind now excludes `>` as well as `.` and word
characters, so Perl's `$obj->system(1)` reads as the method call it is.

Nothing that was refused before is permitted now except those two shapes.
@mangit955
mangit955 merged commit afa69b7 into main Aug 30, 2026
6 checks passed
@mangit955
mangit955 deleted the fix/plan-mode-inline-script-writes branch August 30, 2026 07:52
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.

Plan mode's second gate misses two inline-script write paths

1 participant