Skip to content

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
mainfrom
feat/mobile-ios-ddns
Open

feat(clients): iOS + Mac clients — remote access/DDNS, LAN toggle, mobile WS protocol, Plan Mode, App Store readiness#523
TYRMars wants to merge 35 commits into
mainfrom
feat/mobile-ios-ddns

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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 feature docs/proposals/mobile-ddns.zh-CN.md
describes, landing server-side and on both clients:

  • JARVIS_ACCESS_TOKEN bearer gate (packages/server/src/auth.ts): loopback bypasses,
    every /v1 + WS request from elsewhere must present it; a startup WARN fires on a
    non-loopback bind without one.
  • New @jarvis/ddns package — Cloudflare / DuckDNS / dyndns2 / Aliyun / DNSPod, public+LAN
    IP discovery, zero-dependency UPnP IGD, and the polling DdnsRuntime — behind
    /v1/ddns/* and the ddns.{status,update,configure} tools. Credentials are persisted
    0600 and scrubbed from every GET.
  • iOS: access-token setting, QR pairing (jarvis://pair deep link + camera scan), Bonjour
    LAN discovery, and a remote-access/DDNS configuration screen.
  • Web: Settings → Remote Access with the pairing QR.

Mac shell LAN toggle (4e943bf) — the proposal's last open item. A switch in the
Electron shell restarts the embedded server bound to 0.0.0.0 on a stable port (7001,
ephemeral fallback), generates and persists an access token so pairing links keep working,
and starts the Bonjour advertiser. The serve subcommand always advertised; the embedded
path never did, so zero-config discovery was silently broken in the desktop app. The window
keeps talking loopback, which is what keeps /v1/remote/pairing unlockable.

Mobile WS session protocol on Node (b7e5d8c) — the iOS client was written against the
Rust server's protocol; the Node port only spoke user/reset/resume/new/approve/
deny, so start_turn got unknown frame type and reconnect replay never worked. Now:
start_turn, a per-conversation monotonic seq fanned 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 on
resume, answerable from the new socket), interrupt, and configure {model}. iOS gained
the HITL question card.

App Store readiness (57d95e2) — icon, PrivacyInfo.xcprivacy (no tracking, no
collection, UserDefaults CA92.1), ITSAppUsesNonExemptEncryption, ATS tightened to
NSAllowsLocalNetworking only, and a step-by-step release section in the iOS README.

Plan Mode + permission modes (acbdf6a) — both clients already sent set_mode /
accept_plan / refine_plan and rendered plan_proposed; the server answered
unknown frame type and /v1/permissions always 503'd. Landed across three layers:

  • 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 or failed terminal call does not
    end it), and a plan_proposed event.
  • server: per-socket PermissionMode, a RuleApprover in front of the socket's
    ChannelApprover, 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.
  • composition root: a three-scope FilePermissionStore (session in-memory + project
    .jarvis/permissions.json + user ~/.config/jarvis/permissions.json, merged
    user > project > env). JARVIS_PERMISSION_MODE had never actually governed anything.

Two bugs found by running it (feeaff4) — after wiring the permission store I started the
server and looked at the UI:

  • GET /v1/conversations 500'd 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 shares the directory and is a JSON array. One neighbour took down
    the whole list. list()/loadEnvelope() now require the conversation shape.
  • A default_mode persisted in the permissions file was ignored by the socket, which seeded
    from env only — so persisting a mode changed nothing. The store is now authoritative at
    connect.

Notes for the reviewer

  • Approver.willPrompt is a deliberate divergence from the Rust cut. The loop emits
    approval_request before awaiting the approver (documented invariant), which meant auto
    mode 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_decision is still emitted, so the audit trail loses nothing.
  • Behavioural change for existing installs: a ~/.config/jarvis/permissions.json written
    by 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.
  • Not ported from the Rust design: approval_decision.source plumbed through the core event,
    and the permission_rules_changed broadcast.
  • .claude/launch.json was entirely stale (every entry pointed at the decommissioned Rust
    binary and RUST_LOG); replaced with Node entries.

Verification

  • pnpm lint clean; pnpm -r typecheck green across 26 packages.
  • Tests: core 32 → 38, server 18 → 24 WS, desktop 16 → 21, plus 9 new permission-store and
    20 json-store tests. The only failures are the pre-existing better-sqlite3 ABI mismatch in
    packages/store (built against Electron's NODE_MODULE_VERSION 139, run under Node's 127 —
    rebuilding would break desktop dev).
  • iOS: Tests/run-contract-smoke.sh 39/39 (this machine has Command Line Tools only, no full
    Xcode — archives must be produced elsewhere; the README documents the steps).
  • Browser: server started against the real store — conversation list 200 with records, mode
    chip reads the persisted mode, no console errors.

🤖 Generated with Claude Code

TYRMars and others added 30 commits June 20, 2026 22:42
…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>
TYRMars and others added 5 commits July 24, 2026 23:55
… 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
TYRMars enabled auto-merge (squash) July 28, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant