CodeDocs Vault

Architecture

1. Three deployment topologies

┌──────────────── Topology A — fully local (default) ────────────────┐
│                                                                    │
│  browser ──► Next.js dev / static export (localhost:OD_WEB_PORT)   │
│                  │                                                 │
│                  │ HTTP + SSE (proxy /api/* → daemon)              │
│                  ▼                                                 │
│             Daemon on localhost:OD_PORT (Express + SQLite)         │
│                  │ spawn() + stdin/stdout                          │
│                  ▼                                                 │
│           code-agent CLI (claude / codex / cursor / …)             │
└────────────────────────────────────────────────────────────────────┘

┌──────────── Topology B — Vercel + local daemon (planned) ─────────┐
│   browser ──► od.yourdomain.com  ─tunnel─► daemon on laptop       │
└───────────────────────────────────────────────────────────────────┘

┌────────── Topology C — Vercel + direct-API BYOK proxy ────────────┐
│   browser ──► /api/proxy/{anthropic,openai}/stream                │
│         (no daemon path, internal-IP/SSRF blocked at the edge)     │
└───────────────────────────────────────────────────────────────────┘

A is the shipped default; B is documented as design intent; C is the BYOK fallback (apps/daemon/src/server.ts:2209-2287 for Anthropic, 2289+ for OpenAI). Internal-IP blocking is enforced in validateExternalApiBaseUrl() at server.ts:2187-2207.

2. Component diagram

┌────────────────────────── apps/web (Next.js 16 SPA) ──────────────────────────┐
│                                                                               │
│  ChatPane · ChatComposer · AgentPicker · QuestionForm · FileWorkspace          │
│  PreviewModal · DesignSystemsTab · PromptTemplatesTab · SettingsDialog         │
│  ToolCard · AssistantMessage · DesignSpecView · pet/                           │
│                                                                               │
│  state/ (config, projects, maxTokens, litellm-models)                         │
│  runtime/ (srcdoc, react-component, tool-renderers, todos, markdown,          │
│            exports, zip)                                                      │
│  comments.ts (DOM-snapshot → chat-attachment for surgical edits)              │
└─────────────────┬──────────────────────────────────┬──────────────────────────┘
                  │ /api/* (proxied)                 │ /api/proxy/*/stream
                  ▼                                  │ (Topology C BYOK)
┌────────────────────────────── apps/daemon ────────────────────────────────────┐
│                                                                               │
│   Express server (server.ts ~2400 lines, 70+ routes)                          │
│   ├─ /api/agents · /api/skills · /api/design-systems · /api/prompt-templates  │
│   ├─ /api/projects · /api/projects/:id/conversations · …/messages · …/tabs    │
│   ├─ /api/projects/:id/files (CRUD + raw + preview)                           │
│   ├─ /api/templates · /api/codex-pets · /api/import/claude-design             │
│   ├─ /api/artifacts/{save,lint}                                               │
│   ├─ /api/media/{models,config} · /api/projects/:id/media/{generate,tasks}   │
│   ├─ /api/deploy/config · /api/projects/:id/deploy                            │
│   ├─ /api/runs (Managed Agents)                                               │
│   ├─ /api/chat (SSE) ────────► spawns CLI, streams events                     │
│   └─ /api/proxy/{anthropic,openai}/stream (SSE) ────► fetch upstream API     │
│                                                                               │
│   Subsystems:                                                                 │
│   ├─ prompts/{system,discovery,directions,deck-framework,                     │
│   │           media-contract,official-system}.ts → composeSystemPrompt()     │
│   ├─ agents.ts (12 AGENT_DEFS, capability probe, listModels, buildArgs)      │
│   ├─ stream parsers: claude-stream · copilot-stream · acp · pi-rpc ·         │
│   │                  json-event-stream                                       │
│   ├─ skills.ts + craft.ts + design-systems.ts (filesystem indexers)          │
│   ├─ db.ts (SQLite: projects/conversations/messages/tabs/templates/           │
│   │                  preview_comments/deployments)                           │
│   ├─ projects.ts (path-traversal-safe filesystem CRUD)                       │
│   ├─ lint-artifact.ts (anti-AI-slop linter — 9 P0 + N P1 patterns)           │
│   ├─ media.ts + media-models.ts + media-config.ts (provider router)           │
│   ├─ deploy.ts (Vercel)                                                       │
│   ├─ artifact-manifest.ts · claude-design-import.ts · runs.ts                │
│   └─ codex-pets.ts + community-pets-sync.ts (mascot sprites)                 │
└────────┬─────────────────────────────────────────────────────────────────────┘
         │ child_process.spawn(...)
         ▼
   ┌─────────────────────────────────────────────────────────┐
   │ Detected agent CLI (claude · codex · devin · cursor ·   │
   │ gemini · opencode · qwen · copilot · hermes · kimi ·    │
   │ pi · kiro), cwd pinned to .od/projects/<id>/            │
   └─────────────────────────────────────────────────────────┘

Three thin Electron entry points live alongside this:

3. Workspace boundaries (the packages/)

packages/
├── contracts/        Pure TS DTOs shared between web and daemon.
│                     api/{chat,projects,artifacts,comments,files,proxy,registry}
│                     sse/chat · prompts/system · common
│                     RULE: no Next, Express, fs, browser, sqlite, sidecar deps.
├── sidecar-proto/    Open Design *business* sidecar protocol.
│                     5-field stamp = { app, mode, namespace, ipc, source }
│                     IPC messages: STATUS · EVAL · SCREENSHOT · CONSOLE · CLICK · SHUTDOWN
│                     Namespace validation (alphanumeric + ._-, ≤128 chars).
├── sidecar/          Generic sidecar runtime: bootstrap, IPC server/client,
│                     path resolution, launch env. Consumes a contract descriptor;
│                     does not hard-code OD constants.
└── platform/         OS-level process primitives. createProcessStampArgs(),
                      readProcessStamp(), matchesStampedProcess(),
                      listProcessSnapshots(). Cross-platform (ps/wmic).

The point of this split: tools/dev and tools/pack are orchestration layers that only call package primitives — never hand-build --od-stamp-* argv or process-scan regexes. This keeps stamp formats in one place and makes "two concurrent namespaces on one machine" a property of the system rather than something each entrypoint reinvents.

4. Data flow — a typical "make me a deck" turn

1. User selects skill + design system in EntryView, types a brief.
   (or: types prompt, then NewProjectPanel infers metadata)

2. Web POSTs /api/projects → daemon writes row + creates .od/projects/<id>/ dir.

3. Web POSTs /api/chat with { agentId, projectId, conversationId, message,
   skillId, designSystemId, model, attachments, commentAttachments, ... }.
   The route at server.ts:2174 creates a run via design.runs.create() and
   immediately returns SSE. Body of the run is `startChatRun()`:

   server.ts:1856–2080
     ├─ Load skill → load design-system → load craft refs (via opt-in)
     ├─ composeSystemPrompt({ skillBody, designSystemBody, craftBody,
     │                        skillMode, metadata, template })
     │     stack order = discovery → official designer → DESIGN.md →
     │                   craft → skill body → metadata → deck framework
     │                   (last) | media contract (last)
     ├─ Resolve cwd = .od/projects/<id>/, sanitise attachment paths,
     │   build cwdHint + filesListBlock + attachmentHint + commentHint
     ├─ Build composed user prompt:
     │     "# Instructions (read first)\n…\n# User request\n<message>"
     │     + image @paths (claude-style)
     ├─ Call def.buildArgs(composedPrompt, safeImages, extraAllowedDirs,
     │                     { model, reasoning }, { cwd })
     │   → returns CLI argv. Most agents take prompt via stdin (avoids
     │     Windows ENAMETOOLONG ~32KB cmdline limit).
     ├─ child = spawn(invocation.command, invocation.args, {
     │     cwd, stdio: ['pipe','pipe','pipe'], env: { OD_BIN, OD_DAEMON_URL,
     │     OD_PROJECT_ID, …upstream } })
     └─ Wire stream parser keyed off def.streamFormat:
        claude-stream-json | copilot-stream-json | acp-json-rpc |
        pi-rpc | json-event-stream | plain
        Each parser emits typed events that fan out to:
          (a) SSE to the browser
          (b) DB upsertMessage events_json
          (c) artifact-save → lint feedback to agent

4. Browser displays:
     - Live "Todos" card from TodoWrite tool calls (todos.ts)
     - ToolCard rows for each tool_use
     - AssistantMessage text deltas as they arrive
     - PreviewModal/iframe loads the produced HTML when artifact appears

5. On <artifact> emission OR at done:
     POST /api/artifacts/save runs lint-artifact:
       - Detects 9 P0 anti-slop patterns; returns findings to agent
       - Agent self-corrects on next turn via systemMessage feedback

6. User clicks an element in the preview (comment mode):
     - apps/web/src/comments.ts captures DOM snapshot
       → PreviewCommentTarget { path, selector, position, html_hint }
     - Saved as preview_comments row, sent as commentAttachment with the
       next chat turn → agent receives surgical-edit instruction.

5. Filesystem layout at runtime

<project root>
├── .od/                  # daemon-owned data (or OD_DATA_DIR/...)
│   ├── app.sqlite        # SQLite metadata (WAL mode + foreign keys)
│   └── projects/<id>/    # one dir per project, cwd of every spawned agent
│       ├── index.html    # primary artifact
│       └── …             # whatever the agent wrote
├── .tmp/<source>/<namespace>/...   # transient sidecar runtime files
└── /tmp/open-design/ipc/<namespace>/<app>.sock   # POSIX IPC sockets

skills/<id>/
  ├── SKILL.md            # frontmatter + workflow body
  ├── assets/             # template seeds, base.html, icons
  └── references/         # layouts.md, themes.md, components.md, checklist.md

design-systems/<id>/DESIGN.md   # 9-section schema (awesome-claude-design)
craft/<slug>.md                  # universal brand-agnostic rules
prompt-templates/{image,video}/  # gpt-image-2 / Seedance / HyperFrames examples

6. Sidecar process stamps

Every process the lifecycle layer spawns is stamped. From the AGENTS.md root rule: stamps must have exactly five fieldsapp, mode, namespace, ipc, source.

$ ps ax | grep daemon
node …/dist/server.js \
  --od-stamp-app=daemon \
  --od-stamp-mode=dev \
  --od-stamp-namespace=default \
  --od-stamp-ipc=/tmp/open-design/ipc/default/daemon.sock \
  --od-stamp-source=tools-dev

packages/platform's createProcessStampArgs() writes them; readProcessStamp() reads them; matchesStampedProcess() lets tools-dev status find live processes by stamp without a PID file. Why five fields: app identity, dev-vs-runtime mode, isolation namespace, IPC socket address, what spawned it (tools-dev / tools-pack / packaged). namespace is the killer feature — multiple concurrent local sessions don't trample each other's IPC sockets or .tmp/ paths.

7. Security boundaries

Surface Threat Mitigation in code
/api/proxy/*/stream SSRF to internal services validateExternalApiBaseUrl() rejects localhost / 127/8 / 169.254/16 / 10/8 / 172.16-31/12 / 192.168/16, and non-http(s) schemes (server.ts:2187-2207).
Agent reads outside project Path-escape via attachments safeAttachments filter requires path.resolve(cwd, p).startsWith(cwd + sep) (server.ts:1919-1933). Same pattern in file CRUD, image upload, media --image resolution (media.ts:102-156).
Spawned agent over-reach Reading user $HOME cwd pinned to .od/projects/<id>/; only --add-dir opens skills + design-systems explicitly (server.ts:1977-1979).
User secrets Leak to daemon's logs redactAuthTokens() strips Bearer … from upstream errors before logging (server.ts:2185).
Agent autonomy Hangs on permission prompts All agents launched with --permission-mode bypassPermissions/--full-auto/--dangerously-skip-permissions/--allow-all-tools (per agent in agents.ts). Tradeoff: the agent runs un-prompted, so the project cwd is the only sandbox.
Prompt-template injection User pastes ``` to break out of fence safe = tpl.prompt.replace(/```/g, '')(zero-width-space inserted,system.ts:333`).
Image upload in media Path traversal + DoS resolveProjectImage() checks resolved path is under project root; caps at 16 MB; MIME allowlist png/jpg/webp/gif (media.ts:102-156).
Hallucinated CLI args $$$ over-billing clampNumber() snaps duration/length to nearest allowed bucket; clampCodexReasoning() maps user reasoning effort to per-model valid values (agents.ts:78-95).
ZIP import Zip-slip / billion-laughs claude-design-import.ts rejects encrypted entries, validates compression methods, sanitises paths (no \0, absolute, ..), caps 500 files / 100 MB total / 25 MB per file.

8. Performance & rough budgets

The aspirations stated in docs/architecture.md:325-330 (and the code is consistent with them):

No shared-state caches that I could find — skill scanning is on-demand per request, not watched, in MVP.