Skip to content

fix(server): make JARVIS_PERMISSION_MODE load-bearing on every agent path (#491) - #516

Draft
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-affyd2
Draft

fix(server): make JARVIS_PERMISSION_MODE load-bearing on every agent path (#491)#516
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-affyd2

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Resolves #491. JARVIS_PERMISSION_MODE was parsed, printed in the startup banner, and reported over GET /v1/server/info — but never wired to an Approver. Every server agent entry point except the chat WebSocket called createAgent() with no approver, and @jarvis/core's documented contract for that case is "gated tools run unconditionally".

Net effect: with JARVIS_ENABLE_SHELL_EXEC=1 (or any fs.* write flag) set, POST /v1/chat/completions — and the SSE, conversation, workflow-runtime, and auto-mode paths — executed approval-gated shell.exec / fs.write / fs.edit / fs.patch with no approval gate at all under ask, while the banner and /v1/server/info both reported approval_mode: "ask". The entire /v1/permissions* rule engine (RuleApprover, MemoryPermissionStore) was dead in production for the same reason: nothing ever set AppState.permissionStore.

Fix

The composition root (packages/jarvis-app/src/state.ts) now:

  1. Seeds a MemoryPermissionStore from config.permissionMode and puts it on AppState.permissionStore.
  2. Wraps every createAgent approver in a RuleApprover around that store.

The fallback approver is:

  • the caller-supplied per-socket ChannelApprover on the WS path (an ask decision prompts the socket, exactly as before), and
  • AlwaysDeny on every non-interactive path — matching the established packages/jarvis-cli pipe-mode precedent (ask with no human ⇒ deny, which surfaces tool denied: … so the model adapts).

auto / bypass auto-allow on all paths; accept-edits auto-allows write tools. This makes the mode load-bearing on every path at once and gives /v1/permissions* a live store instead of a permanent 503.

Behavioral note

Autonomous paths (auto-mode, workflow-runtime, blocking/SSE chat) now require the operator to set JARVIS_PERMISSION_MODE=auto (or bypass) for gated tools to run without a human — the same explicit opt-in the CLI already enforces in pipe mode. Under the default ask, gated tools are denied (surfaced to the model), never silently executed.

Changes

  • packages/jarvis-app/src/state.ts — build + seed the MemoryPermissionStore, wrap createAgent's approver in a RuleApprover, expose permissionStore on AppState.
  • packages/server/src/permissions-routes.ts — add a synchronous MemoryPermissionStore.currentMode() so the RuleApprover's ModeHandle reads the live default mode (a PUT /v1/permissions/mode then takes effect on the next gated call); drop the structural-widening PermissionRoutesState seam now that the field is on AppState.
  • packages/server/src/state.ts — fold permissionStore into the AppState interface proper.
  • packages/jarvis-app/src/jarvis-app.test.ts — regression tests driving one gated shell.exec call through the real composition-root agent: denied under ask (and GET /v1/permissions now 200s), executed under auto.

Testing

  • pnpm -r typecheck — clean
  • pnpm lint — clean (0 errors)
  • pnpm -r test — all pass, including 2 new regression tests (jarvis-app 21, server 504)

🤖 Generated with Claude Code


Generated by Claude Code

…path (#491)

`JARVIS_PERMISSION_MODE` was parsed, printed in the banner, and reported over
`GET /v1/server/info`, but never wired to an Approver. Every server agent entry
point except the chat WebSocket called `createAgent()` with no approver, and
@jarvis/core's contract for that case is "gated tools run unconditionally" — so
with a write/exec flag set, `POST /v1/chat/completions` (and the SSE, conversa-
tion, workflow, and auto-mode paths) executed approval-gated `shell.exec` /
`fs.write` / `fs.edit` / `fs.patch` with no gate at all under `ask`. The whole
`/v1/permissions*` rule engine (`RuleApprover`, `MemoryPermissionStore`) was
also dead in production because nothing ever set `AppState.permissionStore`.

Fix: the composition root now seeds a `MemoryPermissionStore` from
`config.permissionMode`, puts it on `AppState.permissionStore`, and wraps EVERY
`createAgent` approver in a `RuleApprover` around it. The WS transport's
per-socket `ChannelApprover` becomes the "ask" fallback (prompt the socket);
every non-interactive path falls back to `AlwaysDeny`, matching the CLI
pipe-mode precedent (`ask` with no human ⇒ deny, surfacing "tool denied: …" so
the model adapts). `auto`/`bypass` auto-allow. This makes the mode load-bearing
on all paths at once and gives `/v1/permissions*` a live store instead of a
permanent 503.

- MemoryPermissionStore gains a synchronous `currentMode()` so the RuleApprover's
  ModeHandle reads the live default mode — a `PUT /v1/permissions/mode` takes
  effect on the next gated tool call with no extra plumbing.
- `permissionStore` folded into the AppState interface proper; the routes drop
  their structural-widening seam.
- Regression tests drive one gated `shell.exec` call through the real
  composition-root agent: denied under `ask`, executed under `auto`, and
  `GET /v1/permissions` now 200s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCPr9wBwnpQX79ysFh3uX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants