feat(app-016): Atmos Computer relay, access tokens, and unified local runtime - #115
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (65)
📝 WalkthroughWalkthroughThis 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. ChangesAtmos Computer relay, runtime manager, and canvas agent bridge
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 liftConvert 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 liftPRD 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 winFix 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 winAdd a language identifier to the fenced code block.
markdownlintMD040 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.mdaround 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 becomestext ... ```, 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.mdaround 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., changetotext) 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 -->
| - 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 |
There was a problem hiding this comment.
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:
- Automatically run
wrangler d1 migrations apply atmos-computer-cp(safest), or - Check that migrations are up-to-date and fail if not, or
- 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
🧩 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.rsRepository: AruNi-01/atmos
Length of output: 5496
🏁 Script executed:
cat -n apps/api/src/relay/ingest.rsRepository: 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 2Repository: 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.
| 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, | ||
| } |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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/ -rRepository: 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.jsonRepository: 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:
- 1: https://v2.tauri.app/reference/javascript/shell/
- 2: "program not allowed on the configured shell scope" despite using correct configuration example from documentation tauri-apps/plugins-workspace#2418
- 3: https://tauri.app/learn/security/using-plugin-permissions
🏁 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 -20Repository: 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:
- 1: https://v2.tauri.app/security/permissions
- 2: https://tauri.app/learn/security/using-plugin-permissions
- 3: https://v2.tauri.app/reference/javascript/shell/
🌐 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:
- 1: https://v2.tauri.app/plugin/shell
- 2: https://v2.tauri.app/learn/security/using-plugin-permissions/
- 3: https://tauri.ubitools.com/plugin/shell/
- 4: https://v2.tauri.app/reference/javascript/shell/
🏁 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 -100Repository: 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 -50Repository: 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.jsonRepository: 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.
| if t.starts_with("http://") || t.starts_with("https://") { | ||
| t.to_string() | ||
| } else { | ||
| format!("https://{t}") |
There was a problem hiding this comment.
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.
| let client = reqwest::Client::new(); | ||
| let res = client | ||
| .post(&url) | ||
| .json(&body) | ||
| .send() | ||
| .await | ||
| .map_err(|e| format!("register request failed: {e}"))?; |
There was a problem hiding this comment.
🧩 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 2Repository: 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.rsRepository: 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 -20Repository: 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.
| 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.
| if (request.headers.get("Upgrade") !== "websocket") { | ||
| return new Response("Expected WebSocket Upgrade", { status: 426 }); |
There was a problem hiding this comment.
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.
| # 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 条目。* |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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.
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (3)
crates/runtime-manager/src/identity.rs (2)
59-60:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWrite the identity to the resolved path, not the default path.
read_server_identity()andclear_server_identity()honorATMOS_SERVER_IDENTITY_PATH, butwrite_server_identity()still writes torelay_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 winPersist
server_secretwith restrictive file permissions.This writes relay credentials with the process default file mode. On a permissive umask, other local users can read
server_secretfrom 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 winPreserve all
HeadersInitvariants when merging auth headers.This still uses object spread for
init.headers, which drops caller headers when they are passed as aHeadersinstance 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
⛔ Files ignored due to path filters (1)
Cargo.lockis 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.ymlapps/api/Cargo.tomlapps/api/src/api/system/computer.rsapps/api/src/api/system/mod.rsapps/api/src/app_state.rsapps/api/src/main.rsapps/api/src/relay/ingest.rsapps/api/src/relay/mod.rsapps/api/src/relay/supervisor.rsapps/cli/src/commands/computer.rsapps/desktop/src-tauri/src/commands.rsapps/desktop/src-tauri/src/main.rsapps/web/next.config.tsapps/web/src/api/rest-api.tsapps/web/src/components/dialogs/AtmosComputerSection.tsxapps/web/src/components/dialogs/ComputerDetailsDialog.tsxapps/web/src/components/dialogs/SettingsModal.tsxapps/web/src/lib/atmos-access-token.tsapps/web/src/lib/atmos-computer-local.tsapps/web/src/lib/atmos-computer-store.tsapps/web/src/lib/desktop-runtime.tsapps/web/src/lib/ws-url.tscrates/runtime-manager/src/computer_name.rscrates/runtime-manager/src/identity.rscrates/runtime-manager/src/lib.rspackages/relay/src/index.tspackages/relay/wrangler.tomlpackages/ui/src/components/icons/computer-icon.tsxscripts/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
| 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))) |
There was a problem hiding this comment.
🛠️ 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).
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.
There was a problem hiding this comment.
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 liftPRD 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 reservesCONTROL_PLANE_KEYonly 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 winRe-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 forserver_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 winHandle “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 valueAdd language identifier to fenced code block.
The code block should specify
textorbashfor 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 winFix 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
📒 Files selected for processing (36)
apps/api/src/api/canvas/agent.rsapps/api/src/api/mod.rsapps/api/src/api/review/mod.rsapps/api/src/api/system/computer.rsapps/api/src/api/system/handlers.rsapps/api/src/api/system/mod.rsapps/api/src/relay/http_gateway.rsapps/api/src/relay/ingest.rsapps/api/src/relay/register.rsapps/api/src/relay/supervisor.rsapps/cli/src/api_client.rsapps/cli/src/commands/canvas.rsapps/cli/src/commands/computer.rsapps/cli/src/commands/local.rsapps/cli/src/commands/review.rsapps/cli/src/commands/runtime.rsapps/cli/src/main.rsapps/desktop/src-tauri/src/runtime.rsapps/web/src/api/relay.tsapps/web/src/api/rest-api.tsapps/web/src/components/dialogs/AtmosComputerSection.tsxapps/web/src/components/dialogs/ComputerDetailsDialog.tsxapps/web/src/components/dialogs/VpsRemoteSetupBlock.tsxapps/web/src/components/layout/LeftSidebar.tsxapps/web/src/components/layout/sidebar/WorkspaceKanbanFilterMenu.tsxapps/web/src/lib/atmos-access-token.tsapps/web/src/lib/atmos-computer-local.tsapps/web/src/lib/fetch-register-token.tsapps/web/src/lib/vps-setup-commands.tscrates/runtime-manager/src/identity.rscrates/runtime-manager/src/supervisor.rspackages/relay/AGENTS.mdpackages/relay/src/index.tsscripts/desktop/layout-runtime-bundle.shspecs/APP/APP-016_atmos-computer/PRD.mdspecs/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
| const serverId = localStatus?.server_id ?? localServerId; | ||
| if (serverId) { | ||
| await cpFetchWithAccessToken( | ||
| controlPlaneUrl, | ||
| accessToken, | ||
| `/v1/computers/${encodeURIComponent(serverId)}/revoke`, | ||
| { method: 'POST', body: '{}' }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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).
| 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, |
There was a problem hiding this comment.
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.
| 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.
| 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', |
There was a problem hiding this comment.
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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Dependencies
feat(canvas): APP-015 terminal agent integration) intomainfirst.aarynlu/app-015-canvas-terminal-agent-86ea; after feat(canvas): APP-015 terminal agent integration #114 lands, merging this PR only adds the APP-016 commits (relay control plane,atmos computer, unifiedruntime-manager). Rebasing ontomainpost-feat(canvas): APP-015 terminal agent integration #114 is optional but will shrink the diff.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.jsonon bind (no manifest token); optional loopback auth viaATMOS_LOCAL_TOKENonly.crates/runtime-manager: Replacesruntime-manifest— manifest discovery,relay_identity.json,register_computer, and process supervisor (ensure/stop/status).apps/cli:atmos computer register|start|status,atmos runtime(andatmos localalias), 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 bundledruntime/current+runtime-manager::ensure_running(quit does not kill shared API).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
Validation
just lintjust testjust fmtcargo check -p api -p atmos -p atmos-desktop -p runtime-manager(local)wrangler deploydocumented inpackages/relay/README.md(ops, not run in CI here)Checklist
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
scripts/desktop/prepare-sidecar.sh(or build) to lay outapps/desktop/src-tauri/binaries/runtime/current/; the app attaches to the shared runtime and no longer uses a per‑launch local token.~/.atmos/runtime_manifest.json; useatmos runtime ensure|status|stop(oratmos local).~/.atmos/computer-client.jsonand shared by Desktop/Web (manage via/api/system/computer-client-settings). Control‑plane requests are proxied through the local API; Desktop allowsrelay.atmos.landin CSP/capabilities.atmos computer register --token <ACCESS_TOKEN>(or setATMOS_REGISTER_TOKEN) to writerelay_identity.jsonand connect outbound.relay_identity.jsonexists, the server starts the outbound relay and proxies REST via the HTTP gateway automatically; mutating computer routes require loopback auth.~/.atmos/client-session.json(gateway base + client token) when present, else falls back to the runtime manifest; clear or switch in Settings.atmos computer start --daemonto register and launch in the background.Refactors
atmos canvas …,atmos runtime …, andatmos computer …commands.computer:<serverId>); add Computer Details dialog (runtime/host tabs), showregistration_meta, and fix online status updates.deploy-relayworkflow; enabled Worker logs/traces; applied D1 updates (includingupdated_atandregistration_meta); dev REST proxy innext.config.ts.Written for commit 6d2c892. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
atmos computer(register/status/start),atmos canvas(shape/frame/draw operations),atmos runtime(ensure/stop/status)Improvements
Documentation