Skip to content

feat(app-016): Atmos Computer relay, access tokens, and unified local runtime - #115

Merged
AruNi-01 merged 35 commits into
mainfrom
aarynlu/app-016-atmos-computer
May 17, 2026
Merged

AruNi-01 merged 35 commits into
mainfrom
aarynlu/app-016-atmos-computer

Conversation

@AruNi-01

@AruNi-01 AruNi-01 commented May 16, 2026 •

Copy link
Copy Markdown
Owner

Dependencies

Summary

Implements APP-016 Atmos Computer: a user-operated relay control plane plus a unified local runtime shared by Desktop, CLI, and local-web.

  • packages/relay: Cloudflare Worker + D1 (tenants via user Access Token, register_token, client_session, outbound server WS).
  • apps/api: Relay ingest/register; writes ~/.atmos/runtime_manifest.json on bind (no manifest token); optional loopback auth via ATMOS_LOCAL_TOKEN only.
  • crates/runtime-manager: Replaces runtime-manifest — manifest discovery, relay_identity.json, register_computer, and process supervisor (ensure / stop / status).
  • apps/cli: atmos computer register|start|status, atmos runtime (and atmos local alias), canvas API URL via manifest.
  • apps/web: Settings → Atmos Computer (access token, register tokens, relay connect); Desktop runtime config without per-launch token.
  • apps/desktop: Drops Tauri sidecar + ATMOS_LOCAL_TOKEN; uses bundled runtime/current + runtime-manager::ensure_running (quit does not kill shared API).
  • CI/docs: Desktop workflow lays out binaries/runtime/current; AGENTS.md and TECH spec updates.

Related Issue

APP-016 — Atmos Computer (see specs/APP/APP-016_atmos-computer/). Builds on APP-015 Canvas work in #114.

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore / tooling

Validation

  • just lint
  • just test
  • just fmt
  • Additional checks (describe below)
    • cargo check -p api -p atmos -p atmos-desktop -p runtime-manager (local)
    • Relay D1 migration / wrangler deploy documented in packages/relay/README.md (ops, not run in CI here)

Checklist

  • I updated documentation if behavior changed
  • I added/updated tests where appropriate
  • I followed repository conventions and AGENTS.md guidance

Summary by cubic

Implements Atmos Computer (APP-016): a Cloudflare Worker relay with per‑user Access Tokens, an HTTP gateway, and outbound WSS from apps/api, plus a unified local runtime for Desktop, CLI, and Web. Adds a Computer details view and registration metadata; includes relay deploy workflow and D1 upkeep.

  • Migration

    • Desktop: Run scripts/desktop/prepare-sidecar.sh (or build) to lay out apps/desktop/src-tauri/binaries/runtime/current/; the app attaches to the shared runtime and no longer uses a per‑launch local token.
    • Local runtime: Tools read ~/.atmos/runtime_manifest.json; use atmos runtime ensure|status|stop (or atmos local).
    • Access token: Generate in Settings; stored in ~/.atmos/computer-client.json and shared by Desktop/Web (manage via /api/system/computer-client-settings). Control‑plane requests are proxied through the local API; Desktop allows relay.atmos.land in CSP/capabilities.
    • Relay: In Web Settings generate/register an access token, then Connect to create a client session; on servers run atmos computer register --token <ACCESS_TOKEN> (or set ATMOS_REGISTER_TOKEN) to write relay_identity.json and connect outbound.
    • API: If relay_identity.json exists, the server starts the outbound relay and proxies REST via the HTTP gateway automatically; mutating computer routes require loopback auth.
    • CLI: Uses ~/.atmos/client-session.json (gateway base + client token) when present, else falls back to the runtime manifest; clear or switch in Settings.
    • VPS: Settings show copyable install and register/start commands; use atmos computer start --daemon to register and launch in the background.
  • Refactors

    • API/CLI: Added code‑review REST endpoints and Canvas agent HTTP ingress; CLI uses a shared HTTP client and adds atmos canvas …, atmos runtime …, and atmos computer … commands.
    • Web: Isolate browser UI prefs by connection instance (local vs computer:<serverId>); add Computer Details dialog (runtime/host tabs), show registration_meta, and fix online status updates.
    • Relay/CI: Added deploy-relay workflow; enabled Worker logs/traces; applied D1 updates (including updated_at and registration_meta); dev REST proxy in next.config.ts.
    • Sidebar: Merge workspace grouping into the filter popover; hide grouping when Kanban is expanded.
    • Theme: Default UI theme is dark; explicit System still follows the OS.

Written for commit 6d2c892. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Added Atmos Computer feature enabling remote API access via Cloudflare Relay with secure token-based authentication
    • Canvas terminal-agent integration allowing CLI commands to control canvas operations in real-time
    • Unified local runtime bootstrap for Desktop and CLI with shared server initialization
    • Web settings section for managing remote computer access, registration, and client credentials
    • New CLI commands: atmos computer (register/status/start), atmos canvas (shape/frame/draw operations), atmos runtime (ensure/stop/status)
  • Improvements

    • Desktop now uses shared local runtime instead of per-launch sidecars
    • Enhanced WebSocket bridge for browser-to-terminal command dispatch with presence tracking
    • Improved API client-session persistence across local and relay connections
  • Documentation

    • Added comprehensive specs for Atmos Computer (APP-016) including architecture and test plans
    • Updated developer guides across all crate and app layers

Review Change Stack

cursoragent and others added 10 commits May 15, 2026 13:38
Adds CLI-driven Canvas control via the existing browser WebSocket,
without requiring a separate Atmos LLM API key.

Backend
- crates/infra: new `canvas_bridge_register / canvas_bridge_unregister /
  canvas_agent_dispatch_result` WsAction variants and
  `canvas_agent_dispatch` WsEvent.
- crates/core-service: `CanvasAgentRelay` in-memory bridge registry +
  pending-waiter map; `WsMessageService` routes bridge messages and
  cleans up on disconnect. Manifest + `ALL_SYSTEM_SKILL_NAMES` register
  the new `atmos-canvas-agent` system skill.
- apps/api: `POST /api/canvas/agent/invoke` HTTP ingress and
  `GET /api/canvas/agent/status`. Resolves target tab, dispatches over
  WS, parks the request, returns the browser's result. Guarded by the
  existing local-token auth middleware.

Frontend (apps/web)
- `canvas-agent-bus.ts`: command bus that translates dispatch envelopes
  into tldraw editor operations (create-note / -frame / -geo / -arrow /
  -draw, select, move, delete --confirm, layout-row/column/grid with
  24x24 cap, update-shape with allow-listed patch keys, viewport,
  get-state, status). Centralised structured error codes.
- `canvas-agent-presence.ts`: virtual Agent presence store driving the
  Follow Agent affordance.
- `use-canvas-agent-bridge.ts` + `CanvasAgentOverlay.tsx`: React hook
  that registers the tab, listens for dispatches, executes via the bus,
  publishes results, and renders the bridge toggle, copy-instructions
  button, agent activity chips, and follow controls.
- `CanvasView.tsx`: mounts the overlay inside <Tldraw> and exposes the
  bridge controls in the share panel.

CLI (apps/cli)
- `atmos canvas` command tree: `skill-dir` / `skill-path` (local-only),
  `status`, `get-state`, and all mutation verbs from TECH §10. Uses the
  existing local-runtime auth for the API call.

Skill
- `skills/atmos-canvas-agent/SKILL.md` documents prerequisites, verb
  table, coordinate conventions, destructive-command policy, error
  codes, and copy-prompt template.

Tests
- 12 unit tests for `CanvasAgentRelay` (cargo).
- 17 unit tests across `canvas-agent-bus` and `canvas-agent-presence`
  (bun).

Co-authored-by: AruNi_Lu <hello@0x3f4.run>
…tartFollowingUser

Previously the agent badge was a pure HTML overlay and 'follow' was an
ad-hoc centerOnPoint loop, which diverged from TECH.md §9 and from the
official tldraw user-following contract (https://tldraw.dev/sdk-features/
user-following). This commit makes Follow Agent honour the documented
API.

apps/web/src/components/canvas/canvas-agent-presence.ts
- Bind a live tldraw Editor via setEditor(editor).
- Every touch / recordResult writes a TLInstancePresence record into
  editor.store via InstancePresenceRecordType.create({ userId:
  'agent:<actor_id>', userName, color, currentPageId, camera,
  screenBounds, cursor, selectedShapeIds, lastActivityTimestamp,
  chatMessage, meta }).
- Synthesise camera + screenBounds so tldraw's
  getViewportPageBoundsForFollowing() frames the agent's last bounds
  (camera.z = 1, screenBounds = padded(last_bounds), camera = -origin).
  Without bounds yet, mirror the user's own viewport.
- setFollowedActor(id) now calls editor.startFollowingUser('agent:<id>')
  / stopFollowingUser(). jumpToActor(id) calls editor.zoomToUser(...).
- TTL eviction + clear() + setEditor(null) all clear the corresponding
  records from editor.store and cancel any active follow so we never
  strand the user on a ghost agent.
- Records live in the presence scope, so getSnapshot(editor.store).
  document — the payload Atmos persists server-side — is unaffected.

apps/web/src/components/canvas/CanvasAgentOverlay.tsx
- Read the followed user id via useValue(() => editor.getInstanceState
  ().followingUserId) so manual pan/zoom (tldraw stops following) is
  reflected in the UI immediately.
- Add a 'Jump to agent' (Crosshair) button alongside the follow toggle;
  follow toggle now drives editor.startFollowingUser via the presence
  store.

apps/web/src/components/canvas/use-canvas-agent-bridge.ts
- Pass editor into presence.setEditor when the editor mounts / unmounts.

skills/atmos-canvas-agent/SKILL.md
- New section on the persistence model: agent mutations flow through
  editor.store and are autosaved server-side via the existing APP-014
  pipeline. tldraw's persistenceKey IndexedDB sync is intentionally not
  used (Atmos persists per workspace, not per browser). Presence is
  ephemeral.
- New 'Follow Agent (M20)' section telling agents to pass --actor-id so
  the user gets a stable follow target across many commands.

Tests
- canvas-agent-presence.test.ts: 10 tests (was 4) covering
  TLInstancePresence write, follow / stop following / zoomToUser
  delegation, unknown actor rejection, TTL eviction with editor cleanup,
  and setEditor(null) teardown. All 33 web tests + 12 relay tests pass.

Co-authored-by: AruNi_Lu <hello@0x3f4.run>
Backend (core-service / api):
- Relay `register` now dedupes by `client_id` globally (not by
  conn_id+client_id), so reconnects from the same browser tab no longer leave
  a stale entry that `resolve_target(Some(client_id))` could route to.
- `begin_pending` rejects duplicate `request_id`s instead of silently
  overwriting the existing oneshot sender (returns `DuplicateRequestError`);
  the HTTP handler surfaces this as 409 VALIDATION_ARG.
- `complete_dispatch` now requires `conn_id` to match the connection that
  originally received the dispatch — prevents a different tab from completing
  another tab's request (returns `CompleteDispatchResult::ConnMismatch`,
  which the WS handler converts to a ServiceError).
- `ResolveTarget::NotFound` returns the distinct `CANVAS_CLIENT_NOT_FOUND`
  code (was conflated with `CANVAS_BRIDGE_OFFLINE`).

CLI:
- `invoke` no longer assumes the response is JSON: on parse failure it
  surfaces the raw HTTP status + body preview so users can diagnose proxy /
  auth / HTML-error pages.

Frontend (apps/web/src/components/canvas):
- `CanvasAgentBus.runGetState` rejects requests for non-current pages
  (VALIDATION_ARG); the downstream `getCurrentPage*` reads always operate on
  the active page, so any other answer was internally inconsistent. The
  status/get_state early-returns are now wrapped by the same try/catch as the
  mutating commands.
- `runDelete` requires `confirm === true` (was a truthy check, which would
  accept strings/numbers/objects).
- `runUpdateShape` validates x/y via `requireNumber` (was raw `Number(value)`
  which silently produced NaN/Infinity for invalid input).
- `runViewport` with `center_ids` now computes the union of the requested
  shapes' page bounds and calls `zoomToBounds` with that — the previous
  implementation used `getSelectionPageBounds()` (ignored the ids) and
  immediately overrode the result with `zoomToFit()`.
- `CanvasAgentPresenceStore.getSnapshot` is now cached and invalidated on
  every emit; required for `useSyncExternalStore` (which compares with
  Object.is and would loop forever otherwise).
- `CanvasAgentOverlay` uses `pageToViewport` instead of `pageToScreen` so the
  agent badge `left/top` lands in the overlay container's local frame (not
  document space, which over-shifted by the editor's screenBounds offset).
- Replace hardcoded `text-emerald-500` with the semantic `text-foreground`
  token (theme-aware); add `aria-label`s to the icon-only Jump / Follow
  buttons for screen readers.
- `useCanvasAgentBridge`: unmount-only `unregister` now skips the WS call
  when the socket is already disconnected (otherwise it would trigger
  wsRequest's auto-reconnect during teardown).

Tests: 15 backend (`canvas_agent_relay`) and 38 frontend
(`canvas-agent-bus` / `canvas-agent-presence`) passing, including new
regression coverage for each fix.

Co-authored-by: AruNi_Lu <hello@0x3f4.run>
Document control plane, relay envelope, rollout phases, and tests;
register in specs index. Paseo reference corrected to Worker+DO.
Truncate non-JSON invoke errors by char count; add clap docs for root and
review/local/update subcommands. Align canvas agent copy in help, overlay
tooltip, and atmos-canvas-agent skill.
Use Agent label and update presence docs; keep TLInstancePresence behavior.
Move specs to specs/APP/APP-016_atmos-computer/ and align PRD/TECH/TEST/brainstorm
with the Atmos Computer product name plus API-anchor CLI guidance.
Ship Cloudflare Worker + D1 for computer registration and relay routing,
API outbound relay ingest, and Web Settings for token-based tenant isolation
without account login or a shared operator secret.
Share control-plane registration in runtime-manifest and expose CLI
commands so VPS and always-on desktops can join the relay fleet.
… layout

Replace runtime-manifest with runtime-manager (manifest, relay identity, supervisor).
Desktop attaches to the same loopback API as CLI/local-web; drop per-launch sidecar token.
Add atmos runtime commands, API manifest write on bind, CI layout bundle, and AGENTS updates.
@vercel

vercel Bot commented May 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
atmos-landing Ready Ready Preview, Comment May 17, 2026 1:49pm

@coderabbitai

coderabbitai Bot commented May 16, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9d91d96-d687-4766-a54b-f03a2288fe71

📥 Commits

Reviewing files that changed from the base of the PR and between e2c2548 and 6d2c892.

⛔ Files ignored due to path filters (1)
  • apps/desktop/src-tauri/gen/schemas/capabilities.json is excluded by !**/gen/**
📒 Files selected for processing (65)
  • apps/api/src/api/system/computer.rs
  • apps/api/src/api/system/mod.rs
  • apps/api/src/relay/register.rs
  • apps/cli/src/commands/computer.rs
  • apps/desktop/src-tauri/capabilities/default.json
  • apps/desktop/src-tauri/tauri.conf.json
  • apps/web/AGENTS.md
  • apps/web/src/api/relay.ts
  • apps/web/src/api/rest-api.ts
  • apps/web/src/app/[locale]/(app)/layout.tsx
  • apps/web/src/app/[locale]/layout.tsx
  • apps/web/src/components/ConnectionBootstrapper.tsx
  • apps/web/src/components/agent/chat-helpers.ts
  • apps/web/src/components/canvas/CanvasView.tsx
  • apps/web/src/components/canvas/canvas-session-storage.ts
  • apps/web/src/components/canvas/use-canvas-agent-bridge.ts
  • apps/web/src/components/chat-sessions/ChatSessionsManagementView.tsx
  • apps/web/src/components/code-review/CodeReviewDialog.tsx
  • apps/web/src/components/dialogs/AtmosComputerSection.tsx
  • apps/web/src/components/dialogs/ComputerDetailsDialog.tsx
  • apps/web/src/components/dialogs/RemoteComputerSetupBlock.tsx
  • apps/web/src/components/diff/ReviewView.tsx
  • apps/web/src/components/files/FileTree.tsx
  • apps/web/src/components/layout/CenterStage.tsx
  • apps/web/src/components/layout/GlobalSearch.tsx
  • apps/web/src/components/layout/QuickOpen.tsx
  • apps/web/src/components/layout/RightSidebar.tsx
  • apps/web/src/components/layout/UsagePopover.tsx
  • apps/web/src/components/run-preview/Preview.tsx
  • apps/web/src/components/run-preview/RunScript.tsx
  • apps/web/src/components/terminal/TerminalGrid.tsx
  • apps/web/src/hooks/use-connection-store.ts
  • apps/web/src/hooks/use-editor-store.ts
  • apps/web/src/hooks/use-review-context.ts
  • apps/web/src/hooks/use-ui-pref-hooks.ts
  • apps/web/src/hooks/use-ui-pref-store.ts
  • apps/web/src/hooks/use-websocket.ts
  • apps/web/src/lib/atmos-access-token.ts
  • apps/web/src/lib/atmos-computer-local.ts
  • apps/web/src/lib/atmos-computer-store.ts
  • apps/web/src/lib/browser-store.ts
  • apps/web/src/lib/connection-instance.ts
  • apps/web/src/lib/connection-ui-prefs.ts
  • apps/web/src/lib/editor-ui-persistence.ts
  • apps/web/src/lib/registration-meta.ts
  • apps/web/src/lib/remote-computer-register-token-cache.ts
  • apps/web/src/lib/remote-computer-setup-commands.ts
  • apps/web/src/lib/restore-editor-from-prefs.ts
  • apps/web/src/lib/sync-computer-client-settings.ts
  • crates/runtime-manager/src/computer_client_settings.rs
  • crates/runtime-manager/src/identity.rs
  • crates/runtime-manager/src/lib.rs
  • crates/runtime-manager/src/register.rs
  • crates/runtime-manager/src/registration_meta.rs
  • packages/relay/AGENTS.md
  • packages/relay/README.md
  • packages/relay/migrations/0002_computers_updated_at.sql
  • packages/relay/migrations/0003_computers_registration_meta.sql
  • packages/relay/package.json
  • packages/relay/src/index.ts
  • packages/relay/src/server-hub.ts
  • packages/shared/src/utils/storage.ts
  • scripts/local-runtime/build-runtime.mjs
  • scripts/relay/d1-maintenance.sql
  • scripts/relay/deploy.sh

📝 Walkthrough

Walkthrough

This PR adds the Atmos Computer relay platform, a shared local runtime manager, API and CLI remote-targeting paths, Desktop runtime bundling and startup changes, Web remote-computer settings and connection state, and a canvas agent bridge spanning CLI, API, websocket, and browser canvas handling.

Changes

Atmos Computer relay, runtime manager, and canvas agent bridge

Layer / File(s) Summary
Docs, specs, and release workflow updates
AGENTS.md, apps/*/AGENTS.md, packages/AGENTS.md, packages/relay/README.md, specs/APP/APP-016_atmos-computer/*, .github/workflows/*, releasenotes/*
Adds APP-016 documentation, relay/runtime guidance, desktop runtime-bundle docs, release notes, and workflow updates for relay deploy and prerelease publishing.
Relay worker, gateway, and deployment
packages/relay/src/*, packages/relay/migrations/*, packages/relay/wrangler.toml, .github/workflows/deploy-relay.yml, scripts/relay/*
Adds the Cloudflare Worker control plane, ServerHub durable object, gateway authorization and forwarding, D1 schema and maintenance scripts, and deployment configuration.
Runtime manager persistence and supervision
crates/runtime-manager/*
Adds manifest, identity, client-session, computer-client settings, registration metadata, local computer naming, and runtime supervision APIs for local runtime discovery and control.
API remote-computer and relay runtime wiring
apps/api/src/main.rs, apps/api/src/relay/*, apps/api/src/api/system/*, apps/api/src/api/review/*, apps/api/src/app_state.rs
Wires runtime-manager and relay supervision into the server, adds remote-computer and client-session endpoints, adds HTTP review routes, and forwards relay HTTP/websocket traffic through the API.
CLI and Desktop shared runtime integration
apps/cli/src/*, apps/desktop/src-tauri/src/*, apps/desktop/src-tauri/*.json, scripts/desktop/*, .github/workflows/release-desktop.yml
Moves CLI review and canvas flows onto API requests, adds runtime and computer commands, and replaces Desktop sidecar startup with shared runtime bundle preparation and runtime bootstrap.
Web remote-computer connection and instance-scoped prefs
apps/web/src/components/dialogs/*, apps/web/src/api/rest-api.ts, apps/web/src/api/relay.ts, apps/web/src/hooks/use-websocket.ts, apps/web/src/hooks/use-ui-pref-*, apps/web/src/lib/*
Adds Atmos Computer settings, relay/local connection switching, live computer details, loopback proxy helpers, disk-backed client settings sync, and per-instance browser preference storage.
Canvas agent relay across websocket, API, CLI, and Web
crates/core-service/src/service/*canvas*, crates/infra/src/websocket/*, apps/api/src/api/canvas/*, apps/cli/src/commands/canvas.rs, apps/web/src/components/canvas/*, skills/atmos-canvas-agent/SKILL.md
Adds canvas bridge websocket actions, in-memory dispatch coordination, invoke/status APIs, CLI canvas commands, browser dispatch handling and presence overlay, plus tests and system skill registration.

Sequence Diagram(s)

sequenceDiagram
  participant Web as Web Settings
  participant API as Atmos Server
  participant CP as Relay Control Plane
  participant Hub as ServerHub
  participant CLI as atmos CLI

  Web->>CP: create register token
  CLI->>API: start/register computer
  API->>CP: register computer
  API->>Hub: connect server websocket
  Web->>CP: create client session
  Web->>Hub: connect client websocket
  Web->>API: sync selected client session
Loading
sequenceDiagram
  participant CLI as atmos canvas
  participant API as Canvas Agent API
  participant WS as WsMessageService
  participant Web as Canvas Bridge
  participant Editor as tldraw Editor

  CLI->>API: POST /api/canvas/agent/invoke
  API->>WS: dispatch request
  WS->>Web: canvas_agent_dispatch
  Web->>Editor: execute command
  Editor-->>Web: result
  Web->>WS: canvas_agent_dispatch_result
  WS-->>API: complete pending request
  API-->>CLI: response envelope
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • AruNi-01/atmos#114: Overlaps directly with the canvas agent API and route wiring that are expanded here into the full bridge flow.
  • AruNi-01/atmos#11: Also changes Desktop Tauri build and bundle configuration around runtime packaging.
  • AruNi-01/atmos#77: Also updates shared websocket action typing in apps/web/src/hooks/use-websocket.ts.

Suggested labels

codex

Poem

🐇 I thumped through wires of loopback light,
and packed a runtime snug and tight.
A relay hums beyond the warren,
while canvas sprites obey by morning.
Carrots for every socket spun—
the little rabbit's build is done.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aarynlu/app-016-atmos-computer

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 95 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/lib/atmos-computer-store.ts">

<violation number="1" location="apps/web/src/lib/atmos-computer-store.ts:97">
P2: Handle empty control-plane input before prefixing `https://`; otherwise an empty value becomes `https://` and produces malformed fetch URLs.</violation>
</file>

<file name="apps/web/src/lib/atmos-access-token.ts">

<violation number="1" location="apps/web/src/lib/atmos-access-token.ts:22">
P2: Handle fetch exceptions in `registerAccessTokenOnRelay`; network failures currently throw instead of returning `{ ok: false }`, which breaks the helper's expected error contract.</violation>
</file>

<file name="apps/api/src/relay/register.rs">

<violation number="1" location="apps/api/src/relay/register.rs:11">
P1: Avoid calling `std::env::remove_var` here; this is unsafe in multithreaded Unix-like runtimes and the function does not guarantee the required preconditions.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread apps/api/src/relay/register.rs Outdated
Comment thread apps/web/src/lib/atmos-computer-store.ts
Comment thread apps/web/src/lib/atmos-access-token.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
specs/APP/APP-016_atmos-computer/BRAINSTORM.md (1)

1-62: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Convert this spec content to English to match repo spec policy.

The structure is good, but the body/headings are currently non-English. Please translate the document to English and keep only necessary inline Chinese quotes if needed.

Based on learnings: “All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/BRAINSTORM.md` around lines 1 - 62,
Translate the entire markdown content of the spec (file with heading "# 头脑风暴 ·
APP-016:Atmos Computer") into English, keeping the original structure, headings,
tables, links and technical identifiers (APP-016, Atmos Computer, Atmos Server,
Relay, DO, Cloudflare Durable Objects, runtime_manifest.json, PRD.md, TECH.md,
APP-012) intact; convert titles, headings and body text to clear English while
retaining any short inline Chinese quotes only if necessary, and do not alter
links, code/file names, or the logic of the decision notes, alternatives, and
open questions sections (including the numbered lists, table content and section
labels like "1. We are solving", "3. Design axis: Relay + outbound WebSocket",
etc.).
specs/APP/APP-016_atmos-computer/PRD.md (1)

1-100: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

PRD language must be English per spec standards.

This PRD is mostly Chinese. Please translate titles/headings/body into English to align with repository spec requirements.

Based on learnings: "All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/PRD.md` around lines 1 - 100, The PRD
(APP-016: Atmos Computer) is written mostly in Chinese and must be converted to
English: translate all titles, headings, and body copy into clear English while
preserving technical identifiers and symbols (e.g. "Atmos Computer",
"server_id", "apps/api", "Cloudflare Relay + Durable Objects", references like
APP-012, TECH.md, M1-1, M1-2, etc.), keep tables and numbered requirement IDs
(M1-1..M2-3) intact, retain any inline Chinese quotes only when necessary for
fidelity, and ensure section references (e.g. TECH §8, TEST.md) remain correct
and unbroken; update the opening summary, background, user stories,
requirements, scope, success metrics, risks, and docs list to fluent English
without changing the meaning or semantics.
🟡 Minor comments (15)
apps/api/src/relay/register.rs-24-29 (1)

24-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix rustfmt-blocking formatting in this call.

CI format checks are currently failing for this block.

Suggested fix
-    let identity = register_computer(
-        &cp,
-        &token,
-        display_name.as_deref(),
-    )
-    .await?;
+    let identity = register_computer(&cp, &token, display_name.as_deref()).await?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/relay/register.rs` around lines 24 - 29, The call to
register_computer is misformatted and failing rustfmt; collapse the call into a
single well-formatted expression such as: let identity = register_computer(&cp,
&token, display_name.as_deref()).await?; or, if you prefer multiline, place each
argument on its own indented line and put ).await? on the same line as the
closing parenthesis so rustfmt is satisfied; update the code around the
register_computer invocation accordingly.
crates/runtime-manager/AGENTS.md-17-24 (1)

17-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced code block.

markdownlint MD040 is triggered here; please annotate the fence (for example, text) to keep docs lint-clean.

Suggested fix
-```
+```text
 crates/runtime-manager/src/
 ├── lib.rs           # Re-exports
 ├── manifest.rs      # ~/.atmos/runtime_manifest.json
 ├── identity.rs      # ~/.atmos/relay_identity.json
 ├── register.rs      # POST control plane /v1/computers/register
 └── supervisor.rs    # ensure / stop / status (feature supervisor)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @crates/runtime-manager/AGENTS.md around lines 17 - 24, The fenced code block
that shows the crates/runtime-manager/src/ tree is missing a language
identifier; update that Markdown block by adding a language tag (e.g., text) after the opening backticks so the snippet becomes text ... ```, ensuring the
docs pass markdownlint MD040.


</details>

</blockquote></details>
<details>
<summary>specs/README.md-78-78 (1)</summary><blockquote>

`78-78`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Restore APP-015 to the specs table with a deprecation note.**

APP-015 directory exists at `specs/APP/APP-015_canvas-terminal-agent-integration/` and was registered as a published spec (per commit b36cbe23). According to coding guidelines, published specs must never be removed from the table—only deprecated with a note inside their files. Restore the APP-015 entry to the table and add a deprecation marker.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/README.md` at line 78, Restore the missing APP-015 row in the specs
README table (re-add the table entry for APP-015 with the same format as other
rows) and mark it as deprecated; then add a clear deprecation marker inside the
APP-015 spec files (add a top-level "DEPRECATED" note in BRAINSTORM.md, PRD.md,
TECH.md, and TEST.md) referencing the publication commit b36cbe23 and guidance
that published specs must not be removed. Ensure the README row uses the
identifier "APP-015" and the spec folder name
"APP-015_canvas-terminal-agent-integration" so it matches existing registry
conventions.
```

</details>

</blockquote></details>
<details>
<summary>apps/api/src/relay/ingest.rs-7-15 (1)</summary><blockquote>

`7-15`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix rustfmt drift in this file before merge.**

Backend format CI is failing on this file (import ordering, let-else wrapping, and assignment formatting). Run `cargo fmt --all` and commit the result.




Also applies to: 96-99, 104-106

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/relay/ingest.rs` around lines 7 - 15, Run rustfmt (cargo fmt
--all) and commit the resulting changes to fix import ordering and formatting
drift in this file; specifically reorder and format the use statements (e.g.,
use futures_util::{SinkExt, StreamExt}; http; infra::ClientType;
serde::Deserialize; tokio::sync::{mpsc, RwLock};
tokio_tungstenite::tungstenite::Message; tokio_tungstenite::{connect_async,
MaybeTlsStream, WebSocketStream}; tracing::{error, info, warn};) and correct any
let-else wrapping and assignment formatting around the let/else blocks and
variable assignments referenced in the review (the blocks around the let-else
and assignments previously flagged on the file), then run cargo fmt --all to
ensure consistent style and commit the formatted file.
```

</details>

</blockquote></details>
<details>
<summary>apps/web/src/lib/atmos-computer-store.ts-95-98 (1)</summary><blockquote>

`95-98`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Handle empty/invalid origin normalization defensively.**

At Line 97, empty input currently normalizes to `https://`, which is not a usable origin and can break downstream URL construction.

 

<details>
<summary>💡 Suggested fix</summary>

```diff
 export function normalizedControlPlaneOrigin(raw: string): string {
   const t = raw.trim().replace(/\/+$/, '');
-  return t.startsWith('http') ? t : `https://${t}`;
+  if (!t) return '';
+  return /^https?:\/\//i.test(t) ? t : `https://${t}`;
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/atmos-computer-store.ts` around lines 95 - 98,
normalizedControlPlaneOrigin currently returns "https://" for empty/blank input
which is invalid; update the function to defensively validate the trimmed value:
after computing t = raw.trim().replace(/\/+$/, ''), if t is empty throw a clear
error (e.g., "empty origin"), then build candidate = t.startsWith('http') ? t :
`https://${t}`, validate it by constructing new URL(candidate) (catch and
rethrow a descriptive error on failure), and finally return url.origin (or the
validated candidate) from normalizedControlPlaneOrigin so downstream URL
construction never receives an invalid origin.
```

</details>

</blockquote></details>
<details>
<summary>crates/runtime-manager/src/lib.rs-23-25 (1)</summary><blockquote>

`23-25`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Apply rustfmt to the re-export block.**

Line 23–25 formatting is currently failing backend format check; please run `cargo fmt --all`.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/lib.rs` around lines 23 - 25, The re-export block
formatting is failing rustfmt; run rustfmt (cargo fmt --all) or manually
reformat the pub use register::{ default_control_plane_url,
normalize_control_plane_url, register_computer, }; so the items are formatted
per rustfmt rules (e.g., each identifier on its own line or compressed to a
single line as rustfmt dictates) — locate the pub use register::{
default_control_plane_url, normalize_control_plane_url, register_computer }
statement and apply cargo fmt to correct spacing/line breaks.
```

</details>

</blockquote></details>
<details>
<summary>apps/cli/src/main.rs-6-9 (1)</summary><blockquote>

`6-9`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix rustfmt import ordering to unblock CI.**

Line 6–9 import ordering is currently failing `cargo fmt --check` in CI; please run formatter and commit the result.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/main.rs` around lines 6 - 9, Run the Rust formatter and commit
the changes to fix the import ordering; specifically reformat the four
standalone use lines for execute_canvas/CanvasCommand,
execute_computer/ComputerCommand, execute_local/LocalCommand, and
execute_runtime/RuntimeCommand (symbols in commands::canvas, commands::computer,
commands::local, commands::runtime) so they conform to rustfmt style — e.g. run
`cargo fmt` (or `rustfmt`) which will collapse/group and reorder these imports
properly and then commit the resulting changes.
```

</details>

</blockquote></details>
<details>
<summary>skills/atmos-canvas-agent/SKILL.md-62-63 (1)</summary><blockquote>

`62-63`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Update API URL discovery order to the unified runtime manifest first.**

The global flag docs still list `~/.atmos/local/state.json` fallback, which is outdated for the unified runtime flow. Prefer `~/.atmos/runtime_manifest.json` as the canonical local discovery source to match current runtime behavior.

 

Based on learnings: "Discovery of local Atmos Server is done via `~/.atmos/runtime_manifest.json` containing `host`, `port`, `url`, and `ws_url` (no auth token)".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/atmos-canvas-agent/SKILL.md` around lines 62 - 63, Update the API URL
discovery order in the global flag docs for `--api-url <url>` so that local
discovery prefers the unified runtime manifest `~/.atmos/runtime_manifest.json`
(which contains host/port/url/ws_url) instead of the deprecated
`~/.atmos/local/state.json`; mention that the explicit flag `--api-url` and env
`ATMOS_API_URL` still take precedence, and clarify that
`~/.atmos/runtime_manifest.json` does not include an auth token (so `--api-token
<token>` or `ATMOS_API_TOKEN` remains the source for tokens when required).
```

</details>

</blockquote></details>
<details>
<summary>crates/runtime-manager/src/identity.rs-56-61 (1)</summary><blockquote>

`56-61`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Rustfmt check is currently failing for this block.**

CI already reports formatting differences here; please run `cargo fmt --all` before merge.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/identity.rs` around lines 56 - 61, The serializer
block using serde_json::to_string_pretty to produce payload (referencing payload
and RELAY_IDENTITY_FILE_NAME) is not formatted to Rustfmt standards; run rustfmt
by executing cargo fmt --all (or apply rustfmt to this file) and reformat the
snippet so that the to_string_pretty call and the map_err closure align with
rustfmt rules, then commit the formatted changes.
```

</details>

</blockquote></details>
<details>
<summary>apps/desktop/src-tauri/src/runtime.rs-20-120 (1)</summary><blockquote>

`20-120`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Rustfmt check is failing for this file.**

CI indicates formatting mismatches in method-chain and wrapping sections; please run `cargo fmt --all`.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src-tauri/src/runtime.rs` around lines 20 - 120, Rustfmt is
failing — run rustfmt to fix formatting issues and reformat the long method
chains/wrapping in this file; run cargo fmt --all and commit the changes.
Specifically, format the file containing resolve_bundled_runtime_dir and
augmented_path (and the ensure_running call chain) so method chains and long
lines adhere to rustfmt rules; if needed, wrap long expressions (like the
runtime_manager::supervisor::ensure_running(...).await.map_err(...) chain and
the std::env::join_paths(...) call) across lines so rustfmt can produce a stable
layout, then re-run cargo fmt --all and include the updated file in the PR.
```

</details>

</blockquote></details>
<details>
<summary>apps/api/src/api/canvas/agent.rs-78-86 (1)</summary><blockquote>

`78-86`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix Rustfmt violations to unblock CI.**

This file is currently failing the `cargo fmt --all -- --check` job; please format it before merge.





Also applies to: 302-305

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/api/canvas/agent.rs` around lines 78 - 86, Format the Rust
source to satisfy rustfmt: run rustfmt/cargo fmt and adjust the impl block for
CanvasAgentInvokeError (the fn new method) and the code around lines referenced
(including the other occurrence at the block around 302-305) so they follow
standard rustfmt style (spacing, indentation, and trailing commas). Specifically
ensure the impl CanvasAgentInvokeError { fn new(code: &str, message: impl
Into<String>, recoverable: bool) -> Self { ... } } is formatted by rustfmt
rather than manually altering semantics; running cargo fmt --all -- --check
locally and fixing formatting will unblock CI.
```

</details>

</blockquote></details>
<details>
<summary>apps/cli/src/commands/computer.rs-4-7 (1)</summary><blockquote>

`4-7`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix rustfmt mismatches to unblock CI.**

`cargo fmt --check` is currently failing for this file (imports/call formatting). Please run formatter and commit the result so backend format check passes.
 


Also applies to: 60-65, 162-164

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/commands/computer.rs` around lines 4 - 7, Run rustfmt (cargo
fmt) and commit the formatted file to resolve CI failures: reformat the
use/import block that currently spans multiple lines so it matches rustfmt style
(the group containing normalize_control_plane_url, read_server_identity,
register_computer, resolve_server_identity_path and supervisor::{EnsureOptions,
EnsureOutcome, DEFAULT_HOST, DEFAULT_PORT}), and fix any other formatting
mismatches in calls mentioning those symbols (e.g., where
EnsureOptions/EnsureOutcome/DEFAULT_HOST/DEFAULT_PORT are used and any calls to
normalize_control_plane_url or register_computer) so the file passes cargo fmt
--check.
```

</details>

</blockquote></details>
<details>
<summary>specs/APP/APP-016_atmos-computer/TECH.md-458-464 (1)</summary><blockquote>

`458-464`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Escape pipe characters inside inline code in the table.**

This table uses `|` inside code spans (e.g., `frame | ctrl`), which breaks Markdown table parsing. Escape as `\|` (or rephrase) so cells render correctly.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/TECH.md` around lines 458 - 464, The table's
inline code spans include unescaped pipe characters (e.g., "frame | ctrl",
"client:<session_id> | broadcast_clients") which breaks Markdown table parsing;
update the table cells that contain "|" by escaping them as "\|" inside the
inline code spans or rephrasing those code spans (for example change `frame |
ctrl` to `frame \| ctrl`, and `client:<session_id> | broadcast_clients` to
`client:<session_id> \| broadcast_clients`) for the fields `kind`, `from`, `to`,
`request_id`, `relay_seq`, and `body` so the table renders correctly.
```

</details>

</blockquote></details>
<details>
<summary>packages/relay/AGENTS.md-49-49 (1)</summary><blockquote>

`49-49`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix table cell parsing by escaping `|` in inline code.**

Line 49 uses `` `atmos computer register|start|status` ``, which splits the row into extra columns in Markdown. Escape pipes (`\|`) or split into plain text.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relay/AGENTS.md` at line 49, The Markdown table row contains inline
code `atmos computer register|start|status` which breaks the table because of
unescaped pipe characters; update the table cell in AGENTS.md by escaping the
pipes inside the code span (e.g., use backticks with `\|` between commands) or
replace the inline code with plain text so the `apps/cli` row no longer
introduces extra columns.
```

</details>

</blockquote></details>
<details>
<summary>crates/core-service/src/service/ws_message.rs-13-15 (1)</summary><blockquote>

`13-15`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Run `cargo fmt` on this file before merge.**

Backend format check is currently failing on this file, so CI will stay red until the rustfmt changes are applied.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core-service/src/service/ws_message.rs` around lines 13 - 15, The file
is failing rustfmt/format checks; run rustfmt by executing `cargo fmt` (or
`rustfmt` on this file) to reformat
crates/core-service/src/service/ws_message.rs so imports and code match project
style—ensure the use statement importing CanvasAgentDispatchOutcome,
CanvasAgentRelay, and CompleteDispatchResult (and any surrounding code) is
reformatted and then recommit the changes.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>apps/AGENTS.md (1)</summary><blockquote>

`9-20`: _💤 Low value_

**Add language specifier to the fenced code block.**

The ASCII diagram is missing a language identifier, triggering a markdown lint warning.




<details>
<summary>📝 Proposed fix</summary>

```diff
-```
+```text
                     ┌─────────────────────────────────────┐
                     │  packages/relay (control + WSS)      │
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @apps/AGENTS.md around lines 9 - 20, The fenced ASCII diagram in AGENTS.md is
missing a language specifier which triggers markdown linting; update the opening
fence for the diagram (the triple-backtick that begins the ASCII block
containing the diagram/relay/apps/api/runtime-manager text) to include a
language identifier such as "text" (e.g., change totext) so the block is
recognized as a plain-text code block and the linter warning is resolved.


</details>

</blockquote></details>
<details>
<summary>packages/relay/migrations/0001_init.sql (1)</summary><blockquote>

`1-35`: _⚡ Quick win_

**Consider adding foreign key constraints for data integrity.**

The schema lacks foreign key constraints between related tables:
- `register_tokens.tenant_id` → `tenants.token_hash`
- `computers.tenant_id` → `tenants.token_hash`  
- `client_sessions.server_id` → `computers.server_id`
- `client_sessions.tenant_id` → `tenants.token_hash`

D1 (SQLite-based) supports FK constraints and they would provide automatic referential integrity, preventing orphaned records if a tenant or computer is deleted. While application logic can enforce consistency, database-level constraints offer an additional safety layer.




<details>
<summary>🔗 Proposed enhancement with FK constraints</summary>

```diff
 CREATE TABLE register_tokens (
   token_hash TEXT PRIMARY KEY,
-  tenant_id TEXT NOT NULL,
+  tenant_id TEXT NOT NULL REFERENCES tenants(token_hash) ON DELETE CASCADE,
   expires_at INTEGER NOT NULL,
   used_at INTEGER,
   created_at INTEGER NOT NULL
 );

 CREATE TABLE computers (
   server_id TEXT PRIMARY KEY,
-  tenant_id TEXT NOT NULL,
+  tenant_id TEXT NOT NULL REFERENCES tenants(token_hash) ON DELETE CASCADE,
   secret_hash TEXT NOT NULL,
   revoked INTEGER NOT NULL DEFAULT 0,
   display_name TEXT,
   created_at INTEGER NOT NULL,
   last_seen_at INTEGER
 );

 CREATE TABLE client_sessions (
   token_hash TEXT PRIMARY KEY,
-  server_id TEXT NOT NULL,
-  tenant_id TEXT NOT NULL,
+  server_id TEXT NOT NULL REFERENCES computers(server_id) ON DELETE CASCADE,
+  tenant_id TEXT NOT NULL REFERENCES tenants(token_hash) ON DELETE CASCADE,
   expires_at INTEGER NOT NULL,
   created_at INTEGER NOT NULL
 );
```

Note: FK constraints in SQLite/D1 require `PRAGMA foreign_keys = ON` to be set at connection time.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relay/migrations/0001_init.sql` around lines 1 - 35, Add explicit
FOREIGN KEY constraints to enforce referential integrity: link
register_tokens.tenant_id -> tenants.token_hash, computers.tenant_id ->
tenants.token_hash, client_sessions.server_id -> computers.server_id and
client_sessions.tenant_id -> tenants.token_hash; add appropriate ON DELETE
behavior (e.g., CASCADE or RESTRICT) per domain rules and keep existing indexes,
and ensure the SQLite/D1 connection enables foreign keys (PRAGMA foreign_keys =
ON) so the constraints are enforced at runtime.
```

</details>

</blockquote></details>
<details>
<summary>apps/api/src/api/ws/handlers.rs (1)</summary><blockquote>

`100-100`: _💤 Low value_

**Consider narrower visibility scope for test-only usage.**

The function is now `pub(crate)`, exposing it to the entire `api` crate. If the primary purpose is to support the test module in this file (lines 155-187), consider using `pub(super)` instead, which would limit visibility to the parent module only.

If other modules in the crate genuinely need to call `push_latest_messages`, the current visibility is appropriate.




<details>
<summary>🔒 Narrow visibility if only used for testing</summary>

```diff
-pub(crate) async fn push_latest_messages(
+pub(super) async fn push_latest_messages(
     message_push_service: Arc<core_service::MessagePushService>,
     mut updates: watch::Receiver<u64>,
     conn_id: String,
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/api/ws/handlers.rs` at line 100, The function
push_latest_messages is exposed as pub(crate) but seems only used by the test
module in the same file; narrow its visibility to pub(super) by changing the
function signature from pub(crate) async fn push_latest_messages(...) to
pub(super) async fn push_latest_messages(...) so it is accessible to the parent
module and the inline tests while not exposing it to the entire crate; if other
modules actually need it, leave as-is.
```

</details>

</blockquote></details>
<details>
<summary>scripts/desktop/layout-runtime-bundle.sh (2)</summary><blockquote>

`14-17`: _💤 Low value_

**Potential for mixed release/debug binaries.**

The fallback logic switches both `api_src` and `cli_src` to debug only when the release API binary is missing. This means if a developer builds `--release --bin api` but separately builds `--debug --bin atmos`, the script will use the release API with the debug CLI. This version mismatch could cause subtle runtime compatibility issues.

Consider adding a warning when mixing build modes or documenting that both binaries should be built with the same profile.




<details>
<summary>🔔 Optional: Add build mode consistency check</summary>

```diff
  if [[ ! -f "$api_src" ]]; then
    api_src="$root_dir/target/$target_triple/debug/api$bin_ext"
    cli_src="$root_dir/target/$target_triple/debug/atmos$bin_ext"
+   echo "⚠️  Using debug builds (release not found)" >&2
+ elif [[ -f "$cli_src" ]] && [[ -f "$root_dir/target/$target_triple/debug/atmos$bin_ext" ]]; then
+   echo "⚠️  Warning: release/api exists but using debug/atmos" >&2
  fi
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/desktop/layout-runtime-bundle.sh` around lines 14 - 17, The fallback
currently sets both api_src and cli_src to the debug path when api_src is
missing, which can mix release/debug binaries; update the logic around api_src
and cli_src (and the variables root_dir, target_triple, bin_ext) to determine
each binary's profile independently and only fallback per-binary (i.e., if api
release missing use api debug, and if cli release missing use cli debug), and
add a clear warning message when the resolved profiles differ (e.g., "warning:
api built in release, cli built in debug — build both with the same profile") so
users are alerted to potential mismatches.
```

</details>

---

`44-50`: _💤 Low value_

**Version extraction assumes specific Cargo.toml formatting.**

The `grep -E '^version\s*='` pattern requires the version line to start at the beginning of the line (no indentation). While this works for typical `[package]` sections, it would fail if the Cargo.toml is reformatted with indentation or if workspace inheritance is used.

For this specific use case (`apps/desktop/src-tauri/Cargo.toml`), the current approach is likely sufficient, but consider using `cargo metadata` for more robust version extraction if this pattern is extended to other contexts.




<details>
<summary>📦 More robust alternative using cargo metadata</summary>

```diff
  if [[ -f "$root_dir/apps/desktop/src-tauri/Cargo.toml" ]]; then
    local version
-   version="$(grep -E '^version\s*=' "$root_dir/apps/desktop/src-tauri/Cargo.toml" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
+   version="$(cd "$root_dir/apps/desktop/src-tauri" && cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
    if [[ -n "$version" ]]; then
      printf '%s\n' "$version" >"$runtime_root/version.txt"
    fi
  fi
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/desktop/layout-runtime-bundle.sh` around lines 44 - 50, The version
extraction is brittle because the current grep pattern (grep -E '^version\s*=')
requires no leading whitespace; update the extraction so the "version" variable
is set robustly—either relax the grep to allow leading spaces (e.g., match
'^\s*version\s*=') when populating version and writing to
runtime_root/version.txt, or replace the grep block with a cargo metadata + jq
call to reliably query the package's version and assign it to the "version"
variable before writing to "$runtime_root/version.txt".
```

</details>

</blockquote></details>
<details>
<summary>crates/runtime-manager/Cargo.toml (1)</summary><blockquote>

`7-10`: _💤 Low value_

**Remove the unused `client` feature from default features.**

The `client` feature (line 9) is defined as empty and included in defaults, but is not used as a conditional compilation flag anywhere in the codebase. Unlike the `supervisor` feature which properly uses `#[cfg(feature = "supervisor")]`, the `client` feature has no corresponding guards, making it non-functional. Either remove it entirely or add `#[cfg(feature = "client")]` guards to the code it's intended to gate.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/Cargo.toml` around lines 7 - 10, The Cargo.toml
currently lists the empty feature "client" in default = ["client"] but there are
no #[cfg(feature = "client")] guards using it; either remove "client" from the
default features or actually gate the intended code with the feature.
Specifically, edit the features table to drop "client" from default = ["client"]
(and remove the empty client = [] entry if unused), or locate the modules/files
meant to be optional and add #[cfg(feature = "client")] (and #[cfg_attr(test,
...)] where appropriate) to the relevant functions/types so the feature is
meaningful; mirror how "supervisor" is used for reference and run cargo
build/test to verify.
```

</details>

</blockquote></details>
<details>
<summary>apps/web/src/components/canvas/CanvasAgentOverlay.tsx (1)</summary><blockquote>

`140-164`: _⚡ Quick win_

**Prefer shared UI atom instead of raw `<button>` for badge actions.**

These action buttons can use the same shared button primitive used elsewhere in this file for consistency and centralized behavior.




As per coding guidelines “UI Components: Use `@workspace/ui/components/ui/*` for atomic parts”.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/canvas/CanvasAgentOverlay.tsx` around lines 140 -
164, Replace the raw <button> elements in CanvasAgentOverlay (the ones using
onJump and onToggleFollow with Crosshair/Eye/EyeOff icons) with the shared
Button primitive from `@workspace/ui/components/ui/button`, preserving props:
type="button", title, aria-label, and onClick handlers (onJump and
onToggleFollow), and move the existing cn(...) classes into the Button's
className (or map to the Button's variant/size props if available) so styling,
hover states and accessibility remain identical; keep the icon children
(Crosshair, Eye, EyeOff) unchanged inside the Button.
```

</details>

</blockquote></details>
<details>
<summary>crates/core-service/src/service/ws_message.rs (1)</summary><blockquote>

`592-596`: _🏗️ Heavy lift_

**Keep the relay behind service methods instead of exposing it directly.**

Returning `Arc<CanvasAgentRelay>` here lets `apps/api` orchestrate dispatch state through a low-level relay object instead of a narrow `core-service` API. That makes the HTTP layer own part of the protocol contract and will be harder to evolve safely once the WS and HTTP flows drift. Prefer dedicated service methods for the relay operations the API needs.
 

Based on learnings Never implement business logic in the API layer — use `crates/core-service` instead.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core-service/src/service/ws_message.rs` around lines 592 - 596, The
public accessor canvas_agent_relay() exposing Arc<CanvasAgentRelay> must be
removed and replaced with narrow service methods that encapsulate the relay
operations the HTTP layer needs (e.g., resolve target lookup, register pending
waiter, dispatch message); keep the CanvasAgentRelay field private and implement
explicit methods like resolve_canvas_target(...), register_pending_waiter(...),
and send_via_relay(...) on the same service type to perform those actions
internally, then update callers in apps/api to use these new service methods
instead of manipulating CanvasAgentRelay directly.
```

</details>

</blockquote></details>
<details>
<summary>crates/runtime-manager/src/supervisor.rs (1)</summary><blockquote>

`169-177`: _⚡ Quick win_

**Reuse System instance in polling loop instead of recreating.**

Creating `System::new_all()` on each iteration is expensive as it scans all processes. Use `refresh_process(pid)` on an existing instance instead.



<details>
<summary>♻️ Proposed fix</summary>

```diff
+    let mut system = System::new();
     for _ in 0..50 {
         tokio::time::sleep(Duration::from_millis(100)).await;
-        let mut refresh = System::new_all();
-        refresh.refresh_process(pid);
-        if refresh.process(pid).is_none() {
+        system.refresh_process(pid);
+        if system.process(pid).is_none() {
             let _ = remove_runtime_manifest();
             return Ok(true);
         }
     }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/supervisor.rs` around lines 169 - 177, The loop
currently calls System::new_all() every iteration which is expensive; create a
single System instance before the for-loop (let mut refresh = System::new_all())
and reuse it inside the loop, calling refresh.refresh_process(pid) on each
iteration and then checking refresh.process(pid).is_none(); keep the existing
sleep, the remove_runtime_manifest() call, and return Ok(true) behavior
unchanged.
```

</details>

</blockquote></details>
<details>
<summary>apps/web/src/components/dialogs/AtmosComputerSection.tsx (1)</summary><blockquote>

`365-376`: _💤 Low value_

**Consider clearing tokenReveal on subsequent actions to reduce exposure window.**

The revealed token stays visible indefinitely after generation. Consider clearing `tokenReveal` when the user navigates away, performs other actions, or after a timeout to minimize the exposure window of the sensitive credential.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx` around lines 365 -
376, The revealed token (tokenReveal) in AtmosComputerSection remains visible
indefinitely; update the component to clear tokenReveal on subsequent actions
and after a short timeout: find the tokenReveal state and the copyRevealedToken
handler inside AtmosComputerSection and implement a cleanup that (1) starts a
configurable timer (e.g., 30s) when tokenReveal is set and clears tokenReveal
when it expires, (2) clears tokenReveal immediately after copyRevealedToken
completes, and (3) clears tokenReveal when the dialog/unmounts or user navigates
away (useEffect cleanup tied to component unmount or modal close). Ensure any
created timers are cleared to avoid leaks.
```

</details>

</blockquote></details>

</blockquote></details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `86f5d80c-f885-4d6c-b28d-fbd496fcba85`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9067b27d42826ef4a18c1c5271bdcf61848b0ff2 and beb19b3a5e4856c13a74821b47ae231e97836391.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`
* `apps/desktop/src-tauri/gen/schemas/capabilities.json` is excluded by `!**/gen/**`
* `bun.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (92)</summary>

* `.github/workflows/deploy-relay.yml`
* `.github/workflows/release-desktop.yml`
* `.gitignore`
* `AGENTS.md`
* `agents/AGENTS.md`
* `apps/AGENTS.md`
* `apps/api/AGENTS.md`
* `apps/api/Cargo.toml`
* `apps/api/src/api/canvas/agent.rs`
* `apps/api/src/api/canvas/mod.rs`
* `apps/api/src/api/ws/handlers.rs`
* `apps/api/src/app_state.rs`
* `apps/api/src/main.rs`
* `apps/api/src/relay/ingest.rs`
* `apps/api/src/relay/mod.rs`
* `apps/api/src/relay/register.rs`
* `apps/cli/AGENTS.md`
* `apps/cli/Cargo.toml`
* `apps/cli/src/commands/canvas.rs`
* `apps/cli/src/commands/computer.rs`
* `apps/cli/src/commands/local.rs`
* `apps/cli/src/commands/mod.rs`
* `apps/cli/src/commands/review.rs`
* `apps/cli/src/commands/runtime.rs`
* `apps/cli/src/commands/update.rs`
* `apps/cli/src/main.rs`
* `apps/desktop/AGENTS.md`
* `apps/desktop/src-tauri/Cargo.toml`
* `apps/desktop/src-tauri/binaries/runtime/current/.gitkeep`
* `apps/desktop/src-tauri/capabilities/default.json`
* `apps/desktop/src-tauri/src/commands.rs`
* `apps/desktop/src-tauri/src/main.rs`
* `apps/desktop/src-tauri/src/runtime.rs`
* `apps/desktop/src-tauri/src/state.rs`
* `apps/desktop/src-tauri/tauri.conf.json`
* `apps/desktop/src-tauri/tauri.debug.conf.json`
* `apps/web/.env.example`
* `apps/web/AGENTS.md`
* `apps/web/src/api/ws-api.ts`
* `apps/web/src/components/canvas/CanvasAgentOverlay.tsx`
* `apps/web/src/components/canvas/CanvasView.tsx`
* `apps/web/src/components/canvas/__tests__/canvas-agent-bus.test.ts`
* `apps/web/src/components/canvas/__tests__/canvas-agent-presence.test.ts`
* `apps/web/src/components/canvas/canvas-agent-bus.ts`
* `apps/web/src/components/canvas/canvas-agent-presence.ts`
* `apps/web/src/components/canvas/use-canvas-agent-bridge.ts`
* `apps/web/src/components/dialogs/AtmosComputerSection.tsx`
* `apps/web/src/components/dialogs/SettingsModal.tsx`
* `apps/web/src/hooks/use-websocket.ts`
* `apps/web/src/lib/atmos-access-token.ts`
* `apps/web/src/lib/atmos-computer-store.ts`
* `apps/web/src/lib/desktop-runtime.ts`
* `apps/web/src/lib/nuqs/searchParams.ts`
* `crates/AGENTS.md`
* `crates/core-service/src/lib.rs`
* `crates/core-service/src/service/canvas_agent_relay.rs`
* `crates/core-service/src/service/mod.rs`
* `crates/core-service/src/service/ws_message.rs`
* `crates/infra/src/utils/system_skill_sync.rs`
* `crates/infra/src/websocket/message.rs`
* `crates/infra/src/websocket/mod.rs`
* `crates/runtime-manager/AGENTS.md`
* `crates/runtime-manager/Cargo.toml`
* `crates/runtime-manager/README.md`
* `crates/runtime-manager/src/identity.rs`
* `crates/runtime-manager/src/lib.rs`
* `crates/runtime-manager/src/manifest.rs`
* `crates/runtime-manager/src/register.rs`
* `crates/runtime-manager/src/supervisor.rs`
* `docs/development.md`
* `packages/AGENTS.md`
* `packages/relay/AGENTS.md`
* `packages/relay/README.md`
* `packages/relay/migrations/0001_init.sql`
* `packages/relay/package.json`
* `packages/relay/src/index.ts`
* `packages/relay/src/server-hub.ts`
* `packages/relay/tsconfig.json`
* `packages/relay/wrangler.toml`
* `scripts/desktop/README.md`
* `scripts/desktop/before-build.mjs`
* `scripts/desktop/layout-runtime-bundle.sh`
* `scripts/desktop/prepare-sidecar.sh`
* `skills/atmos-canvas-agent/SKILL.md`
* `skills/system-skills-manifest.json`
* `specs/AGENTS.md`
* `specs/APP/APP-012_remote-access/TECH.md`
* `specs/APP/APP-016_atmos-computer/BRAINSTORM.md`
* `specs/APP/APP-016_atmos-computer/PRD.md`
* `specs/APP/APP-016_atmos-computer/TECH.md`
* `specs/APP/APP-016_atmos-computer/TEST.md`
* `specs/README.md`

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* apps/desktop/src-tauri/src/state.rs

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +26 to +32
- name: Deploy Worker
working-directory: packages/relay
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# Optional: set repo secret CLOUDFLARE_ACCOUNT_ID if the token spans multiple accounts
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: bunx wrangler deploy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add migration check or step before deploying the Worker.

The workflow deploys the Cloudflare Worker without ensuring that D1 migrations have been applied. If the code depends on schema changes introduced in a migration, deploying before migrating will cause runtime failures or data corruption in production.

Consider adding a step before line 26 to either:

  1. Automatically run wrangler d1 migrations apply atmos-computer-cp (safest), or
  2. Check that migrations are up-to-date and fail if not, or
  3. Add a manual approval gate with a checklist confirming migrations were run.

Based on learnings: Deploy relay only after D1 migrations have been completed (reference relay/README.md for migration procedures).

🛡️ Proposed fix: Add migration step
       - uses: ./.github/actions/setup-bun

+      - name: Apply D1 Migrations
+        working-directory: packages/relay
+        env:
+          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+        run: bunx wrangler d1 migrations apply atmos-computer-cp --remote
+
       - name: Deploy Worker
         working-directory: packages/relay
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-relay.yml around lines 26 - 32, Add a pre-deploy
migration step/check before the "Deploy Worker" job that ensures D1 migrations
are applied: locate the "Deploy Worker" step which runs "bunx wrangler deploy"
and insert a preceding step that either runs the D1 migration command (wrangler
d1 migrations apply atmos-computer-cp) or verifies migrations are up-to-date and
fails the workflow if not (alternatively implement a manual approval gate);
ensure this step uses the same Cloudflare credentials (CLOUDFLARE_API_TOKEN and
CLOUDFLARE_ACCOUNT_ID) and references the relay migration guidance in
relay/README.md so the worker is only deployed after migrations complete.

Comment thread apps/api/src/relay/ingest.rs Outdated
Comment on lines +93 to +99
if env.v != 1 || env.kind != "frame" {
continue;
}
let Some(from) = env.from.clone() else { continue };
let Some(rest) = from.strip_prefix("client:") else {
continue;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect relay worker/hub message kinds and lifecycle events.
fd -i 'server-hub.ts|index.ts' packages/relay/src
rg -n --type=ts -C3 'kind' packages/relay/src
rg -n --type=ts -C3 'disconnect|unregister|close|leave|client:' packages/relay/src

# 2) Inspect ingest session lifecycle handling.
rg -n --type=rs -C3 'ensure_session|sessions|unregister|kind' apps/api/src/relay/ingest.rs

Repository: AruNi-01/atmos

Length of output: 5496


🏁 Script executed:

cat -n apps/api/src/relay/ingest.rs

Repository: AruNi-01/atmos

Length of output: 7400


🏁 Script executed:

rg -n --type=ts 'kind.*disconnect|kind.*close|kind.*leave|client.*disconnect' packages/relay/src -A 2 -B 2

Repository: AruNi-01/atmos

Length of output: 40


Implement per-client session cleanup on relay client disconnect.

Sessions are inserted with registered connections and spawned background tasks (lines 162, 166–189), but cleanup only occurs during full upstream relay disconnect (lines 140–144). With long-lived relay links and client churn, this accumulates stale sessions and spawned tasks—the message forwarding task and push notification task remain running indefinitely for disconnected clients.

The relay protocol provides no explicit disconnect message kind, so cleanup must be triggered by detecting when relay clients no longer send messages. Consider implementing an inactivity timeout or heartbeat mechanism to detect and clean up stale sessions during normal operation, not just on full relay shutdown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/relay/ingest.rs` around lines 93 - 99, Sessions for relay
clients are never cleaned up on per-client disconnects, so add
inactivity/heartbeat-based cleanup: augment the Session struct (and where
sessions are inserted) with a last_seen timestamp and a cancellation handle for
the spawned tasks (message forwarding task and push-notify task), update the
code that processes incoming env messages (checks for env.v, env.kind, env.from)
to refresh last_seen for that client, and spawn a single background monitor (or
per-session timeout) using tokio::time::sleep/interval that periodically scans
the sessions map and removes any session whose last_seen exceeds the configured
timeout by removing it from the sessions map, aborting/awaiting its task handles
and closing any channels; ensure removal uses the same session key logic (the
from.strip_prefix("client:") result) so cleanup targets the correct session
entry.

Comment thread apps/api/src/relay/register.rs Outdated
Comment on lines +99 to +115
fn legacy_status(status: &runtime_manager::supervisor::RuntimeStatus) -> LegacyLocalRuntimeStatus {
LegacyLocalRuntimeStatus {
installed: status.installed,
running: status.running,
healthy: status.healthy,
pid: status.pid,
host: status.host.clone(),
port: status.port,
url: status.url.clone(),
runtime_dir: status.runtime_dir.clone(),
api_bin_path: status.api_bin_path.clone(),
cli_bin_path: None,
web_dir: None,
log_path: status.log_path.clone(),
version: status.version.clone(),
started_at: None,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t emit 0.0.0.0 as the legacy connect address.

When --lan is used, runtime_status() will reflect the wildcard bind host, and this adapter copies it straight into host and url. 0.0.0.0 is not a routable client address, so older consumers that reuse this legacy payload to call the API will fail in LAN mode. Normalize the legacy output to a concrete connect address, or split bind-vs-connect addresses before serializing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/commands/local.rs` around lines 99 - 115, The legacy_status
adapter currently copies the bind host directly into LegacyLocalRuntimeStatus
causing a non-routable "0.0.0.0" to be emitted; in legacy_status (working with
runtime_manager::supervisor::RuntimeStatus and returning
LegacyLocalRuntimeStatus) detect when status.host == "0.0.0.0" and replace it
with a concrete connect address (e.g. "127.0.0.1" or the appropriate local IP)
before assigning host and constructing url so the legacy payload contains a
routable address; ensure the url field is updated to reflect the substituted
host.

}
]
},
"shell:default",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check Tauri shell:default permission scope and runtime-manager requirements

# Check Tauri documentation for shell:default capabilities
echo "=== Checking Tauri shell permission documentation ==="
echo "Tauri shell:default may include shell:allow-execute, shell:allow-open, and other broad permissions."
echo "Reference: https://tauri.app/reference/acl/permissions/#shell"

# Search for shell/execute calls in runtime-manager and desktop code
echo ""
echo "=== Searching for shell execution in runtime-manager ==="
rg -n -C3 --type=rust 'Command::new|spawn|execute' crates/runtime-manager/

echo ""
echo "=== Searching for shell execution in desktop ==="
rg -n -C3 --type=rust 'Command::new|spawn|execute|shell' apps/desktop/src-tauri/

Repository: AruNi-01/atmos

Length of output: 8413


🏁 Script executed:

# Check the actual capabilities/default.json file
cat -n apps/desktop/src-tauri/capabilities/default.json

Repository: AruNi-01/atmos

Length of output: 994


🏁 Script executed:

# Find all shell-related commands and their exact usage in the codebase
echo "=== Checking ensure_running function ==="
rg -n -A 10 'fn ensure_running|pub fn ensure_running' crates/runtime-manager/src/

echo ""
echo "=== All Command::new usages in desktop ==="
rg -n 'Command::new' apps/desktop/src-tauri/src/ -A 2

echo ""
echo "=== Checking for shell:default in Tauri configs ==="
rg -n 'shell:' apps/desktop/src-tauri/ -r

Repository: AruNi-01/atmos

Length of output: 1845


🏁 Script executed:

# Get more context on what binaries are actually spawned
echo "=== Detailed look at supervisor.rs spawning ==="
sed -n '266,360p' crates/runtime-manager/src/supervisor.rs

echo ""
echo "=== Check what cmd is in commands.rs editor opening ==="
sed -n '50,70p' apps/desktop/src-tauri/src/commands.rs

echo ""
echo "=== Look for previous shell capability config in git history ==="
git log --oneline -n 20 apps/desktop/src-tauri/capabilities/default.json

Repository: AruNi-01/atmos

Length of output: 4312


🌐 Web query:

Tauri shell:default permission scope Tauri 2 reference

💡 Result:

In Tauri 2, the shell plugin does not have a single "default permission scope" that grants broad access; instead, it follows a principle of least privilege where access must be explicitly configured via the application's capability system [1][2]. To use the shell plugin, you must define permissions in your capabilities (typically located in src-tauri/capabilities/) [2]. The shell plugin provides specific permission identifiers that you can include in your capability configuration [1]: 1. shell:allow-open: Allows the use of the open API, which opens paths or URLs with the system's default application [3][1]. By default, this is restricted by a regex (defaulting to ^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+) to prevent arbitrary command execution [1]. 2. shell:allow-execute: Allows the execution of programs [1]. When using this, you must define a scope that explicitly lists the allowed programs (CLIs), their commands, and permitted arguments [1]. 3. shell:allow-spawn: Allows spawning processes [2]. If you attempt to execute a command without configuring it in the shell plugin's scope, the request will be rejected [1][2]. To configure the scope for shell:allow-execute, you must add an entry to your capability file that specifies the allowed program [1]: { "identifier": "shell:allow-execute", "allow": [ { "name": "my-program", "cmd": "path/to/executable", "args": ["arg1", { "validator": "\S+" }] } ] } In this configuration, the args field controls what arguments can be passed: true allows any arguments, false allows none, and an array allows specific fixed values or regex validators [1]. If you do not define these permissions and scopes, the shell plugin will not allow the execution of external commands [1][2].

Citations:


🏁 Script executed:

# Check the actual git diff to see what changed
git show beb19b3a:apps/desktop/src-tauri/capabilities/default.json 2>/dev/null | head -30

echo ""
echo "=== Check Tauri version in Cargo.toml ==="
rg -n 'tauri|shell' apps/desktop/src-tauri/Cargo.toml | grep -E 'tauri.*=' | head -10

echo ""
echo "=== Search for shell:default definition in the repo ==="
rg -r 'shell:default|shell.*default' apps/desktop/src-tauri/ --type json --type toml

echo ""
echo "=== Check if there's a shell permissions config file ==="
find apps/desktop -name '*shell*' -o -name '*capabilities*' | head -20

Repository: AruNi-01/atmos

Length of output: 1344


🌐 Web query:

Tauri "shell:default" permission identifier v2

💡 Result:

In Tauri v2, the permission identifier is formatted as ":" and the ":default" suffix denotes the default permission set for a plugin or application. Specifically for the shell plugin, the identifier is: shell:default This grants the shell plugin’s default set of permissions (i.e., the plugin is allowed “out of the box” with its pre-defined minimal feature set), and it is referenced from your app capabilities (e.g., src-tauri/capabilities/.json/.toml) to allow calls like shell.open in the frontend. [1][2] If you instead want to grant only particular shell capabilities, you would typically use more specific identifiers such as shell:allow-open and/or shell:allow-execute (rather than shell:default). [2][3] How to verify/locate it in your project: - Check your src-tauri/capabilities/default.json (or other capability file) for "shell:default". [2] - If you don’t have it, you can add it to the permissions array of the capability for the target window label. [2]

Citations:


🌐 Web query:

Tauri shell:default scope what permissions included allow-execute allow-open

💡 Result:

In Tauri v2, the shell plugin uses a capability-based permission system to manage access to shell commands [1]. The shell:default permission set is designed to provide a secure, minimal feature set out of the box [2]. Regarding the specific permissions you mentioned: 1. shell:allow-open: This permission is included in the shell:default set [1]. It enables the open command, which allows the application to open files or URLs using the system's default application [1][3]. By default, this is configured with a restricted scope that only permits opening http(s)://, tel:, and mailto: links [1][4]. 2. shell:allow-execute: This permission is NOT included in the shell:default set [1]. It is a separate, more powerful permission that enables the execution of arbitrary system commands [1]. Because it is potentially dangerous, it must be explicitly added to your application's capabilities configuration [1]. When using shell:allow-execute, you must define a specific scope that restricts which programs and arguments are allowed to be executed [1][4]. In summary, shell:default provides safe, limited access to the open command, while shell:allow-execute requires manual configuration to ensure security by restricting command execution to a known, safe list of programs [1][4].

Citations:


🏁 Script executed:

# Check the generated capabilities schema to see what permissions are defined
cat apps/desktop/src-tauri/gen/schemas/capabilities.json | grep -A 5 -B 5 '"shell:' | head -100

Repository: AruNi-01/atmos

Length of output: 634


🏁 Script executed:

# Search for all shell permissions in the repo
echo "=== Looking for shell:allow-execute ==="
rg -r 'shell:allow-execute|allow-execute' apps/desktop/src-tauri/

echo ""
echo "=== Check all capability files ==="
find apps/desktop/src-tauri/capabilities -type f -exec echo "File: {}" \; -exec cat {} \;

echo ""
echo "=== Check tauri.conf.json for any shell config ==="
cat apps/desktop/src-tauri/tauri.conf.json 2>/dev/null | head -50

Repository: AruNi-01/atmos

Length of output: 8404


🏁 Script executed:

# Check full tauri.conf.json, especially the shell plugin section
cat apps/desktop/src-tauri/tauri.conf.json | jq '.plugins.shell?' 2>/dev/null || echo "=== Checking raw file ===" && grep -A 20 '"shell"' apps/desktop/src-tauri/tauri.conf.json

echo ""
echo "=== Search for any scope definition in tauri conf ==="
grep -r 'scope\|allow' apps/desktop/src-tauri/tauri.conf.json

Repository: AruNi-01/atmos

Length of output: 118


Add shell:allow-execute with a restrictive scope back to the permissions.

The current shell:default permission only grants shell:allow-open (opening files/URLs with system defaults), but the code requires shell:allow-execute to:

  • Spawn the API binary via shell in supervisor.rs (Command::new(shell))
  • Launch editor processes in commands.rs (Command::new(editor) for code, cursor, zed, vim, idea)

The previous configuration used shell:allow-execute with an explicit allowlist, which is the correct security approach. This should be restored with a scope restricting execution to the necessary binaries:

Suggested configuration
{
  "identifier": "shell:allow-execute",
  "allow": [
    {
      "name": "open-editor",
      "sidecar": false
    },
    {
      "name": "spawn-api",
      "sidecar": false
    }
  ]
}

Without this permission, the desktop application cannot function as the shell execution calls will be rejected by Tauri's ACL system.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src-tauri/capabilities/default.json` at line 22, Add the missing
Tauri permission entry "shell:allow-execute" to the capabilities JSON with a
restrictive allowlist so the app can spawn processes; specifically, update
apps/desktop/src-tauri/capabilities/default.json to include an "identifier":
"shell:allow-execute" entry that only allows the named actions used by the code
(e.g., "spawn-api" for the supervisor.rs Command::new(shell) usage and
"open-editor" for the editor launches in commands.rs Command::new(editor)),
restoring the previous pattern of scoped sidecar=false allow entries to permit
execution only for those binaries.

Comment on lines +24 to +27
if t.starts_with("http://") || t.starts_with("https://") {
t.to_string()
} else {
format!("https://{t}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject insecure control-plane URLs by default.

Line 24–27 preserves http:// input, which can expose register tokens and returned secrets in plaintext. Restrict to https:// (optionally allow http://localhost/127.0.0.1 for local dev only).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/register.rs` around lines 24 - 27, The current
URL-normalization snippet in register.rs accepts plain "http://" inputs
(variable t), which can leak secrets; change the logic to only accept "https://"
by default: if t already starts_with("https://") return it as-is; if it
starts_with("http://") then parse t (e.g., with the URL parser) and only allow
it when the host is "localhost" or "127.0.0.1" (and optionally "::1") to support
local dev; for any other input (including bare hostnames) prepend "https://" and
return that value. Ensure you validate/parse t so you correctly detect the host
for the http exception and avoid accepting arbitrary http URLs.

Comment on lines +48 to +54
let client = reqwest::Client::new();
let res = client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| format!("register request failed: {e}"))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current register path uses reqwest client without timeout configuration.
rg -nP 'Client::new\(\)|Client::builder\(\)|\.timeout\(' crates/runtime-manager/src/register.rs -C 2

Repository: AruNi-01/atmos

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Get full context around lines 48-54 in register.rs
cat -n crates/runtime-manager/src/register.rs | sed -n '40,65p'

Repository: AruNi-01/atmos

Length of output: 1044


🏁 Script executed:

#!/bin/bash
# Check if there's any timeout configuration elsewhere in the module
rg -n 'timeout|Duration|time::' crates/runtime-manager/src/register.rs

Repository: AruNi-01/atmos

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for any wrapper functions or middleware that might add timeout
rg -n 'pub.*fn.*register|async.*fn' crates/runtime-manager/src/register.rs | head -20

Repository: AruNi-01/atmos

Length of output: 93


Add an explicit HTTP timeout for register calls.

Line 48–54 sends a network call without timeout; transient network hangs can block atmos computer register indefinitely.

Proposed fix
+use std::time::Duration;
@@
-    let client = reqwest::Client::new();
+    let client = reqwest::Client::builder()
+        .timeout(Duration::from_secs(15))
+        .build()
+        .map_err(|e| format!("http client build failed: {e}"))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let client = reqwest::Client::new();
let res = client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| format!("register request failed: {e}"))?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()
.map_err(|e| format!("http client build failed: {e}"))?;
let res = client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| format!("register request failed: {e}"))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/register.rs` around lines 48 - 54, The register
code currently creates a reqwest client with reqwest::Client::new() and issues
.post(...).send() without any timeout; replace that with a client built via
reqwest::Client::builder().timeout(Duration::from_secs(<N>)).build() (import
std::time::Duration) so the request will fail after a bounded period, and
propagate any builder build error (e.g., via map_err) before calling
.post(...).send(). This ensures the register request (.post/.send) uses an
explicit HTTP timeout instead of potentially hanging forever.

Comment on lines +445 to +446
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket Upgrade", { status: 426 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make WebSocket Upgrade header check case-insensitive.

Line 445 and Line 493 compare Upgrade with "websocket" using exact case, which can reject valid requests (e.g. WebSocket). Normalize before comparing.

Suggested fix
-  if (request.headers.get("Upgrade") !== "websocket") {
+  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
     return new Response("Expected WebSocket Upgrade", { status: 426 });
   }
...
-  if (request.headers.get("Upgrade") !== "websocket") {
+  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
     return new Response("Expected WebSocket Upgrade", { status: 426 });
   }

Also applies to: 493-494

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relay/src/index.ts` around lines 445 - 446, The WebSocket Upgrade
header check in packages/relay/src/index.ts uses a case-sensitive equality
(request.headers.get("Upgrade") !== "websocket") which can reject valid values
like "WebSocket"; update both checks (the one around the Response("Expected
WebSocket Upgrade") and the similar check at lines ~493-494) to normalize the
header value before comparing by retrieving the header into a variable (e.g.,
const upgrade = request.headers.get("Upgrade")), guarding for null/undefined,
and comparing upgrade.toLowerCase() !== "websocket" (or use optional chaining:
(request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") so the
comparison becomes case-insensitive.

Comment thread packages/relay/src/server-hub.ts
Comment on lines +1 to +101
# TEST · APP-016:Atmos Computer

> **命名**:用户向功能名 **Atmos Computer**;传输层为 **Cloudflare Relay + Durable Objects**(见 `TECH.md`)。
>
> 测试计划:**验什么、怎么算通过**。实现完成后由 `atmos-specs-test-run` 技能将场景落实为自动化测试。

## 1. 测试策略

| 层级 | 范围 |
|------|------|
| **单元** | 信封解析、路由决策、状态机迁移、HMAC/JWT 辅助函数。 |
| **集成** | Worker + DO 本地 **miniflare** / **wrangler dev**;模拟 **Computer** 上 Server 与 Client 双端。 |
| **E2E** | 真实 CF staging;一台真实 **Computer** + Desktop/Web 客户端;配对全流程。 |
| **安全** | 无效 token、错误 `server_id`、过期配对码、重放配对请求。 |

## 2. 关键场景(Given / When / Then)

### S1:配对注册

- **Given** 用户已在 Control Plane 登录
- **When** 用户申请配对码并在 **Computer**(其 Server)上执行注册
- **Then** Control Plane 返回 `server_id` 与 `server_secret`;**Computer** 本地写入 `relay_identity.json`;用户 **Computer** 列表中出现新条目

### S2:Computer 上 Server 出站连接 Relay

- **Given** 有效 `relay_identity.json` 与网络可达 Relay
- **When** **Computer** 上 Server 进程启动出站客户端
- **Then** DO 进入 `READY`(或等价);Control Plane/诊断接口显示该 **Computer** **在线**

### S3:Client 经 Relay 订阅 Computer

- **Given** 目标 **Computer** 已在线(Server 已出站接入 Relay)
- **When** Client 使用合法短期 token 连接 `wss://.../v1/client?server_id=...`
- **Then** Client 收到 `ctrl: subscribed`(或等价);随后业务帧可双向透传

### S4:切换 Atmos Computer

- **Given** 用户已添加 **Computer** A 与 **Computer** B
- **When** 用户在 UI 从 A 切换到 B
- **Then** 与 A 的 WS 关闭或与 A 的订阅取消;与 B 的新连接建立;**无**跨 **Computer** 的终端/Canvas 状态泄漏(新连接后状态来自 B)

### S5:Computer 离线

- **Given** Client 已连接且 **Computer** 上 Server 正常运行
- **When** Server 进程被杀死或网络断开超过阈值
- **Then** Client 收到 `server_offline`(或等价);UI 展示可恢复状态;**不**静默失败

### S6:鉴权失败

- **Given** 攻击者持有错误 token 或他人 `server_id`
- **When** 尝试建立 Client WS
- **Then** 连接在握手阶段被拒绝;无业务数据泄露

### S7:CLI 与 UI 共用当前所选 Atmos Computer(回归)

- **Given** 用户在 Web/Desktop 中 **当前所选 Computer = 本机 loopback**(或未启用 Relay 的等价场景)
- **When** 用户在 Web 打开 Canvas 并开启 bridge,且在终端执行 `atmos canvas status`,且 **CLI 上下文与 UI 为同一 Computer**
- **Then** 在无需手抄 `ATMOS_API_URL` 的前提下(依赖 `runtime_manifest` / `state.json` / 共享上下文等,以实现为准),`bridge` 状态与 UI **一致**
- **And** 若将 UI 切换到 **另一台 Computer**,同一终端在未改 CLI 上下文时不应再假装仍代表原 **Computer**(应报错、提示切换上下文或显式 `--api-url`,具体 UX 在实现时定稿)

### S8:Replay(M2)

- **Given** M2 已启用 `ring` 与 `relay_seq`
- **When** Client 在收到至少一条带 `relay_seq` 的下行帧后断网 5s 再重连,并携带 `last_seen_relay_seq`
- **Then** Client 收到缺失区间内的下行事件(在 `ring` 容量内);无重复执行破坏数据完整性的 **已知** 变异用例通过(具体用例在实现时绑定 Canvas/终端各一条)

### S9:`atmos review` 与 API 单一事实来源(M1-7)

- **Given** **所选 Atmos Computer** 上 `apps/api` 已启动,且 Web 已登录 **同一 Computer**、可看到某 review 会话
- **When** 在终端执行 `atmos review session show <id>`(或等价子命令),且 **CLI 的 Computer 上下文与 Web 一致**
- **Then** 返回的 JSON 与 Web 中同一会话 **一致**;在 CLI 中创建/更新 comment 后,刷新 Web **立即可见**(无「CLI 写本地库、UI 经 API 读另一数据源」的双轨)
- **And** 实现上 **不存在** CLI 进程内 `DbConnection::new()` + `ReviewService` 的 review 数据路径(代码审查或 grep 门禁可验)

## 3. 性能与容量(验收阈值,可调整)

| 项 | 阈值(建议初值) |
|----|------------------|
| 单 DO 并发 Client 数 | ≥ 5(M1);≥ 20(M3 目标) |
| 环形缓冲深度 | M2:≥ 100 条或 ≥ 256KB(先小后大) |
| Server(**Computer** 上进程)重连退避 | 最大间隔 ≤ 60s |

## 4. 回归与兼容

- **APP-015**:Canvas CLI + bridge 在 **仅 loopback** 与 **Relay** 两种模式下各跑一遍 `status` / `get-state`(若 M1 已接 Relay)。
- **APP-016 M1-7**:`review` 子命令 **仅** 命中 `apps/api`;与 **S9** 场景一并回归。
- **APP-002**:终端多路复用基本会话在切换 **Computer** 后重新建立,无僵尸 session(与 TECH 中 `conn_id` 策略一致)。

## 5. 手动测试清单(发版前)

- [ ] 新用户首次配对全流程(录屏)
- [ ] 吊销 **Computer** 后,旧 token 立即失效
- [ ] 同一用户两台 **Computer** 同时在线,切换无串线
- [ ] Relay staging 故障注入(503)时 UI 文案与重试按钮

## 6. Coverage Status

> 实现并跑完自动化测试后,由负责人在此追加一行:**日期 — 工具 — 覆盖范围 — 结论**。

---

*场景增补请同步 PRD Must/Should 条目。*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Spec text should be in English for repository compliance.

Most of this TEST spec (title, headings, and body) is in Chinese. Please convert it to English and keep any Chinese only as inline quotations if needed.

As per coding guidelines “All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/TEST.md` around lines 1 - 101, The spec file
APP-016: Atmos Computer contains Chinese text for title, headings, and body;
convert the entire document to clear, idiomatic English (title, section
headings, tables, scenario descriptions S1–S9, performance/capacity and
checklist items, and the Coverage Status note) while preserving technical
identifiers like "Atmos Computer", "Cloudflare Relay + Durable Objects",
"TECH.md", "atmos-specs-test-run", and symbols like S1–S9 and APP-016; keep any
original Chinese only as inline quotations where context demands, ensure
scenario Given/When/Then semantics remain unchanged, and maintain the same
structure (tables and bullet lists) and references to artifacts (e.g.,
relay_identity.json, wss URL, ring/relay_seq, apps/api) so automated tools
consuming TEST.md still recognize them.

Proxy HTTP to apps/api over the Server outbound WebSocket so canvas,
review, and Web REST work in relay mode; persist gateway URL in client state.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 36 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/cli/src/commands/review.rs">

<violation number="1" location="apps/cli/src/commands/review.rs:18">
P2: `session-list` no longer enforces that exactly one of `--workspace` or `--project` is provided, allowing empty-target requests.</violation>
</file>

<file name="apps/cli/src/api_client.rs">

<violation number="1" location="apps/cli/src/api_client.rs:42">
P2: `--timeout-ms` is not honored as specified because the client always adds an extra 5000ms. This can break script expectations for strict request deadlines.</violation>
</file>

<file name="packages/relay/src/server-hub.ts">

<violation number="1" location="packages/relay/src/server-hub.ts:205">
P1: `fulfillHttpResponse` deletes the pending entry too early, so malformed upstream HTTP responses can hang until timeout instead of failing immediately.</violation>
</file>

<file name="apps/api/src/relay/ingest.rs">

<violation number="1" location="apps/api/src/relay/ingest.rs:107">
P2: Handling relay HTTP requests inline blocks the single ingest loop; a slow proxied HTTP call can stall processing for all relay websocket traffic.</violation>
</file>

<file name="crates/runtime-manager/src/manifest.rs">

<violation number="1" location="crates/runtime-manager/src/manifest.rs:135">
P1: Don’t fail API URL resolution when optional `local/state.json` is unreadable; fall back to the runtime manifest instead.</violation>
</file>

<file name="crates/runtime-manager/src/client_state.rs">

<violation number="1" location="crates/runtime-manager/src/client_state.rs:52">
P2: `state.json` may be written with overly permissive default file permissions while containing a client token. Restrict permissions when writing this file.</violation>
</file>

<file name="skills/atmos-canvas-agent/SKILL.md">

<violation number="1" location="skills/atmos-canvas-agent/SKILL.md:62">
P2: The Global flags table omits `--api-url` and `--api-token`, even though the CLI still supports them and requires token configuration on 401 responses.</violation>

<violation number="2" location="skills/atmos-canvas-agent/SKILL.md:134">
P2: `PERMISSION_DENIED` is documented as connectivity failure, but current CLI behavior ties permission problems to auth/token handling (401), not API reachability.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread packages/relay/src/server-hub.ts
Comment thread crates/runtime-manager/src/manifest.rs Outdated
Comment thread apps/cli/src/commands/review.rs
Comment thread apps/cli/src/api_client.rs Outdated
Comment thread apps/api/src/relay/ingest.rs Outdated
Comment thread crates/runtime-manager/src/client_state.rs Outdated
Comment thread skills/atmos-canvas-agent/SKILL.md Outdated
Comment thread skills/atmos-canvas-agent/SKILL.md Outdated
Split server discovery (runtime_manifest) from UI Computer target;
relay sessions write api_base_url and gateway_token at ~/.atmos root.
Harden control-plane URLs, gateway pending map, async HTTP ingest,
client-session permissions, and CLI session-list validation.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 23 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/runtime-manager/src/client_session.rs">

<violation number="1" location="crates/runtime-manager/src/client_session.rs:42">
P2: Do not silently fall back to `./.atmos` for client-session storage; it can write/read gateway tokens from an unintended working-directory path and break the expected shared `~/.atmos` session behavior.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic


pub fn client_session_path() -> PathBuf {
atmos_home_dir()
.unwrap_or_else(|_| PathBuf::from(".atmos"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not silently fall back to ./.atmos for client-session storage; it can write/read gateway tokens from an unintended working-directory path and break the expected shared ~/.atmos session behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/runtime-manager/src/client_session.rs, line 42:

<comment>Do not silently fall back to `./.atmos` for client-session storage; it can write/read gateway tokens from an unintended working-directory path and break the expected shared `~/.atmos` session behavior.</comment>

<file context>
@@ -0,0 +1,107 @@
+
+pub fn client_session_path() -> PathBuf {
+    atmos_home_dir()
+        .unwrap_or_else(|_| PathBuf::from(".atmos"))
+        .join(CLIENT_SESSION_FILE_NAME)
+}
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

Desktop 1.1.0-rc.10, CLI 0.1.3-rc.1, local-web-runtime 0.1.1-rc.1.
Avoid bash `source` with backslash paths breaking before-build on Windows.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/desktop/layout-runtime-bundle.mjs">

<violation number="1" location="scripts/desktop/layout-runtime-bundle.mjs:43">
P2: Handle missing `system-skills` source explicitly so old artifacts don’t persist and runtime layout stays deterministic.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic

mkdirSync(join(runtimeRoot, "web"), { recursive: true });
}

if (existsSync(skillsSrc)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Handle missing system-skills source explicitly so old artifacts don’t persist and runtime layout stays deterministic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/desktop/layout-runtime-bundle.mjs, line 43:

<comment>Handle missing `system-skills` source explicitly so old artifacts don’t persist and runtime layout stays deterministic.</comment>

<file context>
@@ -0,0 +1,101 @@
+    mkdirSync(join(runtimeRoot, "web"), { recursive: true });
+  }
+
+  if (existsSync(skillsSrc)) {
+    rmSync(join(runtimeRoot, "system-skills"), { recursive: true, force: true });
+    cpSync(skillsSrc, join(runtimeRoot, "system-skills"), { recursive: true });
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

AruNi-01 added 2 commits May 16, 2026 17:42
Newer gh CLI requires a git working tree or GH_REPO; publish-release
jobs had neither, causing "fatal: not a git repository" on publish.
Unified local runtime no longer issues ATMOS_LOCAL_TOKEN; remove the
Tauri guard that blocked unauthenticated loopback fetchApi calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (3)
crates/runtime-manager/src/identity.rs (2)

59-60: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Write the identity to the resolved path, not the default path.

read_server_identity() and clear_server_identity() honor ATMOS_SERVER_IDENTITY_PATH, but write_server_identity() still writes to relay_identity_path(). With the override set, registration writes one file while every later read/delete uses another.

🔧 Suggested fix
 pub fn write_server_identity(data: &ServerIdentity) -> Result<PathBuf, String> {
-    let path = relay_identity_path()?;
+    let path = resolve_server_identity_path();
     let dir = path
         .parent()
         .ok_or_else(|| format!("identity path has no parent: {}", path.display()))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/identity.rs` around lines 59 - 60,
write_server_identity currently resolves the path with relay_identity_path(),
which causes writes to ignore the ATMOS_SERVER_IDENTITY_PATH override; change
write_server_identity to resolve and write to the same path used by
read_server_identity and clear_server_identity (the function they use to honor
ATMOS_SERVER_IDENTITY_PATH) instead of relay_identity_path() so that reads,
writes and deletes operate on the same file.

72-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist server_secret with restrictive file permissions.

This writes relay credentials with the process default file mode. On a permissive umask, other local users can read server_secret from disk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/identity.rs` around lines 72 - 73, The code
currently uses fs::write(&path, ...) which creates the file with process-default
mode; instead create the file with restrictive permissions (owner read/write
only) and write the payload: use std::fs::OpenOptions (and on Unix
std::os::unix::fs::OpenOptionsExt::mode(0o600)) to open/create/truncate the file
and write the data, or after writing call std::fs::set_permissions(&path,
std::os::unix::fs::PermissionsExt::from_mode(0o600)). Update the write site
(where fs::write is called for server_secret) to use OpenOptions+write or
set_permissions and propagate errors similarly so the file is persisted as
0o600.
apps/web/src/lib/atmos-access-token.ts (1)

50-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve all HeadersInit variants when merging auth headers.

This still uses object spread for init.headers, which drops caller headers when they are passed as a Headers instance or tuple array, and it also lets a caller override the bearer token by spreading last.

🔧 Suggested fix
 export async function cpFetchWithAccessToken(
   controlPlaneUrl: string,
   accessToken: string,
   path: string,
   init?: RequestInit,
 ): Promise<Response> {
   const base = resolveControlPlaneUrl(controlPlaneUrl);
   const url = `${base}${path.startsWith('/') ? path : `/${path}`}`;
+  const headers = new Headers(init?.headers);
+  headers.set('Authorization', `Bearer ${accessToken.trim()}`);
+  if (!headers.has('Content-Type')) {
+    headers.set('Content-Type', 'application/json');
+  }
   return fetch(url, {
     ...init,
-    headers: {
-      'Content-Type': 'application/json',
-      Authorization: `Bearer ${accessToken.trim()}`,
-      ...(init?.headers ?? {}),
-    },
+    headers,
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/atmos-access-token.ts` around lines 50 - 56, The current
merge uses object spread for init.headers which loses non-object HeadersInit
variants and allows callers to override Authorization; fix by constructing a
Headers object from init?.headers (e.g., const headers = new
Headers(init?.headers)), then set headers.set('Content-Type','application/json')
and headers.set('Authorization', `Bearer ${accessToken.trim()`) to ensure the
bearer token cannot be overwritten, and pass that Headers instance as the
headers property to fetch (preserving tuple/Headers inputs and preventing
override).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/api/system/computer.rs`:
- Around line 18-51: The get_computer_status handler currently performs business
logic (local_computer_display_name_opt, diagnostics::get_shell_env_info,
read_server_identity, and direct use of state.relay_supervisor for
registration/lifecycle/wait logic); extract that orchestration into a new
core-service API (e.g. a function like
core_service::computer::get_status_orchestrator) that returns a serializable
status struct or serde_json::Value, move identity I/O and relay/supervisor
poll/register logic into that service, and update get_computer_status to be
thin: call AppState-provided service method (via State(state)) to fetch the
prepared status, then wrap it in ApiResponse/Json; keep only minimal
mapping/logging in the handler and remove direct file I/O and lifecycle control
from the HTTP layer (retain use of state.relay_supervisor only inside the new
core-service).

In `@apps/api/src/api/system/mod.rs`:
- Around line 56-59: The three state-mutating computer routes are registered in
routes() but must be behind the stronger auth used by destructive_routes(): move
.route("/computer/register", post(computer::register_local_computer)),
.route("/computer/unregister", post(computer::unregister_local_computer)), and
.route("/computer/relay-sync", post(computer::sync_relay_connection)) out of the
current routes() chain and add them to the destructive_routes() chain alongside
the existing destructive handlers; leave .route("/computer",
get(computer::get_computer_status)) in the normal routes() group so the
read-only get_computer_status remains public.

In `@apps/api/src/relay/ingest.rs`:
- Around line 1-297: The file fails rustfmt CI checks; run cargo fmt to reformat
imports and async/let block formatting in this module (affecting functions like
run and ensure_session and structs like RelayLifecycle/Session), then commit the
formatting changes (ensure rustfmt is configured via rust-toolchain or
rustfmt.toml if needed) so only whitespace/formatting is altered and no logic is
changed.

In `@apps/api/src/relay/supervisor.rs`:
- Around line 97-115: sync_from_disk currently returns early on the
disabled/missing/corrupt-identity branches without stopping an existing relay,
leaving an outbound connection alive; before each early return in sync_from_disk
(the branches that set last_error), call a method to stop the running relay
(e.g. self.stop_relay_task()) to abort the relay join-handle or cancellation
token and clear any outbound connection/state; if stop_relay_task doesn't exist,
add it to the supervisor to cancel/abort the relay task handle and reset related
fields so sync_from_disk always tears down the running relay before returning an
error.

In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx`:
- Around line 386-425: onConnect currently always creates a relay session and
sets connectionMode('relay') even when the user selected "Use locally" (the
local machine), which keeps the app routed through relay; update onConnect to
detect when serverId refers to the local/current machine and instead switch back
to local mode: skip calling cpFetchWithAccessToken and syncClientSessionRelay,
clear any relay-specific state (e.g. setRelayWebSocketUrl,
setRelayGatewayHttpBase, setRelayClientToken) and call
setConnectionMode('local') and reconnectWs as needed, then return early; keep
existing behavior for non-local serverIds (retain cpFetchWithAccessToken,
setRelay* calls, syncClientSessionRelay and setConnectionMode('relay')) so only
the local-selection path changes.

In `@apps/web/src/components/dialogs/ComputerDetailsDialog.tsx`:
- Around line 65-86: The dialog currently performs a relay fetch inside the
component (references: useAtmosComputerStore.getState(), connectionMode,
relayGatewayHttpBase, relayClientToken, selectedServerId, getRuntimeApiConfig(),
httpBase) — move this networking and auth wiring into a new API helper (e.g.,
src/api/relay.ts) that exposes a function like
fetchRelayTerminalOverview(relayGatewayHttpBase, relayClientToken) which handles
runtime config, base URL normalization, header assembly (including
X-Atmos-Local-Token from NEXT_PUBLIC_API_TOKEN) and the fetch call; then update
ComputerDetailsDialog to call that helper and handle the returned data/error
without performing fetch directly in the component.

In `@apps/web/src/lib/atmos-computer-local.ts`:
- Around line 77-95: The JSON parse cast is self-referential and breaks
TypeScript narrowing; define an explicit generic envelope type (e.g. interface
Envelope<T> { success?: boolean; data?: T; message?: string; error?: string })
and replace the current cast JSON.parse(raw) as typeof json with parsing into
that envelope (e.g. const parsed = JSON.parse(raw) as Envelope<T>; set json =
parsed). Update uses of json, raw, and res in localFetch (the same checks around
res.status and json?.success/?.error/?.message and the final return json.data as
T) to rely on the explicit Envelope<T> type so TypeScript can correctly narrow
and the property accesses stop resolving to never.

In `@scripts/desktop/layout-runtime-bundle.sh`:
- Around line 8-18: The top-level strict mode (set -euo pipefail) must not be
applied when this file is sourced; move that invocation into the
layout_runtime_bundle function so strict options only apply while the function
runs. Edit the layout_runtime_bundle() body to enable strict mode at its start
(and leave the ROOT_DIR_SCRIPT assignment at top-level since callers may need
it), ensuring the script still calls layout_runtime_bundle when run directly so
behavior for direct execution is unchanged.

---

Duplicate comments:
In `@apps/web/src/lib/atmos-access-token.ts`:
- Around line 50-56: The current merge uses object spread for init.headers which
loses non-object HeadersInit variants and allows callers to override
Authorization; fix by constructing a Headers object from init?.headers (e.g.,
const headers = new Headers(init?.headers)), then set
headers.set('Content-Type','application/json') and headers.set('Authorization',
`Bearer ${accessToken.trim()`) to ensure the bearer token cannot be overwritten,
and pass that Headers instance as the headers property to fetch (preserving
tuple/Headers inputs and preventing override).

In `@crates/runtime-manager/src/identity.rs`:
- Around line 59-60: write_server_identity currently resolves the path with
relay_identity_path(), which causes writes to ignore the
ATMOS_SERVER_IDENTITY_PATH override; change write_server_identity to resolve and
write to the same path used by read_server_identity and clear_server_identity
(the function they use to honor ATMOS_SERVER_IDENTITY_PATH) instead of
relay_identity_path() so that reads, writes and deletes operate on the same
file.
- Around line 72-73: The code currently uses fs::write(&path, ...) which creates
the file with process-default mode; instead create the file with restrictive
permissions (owner read/write only) and write the payload: use
std::fs::OpenOptions (and on Unix
std::os::unix::fs::OpenOptionsExt::mode(0o600)) to open/create/truncate the file
and write the data, or after writing call std::fs::set_permissions(&path,
std::os::unix::fs::PermissionsExt::from_mode(0o600)). Update the write site
(where fs::write is called for server_secret) to use OpenOptions+write or
set_permissions and propagate errors similarly so the file is persisted as
0o600.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86c04726-5296-4cd5-aa41-c97c18045d15

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef8f13 and ec07f26.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .github/workflows/deploy-relay.yml
  • .github/workflows/release-desktop.yml
  • .github/workflows/release-local-model-runtime.yml
  • .github/workflows/sync-r2.yml
  • apps/api/Cargo.toml
  • apps/api/src/api/system/computer.rs
  • apps/api/src/api/system/mod.rs
  • apps/api/src/app_state.rs
  • apps/api/src/main.rs
  • apps/api/src/relay/ingest.rs
  • apps/api/src/relay/mod.rs
  • apps/api/src/relay/supervisor.rs
  • apps/cli/src/commands/computer.rs
  • apps/desktop/src-tauri/src/commands.rs
  • apps/desktop/src-tauri/src/main.rs
  • apps/web/next.config.ts
  • apps/web/src/api/rest-api.ts
  • apps/web/src/components/dialogs/AtmosComputerSection.tsx
  • apps/web/src/components/dialogs/ComputerDetailsDialog.tsx
  • apps/web/src/components/dialogs/SettingsModal.tsx
  • apps/web/src/lib/atmos-access-token.ts
  • apps/web/src/lib/atmos-computer-local.ts
  • apps/web/src/lib/atmos-computer-store.ts
  • apps/web/src/lib/desktop-runtime.ts
  • apps/web/src/lib/ws-url.ts
  • crates/runtime-manager/src/computer_name.rs
  • crates/runtime-manager/src/identity.rs
  • crates/runtime-manager/src/lib.rs
  • packages/relay/src/index.ts
  • packages/relay/wrangler.toml
  • packages/ui/src/components/icons/computer-icon.tsx
  • scripts/desktop/layout-runtime-bundle.sh
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/relay/wrangler.toml
  • apps/web/src/components/dialogs/SettingsModal.tsx
  • .github/workflows/deploy-relay.yml
  • crates/runtime-manager/src/lib.rs
  • apps/desktop/src-tauri/src/commands.rs
  • apps/web/src/api/rest-api.ts
  • .github/workflows/release-desktop.yml
  • apps/api/src/app_state.rs
  • apps/cli/src/commands/computer.rs
  • apps/web/src/lib/desktop-runtime.ts
  • packages/relay/src/index.ts
  • apps/desktop/src-tauri/src/main.rs

Comment on lines +18 to +51
pub async fn get_computer_status(
State(state): State<AppState>,
) -> ApiResult<Json<ApiResponse<Value>>> {
let shell_env = diagnostics::get_shell_env_info();
let hostname = shell_env
.get("hostname")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string);
let computer_name = local_computer_display_name_opt().or(hostname.clone());

let identity = read_server_identity().map_err(ApiError::BadRequest)?;
let relay_connected = state.relay_supervisor.is_upstream_connected().await;
let relay_last_error = state.relay_supervisor.last_error().await;

let mut body = json!({
"hostname": hostname,
"computer_name": computer_name,
"registered": identity.is_some(),
"relay_connected": relay_connected,
"relay_last_error": relay_last_error,
"server_id": identity.as_ref().map(|i| i.server_id.clone()),
"control_plane_url": identity
.as_ref()
.and_then(|i| i.control_plane_url.clone())
.unwrap_or_else(|| default_control_plane_url().to_string()),
"relay_ws_url": identity.as_ref().map(|i| i.relay_ws_url.clone()),
});

if let Some(obj) = body.as_object_mut() {
obj.insert("shell_env".to_string(), shell_env);
}

Ok(Json(ApiResponse::success(body)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move the computer lifecycle orchestration out of the HTTP handlers.

These handlers are doing local-name resolution, identity file I/O, relay registration, supervisor lifecycle, and wait/poll logic directly in the API layer. That makes the endpoint responsible for business rules that Desktop/CLI/web now all depend on, which will be harder to keep aligned.

As per coding guidelines apps/api/src/api/**/*.rs: “Keep request handlers thin — call core-service for business logic”, apps/api/src/api/**/*.rs: “Always use AppState for accessing services”, and apps/api/src/**/*.rs: “Never implement business logic in the API server — use crates/core-service”.

Also applies to: 62-132

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/api/system/computer.rs` around lines 18 - 51, The
get_computer_status handler currently performs business logic
(local_computer_display_name_opt, diagnostics::get_shell_env_info,
read_server_identity, and direct use of state.relay_supervisor for
registration/lifecycle/wait logic); extract that orchestration into a new
core-service API (e.g. a function like
core_service::computer::get_status_orchestrator) that returns a serializable
status struct or serde_json::Value, move identity I/O and relay/supervisor
poll/register logic into that service, and update get_computer_status to be
thin: call AppState-provided service method (via State(state)) to fetch the
prepared status, then wrap it in ApiResponse/Json; keep only minimal
mapping/logging in the handler and remove direct file I/O and lifecycle control
from the HTTP layer (retain use of state.relay_supervisor only inside the new
core-service).

Comment thread apps/api/src/api/system/mod.rs Outdated
Comment thread apps/api/src/relay/ingest.rs
Comment thread apps/api/src/relay/supervisor.rs
Comment thread apps/web/src/components/dialogs/AtmosComputerSection.tsx
Comment thread apps/web/src/components/dialogs/ComputerDetailsDialog.tsx Outdated
Comment thread apps/web/src/lib/atmos-computer-local.ts Outdated
Comment thread scripts/desktop/layout-runtime-bundle.sh Outdated
Move mutating computer routes behind loopback auth, fix relay HTTP errors
and identity I/O, improve local vs relay UI wiring, and align APP-016 specs
with per-user Access Token tenancy.
Use a non-generic envelope type in localFetch so Next.js typecheck no longer
narrows json to never during the desktop beforeBuildCommand.
Move workspace group-by options into the filter menu on the left sidebar,
hide grouping when Kanban is expanded via lsKanban, add a top divider on
the bottom toolbar, and place Add Project before Filter.
…emon

Add copyable install and register-and-start commands for remote servers,
and a --daemon flag so VPS registration exits quickly after launching API.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
specs/APP/APP-016_atmos-computer/PRD.md (1)

1-100: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

PRD is written in Chinese; spec guidelines require English.

The entire PRD document uses Chinese for titles, headings, and body text. Spec guidelines state: "All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed."

Note: The auth model content at line 43 (M1-2) correctly specifies tenant_id = sha256(access_token) and reserves CONTROL_PLANE_KEY only for system/ops management, which aligns with requirements.

Based on learnings: "All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/PRD.md` around lines 1 - 100, The PRD is
written in Chinese and must be converted to English: translate all titles,
headings, and body text of APP-016:Atmos Computer into English while preserving
inline Chinese quotes where necessary, keep identifiers like "Atmos Computer",
"server_id", "client_id", "M1-2", and the exact auth statement "tenant_id =
sha256(access_token)" unchanged, ensure references to
TECH.md/TEST.md/BRAINSTORM.md remain intact and that the scope/out-of-scope
sections, M1/M2 lists, and success metrics are accurately translated without
altering technical meaning.
♻️ Duplicate comments (2)
crates/runtime-manager/src/identity.rs (1)

81-91: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-apply restrictive permissions after write for existing identity files.

Using .mode(0o600) only guarantees permissions on create. If the file already exists with broader mode, it can stay too permissive for server_secret.

Suggested fix
 #[cfg(unix)]
 {
     use std::os::unix::fs::OpenOptionsExt;
+    use std::os::unix::fs::PermissionsExt;
     let mut file = std::fs::OpenOptions::new()
         .create(true)
         .write(true)
         .truncate(true)
         .mode(0o600)
         .open(path)
         .map_err(|err| format!("Failed to write {}: {}", path.display(), err))?;
     file.write_all(contents.as_bytes())
         .map_err(|err| format!("Failed to write {}: {}", path.display(), err))?;
+    fs::set_permissions(path, fs::Permissions::from_mode(0o600))
+        .map_err(|err| format!("Failed to set permissions on {}: {}", path.display(), err))?;
     return Ok(());
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/identity.rs` around lines 81 - 91, The current
write uses .mode(0o600) which only applies on create and leaves existing files
with broader perms; after writing (after file.write_all(contents.as_bytes())),
explicitly set restrictive permissions on the path by calling
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) (and
import std::os::unix::fs::PermissionsExt) so the file backing 'file' (referenced
as file/path) ends up with 0o600 regardless of prior mode; you may also call
file.sync_all() before setting perms to ensure data is flushed.
apps/web/src/components/dialogs/AtmosComputerSection.tsx (1)

387-404: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle “Use locally” before access-token validation.

Switching back to local mode should not require a valid access token. Current ordering can block users from exiting relay mode.

Suggested fix
 async function onConnect(serverId: string) {
-  if (!(await ensureAccessTokenReady(accessToken))) {
-    return;
-  }
   const isLocalMachine = serverId === (localStatus?.server_id ?? localServerId);
   if (isLocalMachine) {
@@
     return;
   }
+  if (!(await ensureAccessTokenReady(accessToken))) {
+    return;
+  }
   setBusy(`connect-${serverId}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx` around lines 387 -
404, The onConnect function currently validates the access token before handling
the local-machine branch; move the local-machine check (isLocalMachine computed
from serverId, localStatus?.server_id, localServerId) to run before calling
ensureAccessTokenReady(accessToken) so switching to local mode does not require
a valid token. Specifically, in onConnect, evaluate isLocalMachine first and if
true perform resetRelaySession(), setConnectionMode('local'),
syncClientSessionLocal(), reconnectWs(), toastManager.add(...) and setBusy(...)
handling as currently implemented, then return; only call
ensureAccessTokenReady(accessToken) for non-local branches. Ensure you keep the
same calls (resetRelaySession, setConnectionMode, syncClientSessionLocal,
reconnectWs, toastManager, setBusy) and their try/finally behavior.
🧹 Nitpick comments (2)
packages/relay/AGENTS.md (1)

17-25: 💤 Low value

Add language identifier to fenced code block.

The code block should specify text or bash for proper syntax highlighting and linter compliance.

📝 Proposed fix
-```
+```text
 packages/relay/
 ├── src/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relay/AGENTS.md` around lines 17 - 25, The fenced code block in
packages/relay/AGENTS.md lacks a language identifier which fails linter/syntax
highlighting; update the opening triple-backtick for the directory tree block to
include a language (e.g., "text" or "bash") so it reads ```text (or ```bash)
before the tree content; ensure only that code fence is modified and nothing
else in AGENTS.md.
specs/APP/APP-016_atmos-computer/TECH.md (1)

196-196: ⚡ Quick win

Fix table column count mismatches.

Several tables have column count mismatches where cells contain unescaped pipes or extra delimiters, causing parsing errors. Escape pipes within cell content or verify table structure matches header column count.

Affected lines: 196, 208, 461, 462, 463, 466 (per markdownlint).

Also applies to: 208-208, 461-466

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/TECH.md` at line 196, The table rows contain
literal pipe characters that break the Markdown table column count (e.g., the
cell containing "`apps/cli` ... `atmos runtime ensure|stop|status`" where the
vertical bar in "ensure|stop|status" is parsed as a column delimiter); fix by
escaping internal pipes (use \|), wrapping the entire fragment in inline code
(backticks) or replacing with HTML entity &`#124`;, and verify the header and each
affected row (the row with "`apps/cli`" and the rows flagged at lines 208,
461–466) have the same number of | delimiters so the table column count matches.
Ensure any other cells containing unescaped `|` or extra `|` delimiters are
corrected similarly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx`:
- Around line 351-359: The unregister flow calls cpFetchWithAccessToken for the
revoke endpoint using serverId but doesn't check the response, so success is
shown even on failure; update the code around where cpFetchWithAccessToken is
called (the revoke POST to
`/v1/computers/${encodeURIComponent(serverId)}/revoke`) to capture the fetch
response, verify response.ok (or equivalent status), and if not OK read and
surface the error (throw or return a failure) so the UI only shows unregister
success on a true 2xx result; apply the same check-and-handle change to the
second occurrence of the revoke call later in the file (the block around the
other cpFetchWithAccessToken revoke usage).

In `@apps/web/src/lib/fetch-register-token.ts`:
- Around line 17-26: The code currently only validates register_token; update
the function that handles the response (the code using data, res and returning
register_token/expires_at/register_command) to also validate expires_at from the
parsed data: ensure data.expires_at exists and is a valid numeric timestamp
(e.g., typeof === 'number' and isFinite, or parseable to a number/Date) and if
not throw an Error similar to the existing check (e.g., throw new
Error(data?.error ?? 'Invalid or missing expires_at')). Keep the same return
shape (register_token, expires_at, register_command) once both register_token
and expires_at are validated, and reference the RegisterTokenResponse type when
doing the checks.

In `@apps/web/src/lib/vps-setup-commands.ts`:
- Around line 15-23: The generated command leaves the control plane URL unquoted
which can break/shell-inject; escape single quotes in the controlPlaneUrl
(similar to how registerToken is handled) via
resolveControlPlaneUrl(opts.controlPlaneUrl).replace(/'/g, `'\\''`) (or do the
replace before/after calling resolveControlPlaneUrl) and wrap the value in
single quotes in the returned command array where `--control-plane ${cp}` is
emitted (replace with `--control-plane '${cp}' \\`), updating the code that
builds the array in vps-setup-commands.ts to use the escaped-and-quoted cp.

In `@crates/runtime-manager/src/identity.rs`:
- Around line 59-74: The file fails rustfmt checks; run rustfmt and reformat the
function write_server_identity (and file) to satisfy cargo fmt --check—e.g., run
`cargo fmt --all` locally or apply rustfmt to
crates/runtime-manager/src/identity.rs—then stage the formatted changes so the
write_server_identity function (including its chaining on path.parent(), map_err
closures, and the payload formatting call) matches rustfmt output before
pushing.

In `@specs/APP/APP-016_atmos-computer/TECH.md`:
- Around line 1-634: The TECH.md for APP-016 is written entirely in Chinese but
must follow the spec guideline requiring English; please translate all titles,
headings, and body text into fluent English while preserving original semantics,
examples and inline Chinese phrases where helpful. Update the document
header/title ("TECH · APP-016:Atmos Computer"), section headings (e.g., "1.0",
"1.1", "2.4.5 Relay WebSocket", "4.2 Envelope fields", "8.2 atmos review",
etc.), all tables, diagrams, and prose to English, keeping identifiers like
server_id, server_secret, relay_identity.json, runtime_manifest.json,
register_token, client_token, ServerHub, Durable Object, and envelope field
names (v, stream, kind, from, to, body) unchanged; ensure any quoted commands,
code blocks, and SQL remain intact but with surrounding explanatory text
translated; finally run a pass to keep formatting (mermaid, tables, code blocks)
identical and ensure any inline Chinese notes are preserved only as
parenthetical annotations.

---

Outside diff comments:
In `@specs/APP/APP-016_atmos-computer/PRD.md`:
- Around line 1-100: The PRD is written in Chinese and must be converted to
English: translate all titles, headings, and body text of APP-016:Atmos Computer
into English while preserving inline Chinese quotes where necessary, keep
identifiers like "Atmos Computer", "server_id", "client_id", "M1-2", and the
exact auth statement "tenant_id = sha256(access_token)" unchanged, ensure
references to TECH.md/TEST.md/BRAINSTORM.md remain intact and that the
scope/out-of-scope sections, M1/M2 lists, and success metrics are accurately
translated without altering technical meaning.

---

Duplicate comments:
In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx`:
- Around line 387-404: The onConnect function currently validates the access
token before handling the local-machine branch; move the local-machine check
(isLocalMachine computed from serverId, localStatus?.server_id, localServerId)
to run before calling ensureAccessTokenReady(accessToken) so switching to local
mode does not require a valid token. Specifically, in onConnect, evaluate
isLocalMachine first and if true perform resetRelaySession(),
setConnectionMode('local'), syncClientSessionLocal(), reconnectWs(),
toastManager.add(...) and setBusy(...) handling as currently implemented, then
return; only call ensureAccessTokenReady(accessToken) for non-local branches.
Ensure you keep the same calls (resetRelaySession, setConnectionMode,
syncClientSessionLocal, reconnectWs, toastManager, setBusy) and their
try/finally behavior.

In `@crates/runtime-manager/src/identity.rs`:
- Around line 81-91: The current write uses .mode(0o600) which only applies on
create and leaves existing files with broader perms; after writing (after
file.write_all(contents.as_bytes())), explicitly set restrictive permissions on
the path by calling std::fs::set_permissions(path,
std::fs::Permissions::from_mode(0o600)) (and import
std::os::unix::fs::PermissionsExt) so the file backing 'file' (referenced as
file/path) ends up with 0o600 regardless of prior mode; you may also call
file.sync_all() before setting perms to ensure data is flushed.

---

Nitpick comments:
In `@packages/relay/AGENTS.md`:
- Around line 17-25: The fenced code block in packages/relay/AGENTS.md lacks a
language identifier which fails linter/syntax highlighting; update the opening
triple-backtick for the directory tree block to include a language (e.g., "text"
or "bash") so it reads ```text (or ```bash) before the tree content; ensure only
that code fence is modified and nothing else in AGENTS.md.

In `@specs/APP/APP-016_atmos-computer/TECH.md`:
- Line 196: The table rows contain literal pipe characters that break the
Markdown table column count (e.g., the cell containing "`apps/cli` ... `atmos
runtime ensure|stop|status`" where the vertical bar in "ensure|stop|status" is
parsed as a column delimiter); fix by escaping internal pipes (use \|), wrapping
the entire fragment in inline code (backticks) or replacing with HTML entity
&`#124`;, and verify the header and each affected row (the row with "`apps/cli`"
and the rows flagged at lines 208, 461–466) have the same number of | delimiters
so the table column count matches. Ensure any other cells containing unescaped
`|` or extra `|` delimiters are corrected similarly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 817e21dd-743e-4df3-afa0-8173aa4800b4

📥 Commits

Reviewing files that changed from the base of the PR and between ec07f26 and e2c2548.

📒 Files selected for processing (36)
  • apps/api/src/api/canvas/agent.rs
  • apps/api/src/api/mod.rs
  • apps/api/src/api/review/mod.rs
  • apps/api/src/api/system/computer.rs
  • apps/api/src/api/system/handlers.rs
  • apps/api/src/api/system/mod.rs
  • apps/api/src/relay/http_gateway.rs
  • apps/api/src/relay/ingest.rs
  • apps/api/src/relay/register.rs
  • apps/api/src/relay/supervisor.rs
  • apps/cli/src/api_client.rs
  • apps/cli/src/commands/canvas.rs
  • apps/cli/src/commands/computer.rs
  • apps/cli/src/commands/local.rs
  • apps/cli/src/commands/review.rs
  • apps/cli/src/commands/runtime.rs
  • apps/cli/src/main.rs
  • apps/desktop/src-tauri/src/runtime.rs
  • apps/web/src/api/relay.ts
  • apps/web/src/api/rest-api.ts
  • apps/web/src/components/dialogs/AtmosComputerSection.tsx
  • apps/web/src/components/dialogs/ComputerDetailsDialog.tsx
  • apps/web/src/components/dialogs/VpsRemoteSetupBlock.tsx
  • apps/web/src/components/layout/LeftSidebar.tsx
  • apps/web/src/components/layout/sidebar/WorkspaceKanbanFilterMenu.tsx
  • apps/web/src/lib/atmos-access-token.ts
  • apps/web/src/lib/atmos-computer-local.ts
  • apps/web/src/lib/fetch-register-token.ts
  • apps/web/src/lib/vps-setup-commands.ts
  • crates/runtime-manager/src/identity.rs
  • crates/runtime-manager/src/supervisor.rs
  • packages/relay/AGENTS.md
  • packages/relay/src/index.ts
  • scripts/desktop/layout-runtime-bundle.sh
  • specs/APP/APP-016_atmos-computer/PRD.md
  • specs/APP/APP-016_atmos-computer/TECH.md
🚧 Files skipped from review as they are similar to previous changes (24)
  • apps/web/src/lib/atmos-access-token.ts
  • apps/cli/src/commands/runtime.rs
  • apps/cli/src/main.rs
  • apps/api/src/api/system/handlers.rs
  • apps/api/src/api/system/computer.rs
  • apps/desktop/src-tauri/src/runtime.rs
  • apps/api/src/api/mod.rs
  • apps/api/src/relay/supervisor.rs
  • scripts/desktop/layout-runtime-bundle.sh
  • apps/api/src/relay/register.rs
  • apps/web/src/components/dialogs/ComputerDetailsDialog.tsx
  • apps/web/src/api/rest-api.ts
  • apps/cli/src/commands/local.rs
  • apps/api/src/relay/ingest.rs
  • apps/api/src/relay/http_gateway.rs
  • apps/web/src/lib/atmos-computer-local.ts
  • apps/api/src/api/review/mod.rs
  • apps/cli/src/api_client.rs
  • apps/api/src/api/system/mod.rs
  • crates/runtime-manager/src/supervisor.rs
  • apps/cli/src/commands/computer.rs
  • apps/cli/src/commands/canvas.rs
  • apps/api/src/api/canvas/agent.rs
  • packages/relay/src/index.ts

Comment on lines +351 to +359
const serverId = localStatus?.server_id ?? localServerId;
if (serverId) {
await cpFetchWithAccessToken(
controlPlaneUrl,
accessToken,
`/v1/computers/${encodeURIComponent(serverId)}/revoke`,
{ method: 'POST', body: '{}' },
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check revoke API result before showing unregister success.

The unregister flow proceeds and shows success even if /revoke returns non-OK, which can leave control-plane state and local state out of sync.

Suggested fix
       const serverId = localStatus?.server_id ?? localServerId;
       if (serverId) {
-        await cpFetchWithAccessToken(
+        const revokeRes = await cpFetchWithAccessToken(
           controlPlaneUrl,
           accessToken,
           `/v1/computers/${encodeURIComponent(serverId)}/revoke`,
           { method: 'POST', body: '{}' },
         );
+        if (!revokeRes.ok) {
+          toastManager.add({
+            title: 'Could not unregister',
+            description: 'Failed to revoke this computer on the control plane.',
+            type: 'error',
+          });
+          return;
+        }
       }

Also applies to: 368-373

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/dialogs/AtmosComputerSection.tsx` around lines 351 -
359, The unregister flow calls cpFetchWithAccessToken for the revoke endpoint
using serverId but doesn't check the response, so success is shown even on
failure; update the code around where cpFetchWithAccessToken is called (the
revoke POST to `/v1/computers/${encodeURIComponent(serverId)}/revoke`) to
capture the fetch response, verify response.ok (or equivalent status), and if
not OK read and surface the error (throw or return a failure) so the UI only
shows unregister success on a true 2xx result; apply the same check-and-handle
change to the second occurrence of the revoke call later in the file (the block
around the other cpFetchWithAccessToken revoke usage).

Comment on lines +17 to +26
const data = (await res.json().catch(() => null)) as
| (RegisterTokenResponse & { error?: string })
| null;
if (!res.ok || !data?.register_token) {
throw new Error(data?.error ?? `HTTP ${res.status}`);
}
return {
register_token: data.register_token,
expires_at: data.expires_at,
register_command: data.register_command,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate expires_at before returning token data.

Right now only register_token is validated. If expires_at is missing/non-numeric, callers can treat an expired token as valid.

Suggested fix
-  if (!res.ok || !data?.register_token) {
+  if (!res.ok || !data?.register_token || typeof data.expires_at !== 'number') {
     throw new Error(data?.error ?? `HTTP ${res.status}`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = (await res.json().catch(() => null)) as
| (RegisterTokenResponse & { error?: string })
| null;
if (!res.ok || !data?.register_token) {
throw new Error(data?.error ?? `HTTP ${res.status}`);
}
return {
register_token: data.register_token,
expires_at: data.expires_at,
register_command: data.register_command,
const data = (await res.json().catch(() => null)) as
| (RegisterTokenResponse & { error?: string })
| null;
if (!res.ok || !data?.register_token || typeof data.expires_at !== 'number') {
throw new Error(data?.error ?? `HTTP ${res.status}`);
}
return {
register_token: data.register_token,
expires_at: data.expires_at,
register_command: data.register_command,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/fetch-register-token.ts` around lines 17 - 26, The code
currently only validates register_token; update the function that handles the
response (the code using data, res and returning
register_token/expires_at/register_command) to also validate expires_at from the
parsed data: ensure data.expires_at exists and is a valid numeric timestamp
(e.g., typeof === 'number' and isFinite, or parseable to a number/Date) and if
not throw an Error similar to the existing check (e.g., throw new
Error(data?.error ?? 'Invalid or missing expires_at')). Keep the same return
shape (register_token, expires_at, register_command) once both register_token
and expires_at are validated, and reference the RegisterTokenResponse type when
doing the checks.

Comment on lines +15 to +23
const cp = resolveControlPlaneUrl(opts.controlPlaneUrl);
const token = opts.registerToken.replace(/'/g, `'\\''`);
return [
'export PATH="$HOME/.atmos/bin:$PATH"',
'atmos computer start \\',
` --token '${token}' \\`,
' --display-name "$(hostname -s)" \\',
` --control-plane ${cp} \\`,
' --daemon',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shell-escape and quote controlPlaneUrl in the generated command.

--control-plane ${cp} is unquoted. A URL with spaces or shell metacharacters can break execution or inject unintended shell tokens.

Suggested fix
   const cp = resolveControlPlaneUrl(opts.controlPlaneUrl);
   const token = opts.registerToken.replace(/'/g, `'\\''`);
+  const cpEscaped = cp.replace(/'/g, `'\\''`);
   return [
@@
-    `  --control-plane ${cp} \\`,
+    `  --control-plane '${cpEscaped}' \\`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cp = resolveControlPlaneUrl(opts.controlPlaneUrl);
const token = opts.registerToken.replace(/'/g, `'\\''`);
return [
'export PATH="$HOME/.atmos/bin:$PATH"',
'atmos computer start \\',
` --token '${token}' \\`,
' --display-name "$(hostname -s)" \\',
` --control-plane ${cp} \\`,
' --daemon',
const cp = resolveControlPlaneUrl(opts.controlPlaneUrl);
const token = opts.registerToken.replace(/'/g, `'\\''`);
const cpEscaped = cp.replace(/'/g, `'\\''`);
return [
'export PATH="$HOME/.atmos/bin:$PATH"',
'atmos computer start \\',
` --token '${token}' \\`,
' --display-name "$(hostname -s)" \\`,
` --control-plane '${cpEscaped}' \\`,
' --daemon',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/vps-setup-commands.ts` around lines 15 - 23, The generated
command leaves the control plane URL unquoted which can break/shell-inject;
escape single quotes in the controlPlaneUrl (similar to how registerToken is
handled) via resolveControlPlaneUrl(opts.controlPlaneUrl).replace(/'/g, `'\\''`)
(or do the replace before/after calling resolveControlPlaneUrl) and wrap the
value in single quotes in the returned command array where `--control-plane
${cp}` is emitted (replace with `--control-plane '${cp}' \\`), updating the code
that builds the array in vps-setup-commands.ts to use the escaped-and-quoted cp.

Comment on lines +59 to +74
pub fn write_server_identity(data: &ServerIdentity) -> Result<PathBuf, String> {
let path = resolve_server_identity_path();
let dir = path
.parent()
.ok_or_else(|| format!("identity path has no parent: {}", path.display()))?;
fs::create_dir_all(dir)
.map_err(|err| format!("Failed to create {}: {}", dir.display(), err))?;
let payload = serde_json::to_string_pretty(data).map_err(|err| {
format!(
"Failed to serialize {}: {}",
RELAY_IDENTITY_FILE_NAME, err
)
})?;
write_identity_file_restricted(&path, &format!("{payload}\n"))?;
Ok(path)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

cargo fmt --check is failing on this file.

Please run cargo fmt --all before merge; CI is currently blocked by rustfmt differences here.

🧰 Tools
🪛 GitHub Actions: CI - Backend (Rust) / 2_Format Check.txt

[error] 63-63: cargo fmt formatting difference detected in identity.rs (line 63). Run cargo fmt --all.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/runtime-manager/src/identity.rs` around lines 59 - 74, The file fails
rustfmt checks; run rustfmt and reformat the function write_server_identity (and
file) to satisfy cargo fmt --check—e.g., run `cargo fmt --all` locally or apply
rustfmt to crates/runtime-manager/src/identity.rs—then stage the formatted
changes so the write_server_identity function (including its chaining on
path.parent(), map_err closures, and the payload formatting call) matches
rustfmt output before pushing.

Comment on lines +1 to +634
# TECH · APP-016:Atmos Computer

> **命名**:用户向功能名 **Atmos Computer**(`server_id`);其上 `**apps/api` 进程**为 **Atmos Server**。下文 **Relay / DO / Control Plane** 为连接与控制面实现。
>
> 技术设计:**如何实现**。产品范围见 `PRD.md`。**本文不依赖 [APP-012](../APP-012_remote-access/TECH.md) remote-access。**

## 1. 架构总览

### 1.0 图示(Mermaid)

下列图为 **整体边界 + 关键流程** 的摘要;字段级契约仍以正文表格与各小节为准。

#### 逻辑组件与部署边界

```mermaid
flowchart TB
subgraph clients [Clients]
Web["apps/web"]
Desktop["apps/desktop"]
CLI["apps/cli"]
end
subgraph computer ["一台 Atmos Computer"]
API["apps/api · Atmos Server"]
RM["runtime_manifest.json"]
RI["relay_identity.json"]
API --- RM
API --- RI
end
subgraph edge ["Cloudflare Edge · packages/relay"]
WK["Worker · TLS / 路由"]
CP["Control Plane · REST + D1"]
DO["Durable Object · ServerHub"]
WK --> CP
WK --> DO
end
Web --> WK
Desktop --> WK
CLI -.->|"HTTP 业务(canvas / review 等)"| API
API <-->|"出站 WSS · 信封"| DO
Web <-->|"客户端 WSS"| WK
Desktop <-->|"客户端 WSS"| WK
```



#### Relay 模式连接拓扑(与 §1.2 ASCII 对照)

```mermaid
flowchart LR
S["Atmos Server"]
DO["Relay Hub · DO(server_id)"]
WK["Worker"]
C["Web/Desktop"]
S -->|"出站 WSS"| DO
DO -->|"下行帧"| S
C -->|"WSS"| WK
WK --> DO
```



#### 本地 loopback vs Relay(双模式对照)

同一台 **Atmos Server** 可在 **无 Relay 凭据** 时走纯本地;配对并写入 `relay_identity.json` 后,浏览器经 **Worker/DO** 与 **仅出站** 的 Server 建业务 WS(与 §7.1、不变量一致)。

```mermaid
flowchart TB
subgraph modeA ["模式 A · 仅本地 loopback"]
direction LR
WA["Web/Desktop"]
APIA["apps/api"]
CA["CLI"]
WA <-->|"WS + HTTP · 127.0.0.1 等"| APIA
CA -.->|"HTTP · runtime_manifest 等"| APIA
end
subgraph modeB ["模式 B · Relay"]
direction LR
WB["Web/Desktop"]
WK2["Worker / DO"]
APIB["apps/api"]
CB["CLI"]
WB <-->|"WSS · 经边缘"| WK2
APIB <-->|"出站 WSS · 信封"| WK2
CB -.->|"HTTP · 仍到该 Computer API"| APIB
end
```




| 维度 | 模式 A · 本地 | 模式 B · Relay |
| ------------------------- | ------------------------------------------ | -------------------------------------------------------- |
| **WS 路径** | Client 直连 `ws://127.0.0.1:port/...`(或 LAN) | Client 仅连 `wss://…/ws/client?...`;**不**与 Server 公网 IP 建连 |
| `**relay_identity.json`** | 无或忽略;不启 relay 出站任务 | 有;Server 向 DO 附着 |
| **典型场景** | 本机开发、Desktop 侧车、同一网段 | Server 在 NAT/VPS、仅出站即可被 UI 访问 |


#### 配对与注册(对照 §3)

```mermaid
sequenceDiagram
participant U as 用户/UI
participant CP as Control Plane
participant S as Atmos Server
participant R as Relay Worker/DO

U->>CP: POST pair_codes(已登录)
CP-->>U: code, expires_at
Note over U,S: 展示配对码或扫码
S->>CP: POST servers/register(code)
CP-->>S: server_id, server_secret, relay_ws_url
S->>S: 写入 relay_identity.json
S->>R: 出站 WS 附着 server_id + 凭证
R-->>S: 验证通过 · Hub READY
```



#### 运行时信封路由(对照 §4)

```mermaid
sequenceDiagram
participant C as Client WS
participant DO as ServerHub DO
participant S as Atmos Server WS

Note over C,S: Relay 只解析信封;body 透传业务 WS 载荷
C->>DO: Envelope · to server
DO->>S: 投递至 Server 连接
S->>DO: Envelope · to client session
DO->>C: 下行
```



#### ServerHub 状态机摘要(对照 §5)

```mermaid
stateDiagram-v2
[*] --> EMPTY
EMPTY --> SERVER_PENDING: 有 Client、尚无 Server
SERVER_PENDING --> READY: Server 出站接入
EMPTY --> READY: Server 先接入且路由就绪
READY --> DEGRADED: Server 断开
DEGRADED --> READY: Server 重连成功
DEGRADED --> SERVER_PENDING: 按策略清空或降级为待 Server
```



### 1.1 逻辑组件


| 组件 | 职责 | 部署位置 |
| ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------- |
| **Atmos Computer** | 用户可选中的一台计算环境(产品对象);由 `server_id` 标识;**一台 Computer 上运行一个 Atmos Server** | 用户本机 / **云端 VPS** / Desktop 侧车等 |
| **Atmos Server** | 现有 `apps/api`:HTTP + WS、业务逻辑、终端、Canvas relay 等 | 驻留在某台 **Computer** 内 |
| **Control Plane** | 账号、**Computer** 注册、配对码、访问令牌签发、**Computer** 元数据 | Cloudflare Worker + D1(或等价持久化) |
| **Relay Hub(DO)** | 每个 `server_id` 一个 DO 实例:维护 **1 条 Server 出站 WS** + **N 条 Client WS**;路由、限流、(M2+)事件缓冲与 replay | Cloudflare Durable Object |
| **Client** | `apps/web`、`apps/desktop`;可选「经 Relay 的 CLI」 | 用户浏览器 / 本机应用 |


### 1.2 连接拓扑

```
┌─────────────┐ 出站 WSS ┌──────────────────┐
│ Atmos Server│ ───────────────► │ DO(server_id) │
│ (任意网络) │ ◄─────────────── │ Relay Hub │
└─────────────┘ 下行帧 └────────▲───────────┘
│
WSS(客户端路径) │
┌─────────────┐ │
│ Web/Desktop │ ─────────────────────────┘
└─────────────┘
│
▼
┌──────────────────┐
│ Worker (路由) │ TLS 终止、鉴权、将 client 绑定到对应 DO
└──────────────────┘
```

**不变量**:

- Server **不向公网开放监听**即可加入 Relay(仅出站)。
- Client **永不直接**与 Server 建立 TCP(除非显式「仅本地」模式 loopback)。
- **业务 WS 帧**在理想设计中 **对 Relay 透明**(见 §4)。

### 1.3 与现有 Atmos 代码的边界


| 现有模块 | Atmos Computer(本 spec)中的角色 |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/api` WS 层 | **不变语义**:对「已认证的本地 client」的处理逻辑复用;新增「来自 Relay 的虚拟 client」注入点(见 §6)。 |
| `apps/web` `useWebSocket` | **连接 URL 与握手参数**变化;消息编解码尽量不变。 |
| `apps/desktop` | 启动时 **`runtime-manager::supervisor::ensure_running`**(与 CLI / `npx @atmos/local-web-runtime` 共用同一 API 进程);**不再**使用 Tauri sidecar + 每实例 `ATMOS_LOCAL_TOKEN`。退出 Desktop **不**终止共享 API。 |
| `apps/cli` | CLI 为 **client**:`canvas` / `review` 等 **HTTP 能力**的 **API 基址 = 当前所选 Atmos Computer**(与 Web/Desktop 同源,见 §8);`atmos runtime ensure|stop|status` 管理本机 API;`atmos local` 为兼容别名。**Review** 不得以 CLI 内 `infra::DbConnection` + `ReviewService` 为数据平面(见 §8.2)。 |
| `crates/runtime-manager` | **本机 Runtime 库**:`runtime_manifest.json`(仅 host/port/ws_url,**无 token**)、`relay_identity.json`、控制面 `register_computer`;feature `supervisor` 供 CLI/Desktop 拉起 `~/.atmos/runtime/current` 或打包布局中的 `bin/api`。 |


### 1.4 统一本机 Runtime(M1 已实现)

**目标**:Desktop、CLI、`local-web-runtime` 安装物、日常 `cargo run -p api` **同一类产品**——一个本机 **Atmos Server**(`apps/api`),多种入口;避免长期维护两套 sidecar / 双 token 模型。

| 能力 | 落点 |
|------|------|
| 发现 | `~/.atmos/runtime_manifest.json`,由 API 在 bind 后写入(`source: "api"`)或 supervisor 在 `ensure` 后写入(`source: "runtime-manager"`) |
| 拉起/健康检查 | `runtime-manager` feature `supervisor`:`apps/cli`(`atmos runtime`)、`apps/desktop`(`runtime.rs`) |
| Relay 注册 | `register_computer` → `relay_identity.json`;`atmos computer register|start`、API `ATMOS_REGISTER_TOKEN` 一次性消费 |
| 本机 HTTP 鉴权 | **默认无 manifest token**;loopback 上 `require_local_token` 仅在设置 `ATMOS_LOCAL_TOKEN` 时生效(可选加固,非默认路径) |

**Desktop 打包布局**(`prepare-sidecar.sh` / `layout-runtime-bundle.sh`):

```text
apps/desktop/src-tauri/binaries/runtime/current/
bin/api, bin/atmos, web/, system-skills/
```

Tauri 资源映射为 `runtime/current`;开发机需先执行 `prepare-sidecar.sh`(见 `scripts/desktop/README.md`)。

---

## 2. 身份与配置模型

### 2.1 标识符


| 字段 | 说明 |
| ------------------- | --------------------------------------------------------------------------------- |
| `server_id` | UUID;全局唯一;**注册成功时**由 Control Plane 分配。 |
| `server_secret` | 高熵密钥;**仅注册响应中出现一次**;持久化于 Server 本机 `~/.atmos/relay_identity.json`;用于 Relay 出站 WS(§3.3)。 |
| `register_token` | 高熵、**单次**、短时效(§2.4);仅用于 `POST /v1/computers/register`,**不得**长期存放在 Server。 |
| `client_token` | 高熵、短时效;由控制面签发;浏览器/Desktop 连 Relay 时使用(§2.4、§3.3)。 |
| `client_session_id` | Relay 为每条 Client WS 分配;用于信封路由与审计。 |
| `tenant_id` | 控制面租户键;**M1 实现**为 `sha256(user_access_token)`(用户在 Settings 创建 **Access Token**)。**M2+** 映射为 `user_id` / `workspace_id`(见 §2.4.6)。 |


### 2.2 本机文件(Computer / Server 侧)

**`~/.atmos/relay_identity.json`(注册成功后写入)**

```json
{
"server_id": "uuid",
"server_secret": "opaque-high-entropy",
"relay_ws_url": "wss://relay.atmos.land/ws/server",
"control_plane_url": "https://relay.atmos.land"
}
```

**`~/.atmos/runtime_manifest.json`**

- 描述 **本机监听** `host` / `port` / `url` / `ws_url`、可选 `pid`、`started_at`、`source`(**不含** auth token)。
- 当 **Web/Desktop/CLI 的当前上下文 = 运行于本机的该 Computer(其 Server)** 时,与本地工具共用,用于发现 **loopback API**;**不**作为「CLI 永远连本机」的全局规则,也**不**作为跨公网发现源。
- CLI 解析(`canvas` / `review` 等):`--api-url` → `ATMOS_API_URL` → `~/.atmos/client-session.json`(**仅 relay**)→ `runtime_manifest.json`。

**`~/.atmos/client-session.json`(客户端写入;relay 时存在)**

- **本地模式**:文件**不存在**;CLI 以 `runtime_manifest.json` 为准。
- **Relay 模式**:Web/Desktop 写入 `{ version, server_id, api_base_url, gateway_token }`(HTTP gateway 基址 + `client_token`)。
- 与 `runtime_manifest.json` 分离:manifest = **本机 Server 监听事实**;client-session = **UI 当前选中的 Computer**(避免笔记本上 manifest 与 relay 目标冲突)。

### 2.3 Computer 列表缓存(Nice to Have · 未实现)

若 Desktop 需要离线展示最近连过的 Computer,可用明确命名如 `~/.atmos/computers-cache.json`(**禁止**使用 `contexts` 这种泛称)。权威列表仍以 Control Plane 为准;浏览器侧亦可继续用 `localStorage`(`atmos-computer` store)。

### 2.4 注册与连接(定稿架构 · 上线前)

> **范围**:仅解决 **Server 注册** + **Client 经 Relay 连接**。**不含** Atmos 用户登录、**不含** E2EE。
> **不向后兼容**:废弃 8 位 `pair_codes`、`SHARED_CP_SECRET` 命名及公开短码 `register`。实现落点:`packages/relay`、`apps/api`、`apps/web`。

#### 2.4.1 设计原则

| 原则 | 说明 |
|------|------|
| **职责分离** | **控制面密钥**只存在于「能管理 fleet 的客户端」(Web Settings、运维脚本);**Server 永不长期持有**控制面密钥。 |
| **注册凭证一次性** | Server 只用 **`register_token`**(高熵、单次、短 TTL)完成注册;不用 8 位码、不把控制面密钥写入 VPS。 |
| **连接凭证短期** | Client 用 **`client_token`**(高熵、短 TTL)连 Relay;与 `server_secret` 生命周期分离。 |
| **云端目录** | D1 维护 **`computers`** 表 = 权威 Computer 列表(产品对象);Client **不**靠记 IP。 |
| **数据面最小暴露** | `POST /v1/computers/register` 可公网可达,但 **无有效 register_token 则无法注册**;对 register 做 **IP 限速**。 |

#### 2.4.2 凭据一览

| 凭据 | 谁持有 | 用途 | 寿命 |
|------|--------|------|------|
| **Access Token**(Bearer) | 用户在 Web Settings 创建;`tenant_id = sha256(token)` | 签发 `register_token`、列 Computer、吊销、`client_session` | 用户可轮换/吊销 |
| **`CONTROL_PLANE_KEY`**(已废弃) | — | 原单租户运维密钥模型 | 由 Access Token 替代 |
| **`register_token`** | 一次性交给 VPS(env/CLI) | 仅 `POST /v1/computers/register` | 建议 **15 分钟**、**单次** |
| **`server_secret`** | 仅该 Server 本机 `relay_identity.json` | Relay 出站 `GET /ws/server` | 长期;可随吊销失效 |
| **`client_token`** | 浏览器/Desktop 内存 | Relay `GET /ws/client` | 建议 **24h** 或可配置更短 |

`tenant_id`:M1 = `sha256(access_token)` 的定长十六进制(每用户 Access Token 一个租户)。**禁止**用 `CONTROL_PLANE_KEY` 计算用户 `tenant_id`;该密钥仅用于系统/运维管理面(若仍配置)。

#### 2.4.3 端到端流程

```mermaid
sequenceDiagram
participant UI as Web Settings
participant CP as Control Plane
participant S as Atmos Server
participant R as Relay DO

UI->>CP: POST /v1/register_tokens<br/>Bearer Access Token
CP-->>UI: register_token, expires_at
Note over UI,S: 复制命令 / 二维码(仅含 token + cp URL)
S->>CP: POST /v1/computers/register<br/>{ register_token }
CP-->>S: server_id, server_secret, relay_ws_url
S->>S: 写入 relay_identity.json
S->>R: WSS /ws/server + Bearer server_secret

UI->>CP: GET /v1/computers
UI->>CP: POST /v1/computers/{id}/client_sessions
CP-->>UI: client_token, ws_url
UI->>R: WSS ws_url(client)
```

**本机 loopback 模式**:不经过上述流程;`connectionMode=local` + `runtime_manifest.json`(与 Relay 正交)。

#### 2.4.4 控制面 REST(定稿)

基址:`https://relay.atmos.land`(生产)。所有 JSON;CORS 按现有 Worker。

| 方法 | 路径 | 鉴权 | 请求体 | 响应(要点) |
|------|------|------|--------|----------------|
| `POST` | `/v1/register_tokens` | Bearer **Access Token** | `{}` 或 `{ "display_name_hint": "..." }` | `{ "register_token", "expires_at", "register_command" }` |
| `POST` | `/v1/computers/register` | **无**(凭 token) | `{ "register_token", "display_name"?: string }` | `{ "server_id", "server_secret", "relay_ws_url", "control_plane_url", "display_name" }` |
| `GET` | `/v1/computers` | Bearer **Access Token** | — | `{ "computers": [{ server_id, display_name, revoked, created_at, online? }] }` |
| `POST` | `/v1/computers/{server_id}/revoke` | Bearer **Access Token** | `{}` | `{ "ok": true }`;Relay 断开该 hub |
| `POST` | `/v1/computers/{server_id}/client_sessions` | Bearer **Access Token** | `{ "client_kind"?: "web" \| "desktop" \| "cli" }` | `{ "client_token", "expires_at", "ws_url" }` |

`register_command` 示例(供 UI 展示):

```bash
atmos computer register \
--control-plane https://relay.atmos.land \
--token <register_token>
```

或一次性环境变量:`ATMOS_REGISTER_TOKEN=<register_token>`(`apps/api` 启动时消费后 **清除/忽略** 环境变量,避免残留)。

**错误码(统一形状)**:`{ "error": "snake_case_code" }` — 如 `unauthorized`、`invalid_register_token`、`register_token_expired`、`computer_revoked`、`rate_limited`。

#### 2.4.5 Relay WebSocket(定稿)

| 角色 | URL | 鉴权 |
|------|-----|------|
| **Server 出站** | `wss://relay.atmos.land/ws/server?server_id=<uuid>` | `Authorization: Bearer <server_secret>`(禁止 query 传 secret) |
| **Client** | `wss://relay.atmos.land/ws/client?server_id=<uuid>&token=<client_token>&client_type=web` | query `token`;CP 已校验 token 与 `server_id` 绑定 |

Server 连接成功后,DO 登记 `active_server_transport`;Client 连接绑定 `client_session_id`;业务帧仍走 §4 信封。

`online`(列表可选字段):Control Plane 可查询 DO 或由 Server 心跳更新 `last_seen_at`(实现二选一,M1 可仅 `last_seen_at` 来自 relay 连接事件写 D1)。

#### 2.4.6 D1 schema(定稿,替代 `pair_codes`)

```sql
CREATE TABLE register_tokens (
token_hash TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
created_at INTEGER NOT NULL
);

CREATE TABLE computers (
server_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
secret_hash TEXT NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0,
display_name TEXT,
created_at INTEGER NOT NULL,
last_seen_at INTEGER
);

CREATE TABLE client_sessions (
token_hash TEXT PRIMARY KEY,
server_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);

CREATE INDEX idx_computers_tenant ON computers(tenant_id);
CREATE INDEX idx_client_sessions_server ON client_sessions(server_id);
```

- 表中 **只存 hash**:`token_hash = sha256(token)`,`secret_hash = sha256(server_secret)`。
- **废弃**:`pair_codes`、`owner_tag`、`SHARED_CP_SECRET` 字段名。

#### 2.4.7 与 Paseo 的定位(简)

| | **本方案** | **Paseo** |
|---|-----------|-----------|
| 云端列表 | **有**(`GET /v1/computers`) | 无;客户端自维护连接 |
| 注册信任 | **一次性 register_token**(高熵) | QR / link 内公钥(E2EE 信任锚) |
| Server 上长期密钥 | 仅 **`server_secret`** | daemon 密钥对 + relay session |
| 多机 | 每台 Server 各注册一次;Web 统一列表 | 每台 daemon 各添加一条连接 |

#### 2.4.8 实现落点(仓库)

| 组件 | 变更 |
|------|------|
| `packages/relay` | 新 REST + D1 迁移;删除 `pair_codes` / `client_ws_token` 旧路径;Wrangler secret 改名为 **`CONTROL_PLANE_KEY`** |
| `apps/api` | 读 `relay_identity.json`;启动时可选消费 `ATMOS_REGISTER_TOKEN`;`relay/ingest` 用 `server_secret` 连 `/ws/server` |
| `apps/web` | Settings:**Add computer** → `register_tokens` → 展示命令;选 Computer → `client_sessions` → `useWebSocket` 用返回的 `ws_url` |
| `apps/cli`(可选 M1) | `atmos computer register --token` |

#### 2.4.9 明确不做(本阶段)

- Atmos 用户登录 / JWT 替换 CP key(**M2+**)。
- E2EE / QR 内嵌公钥(**M5**)。
- 8 位配对码、`POST /v1/pair_codes`、Server 长期保存 `CONTROL_PLANE_KEY`。
- Client 通过 VPS IP 连接 Relay(仅 **local loopback** 或 **Relay** 二选一)。

#### 2.4.10 后续扩展(仅占位)

| 阶段 | 变更 |
|------|------|
| **M2 用户登录** | Bearer 改为 Atmos JWT;`tenant_id := sub`(用户或 workspace) |
| **M5 E2EE** | 在 §4 信封之上增加加密层;与注册/连接凭据正交 |

---

## 3. 控制面(Control Plane)协议(逻辑)

> **权威定义见 §2.4.4**(路径、鉴权、请求/响应)。本节仅保留与数据面衔接的要点。

### 3.1 安全要求(注册与凭据)

- **`register_token`**:≥ 32 字节随机(建议 base64url 43 字符);TTL **15 分钟**;**原子** `used_at` 写入;D1 只存 `token_hash`。
- **`server_secret` / `client_token`**:≥ 32 字节随机;`server_secret` **仅**注册响应明文一次;DB 只存 hash。
- **`POST /v1/computers/register`**:按 IP **限速**(如 30 次/分钟);连续失败可临时封禁。
- **`CONTROL_PLANE_KEY`**:仅 Wrangler secret + 可信管理端;**禁止**写入公开前端构建产物(Web 可继续「运维粘贴」模式直至 M2 服务端代理)。

### 3.2 Server → Relay 认证(出站)

**M1 定稿**:首次及后续重连均使用

- URL:`wss://<relay-host>/ws/server?server_id=<server_id>`
- Header:`Authorization: Bearer <server_secret>`
- Worker/DO:查 D1 `computers.secret_hash` 与 `revoked=0`;失败则 `401` 关闭 WS。

可选 **M2+**:升级为 challenge-response(非本阶段)。

---

## 4. Relay 数据面:信封(Envelope)规范

### 4.1 设计原则

1. **Relay 不解析 Atmos 业务 JSON** 的内部字段(如 `canvas_agent_dispatch` 的 payload 结构)。
2. Relay 只处理 **信封** + **长度/配额/连接状态**。
3. 业务层仍可使用现有 **request_id** 做关联;信封层可再带 **relay_seq** 用于 replay(M2)。

### 4.2 信封字段(建议最小集)


| 字段 | 类型 | 说明 |
| ------------ | ------------------ | ----------------------------------------------------------- |
| `v` | `uint` | 信封协议版本;M1 固定 `1`。 |
| `stream` | `string` | 逻辑子流:`app`(主应用 WS)/ `diag`(可选);M1 可仅 `app`。 |
| `kind` | `string` | `frame` | `ctrl`;`ctrl` 用于 ping/pong、订阅确认。 |
| `from` | `string` | `server` | `client:<session_id>`。 |
| `to` | `string` | `server` | `client:<session_id>` | `broadcast_clients`(慎用)。 |
| `request_id` | `string?` | 透传业务关联;Relay 不解释。 |
| `relay_seq` | `uint64?` | M2+ 单调递增,用于 replay 游标。 |
| `body` | `bytes` | `string` | **透明载荷**:即现有 WS 文本帧或二进制帧内容(实现二选一并在网关固定)。 |


### 4.3 路由规则(DO 内)

1. **Client → Server**:`to == "server"` 时,若 Server 已连接,写入 Server 出站队列;否则进入 **短时缓冲**(仅 M2+ 明确容量与 TTL)或返回 `ctrl` 错误帧。
2. **Server → Client**:`to` 指定 `client:<session_id>` 单播;或 `broadcast_clients` 多播(例如全局通知,需白名单事件类型)。
3. **Server 未连接**:Client 侧收到 `ctrl`:`server_offline`;UI 展示可恢复状态。

---

## 5. Durable Object:`ServerHub` 状态机(逻辑)

### 5.1 状态


| 状态 | 含义 |
| ---------------- | ---------------------------- |
| `EMPTY` | 无 Server、无 Client;可休眠或延迟创建。 |
| `SERVER_PENDING` | 有 Client 无 Server;可缓冲或拒绝业务帧。 |
| `READY` | Server 已连接;可双向路由。 |
| `DEGRADED` | Server 刚断;按策略缓冲或快速失败。 |


### 5.2 存储(DO Storage)


| 键 | 用途 |
| ---------------- | ------------------------------------------------ |
| `last_relay_seq` | 单调递增;replay 游标基准。 |
| `ring` | 可选:最近 N 条 **信封+body** 或仅 **信封+body hash**(合规驱动)。 |
| `clients` | `session_id → WebSocket` 映射。 |
| `server_ws` | 单条 Server 连接引用。 |


### 5.3 Replay(M2)

- Client 重连握手携带 `last_seen_relay_seq`。
- DO 从 `ring` 中 **顺序重放** `relay_seq > last_seen_relay_seq` 的条目。
- **与业务 request/response 的交互**:若业务层已有 `request_id`,replay 仅 **重放下行事件**;重复上行需业务幂等(Canvas/终端模块各自约定,可引用现有 idempotency)。

---

## 6. Atmos Server 集成方式

### 6.1 出站客户端模块(建议新 crate 或 `apps/api` 子模块)

职责:

1. 读取 `relay_identity.json`;无则跳过(纯本地模式)。
2. 向 Control Plane 刷新令牌(若采用 JWT)。
3. 维护与 Relay 的 **单连接**(每 Server 进程一条);自动重连带指数退避。
4. 从 Relay 收到的 `body` **写入** 现有 `WsMessageService` 的「虚拟连接」入口,等价于本地 TCP client 的第一条消息之后的行为。

### 6.2 与现有 `ConnectInfo` / 鉴权中间件的关系

- **本地 loopback**:`require_local_token` **仅当** 配置了 `ATMOS_LOCAL_TOKEN`(或等价)时强制;统一 runtime 默认 **不设** manifest token,与 Desktop/CLI 零配置发现一致。
- **Relay 注入路径**:需新增 **可信内部路径**(例如仅接受来自本机 `relay_ingest` 任务队列的帧),**禁止**未经鉴权的外网直连接替。

### 6.3 与 Canvas Agent Relay 的关系

- `CanvasAgentRelay` 仍以 **同一 Server 进程内** 的 `conn_id` 为键。
- 经 Relay 的 Web client 与经本地 WS 的 client **在 Server 侧汇聚为同类连接**(需统一 `conn_id` 生成与生命周期)。

---

## 7. 客户端(Web / Desktop)改动要点

### 7.1 连接 URL

- **本地模式**:`ws://127.0.0.1:<port>/...`(现有)。
- **Relay 模式**:`wss://relay.../v1/client?server_id=...&token=...`(token 可为短期,见 Control Plane)。

### 7.2 UI

- **Server 选择器**:**Computer** 列表来自 Control Plane;当前选中项写入本地偏好。
- **配对入口**:展示配对码或二维码(内容由 Control Plane 返回)。

### 7.3 Web 开发环境

- Next 继续可代理 `runtime_manifest` 用于 **本地 API 端口**;Relay 模式下 **以 Control Plane 返回的 `relay_ws_url` 为准**。

---

## 8. CLI 行为(规范)

**锚点**:CLI 的 **业务 HTTP 基址**与 Web/Desktop 一致,绑定 **用户当前所选 Atmos Computer**(`server_id` / 上下文),**不是**「跑在哪台笔记本上」或「是否 SSH 在 VPS 上」的隐含本机。


| 上下文 | 默认行为(API 基址) |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **当前所选 Computer = 本机进程**(常见:日常开发、SSH 到某台 **Computer**(VPS)且 UI 也指向该 **Computer**) | 可读 `runtime_manifest.json` 等得到 **该 Computer 上 Server 的** `http://127.0.0.1:<port>` 或等价 loopback;**零 `ATMOS_API_URL`** 的前提是「上下文已指向这台机器上的 **Computer**」,而非「CLI 进程所在机器一定是 API 宿主」。 |
| **当前所选 Computer = 远程**(笔记本终端 + UI 已切到云端) | 使用上下文中的 **远程 API 基址**;M1 可允许 `**ATMOS_API_URL` / `--api-url` 显式覆盖** 作为过渡;M2+ 目标为 **Relay HTTP gateway**、`atmos context use <server_id>` 等写入共享上下文,使 CLI 与 UI **免手抄 URL**(与 §9 里程碑一致)。 |


M1 **不强制**实现「笔记本 CLI 不经显式 URL 即连远端 **Computer**」,但 **不得**再产品化「CLI 永远默认 127.0.0.1 而可与 UI 所选 **Computer** 脱钩」。

### 8.1 单一事实来源(API anchor)

- **原则**:凡涉及 **与 UI 同一份业务状态** 的 CLI 能力(含 `**atmos review`**),**一律经 `apps/api`** 发起请求;CLI 是 **client**,不承载第二套持久化入口。
- **与仓库传输偏好一致**:Review 以 **HTTP(REST 或 RPC 形状)** 暴露为宜(与现有「bootstrap / 一次性操作用 REST」例外一致);若后续某条 review 流必须流式,再在 **已连上当前 Computer 上 Server 的 WS** 上扩展消息,而不是让 CLI 直连 DB。
- **鉴权**:与 Web/Desktop 调用同一 API 的凭证模型(如 Bearer / session cookie 的 CLI 等价物);loopback 下默认 **无** manifest token;可选 `ATMOS_LOCAL_TOKEN` 加固。数据 **仍由 API 读写**,而非 CLI 打开 `~/.atmos/db/atmos.db`。

### 8.2 `atmos review` 重构要点(相对现状)


| 项 | 现状(待废弃) | 目标 |
| ---- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| 数据平面 | CLI `main` 内 `DbConnection::new()` + `ReviewService` | 解析 **当前所选 Atmos Computer 的 API base URL**(与 `canvas` **同源**的解析链),HTTP 调用 `apps/api` 上 **Review 契约** |
| 事实来源 | 本机 `~/.atmos/db/atmos.db` 与 Server 上 API 使用的库 **可能不一致** | **仅** Server 进程内的 DB(由 API 迁移与服务层访问) |
| 实现落点 | `apps/cli` 依赖 `core-service` + `infra` 做 review | `apps/cli` **薄客户端**(HTTP + 序列化);路由与 DTO 与 `apps/api` 对齐;缺失端点则在 `**apps/api` 增补** |


**开放项(实现前定稿)**:具体路径/DTO 是 **新增 `/v1/review/...` 资源树** 还是合并进既有模块;是否与 Desktop/Web 共享同一 Rust/TS client crate 由实现选择,但 **契约唯一源为 `apps/api`**。

### 8.3 例外(仍允许本机、不经业务 API)

- `**atmos runtime**` / `**atmos local**`(别名):管理本机 API 进程与 `runtime_manifest.json` / 安装物,属于 **宿主运维**,不要求走 review/canvas 类业务 API。
- `**atmos computer**`:在 **Computer** 上注册 Relay 身份并 `ensure` 本机 API(VPS 或本机)。
- **CLI 自检/更新**:如 `atmos update` 访问 GitHub 等,与 Atmos Server 数据平面无关。

---

## 9. 分阶段落地(Rollout)


| 阶段 | 交付物 |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **M1** | 控制面 **register_token + client_session** 流程(§2.4);Relay Worker + `ServerHub` DO;Server 出站;Web 完成列表/注册/建连;`**atmos review` 经 API(§8.2)**(可与本项并行,PRD **M1-7**)。 |
| **M2** | DO `ring` + `relay_seq` + replay;离线 Computer 策略;**用户登录**替换 `CONTROL_PLANE_KEY`(§2.4.10)。 |
| **M3** | 多 Client 广播语义、Canvas/终端事件分类白名单。 |
| **M4** | 云端拉起 **VPS/虚拟机** 与控制面对接;镜像内预置 `relay_identity.json` 或启动时 pair。 |
| **M5** | 组织权限、审计、配额、E2EE 可选模块。 |


---

## 10. 非功能需求


| 类别 | 要求 |
| -------- | -------------------------------------------------- |
| **延迟** | Relay 增加一跳;目标 **P95 额外 RTT < 50ms**(同区域部署下,内测可调整)。 |
| **可用性** | Relay 故障时,客户端降级提示;**本机 Server 仍可 loopback**。 |
| **可观测性** | DO 暴露聚合指标:连接数、帧率、丢弃数、replay 命中率。 |


---

## 11. 安全清单(摘要)

- TLS 全链路;HSTS(Worker 侧)。
- **控制面**:轮换 `CONTROL_PLANE_KEY`、禁止入库;`register_token` / `client_token` 单次或短 TTL;`register` 端点 IP 限速(§2.4)。
- 速率限制:每 `tenant_id` / 每 `server_id` / 每 IP。
- 日志:**默认不记录 body**;如需调试,脱敏 + 采样。
- **隐私**:M1 Relay 载荷对边缘 **技术上可读**;产品承诺为不解析业务 JSON + 不落库 body;E2EE 见 M5(§2.4.3)。

---

## 12. 开放实现项(实现前闭环)

1. `body` 使用 **文本**(与现有 WS JSON 一致)还是 **二进制**(CBOR);网关统一。
2. Control Plane 与 Relay **是否共享密钥**验证 Server(HMAC vs JWT)。
3. Desktop 与 Web **谁先交付 M1**(PRD 已要求至少一端)。
4. **M2 控制面**:Atmos JWT 签发方、与现有 Web/Desktop 会话的集成点(§2.4.4)。

---

*本文档随实现迭代;与 PRD 冲突时以 PRD 为准并回写本文。* No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

TECH document is written in Chinese; spec guidelines require English.

The entire TECH document uses Chinese for all content. Spec guidelines require English for titles, headings, and body text.

Note: The auth model at lines 285-291 correctly specifies per-user Access Tokens with tenant_id = sha256(access_token) and explicitly deprecates CONTROL_PLANE_KEY for end-user auth, which properly aligns with APP-016 M1 requirements.

Based on learnings: "All spec files must use English for titles, headings, and body text. Inline Chinese quotes from source material are acceptable when needed."

🧰 Tools
🪛 LanguageTool

[grammar] ~342-~342: Ensure spelling is correct
Context: ...oked、rate_limited`。 #### 2.4.5 Relay WebSocket(定稿) | 角色 | URL | 鉴权 | |------|-----|------| ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~501-~501: Ensure spelling is correct
Context: ... | ### 5.3 Replay(M2) - Client 重连握手携带 last_seen_relay_seq。 - D...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.22.1)

[warning] 165-165: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 196-196: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 196-196: Table column count
Expected: 2; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 208-208: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 208-208: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 461-461: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 462-462: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 463-463: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 466-466: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/APP/APP-016_atmos-computer/TECH.md` around lines 1 - 634, The TECH.md
for APP-016 is written entirely in Chinese but must follow the spec guideline
requiring English; please translate all titles, headings, and body text into
fluent English while preserving original semantics, examples and inline Chinese
phrases where helpful. Update the document header/title ("TECH · APP-016:Atmos
Computer"), section headings (e.g., "1.0", "1.1", "2.4.5 Relay WebSocket", "4.2
Envelope fields", "8.2 atmos review", etc.), all tables, diagrams, and prose to
English, keeping identifiers like server_id, server_secret, relay_identity.json,
runtime_manifest.json, register_token, client_token, ServerHub, Durable Object,
and envelope field names (v, stream, kind, from, to, body) unchanged; ensure any
quoted commands, code blocks, and SQL remain intact but with surrounding
explanatory text translated; finally run a pass to keep formatting (mermaid,
tables, code blocks) identical and ensure any inline Chinese notes are preserved
only as parenthetical annotations.

Store user Access Token in ~/.atmos/computer-client.json via loopback API
so Desktop and browser share state; proxy control-plane requests in Tauri
and allow relay.atmos.land in CSP/capabilities for Generate Token.
Use an explicit AtmosComputerPersisted type for zustand partialize so Next.js
typecheck passes in the desktop release pipeline.
Wrap invoke payload as { req: { control_plane_url, ... } } to match
other desktop commands and serde field names.
Desktop and loopback web call POST /api/system/computer/control-plane
instead of Tauri relay_http_request, unifying paths and avoiding WKWebView
fetch/CSP issues. Remove the unused desktop invoke command.
Scope editor, canvas, sidebar, and connection UI state to local vs
computer:serverId keys so local and relay targets do not clobber each
other; keep theme and panel layout global.
Fixes atmos-desktop compile error in release-desktop CI.
@AruNi-01

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

New users and cleared prefs start in dark mode; explicit System choice still follows OS.
Store registration channel and client version as JSON on register; show
it in the computer details dialog with tabbed runtime/host sections.
Add D1 updated_at and registration_meta migrations, relay deploy scripts,
and fix online status when the server WebSocket disconnects.
@AruNi-01
AruNi-01 merged commit d00723b into main May 17, 2026
16 of 20 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6d2c8920 Deployed May 17, 2026 by vercel[bot]
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.

2 participants