EM-WS8: headless hardening + standalone MCP server - #15
Merged
Merged
Conversation
- mcp/lua-platform: bundle @modelcontextprotocol/sdk and every other npm dependency into dist/server.js (createRequire banner for CJS deps), so the server runs with no node_modules (marketplace install, Job-tier image). tests/standalone-bundle.test.mjs copies the bundle + package.json into an empty temp dir and lists the five tools over stdio. - lib/tokenizer.mjs: replace the start-anchored regex with a shell lexer. Production verbs are found in any simple command of a &&/;/||/pipe chain, groups, $()/backticks/<(), bash -c/eval/ssh/| sh strings and heredocs, behind env-assignment prefixes and wrappers, via npx lua / npx lua-cli / pnpm exec / yarn / node .../lua-cli, and at any binary path; shell variables in the binary/verb position are blocked; unclosable input falls back to a textual search. LUA_DEPLOY_CONFIRMED=1 counts only as the first word of the same simple command, binary spelled bare, outside pipes/groups/substitutions. Closes the five Job-tier audit bypasses and the marketplace `templates`/`agent-template` and upper-case alias gaps. - LUA_PLUGIN_HEADLESS=1 (lib/headless.mjs): hooks name no slash command; check-lua-auth emits a neutral note instead of "run /lua-auth"; confirm-deploy treats the prefix as void and blocks every production verb; post-deploy-smoke sends no production ping. - docs/JOB_TIER.md "Running in the Lua Job tier"; SECURITY.md, READMEs, CHANGELOG; version 1.5.0 -> 1.6.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…king in the textual fallback The textual-fallback option group could split --a two ways and backtracked exponentially on a failing match (28 options ~200 s), past the 10 s hook timeout, where the hook fails open. One parse per option now (<1 ms); an internal classifier error blocks any command that mentions a lua binary. Adds timing bounds, a 3000-case shell fuzz, and the fail-closed seam test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…close bypasses, cut false positives
1. CRITICAL (hook times out → fails open): the textual fallback is now a
linear token scan reusing matchArgs (the RAW regexes were quadratic on
`lua/` runs); findClose/heredoc skipping, matchArgs and command-position
walking are linear. Commands over 32 KB, a spent 1.5 s budget, or an
internal error block the command when it mentions lua
(DEPLOY_DENIED_UNCLASSIFIABLE). New test/hooks/confirm-deploy.timeout
spawns the real hook on adversarial inputs up to 100 KB and asserts a
decision in < 2 s.
2. HIGH bypasses: computed binary + computed verb when the command mentions
lua; `lua${IFS}deploy` split at expansions; unquoted globs and brace
expansion are computed words; unicode spaces separate.
3. MEDIUM: env -S / --split-string, awk system()/pipes, osascript -e,
editor -c '!…', sed e, git -c alias '!…', tmux/screen, docker exec sh -c,
bash < <(…) / source <(…), $(…) in unquoted heredocs, heredoc written to
a script that is then run.
4. MEDIUM false positives: findClose skips heredoc bodies and comments
(Claude Code's own `gh pr create --body "$(cat <<'EOF' … EOF)"` passes);
the binary is matched only in command position (`cp -r lua deploy`,
`ls lua deploy`, `echo lua deploy all` pass); runners count only in
command position (`rg x .` is not `source`).
5. LOW: dist/THIRD_PARTY_NOTICES.txt generated from the esbuild metafile
(SDK + zod, MIT, full texts); legalComments: 'eof'. dist/server.js is
byte-identical.
The review's probe sets (bypass.json, fp.json, perf.mjs, e2e.mjs) are unit
and spawned tests. 808 plugin tests + 149 MCP tests pass; coverage gates
and all lints green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EM-WS8 of the EM v1 program. This fixes three findings from the Lua Job-tier audit, where the plugin runs unattended in
claude -p:node_modules;The plugin moves 1.5.0 → 1.6.0. The lua-cli pin is unchanged (3.38.0).
1. Standalone
lua-platformMCP serverscripts/bundle.mjsno longer leaves@modelcontextprotocol/sdkexternal.dist/server.jsinlines every npm dependency from the lockfile (106 KB → 529 KB; the budget is 5 MB). AcreateRequirebanner handles CommonJS dependencies. The build is deterministic: rebuilding gives an identical sha.tests/standalone-bundle.test.mjs:package.jsonanddist/server.jsinto an empty temp dir and asserts there is nonode_modulesabove it;NODE_PATHremoved and runsinitializethentools/list;serverInfo.versionmatchespackage.json.ERR_MODULE_NOT_FOUND.2. confirm-deploy parser hardening (
lib/tokenizer.mjs)The old parser was a single regex anchored at the start of the command. It is replaced by a POSIX-ish shell lexer, and every simple command is inspected.
&&,;,||,|,|&,&and newline chains, and in( )/{ }groups.$( ), backticks and<( )are parsed recursively, including inside double quotes.bash -c,eval,ssh host "…",… | sh, heredocs and here-strings fed to a shell, andalias/trap.node -e,python -candperl -ebodies are searched as text.sudo,env,timeout,commandandexec.npx lua,npx lua-cli[@v],pnpm exec,pnpm dlx,yarn,npm exec --,bunx,node [opts] …/lua-cli/dist/index.js, and any binary path.lua --ci deploy,lua workflows -v 3 deploy x.$L deploy,lua $VERB,| xargs lua).marketplace templates/agent-template(s), and upper-case action words (lua-cli lower-cases them).LUA_DEPLOY_CONFIRMED=1counts only as the first word of the same simple command, with the binary spelled bare, outside pipes, groups, substitutions and wrapper strings. Each verb in a chain needs its own prefix.export LUA_DEPLOY_CONFIRMED=1; lua deploy all,LUA_DEPLOY_CONFIRMED=1 true && lua deploy all,LUA_DEPLOY_CONFIRMED=1 npx lua …./lua-templateemit is still allowed.The five audit bypasses (
cd x && lua deploy …,true; lua version promote 3,FOO=1 lua deploy all,/usr/local/bin/lua deploy all,npx lua deploy all) are each tested. They are in the unit tests and the spawned integration test.New intentional false positives, which fail closed:
echo lua deploy all;sshornode/python/perl(for examplenode x.js "lua deploy all").Quoted mentions in
git commit -m,grep,gh pr create --body,lua chat -mand heredocs written to files stay allowed.This is still a belt. Commands assembled at runtime (
base64 -d | sh, script files, npm scripts, raw HTTP) are out of reach. SECURITY.md now says so.3. Headless mode:
LUA_PLUGIN_HEADLESS=1(lib/headless.mjs)check-lua-auth: every failure path emits a neutral note ("could not be confirmed; behind a proxy this may just be the probe route"). It never says "run /lua-auth".check-lua-versionanddetect-project: the same findings, with no/lua-doctor,/lua-updateornpm i -g.confirm-deploy: the prefix is void. Every production verb is blocked withDEPLOY_DENIED_HEADLESS, because the model would be confirming to itself.block-auto-deployhas no slash pointer.post-deploy-smoke: never sends the productionlua chatping.docs/JOB_TIER.md, "Running in the Lua Job tier". It covers:Agenttool is denied;--strict-mcp-configplus--mcp-config;GET /agents/self-serve/models;Review fixes (commit 8c14fee)
The independent review returned BLOCK. Every finding is addressed, and the review's probe sets are now tests.
DEPLOY_DENIED_UNCLASSIFIABLE).test/hooks/confirm-deploy.timeout.test.mjsspawns the real hook on adversarial inputs up to 100 KB, including the review's reproductions and inputs just under the cap, and asserts a decision in under 2 s. Observed times are about 0.2–0.4 s, including node startup.lua${IFS}deployis split at its expansions.env -S, awksystem()and pipes,osascript -e, editor-c '!…', sede,git -c alias '!…',tmux,docker exec … sh -c,bash < <(…)/source <(…),$(…)in unquoted heredocs, and heredoc-to-script-then-run.$(…)are treated as text, so Claude Code'sgh pr create --body "$(cat <<'EOF' … EOF)"passes.cp -r lua deploy,ls lua deployandecho lua deploy allpass.dist/THIRD_PARTY_NOTICES.txtis generated from the esbuild metafile (SDK and zod, both MIT, full texts), withlegalComments: 'eof'.dist/server.jsis byte-identical.Three review probes are not bypasses, and they are kept as asserted tests:
… # ; lua version promote 3is a comment;lua workflowandlua skillare not lua-cli commands.By design, a pipe or a launcher still voids the prefix.
Tests: 808 plugin tests pass, with hooks at 100% coverage and the tokenizer at 99.6%. 149 MCP tests pass. All lints pass.
Notes for WS3 / WS7
GET /agents/self-serve/models. Otherwise the headless auth note appears; it is harmless noise. Do not usedisableAllHooks, because that also removes confirm-deploy.lua-platformserver via--mcp-configneeds a harness change, becauseclaudeMcpConfigonly emits{type:'http'}servers today.maintriggersrelease-prod.yml(tagv1.6.0). That is the maintainers' call; no release was cut from this branch.Tests
--coverage. Hooks are at 100% on every file,lib/tokenizer.mjsis at 99.8% statements, andcheck-coveragepasses. All 17 lints pass.Rollback: revert this PR. No server-side state is involved.
🤖 Generated with Claude Code