feat(harness): wire the permission engine + make Plan Mode enforced - #524
Open
TYRMars wants to merge 29 commits into
Open
feat(harness): wire the permission engine + make Plan Mode enforced#524TYRMars wants to merge 29 commits into
TYRMars wants to merge 29 commits into
Conversation
…odegen (P7.9)
Sever the web SPA's last build-time dependency on Rust — the load-bearing
prerequisite for decommissioning the Rust server. Wire-shape types crossing
the SPA boundary were generated by `ts_rs` into apps/jarvis-web/src/types/
generated/; that codegen is removed and the types now live Node-side.
- New @jarvis/shared-types: a pure type-only leaf (no runtime, no deps, single
file with no relative imports) hosting the channel + workflow wire types the
SPA actually imports. Consumable by both NodeNext (packages) and Bundler
(standalone web) resolvers.
- @jarvis/workflow and @jarvis/channel re-export the types from shared-types
and keep their runtime constructors/validators. ChannelInstance.config is now
Record<string, unknown> (the accurate "config is a JSON object" wire shape),
collapsing the server-side JsonValue bridging.
- Web SPA: tsconfig paths + vite alias to packages/shared-types/src; services/
{workflows,channels}.ts repointed; all 29 generated/*.ts deleted. Fixed three
call sites that set optional step fields to null (the real wire omits them).
- Rust: stripped #[derive(TS)] + #[ts(export/type)] + use ts_rs from
harness-channel/harness-project/harness-workflow, the ts-rs workspace dep, the
Makefile ts-codegen target, and updated CLAUDE.md + the convention docs.
Verified: cargo check + clippy -D warnings + cargo test (workspace) green;
pnpm -r typecheck + test green (24 packages, 0 fail); web tsc -b && vite build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion)
Close a Rust-decommission blocker from the P8.1 contract audit: the web
Settings → Tools section (services/tools.ts) calls GET /v1/tools and
PATCH /v1/tools/:name, which the Node server didn't serve. End-to-end real
feature — the registry's mute set is shared with createAgent, so muting a tool
hides it from the next turn's LLM catalogue.
- @jarvis/core: new tool-metadata.ts porting harness-core/src/tool_metadata.rs —
ToolSource/ToolPackCategory/ToolRisk/ToolMetadata (camelCase fields,
kebab-case enum values), derivePack + deriveRisk name-convention heuristics,
toolMetadataFromTool + toolMetadataToWire (omits defaults like the Rust serde
skips). ToolRegistry gains allNames/contains/resolveUnchecked/isMuted/
mutedCount (the catalog needs muted tools; `contains` is the 404 check that
`has` — which excludes muted — can't be).
- @jarvis/server: tools-routes.ts (GET catalog incl. muted, subagent.* source
reclassify, source/pack/risk facet filters, sort by name; PATCH mute toggle →
{name,enabled,muted_count}, 404 unknown, 400 non-boolean). AppState gains the
shared `tools` registry handle; registered in server.ts.
- jarvis-app wires state.tools = toolBundle.registry (same instance createAgent
builds from).
Verified: pnpm -r typecheck green; @jarvis/core 30 tests, @jarvis/server 466
tests (+5), eslint clean. Matches the web services/tools.ts wire contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Durable, prioritized P8.1 audit so the Rust-decommission can proceed across sessions: docs/proposals/rust-decommission-p8-gaps.md (70 gaps deduped into blockers / operator-only, each with the consuming client, port-size, and order). Update the tasklist P8.1 line to reference it and record what's landed (shared-types/ts_rs removal, GET/PATCH /v1/tools, full audit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (P8.1)
Close the top Rust-decommission blocker — the one that breaks TWO clients (web
sidebar + iOS) with a silent wrong shape, not a 404. Node returned the bare
ConversationStore.list(); the web ConvoListRow + iOS ConversationSummary expect
enriched rows.
GET /v1/conversations now derives, per row:
- title — first user message's first line (skips the auto-mode seed prompt,
capped at 60 chars + ellipsis), mirroring first_user_title.
- source + requirement_id/title/status — joined from the RequirementStore (the
most-recently-updated requirement whose conversation_ids lists the row).
- workspace_path — WorkspaceStore.lookup.
- lifecycle — carried through; plus ?lifecycle= and ?project_id= (slug/uuid)
facet filters (unknown lifecycle → 400, unknown project → 404).
Also adds POST /v1/conversations/:id/lifecycle (returns {id,lifecycle,
previous_lifecycle}; abandoning best-effort-cancels the conversation's in-flight
requirement runs) and GET /v1/conversations/:id/work-context (primary
requirement + latest run + recent activities, degrades to nulls when unbound).
The bare-array contract + __-internal-id filtering are preserved.
Verified: @jarvis/server typecheck + 471 tests (+5), all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(P8.1)
Port the live git-state probes the web WorkspaceBadge + folder picker poll
(Rust workspace_snapshot). Reuse the existing workspace-routes git helpers
(resolveWorkspace / runGitOk / filterBranch). Returns {root, vcs, branch?,
head?, dirty?}; non-git roots report vcs:"none". GET /v1/workspace uses the
pinned root (503 when unset); /probe?path= inspects a candidate folder
(400 without a path).
Verified: @jarvis/server typecheck + tests (workspace 23, +3; git-repo test
skips cleanly without git on PATH).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…8.1) Port the in-process chat-run registry the web polls for the turn-status badge, reconnect event replay, and the Stop button (Rust chat_runs.rs, MVP subset). - ChatRunRegistry (packages/server/src/chat-runs.ts): per persisted-conversation run record (status / started_at / latest_seq / current_tool / last_error) + a count-capped event ring buffer + a per-turn AbortController. Status is derived from the AgentEvent stream (tool_start→current_tool, approval_request→ waiting_approval, …); terminal status is sticky; terminal runs are retained briefly then evicted. - Routes: GET /v1/chat/runs (bare array — the web decodes `as ServerChatRun[]`; ?active drops terminal), GET /v1/chat/runs/:id/events?after= (bare array), POST /v1/chat/runs/:id/interrupt (404 when no active run). - WS turn loop now starts a run, buffers every event, finishes completed/failed, and races the agent stream against the abort signal so an interrupt stops emission promptly and marks the run cancelled. AppState.chatRuns wired from jarvis-app. Interrupt is cooperative (documented): it stops emission + marks cancelled but does not hard-cancel the in-flight LLM call (no tokio::abort analogue) — matches the audit's noted limitation. MVP also omits the live broadcast channel + byte budget (poll-only, count cap). Verified: @jarvis/server typecheck + 480 tests (+6); pnpm -r typecheck green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the dynamic MCP manager + REST surface the web Settings → McpSection drives
(Rust harness-mcp McpManager + mcp_routes.rs). The largest greenfield gap from
the contract audit — Node had no /v1/mcp surface at all.
- @jarvis/core: ToolRegistry.unregister / unregisterPrefix (the manager removes
a server's whole `<prefix>.*` tool set on remove/reload).
- @jarvis/server: McpManager (packages/server/src/mcp-manager.ts) tracks servers
by prefix over the SHARED ToolRegistry — add() connects + collision-checks +
registers tools, remove() unregisters + shuts the client, health() probes,
replace()/reload() restart (reload restores a Stopped slot on failure).
Mutations serialize through a promise chain; a `connect` seam keeps it
testable without spawning child processes. Wire config is snake_case
(allow_tools/…) <-> internal camelCase via wireToConfig/configToWire.
- Routes (mcp-routes.ts): GET/POST /v1/mcp/servers, GET/PUT/DELETE
/v1/mcp/servers/:prefix, POST /:prefix/health (always 200, {ok} in body),
POST /:prefix/reload — error codes 409 conflict / 400 bad / 404 missing.
- jarvis-app now boots the configured JARVIS_MCP_SERVERS THROUGH the manager
(replacing the direct connectAllMcp) so startup servers are listable +
reloadable; AppState.mcpManager wired. @jarvis/server gains an @jarvis/mcp dep.
Verified: pnpm -r typecheck green; @jarvis/server 489 tests (+9 mcp), eslint
clean. Matches the web services/mcp.ts wire contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the gap inventory + tasklist: GET /v1/conversations enrichment (+work- context/lifecycle), chat-runs, workspace git reads, and the MCP manager are done. Re-order the remaining work (server/info, providers probe, routing, workspace commit/PR, memory-sync) and note P8.2 cutover as the next phase gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ame/probe (P8.1)
Three operator/meta routes the web Settings·ServerSection / ProvidersSection +
SDK depend on (Rust routes.rs get_server_info/probe_provider, openapi.rs version).
- AppState gains a config-derived `serverInfo` snapshot (no secrets) + the
configured `provider`. jarvis-app builds the snapshot from JarvisConfig.
- GET /v1/server/info merges the static snapshot with LIVE tools (registry),
mcp prefixes (manager), and the provider catalog so runtime mute/mcp changes
show on refresh. GET /v1/version → {name:"jarvis", version}.
- POST /v1/providers/:name/probe pings the provider with a 1-token completion
(auth_ok/default_model_ok), optionally a tool-spec request (supports_tool_calls);
always 200 with the ProbeResult in-body, matching the web contract.
Verified: @jarvis/server 493 tests (+4), jarvis-app 19, typecheck + eslint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the operator model route-policy CRUD the web Settings·RoutingSection drives (Rust route_policy.rs + routes.rs). NOT hollow: the `summarization` slot is wired into SummarizingMemory, mirroring the Rust LlmRouteResolver. - @jarvis/server: RoutePolicy/ModelTarget types + RoutePolicyStore (mutable in-process holder, snapshot omits empty slots, parseModelTarget for "provider/model"); routing-routes.ts (GET/PUT /v1/routing, PATCH/DELETE /v1/routing/:slot with slot + target validation → 400). AppState.routePolicy. - @jarvis/memory: SummarizingMemory.withModelResolver() — an optional per-call model override; compact() uses it over the constructor model. - jarvis-app: parse JARVIS_ROUTE_* into config.routeSlots, seed one shared RoutePolicyStore at boot, hand it to both the routes (AppState) and the summariser resolver, so a /v1/routing PATCH takes effect on the next summary. Verified: routing 4 tests; @jarvis/server 497 (+4), memory 41, jarvis-app 19; typecheck + eslint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the workspace mutation cluster the web Commit/CreatePr dialogs use (Rust
workspace_diff.rs). Reuses the existing workspace-routes git helpers (resolveWorkspace
/ runGit-with-timeout / safeBranch / filterBranch).
- POST /v1/workspace/commit — stage-all (git add -A), guard "nothing staged",
git commit -m <message via argv>, optional `git push -u origin <branch>`
(60s net timeout; detached-HEAD + push failure surface as push_error, the
commit still succeeds). → {ok, head, pushed, push_error}.
- GET /v1/workspace/pr/preview — suggested title (top commit subject, else a
humanised branch name) + body (commit bullets) + gh availability probe. →
{branch, base, gh_available, suggested_title, suggested_body}.
- POST /v1/workspace/pr — gh-check (400 + install hint when absent), push the
branch, `gh pr create --base --title --body [--draft]` (always --body to dodge
$EDITOR). → {ok, url, draft}.
Verified: @jarvis/server 500 tests (+3; git-repo cases skip without git on PATH),
typecheck + eslint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the RequirementDetail checklist + link routes (Rust requirements_routes.rs).
TODOs are embedded in the Requirement row, so all CRUD is a read-modify-write +
Activity record (no separate store).
- GET /v1/requirements/:id/todos → {requirement_id, items}
- POST /v1/requirements/:id/todos → create (title/kind/status/command/created_by
validated via *FromWire) → 201 {todo, requirement}
- PATCH /v1/requirements/:id/todos → batch status update by ids in ONE
read-modify-write (avoids the lost-update race of N per-item PATCHes)
- PATCH /v1/requirements/:id/todos/:todo_id → update one item
- DELETE /v1/requirements/:id/todos/:todo_id → remove (idempotent)
- POST /v1/requirements/:id/conversations → idempotent conversation link
Verified: @jarvis/server 501 tests (+1), typecheck + eslint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on (P8.1) Port the requirement-run verification routes (Rust requirements_routes.rs + verification.rs). The web RequirementDetail "Verify" button uses /verify. - New packages/server/src/verification.ts: executePlan(workspace, plan, timeout) runs each command via `sh -c` / `cmd /C` in the run's workspace, captures exit_code + truncated stdout/stderr (16 KiB), aggregates Failed if any command exits non-zero or times out, else NeedsReview when require_human_review, else Passed. - POST /v1/runs/:id/verify executes the plan (resolves workspace: worktree → bound workspace → root; 30s default per-command timeout), POST /v1/runs/:id/verification attaches an externally-computed result. Both share applyVerification (terminal flip passed→completed / failed→failed, upsert, Verified + Finished broadcasts, verification_finished + run_finished Activity). Verified: @jarvis/server 503 tests (+2; verify-exec skips on Windows), typecheck + eslint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The git/iCloud memory-sync + include-directive subsystem (Rust memory_sync_routes.rs) is a substantial port deferred for now. Register the 8 routes (sync_status/sync/sync_setup/sync_setup_icloud + includes GET/POST/DELETE/ refresh) returning 503 "not configured" instead of 404 — the web MemorySyncSection + MemoryIncludesPanel are 503-aware and degrade to a "sync not configured" state, so the Rust-decommission cutover stays non-breaking. Porting the real subsystem is tracked in docs/proposals/rust-decommission-p8-gaps.md. Verified: @jarvis/server 504 tests (+1), typecheck + eslint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…time
Node reached blocker-level /v1 parity with the Rust server (504 server tests
green; conversations enrichment, tools, chat-runs, mcp, routing, server-info,
providers-probe, workspace reads + commit/PR, requirement todos + verify all
ported; memory-sync graceful-stubbed). With the cutover gate green
(pnpm -r typecheck + test, web build), retire Rust.
Removed (history preserved; tag `rust-archive-pre-takedown`):
- crates/harness-* (20 crates), apps/jarvis, apps/jarvis-cli, apps/jarvis-desktop
(Tauri), Cargo.toml, Cargo.lock.
- Rust CI: .github/workflows/{rust,release,desktop-release}.yml (node*/ios kept).
Repointed infra to Node:
- Makefile: cargo → pnpm (typecheck/lint/test/dev) over the workspace.
- Dockerfile: multi-stage Node (web bundle + pnpm deps + `node
--experimental-strip-types packages/jarvis-app/src/main.ts serve`). NOTE:
needs a real `docker build` verification — not covered by the unit gate.
- docker-compose: backend chosen from JARVIS_DB_URL scheme (no cargo features).
- CLAUDE.md: decommission banner + Commands rewritten to Node; the architecture
sections remain a valid map via crate ↔ @jarvis/* (full prose rewrite = P8.5).
Remaining P8: 8.3 perf/load compare, 8.4 OTel verify, 8.5 docs rewrite,
8.6 security baseline; plus the full memory-sync subsystem (gaps doc).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the last P8 decommission gap: the `/v1/memory/sync*` +
`/v1/memory/includes*` routes go from graceful 503 stubs to functional,
backed by ported `memory.{sync,sync_setup,sync_setup_icloud,sync_status}`
tool impls (from crates/harness-tools/src/memory_sync.rs).
- @jarvis/tools: 4 sync tools + MemorySyncBackend + iCloud root helpers,
reusing the existing protocol-guarded runGit + validateGitUrl; remote/
branch arg validation rejects flag-smuggling.
- @jarvis/server: AppState.memoryRuntime ({workspaceRoot,userRoot?,backend});
memory-sync-routes.ts invokes the tool impls directly per request
(mirrors the Rust REST contract: backend-mismatch 503s, 400 on tool err).
- composition root: JARVIS_ENABLE_MEMORY / JARVIS_MEMORY_USER_ROOT /
JARVIS_MEMORY_SYNC_BACKEND parsed in config; agent-side memory.* tools
registered when enabled, sync set adapts to backend.
- tests: git roundtrip (setup→status→sync→force) over a local bare remote,
backend parsing, arg guards, + route-level 503/400/envelope coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarially-verified 6-dimension audit (27 confirmed / 27 dismissed). Fixes the clear high/critical issues; accepts the unauthenticated /v1 surface as designed (local-first) and documents deferred hardening. - OAuth: auth.json written 0o600; refresh errors no longer echo the response body (token-leak) — packages/llm/src/codex-auth.ts. - http.fetch SSRF guard: scheme allowlist + loopback/private/link-local/ metadata block, default-on, JARVIS_HTTP_ALLOW_PRIVATE opt-out; strips set-cookie/www-authenticate response headers — packages/tools/src/http.ts. - code.grep: 1000-char regex cap (ReDoS blast-radius); sandbox: 256 path-depth cap (syscall DoS). - WeCom inbound: opt-in replay_window_secs timestamp validation. - docs/security/p8-security-baseline.md records fixed/accepted/deferred. - chore: drop a now-unused eslint-disable in meta-routes.test.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jarvis/core emits jarvis.agent.run + gen_ai.tool.call spans via @opentelemetry/api — a no-op until a TracerProvider is registered, so core stays a leaf and the env-free rule holds. The SDK/exporter bootstrap lives in the jarvis-app composition root, off unless enabled. - core/src/tracing.ts: startAgentSpan / endAgentSpan / withToolSpan; run() nests tool spans via active context, runStream() threads the agent span in explicitly (ambient context isn't kept across yields). Tool spans wrap the raw tool.invoke so thrown errors are recorded. - jarvis-app/src/otel.ts: NodeSDK + OTLP/HTTP (or ConsoleSpanExporter for JARVIS_OTEL_CONSOLE) bootstrap; gated on JARVIS_OTEL_ENABLED / JARVIS_OTEL_CONSOLE / OTEL_EXPORTER_OTLP_ENDPOINT. Wired into runServe with SIGTERM/SIGINT flush. - verify: in-memory-exporter test asserts both span names + attributes (model, iterations, tool name/id) from a real Agent.run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rust is gone, so there's no cross-runtime comparison; establish the Node harness's own overhead as a reference. scripts/perf-baseline.ts (run via `make perf`) measures streaming throughput + TTFT, blocking-loop turn latency, and memory with a deterministic in-process stub provider. Reference (node 22, arm64): ~4.3M chunks/sec relay, sub-µs turn latency, ~80MB RSS — harness overhead is negligible vs provider/network latency. Documented in docs/observability/perf-baseline.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lose-out - CLAUDE.md: banner → "P8 complete"; document JARVIS_ENABLE_MEMORY / MEMORY_USER_ROOT / MEMORY_SYNC_BACKEND, JARVIS_HTTP_ALLOW_PRIVATE, and the OTel knobs; expand the /v1/memory/* REST surface; drop dead RUST_LOG. - rust-decommission-p8-gaps.md: backfill the blocker table ☐→✅ (all gaps closed, no stubs). - nodejs-rewrite-tasklist.zh-CN.md: mark 8.1/8.3/8.4/8.5/8.6 done. - .env.example: add memory/OTel/SSRF knobs; remove the dead RUST_LOG block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Asserts AppState.memoryRuntime is absent by default and populated (workspaceRoot/userRoot/backend) when JARVIS_ENABLE_MEMORY + JARVIS_MEMORY_SYNC_BACKEND are set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Node-native Replace the Rust-era framing across the top-level docs with what the repo actually is — a Node/TypeScript pnpm workspace (no cargo/crates/Cargo.toml/ RUST_LOG; JSON-file persistence by default, not SQLite). Commands switch to make/pnpm/`node --experimental-strip-types`; the architecture tree maps to packages/* (@jarvis/*) + apps/*; env tables list only vars that exist in config.ts. Adds the P8 surfaces (git/iCloud memory sync, OTel, http.fetch SSRF guard) + links to the security + perf baselines. README.zh-CN is a faithful translation of the new English. docker-compose: fix the stale "--features postgres" comment (backend is URL-scheme-selected at runtime). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up Rust-decommission cleanup:
- rm -rf target/ (134 GB of dead Rust build output; gitignored, not committed).
- Delete the broken cargo-era scripts superseded by the Makefile: start.sh,
scripts/{build,dev,install}.sh (kept fix-node-pty.mjs + perf-baseline.ts).
- AGENTS.md (auto-loaded agent context): Node banner + Node-accurate overview/
layout/commands/conventions; replace the stale 170-line Rust architecture
duplicate with a pointer to CLAUDE.md (the authoritative version); drop dead
RUST_LOG / --mcp-serve flag form.
- Fix broken `cargo run -p jarvis` run instructions → the node entrypoint in
DB.md, apps/jarvis-ios/{README.md,bootstrap.command}, apps/jarvis-web/README.md.
- DB.md "adding a backend" → @jarvis/store TS paths; App.tsx comment → ui.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Borrows two loop-closing capabilities from opencode (see docs/proposals/opencode-borrow.zh-CN.md):
P0.1 HITL — wire the pause→interact loop the web UI already shipped. New
@jarvis/core HITL channel (requestHuman/withHitl + HumanLayer/ChannelHuman,
symmetric with the approval gate); agent loop emits hitl_request/hitl_response
between tool_start/tool_end; ask.text drops its deferred stub; chat WS builds a
per-socket ChannelHuman + handles the flat hitl_response frame and echoes it.
P0.2 LSP diagnostics — close the edit→verify loop. New dependency-free
@jarvis/lsp package (push-diagnostics only: spawn + hand-rolled Content-Length
JSON-RPC + settle window; PATH-probed, no auto-download). fs.{write,edit,patch}
append an errors-only <diagnostics> block via an injected best-effort hook;
gated by JARVIS_ENABLE_LSP. Composition root threads the OS PATH so the library
stays env-free.
Tests: core 32, lsp 8, tools 282, server 510, jarvis-app 25 — all green;
typecheck + eslint clean. End-to-end HITL frame round-trip + mock-LSP-server
diagnostics proven deterministically.
Also finalizes pre-existing feat/p8-finish WIP already in the working tree
(MCP/Composio config refactor in @jarvis/mcp + jarvis-app, mcp-manager, docs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wn (P0.4) P0.3 reactive context compaction: - resolveMemoryBudget() derives a default compaction budget from the model's catalog contextWindow (budget = ctx - clamp(0.2*ctx, 4k, 20k)) when JARVIS_MEMORY_TOKENS is unset, instead of installing no memory at all; unknown models keep the historical no-memory behaviour. buildMemory + serverInfo.memory.budget_tokens use it. - SummarizingMemory's DEFAULT_SUMMARY_PROMPT swapped from a free paragraph to opencode's structured anchor template (Goal/Constraints/Progress/Decisions/ Next Steps/Critical Context/Relevant Files) for far better recall; summary cap 400->800. (Prior-summary update-merge deferred: the shared single-slot cache can't safely source a per-conversation prior summary.) P0.4 native web read: - http.fetch gains format:"markdown" -> converts HTML responses to clean Markdown via node-html-markdown (Node-native, no jsdom, lazy-imported); non-HTML (JSON/XML/text) passes through (looksLikeHtml gate). SSRF guard + sensitive-header stripping preserved. - web.search: reuse the existing @jarvis/mcp bridge (point JARVIS_MCP_SERVERS at a search MCP server) rather than building a redundant native tool; documented in CLAUDE.md + the proposal. Tests: tools 287, memory 42, jarvis-app 27 — all green; typecheck + eslint clean. (Repo-wide: the only failures are @jarvis/store sqlite tests, an environmental better-sqlite3 ABI mismatch from a Node 22/23 split in this machine's pnpm vs test runtime — unrelated to these changes, not committed, CI-unaffected.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # CLAUDE.md # Makefile # docs/proposals/nodejs-rewrite-tasklist.zh-CN.md # docs/proposals/rust-decommission-p8-gaps.md # packages/jarvis-app/src/config.ts # packages/jarvis-app/src/state.ts # packages/jarvis-app/src/tools.ts # packages/server/src/chat-routes.ts # packages/server/src/mcp-manager.test.ts # packages/server/src/mcp-manager.ts # packages/server/src/memory-sync-routes.test.ts # packages/server/src/memory-sync-routes.ts # packages/server/src/meta-routes.test.ts # packages/server/src/state.ts
Land accumulated opencode-borrow-p1 web SPA work plus this round of macOS desktop-shell (.jarvis-desktop-shell) fixes: - Glass invariant: glass sidebar + fully-opaque content on every route (auto-mode/worktrees panes added to the opaque lists, opaque dark --desktop-main-bg, opaque workspace rail). - Sidebar restyled to the Codex reference: 16px text, airier rows; fix the #new-convo shortcut wrap; theme-aware nav-shortcut pills. - Composer/welcome polish: flush-left toolbar, readable placeholder, legible "connected" cards; removed the three welcome connect cards and re-centered the heading. - Removed the wallpaper strip above the chat pane. - Full-screen-aware traffic-light inset: Electron main toggles a jarvis-fullscreen class so the sidebar top bar drops the reserved padding when macOS hides the traffic lights. - Ignore local output/ test/debug artifacts. Verified green: web eslint (0 errors) + 386 vitest tests, packages typecheck, @jarvis/desktop 16 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The permission/Plan-Mode layer was built on both ends but disconnected in
the middle — the same "ported but never wired" shape as the earlier HITL
work:
* `RuleApprover` / `MemoryPermissionStore` / `modeDefault` / the glob
matcher were complete and unit-tested in `permissions-routes.ts` and
exported from `@jarvis/server`, but constructed nowhere. So
`JARVIS_PERMISSION_MODE` was parsed, logged and reported in
`/v1/server/info` while no approver read it: `auto`, `bypass` and
`plan` all behaved exactly like `ask`, and `/v1/permissions*` always
503'd.
* `ToolRegistry.specsFiltered` was dead code and `Tool.isTerminal` was
metadata the agent loop never read, so `exit_plan` did not end a turn.
* The web SPA already shipped ModeBadge / PlanModeBanner /
PlanProposedCard / DecisionSourceChip and sent `set_mode`,
`accept_plan` and `refine_plan` — the server answered
`unknown frame type`.
@jarvis/core
New `mode.ts`: the `AgentMode` vocabulary plus an AsyncLocalStorage
mode-signal channel shaped like `plan.ts` / `hitl.ts`.
`AgentConfig.toolFilter` is applied twice — once to build the request
catalogue via `specsFiltered`, and again at dispatch in `runOne`. The
second application is what makes Plan Mode a guard rather than a hint:
a write tool named from conversation history or hallucination is
refused instead of run. `isTerminal` now ends the turn on both the
blocking and streaming paths, skipping the rest of the batch; a denied
terminal call does not terminate, since it never ran.
@jarvis/server
Each chat socket builds a `SocketModeHandle` seeded from the store's
persisted `default_mode` and a `RuleApprover` wrapping its
`ChannelApprover`. Handles `set_mode` / `accept_plan` / `refine_plan`;
emits `permission_mode` / `plan_proposed` and attaches `HitSource` to
`approval_decision`. Turn frames go to the client and the reconnect
replay buffer in the same shape. `PermissionMode` becomes an alias of
core's `AgentMode` so the two cannot drift.
packages/jarvis-app
Constructs `MemoryPermissionStore(config.permissionMode)` and puts it
on `AppState`, so the env var gates real dispatch.
Two judgement calls: auto-allowed calls still emit an `approval_request`
card, because suppressing it would make them invisible in the audit
trail — provenance rides on the decision frame instead. And
`mode_changed` is not forwarded raw; the WS applies it and republishes
as `permission_mode{via:"tool"}`, which is what the client listens for.
Also fixes two bugs found on the way:
* `@jarvis/core` was the only one of 25 packages with a hand-listed test
script instead of the `src/*.test.ts` glob, so `hitl.test.ts` from the
P0 work had never run in CI. Core goes 32 -> 50 tests.
* The WS test harness attached its `message` listener after awaiting
`open`, silently dropping any frame sent on connect. Pre-existing;
exposed by the new `permission_mode` frame.
Closes P1 item 5 of docs/proposals/opencode-borrow.zh-CN.md.
`make check` clean: typecheck + eslint + 2161 tests.
Co-Authored-By: Claude Opus 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.
Why
Jarvis's permission and Plan-Mode layer was built on both ends but disconnected in the middle — the same "ported but never wired" shape as the earlier HITL work:
RuleApprover/MemoryPermissionStore/modeDefault/ the glob matcher were complete and unit-tested inpackages/server/src/permissions-routes.tsand exported from@jarvis/server— but constructed nowhere. SoJARVIS_PERMISSION_MODEwas parsed, logged at startup, and reported in/v1/server/infowhile no approver ever read it:auto,bypassandplanall behaved exactly likeask, and/v1/permissions*always 503'd.ToolRegistry.specsFilteredwas dead code andTool.isTerminalwas metadata the agent loop never read, soexit_plandid not end a turn.ModeBadge,PlanModeBanner,PlanProposedCard,BypassBannerandDecisionSourceChip, and sentset_mode/accept_plan/refine_plan— the server answeredunknown frame type.Net effect before this change: an operator who set
JARVIS_PERMISSION_MODE=plangot no protection at all, and one who setautogot no convenience.Closes P1 item 5 of
docs/proposals/opencode-borrow.zh-CN.md.What changed
@jarvis/coremode.ts— theAgentModevocabulary plus anAsyncLocalStoragemode-signal channel shaped like the existingplan.ts/hitl.ts.AgentConfig.toolFilteris applied twice: once to build the request catalogue (specsFiltered), and again at dispatch inrunOne. The second application is the load-bearing one — it's what makes Plan Mode a guard rather than a hint, since a write tool named from conversation history or hallucination is refused instead of run.Tool.isTerminalnow ends the turn on both the blocking and streaming paths, skipping the rest of the batch and not returning to the model. A denied terminal call does not terminate, since it never ran.@jarvis/serverSocketModeHandle(seeded from the store's persisteddefault_mode) and aRuleApproverwrapping itsChannelApprover.set_mode/accept_plan/refine_plan. New frames out:permission_mode{mode,via?}/plan_proposed{plan}, plusHitSourceattached toapproval_decision.chatRunsreconnect replay buffer in the same shape, so a replaying client sees what a live one saw.PermissionModebecomes an alias of core'sAgentModeso the two vocabularies can't drift.packages/jarvis-app— constructsMemoryPermissionStore(config.permissionMode)ontoAppState, so the env var gates real dispatch and the REST routes stop 503-ing.Reviewer notes
Two judgement calls worth a look:
approval_requestcard. Core yields the request event before awaiting the approver, so it can't know the approver will approve instantly. Suppressing it would make auto-allowed tool calls invisible in the audit trail, so provenance rides on the decision frame instead (source: {kind: "mode_default" | "rule" | "user_prompt"}) — which is exactly what the pre-existingDecisionSourceChipwas built to render.mode_changedis not forwarded raw. The WS applies it to its own mode handle and republishes aspermission_mode{via:"tool"}, which is what the client actually listens for.Behaviour change to be aware of: anyone currently running with
JARVIS_PERMISSION_MODE=autoorbypasswill now genuinely skip approval prompts. That is the documented intent of the flag, but it was inert until this commit.Bugs fixed along the way
@jarvis/corewas the only one of 25 packages with a hand-listed test script instead of thesrc/*.test.tsglob, sohitl.test.tsfrom the P0 HITL work had never run in CI. Switched to the glob; core goes 32 → 50 tests, all passing.messagelistener after awaitingopen, silently dropping any frame the server sends on connect. Pre-existing latent flaw, exposed by the newpermission_modeframe; fixed to match real browser client ordering.Not in scope
POST /v1/permissions/rulesdoesn't broadcastpermission_rules_changedto live sockets — that needs a socket registry, and the web already works around it with a local version counter./v1/chat/completionsand SSE routes still callcreateAgent()with no approver, so gated tools run unconditionally there. That's the documented historical default rather than something this change introduces, but it does mean the permission engine covers the WS surface only.Testing
make checkclean — typecheck + eslint + 2161 tests, zero failures.New coverage:
packages/core/src/mode.test.ts(12): catalogue filtering; dispatch refusal on both paths; filtered tools never reaching the approver;isTerminalstopping the turn and skipping the rest of the batch on both paths; denied terminal tools not terminating; the mode channel (no-op without a sink, delivery, relay asmode_changed);isAgentModeboundaries.packages/server/src/server.test.ts(9 WS): mode announced on connect;set_modeecho + invalid-value error;autoresolving a gated tool with no operator input taggedmode_default; a deny rule blocking withsource.kind=rule;planrefusing a write tool without prompting;exit_plan→plan_proposed→accept_planswitching mode and resuming; the model callingenter_plan_mode;refine_planempty-feedback error; graceful degradation with no permission store.Branch note
This branch sits 29 commits ahead of
main; 28 of those are pre-existing unmerged work from earlier sessions (P8 Rust decommission, opencode-borrow P0, desktop shell polish). This session's change is the single commitdd3d63a— review that one if the rest has already been reviewed elsewhere.🤖 Generated with Claude Code