Skip to content

fix(core): ShellParse ARITY table reads through the prototype chain — a "constructor" command head yields an empty permission prefix and a " *" saved pattern #792

Closed

Description

@dezren39

Summary

ShellParse.prefix() looks the command head up in the ARITY table with a plain index, so a shell command whose head is an Object.prototype member name — constructor, __proto__ — is treated as a known multi-word command and produces an empty permission prefix. The saved "always allow" pattern for that command becomes " *" instead of "<command> *": the grant the user approves can never match a future command, and the permission prompt's saved pattern is meaningless.

Evidence (verified this run, main fd36da324a)

  • packages/core/src/shell/parse.ts:23-161 — ARITY: Record<string, number> is a plain object literal.
  • packages/core/src/shell/parse.ts:375-382 — prefix(): const arity = ARITY[tokens.slice(0, length).join(" ")]; if (arity !== undefined) return tokens.slice(0, arity). For constructor, ARITY["constructor"] is the inherited Object function (not undefined), and tokens.slice(0, <function>) coerces the function to NaN, i.e. slice(0, 0) → []. For __proto__ the inherited value is Object.prototype, same result.
  • Both scan paths consume it identically: legacy parse.ts:198 (save: ${prefix(tokens).join(" ")} *) and portable `parse.ts:267-271` (`selected = prefix(...)`, `conventional = `${selected.join(" ")} *, and wordEnds[selected.length - 1] is wordEnds[-1]).
  • packages/core/src/tool/plugin/shell.ts:150-166 — the scan's save patterns go into permission.assert({ save }); on an "always" reply packages/core/src/permission.ts:299-306 persists them as saved allow rules, later matched with Wildcard.match (packages/core/src/util/wildcard.ts) — the pattern " *" compiles to ^( .*)?$, which no real command resource (no leading space) can match.

Laptop probe (verbatim ARITY table, lines 23-161, plus prefix() spliced unchanged from shell/parse.ts, md5 9c8057aa2… byte-identical on both checkouts; probe at laptop ~/git/fleet/probe-2026-10-28-creator3/arity-table.ts):

  • ["constructor","foo.txt"] → prefix [], save " *"
  • ["__proto__","x"] → prefix [], save " *"
  • Controls: ["git","status"] → "git status *"; ["ls","-la"] → "ls *"; ["cargo","run","--release"] → "cargo run --release *"; unknown head ["unknowncmd","arg"] → "unknowncmd *" (the correct fall-through the prototype names bypass)

Why it matters

This is the permission-analysis surface: the save pattern is what the user sees and approves as the persistent grant for a shell tool call. A command head colliding with a prototype name silently degrades that grant to a pattern that matches nothing, so "always" approvals for such a command never take effect (the user is re-prompted forever) — and the corrupted empty prefix also feeds the portable path's wordEnds[-1] boundary logic. Same prototype-chain class as #747/#774/#791, in a table none of those covered. Any executable, script, or package binary named constructor (e.g. invoked via a project-local bin) hits it.

Acceptance criteria

  • prefix(["constructor", …]) and prefix(["__proto__", …]) fall through to the unknown-head behaviour: [firstToken], save "<head> *".
  • Known commands keep their exact arities (git status, cargo run, single-word heads, multi-word keys like "bun run").
  • The ARITY lookup is own-property only (Object.hasOwn, Map, or null-prototype table); no other change to scan output for ordinary commands.
  • Tests pin prototype-named heads in the shell parse/scan tests, for the saved pattern in at least one scan path.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/corePackage: core (pkgs/ or apps/ in this repo)
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Filed this run after verification against main fd36da324a (laptop probe with the verbatim ARITY table + prefix(); see issue body).

    Found via: filed this run

  4. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    🤖 Dispatcher hand-back — executor-saturated · openchamber-dispatcher run 2026-10-28 20:15 CDT

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    Picked up by issue sweep (rotating) this run; will fix on the laptop in a fleet worktree, branch sweep/792-shell-arity-prototype, PR to follow. Queue task 2937 is being cancelled in the same run per the lifecycle protocol (self-assigned pickup).

    Agent: issue sweep (issue-sweep-rotating)
    Session/ID: issue-sweep-rotating run 2026-10-28 20:24 CDT
    Found via: sweep pickup
    Task: own-property ARITY lookup in ShellParse.prefix + prototype-name tests
    Expectation: effort/xs — done = PR opened with prefix(["constructor",…]) / prefix(["proto",…]) falling through to unknown-head behaviour, known arities unchanged

  6. added
    work/self-assignedAn agent grabbed this item directly, outside the queue
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  7. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    PR #799 opened: #799 (branch sweep/792-shell-arity-prototype @ 8a96fe7, laptop worktree ~/git/fleet/sweep-792-shell-arity).

    Fix: prefix() in packages/core/src/shell/parse.ts now does an own-property ARITY lookup (Object.hasOwn), so constructor/__proto__ heads fall through to the unknown-head [firstToken] behaviour. Tests pin prototype-named heads in the legacy scan, the portable scan, and scanPortable, plus a git status arity control.

    Laptop verification: shell-parse 22/22 pass; shell-parse-parity + shell-parse-compatibility 94/94 pass.

    Parked on PR #799 CI — resume: merge when green except the known standings (unit #565 pair; e2e app-2 #631). Push note: the first push's SSH session hung after the ref landed; the branch was independently verified on the remote at 8a96fe7 before the PR was created.

    Agent: issue sweep (issue-sweep-rotating)
    Session/ID: issue-sweep-rotating run 2026-10-28 20:24 CDT
    Found via: sweep pickup
    Task: own-property ARITY lookup in ShellParse.prefix + prototype-name tests
    Expectation: parked on CI; done = PR #799 merged and #792 closed

  8. added
    status/parkedDeliberately paused with a recorded resume condition
    and removed on Oct 29, 2026
  9. dezren39 commented on Oct 29, 2026

    @dezren39
    Author

    agent-note:v1
    State: resumed / merging
    Agent: issue sweep (issue-sweep-rotating)
    Session/ID: issue-sweep-rotating run 2026-10-28 21:24 CDT
    Found via: sweep pickup (own parked PR from previous sweep run)
    Task: verify PR #799 CI and merge if failures are only the known standings
    Expectation: done — unit (linux+windows) log-verified as exactly the 2 known #565 permission-focus tests; typecheck/build/nix-eval/affected/standards/compliance all SUCCESS; e2e still QUEUED (fleet runner congestion, any app-2 failure is tracked in #631). Merging PR #799 (squash) per the park resume brief and Drew's standing merge directive.

  10. removed
    work/self-assignedAn agent grabbed this item directly, outside the queue
    status/parkedDeliberately paused with a recorded resume condition
    on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/corePackage: core (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions