feat(clients): iOS + Mac clients — remote access/DDNS, LAN toggle, mobile WS protocol, Plan Mode, App Store readiness - #523
Open
TYRMars wants to merge 35 commits into
Open
feat(clients): iOS + Mac clients — remote access/DDNS, LAN toggle, mobile WS protocol, Plan Mode, App Store readiness#523TYRMars wants to merge 35 commits into
TYRMars wants to merge 35 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>
…dns, iOS pairing, web remote-access page
Server: JARVIS_ACCESS_TOKEN onRequest bearer gate (loopback bypass,
/v1+WS covered, startup WARN on unauthenticated non-loopback bind);
GET /v1/remote/info; JARVIS_MDNS Bonjour advertise; @jarvis/ddns
runtime (cloudflare/duckdns/dyndns2/aliyun/dnspod + UPnP) behind
/v1/ddns/* routes and ddns.{status,update,configure} tools;
credentials persisted 0600 and scrubbed from every GET.
iOS: access-token setting + auto-attach in JarvisAPI/ChatSocket; QR
pairing (jarvis://pair deep link + camera scan) via QRScannerView;
Bonjour LAN discovery; DDNSView remote-access configuration screen;
contract smoke extends to DdnsStatus/RemoteInfo/Pairing goldens.
Web: Settings > RemoteAccessSection (pairing QR + token) with i18n.
Verified: pnpm -r typecheck + lint green; tests green except the
pre-existing better-sqlite3 ABI mismatch in packages/store (82
sqlite tests, unrelated); iOS contract smoke 35/35.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 07-22) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Bonjour from the shell The mobile-ddns proposal's last pending item. New setLanExposure IPC persists prefs (lanExposure/lanPort/accessToken, prefs.json now 0600) and restarts the embedded server: binds 0.0.0.0 on a stable port (default 7001, ephemeral fallback), injects a generated JARVIS_ACCESS_TOKEN so the auth gate arms, and starts the Bonjour advertiser (the serve-subcommand path does this in main.ts; the embedded path now matches). The window keeps talking loopback so /v1/remote/pairing stays unlockable. stop() now also reaps the mdns advertiser + env-seeded DdnsRuntime. Web: RemoteAccessSection renders the toggle inside the Electron shell (bridge-detected, optimistic, reload-aware) with en/zh strings. Desktop tests 16→21 (LAN e2e: requires_auth flips, token persists, stable port honoured; pickPort preferred-port fallback; prefs fields). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… seq replay, cross-socket adoption; iOS HITL card
The iOS client was built against the Rust harness-server's richer WS
protocol; the Node port only spoke user/reset/resume/new/approve/deny,
so start_turn got 'unknown frame type' and reconnect replay never
worked. Server (chat-routes + ChatRunRegistry):
- start_turn {mode:new|resume, id, content, after_seq?} → started /
resumed {message_count, live} bookkeeping frames.
- Per-conversation monotonic seq stamped on every tracked event; the
registry fans events out to all subscribed sockets, so a socket that
resumes mid-run keeps receiving the live turn (the old MVP only fed
the originating socket).
- resume {after_seq} replays the missed tail in tail_replay_start
{first_seq} / tail_replay_done; an evicted cursor answers
resume_error {evicted} (client falls back to full REST reload).
- Pending approvals + ask.* HITL requests are adoptable across
sockets: registered in the registry, re-prompted on resume
(approval_pending / re-sent hitl_request), answerable from the new
socket. Terminal runs clear them.
- interrupt frame (cross-socket, cooperative) + interrupted event;
configure {model} sets a sticky per-socket model via a new
createAgent opts.model (provider switching rejected).
iOS: hitl_request now renders an inline question card (confirm /
choice / free-text via new Models/Hitl.swift + Views/HitlCardView) and
answers with the flat hitl_response frame; hitl_response events
dismiss stale cards cross-device. ChatSocket joins the contract-smoke
compile for ClientFrame goldens (35→39 checks).
Server WS tests 522→528 (replay, adoption, interrupt, configure).
Plan-mode frames (set_mode/accept_plan) remain dormant pending a core
mode state machine — README/CLAUDE.md now say so explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…S, export compliance - Assets.xcassets/AppIcon: generated 1024px opaque icon (single-size, ASSETCATALOG_COMPILER_APPICON_NAME wired in project.yml). - PrivacyInfo.xcprivacy: no tracking / no data collection; declares the UserDefaults required-reason API (CA92.1). Added to target sources. - ATS tightened to NSAllowsLocalNetworking only — LAN plain http/ws keeps working, remote (DDNS) origins must be https in store builds; README documents the TLS-proxy path and the dev-only escape hatch. - ITSAppUsesNonExemptEncryption=false answers the export-compliance question at upload time. - README: step-by-step 发布到 App Store section (bundle id/team, versioning, archive, Connect metadata, review notes for a self-hosted-server client). xcodegen-regenerated project checked. Contract smoke stays 39/39. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The permission engine was ported to Node but never wired: /v1/permissions
always 503'd, JARVIS_PERMISSION_MODE only appeared in server info, and the
set_mode/accept_plan frames both clients already send were answered with
'unknown frame type'.
core: AgentConfig.toolFilter (structural — filtered tools leave the LLM
catalogue AND can't be dispatched if the model guesses a name),
Tool.isTerminal honoured by both loops (ends the turn, skips the rest of
the batch; a denied/failed terminal call does NOT end it), and a
plan_proposed event carrying the terminal tool's output. New optional
Approver.willPrompt lets the loop skip approval_request when the policy
auto-decides — 'auto' mode no longer flashes an approval sheet on the
phone — while approval_decision still records every auto-decision.
server: per-socket PermissionMode announced on connect and switched by
set_mode; a RuleApprover in front of the socket's ChannelApprover;
plan mode installs the read-only tool filter per turn; accept_plan
{post_mode} switches mode (never back into plan) and re-briefs the model
with the accepted plan; refine_plan {feedback} re-runs still filtered.
AppState gains permissionStore + defaultPermissionMode, which also
retires the structural-widening hack in permissions-routes.
FilePermissionStore: session in-memory + project <workspace>/.jarvis and
user ~/.config/jarvis JSON files, merged user > project > session, read
per snapshot so a hand-edit or git pull applies without a restart.
Wired from the composition root.
Tests: core 32→38, server 18→23 WS + 9 store, all green; iOS contract
smoke 39/39 unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sisted mode governs Two bugs found by actually running the server and looking at the UI after wiring the permission store. 1. GET /v1/conversations 500'd on this machine with "Cannot read properties of undefined (reading 'length')" — emptying the sidebar in every client. JsonFileConversationStore.list() skipped directories and unparseable files but not a SIBLING STORE's file that parses fine: channel_instances.json lives in the same directory and is a JSON array, so `stored.messages.length` threw and one neighbour took down the whole list. list()/loadEnvelope() now require the conversation shape (string id + messages array). 2. A `default_mode` persisted in the user/project permissions file was ignored by the socket, which seeded its mode from the env only — so persisting a mode changed nothing. The store is now authoritative at connect (it already resolves user > project > env-seeded session), which is how this machine's pre-existing ~/.config/jarvis/ permissions.json (default_mode: auto, written by the Rust build) started taking effect. Also: the connect-time permission_mode frame no longer carries `via`. Both clients treat a `via` as an out-of-band switch worth surfacing (web toast, iOS transcript line) and were announcing "mode changed to auto" on every page load; `via: "tool"` stays reserved for a future agent-initiated enter_plan_mode, and accept_plan reports `via: "user"` since the operator picked the post-mode themselves. .claude/launch.json was entirely stale (every entry pointed at the decommissioned Rust binary + RUST_LOG); replaced with Node entries. Verified in the browser: list 200 with 3 records, mode chip reads 自动通过, no toast, no console errors. store 20 tests, server 24. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TYRMars
enabled auto-merge (squash)
July 28, 2026 16:24
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.
Brings the iOS and Mac clients to a shippable state, and closes the gaps that made them
half-connected to the Node runtime. Seven commits, reviewable in order.
What changed
Remote access + DDNS (
fb93bf5) — the featuredocs/proposals/mobile-ddns.zh-CN.mddescribes, landing server-side and on both clients:
JARVIS_ACCESS_TOKENbearer gate (packages/server/src/auth.ts): loopback bypasses,every
/v1+ WS request from elsewhere must present it; a startup WARN fires on anon-loopback bind without one.
@jarvis/ddnspackage — Cloudflare / DuckDNS / dyndns2 / Aliyun / DNSPod, public+LANIP discovery, zero-dependency UPnP IGD, and the polling
DdnsRuntime— behind/v1/ddns/*and theddns.{status,update,configure}tools. Credentials are persisted0600and scrubbed from every GET.jarvis://pairdeep link + camera scan), BonjourLAN discovery, and a remote-access/DDNS configuration screen.
Mac shell LAN toggle (
4e943bf) — the proposal's last open item. A switch in theElectron shell restarts the embedded server bound to
0.0.0.0on a stable port (7001,ephemeral fallback), generates and persists an access token so pairing links keep working,
and starts the Bonjour advertiser. The
servesubcommand always advertised; the embeddedpath never did, so zero-config discovery was silently broken in the desktop app. The window
keeps talking loopback, which is what keeps
/v1/remote/pairingunlockable.Mobile WS session protocol on Node (
b7e5d8c) — the iOS client was written against theRust server's protocol; the Node port only spoke
user/reset/resume/new/approve/deny, sostart_turngotunknown frame typeand reconnect replay never worked. Now:start_turn, a per-conversation monotonicseqfanned out to all subscribed sockets,resume {after_seq}tail replay (tail_replay_start/_done,resume_error {evicted}),cross-socket adoption of pending approvals and
ask.*HITL requests (re-prompted onresume, answerable from the new socket),
interrupt, andconfigure {model}. iOS gainedthe HITL question card.
App Store readiness (
57d95e2) — icon,PrivacyInfo.xcprivacy(no tracking, nocollection, UserDefaults CA92.1),
ITSAppUsesNonExemptEncryption, ATS tightened toNSAllowsLocalNetworkingonly, and a step-by-step release section in the iOS README.Plan Mode + permission modes (
acbdf6a) — both clients already sentset_mode/accept_plan/refine_planand renderedplan_proposed; the server answeredunknown frame typeand/v1/permissionsalways 503'd. Landed across three layers:AgentConfig.toolFilter(structural — filtered tools leave the LLM catalogue andcan't be dispatched if the model guesses a name),
Tool.isTerminalhonoured by both loops(ends the turn, skips the rest of the batch; a denied or failed terminal call does not
end it), and a
plan_proposedevent.PermissionMode, aRuleApproverin front of the socket'sChannelApprover, the plan tool filter installed per turn,accept_plan {post_mode}re-briefing the model with the accepted plan,
refine_plan {feedback}re-running filtered.FilePermissionStore(session in-memory + project.jarvis/permissions.json+ user~/.config/jarvis/permissions.json, mergeduser > project > env).
JARVIS_PERMISSION_MODEhad never actually governed anything.Two bugs found by running it (
feeaff4) — after wiring the permission store I started theserver and looked at the UI:
GET /v1/conversations500'd withCannot read properties of undefined (reading 'length'),emptying the sidebar in every client.
JsonFileConversationStore.list()skipped directoriesand unparseable files but not a sibling store's file that parses fine —
channel_instances.jsonshares the directory and is a JSON array. One neighbour took downthe whole list.
list()/loadEnvelope()now require the conversation shape.default_modepersisted in the permissions file was ignored by the socket, which seededfrom env only — so persisting a mode changed nothing. The store is now authoritative at
connect.
Notes for the reviewer
Approver.willPromptis a deliberate divergence from the Rust cut. The loop emitsapproval_requestbefore awaiting the approver (documented invariant), which meantautomode flashed an approval sheet on the phone that was dismissed a millisecond later. The new
optional hook lets the loop skip the prompt when the policy auto-decides;
approval_decisionis still emitted, so the audit trail loses nothing.~/.config/jarvis/permissions.jsonwrittenby the old Rust build now actually takes effect. On the dev machine here that file pins
default_mode: auto, so gated tools stop prompting. Worth calling out in release notes.approval_decision.sourceplumbed through the core event,and the
permission_rules_changedbroadcast..claude/launch.jsonwas entirely stale (every entry pointed at the decommissioned Rustbinary and
RUST_LOG); replaced with Node entries.Verification
pnpm lintclean;pnpm -r typecheckgreen across 26 packages.20 json-store tests. The only failures are the pre-existing
better-sqlite3ABI mismatch inpackages/store(built against Electron'sNODE_MODULE_VERSION 139, run under Node's 127 —rebuilding would break desktop dev).
Tests/run-contract-smoke.sh39/39 (this machine has Command Line Tools only, no fullXcode — archives must be produced elsewhere; the README documents the steps).
chip reads the persisted mode, no console errors.
🤖 Generated with Claude Code