Surfaced by the S2 route-matrix audit (PR #1014). Both the v1 and v2 shell HTTP routes reach SessionPrompt.shell (raw spawn, src/session/prompt.ts:2580) with only a session-existence check — no permission/Ruleset gate of any kind:
- v2 HttpApi:
src/server/routes/instance/httpapi/handlers/session.ts:367-388 — gated only by requireSession(ctx.params.sessionID).
- v1 Hono:
POST /session/:sessionID/shell, src/server/routes/session.ts:1004-1034 — handler calls SessionPrompt.shell directly, no permission check.
This is outside HardPolicy's model-invoked-tool boundary (it's a direct HTTP raw-exec route, not a model tool call), so it's out of the de-fork spike's scope — but it's a real gap: anyone who can hit the session's shell endpoint gets unrestricted process execution. Worth a dedicated permission gate on both routes.
Pre-existing (not introduced by the de-fork work); filing so it's tracked rather than lost in a PR doc.
Surfaced by the S2 route-matrix audit (PR #1014). Both the v1 and v2
shellHTTP routes reachSessionPrompt.shell(rawspawn,src/session/prompt.ts:2580) with only a session-existence check — no permission/Ruleset gate of any kind:src/server/routes/instance/httpapi/handlers/session.ts:367-388— gated only byrequireSession(ctx.params.sessionID).POST /session/:sessionID/shell,src/server/routes/session.ts:1004-1034— handler callsSessionPrompt.shelldirectly, no permission check.This is outside HardPolicy's model-invoked-tool boundary (it's a direct HTTP raw-exec route, not a model tool call), so it's out of the de-fork spike's scope — but it's a real gap: anyone who can hit the session's shell endpoint gets unrestricted process execution. Worth a dedicated permission gate on both routes.
Pre-existing (not introduced by the de-fork work); filing so it's tracked rather than lost in a PR doc.