Remote Updates - #14
Open
kaka-sangi wants to merge 67 commits into
Open
Conversation
The deck had no notion of a user. That was defensible while it bound to loopback, where the OS is the perimeter — but self-hosting puts it on a public hostname, and there the open port is a remote shell with the user's provider credentials attached. Two gaps followed from the same assumption "the deck is on your laptop": no authentication, and every URL the deck says about itself pointing at localhost. Authentication - Argon2id passwords (Bun.password) and server-side sessions in SQLite. The cookie carries a random 256-bit token and only its SHA-256 is stored, so a copy of the database can't be replayed as a live login. Server-side rows rather than a signed cookie are what make revocation real: changing a password ends every other session at once. - The gate lives in Bun.serve's fetch, the one point where the API, the WebSocket and the uploads reader all converge. Enforcing it in Hono middleware would have left /ws open, which is the half that drives agent sessions. Static assets stay ungated so a login screen can render. - Three principals satisfy it: a browser session cookie, a bearer API token for callers that can't hold a cookie, and the routine runner's per-run HMAC. That last scheme already existed but nothing ever verified it; the gate is finally its consumer, which is what keeps routine http steps working now that anonymous access is gone. - Mode is resolved against the bind host: loopback with no password stays open so local dev is unchanged, and any other bind requires an account. `off` is ignored on a public bind — one stale env var should not be able to publish an unauthenticated agent to the internet. - Credentials can come from the environment so a container boots already protected; otherwise a first-run setup screen takes the place of the app, optionally pinned to a setup token. Localhost - OMP_DECK_PUBLIC_URL is what the deck calls itself in text. Serving never needed it (the app is same-origin); the onboarding copy, the agent's API hint and the OAuth instructions did. - The loopback base stays loopback for in-process callers, and now carries the bearer header an agent needs, so `curl` from a session doesn't just start 401ing with no explanation. - Provider OAuth can't be repointed: Anthropic and Codex pin localhost:54545/1455 in the SDK and in their app registrations, so on a remote deck the redirect dies on the user's own machine. The modal now says so instead of promising a listener will catch it, shows the consent URL as copyable text, and promotes paste-the-code from a disclosure triangle to the primary control. A landing route completes the flow when the user edits that dead URL's host to the deck's own. docs/oauth-deck-sdk-findings.md claimed no redirectUri override exists in the SDK. One does — it's just unused by the two providers that matter. Corrected rather than deleted, since the conclusion still holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
A container starts with an empty agent directory. On a laptop that
directory accumulates for months — subagents, skills, extensions, rules,
MCP servers, model routing — and it is most of what makes the agent
behave like yours rather than like a fresh install. Nothing carried it
into a self-hosted deployment, so every image rebuild kept the deck and
lost the agent.
agent-defaults/ is that content, seeded into OMP_AGENT_DIR at container
start. Existing files are never overwritten, so anything edited in the
volume — by the user or by the agent itself — survives redeploys;
OMP_DECK_SEED_FORCE=1 re-applies the image copy deliberately.
Two things the copy alone would have got wrong:
- The agent directory has two names. OMP_AGENT_DIR points the SDK's
session and auth storage at a volume, but parts of the deck and SDK
still resolve ~/.omp/agent directly — installed skills, slash
commands — and in a container that path is not persistent. The seed
script symlinks the two together, migrating anything already at the
home path rather than dropping it.
- mcp.json and models.yml carry live API keys and bearer tokens, and
this repository is public. They ship as .tmpl files with ${VAR}
placeholders, rendered from the environment at start. An unset
variable renders empty rather than leaving a literal ${VAR} behind,
because a server that fails to authenticate is easier to diagnose
than one whose token is the string "${MCP_FOO_TOKEN}".
The MCP configs also came from a Windows machine and invoked
`cmd.exe /c C:\nvm4w\nodejs\npx.CMD`, which does not exist in a Linux
image; those servers are ported to plain npx. One work-log line in
WATCHDOG.md quoted a real API key and is redacted.
.gitignore now blocks the file shapes that must never join this
directory: the SDK auth database, the caches, and the rendered configs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
Self-hosting: authentication, public-URL awareness, and a seeded agent directory
The gate sits in front of the entire app, so a single failed /api/auth/status left the user on a dead-end error screen until they thought to reload — a transient blip at boot became a hard stop. That is a regression against the previous behavior, where a dropped request during bootstrap self-healed as the WebSocket reconnected. Failures now retry on a capped backoff (1s → 15s), and the screen offers a manual "Try again" for anyone who doesn't want to wait. A successful probe resets the attempt counter, and the timer is cleared on unmount. Flagged during review of #1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
…config manager Four new backend surfaces, all mounted under the existing auth gate so nothing here is reachable without a session or bearer token: - path-guard.ts: shared containment for any route that lets a client name a filesystem path. Symlinks are resolved via realpath before the containment check — a symlink planted inside an allowed root can't walk the check outside it the way a plain string-prefix comparison would let it. Two independent root sets (workspaces vs. OMP_AGENT_DIR) so a workspace-scoped browser can never be pointed at the agent's own auth database by a crafted path. - files-service.ts + routes-files.ts: list/read/write/mkdir/rename/delete confined to workspace roots. Binary content is rejected on read rather than mangled through a lossy decode; a delete refuses to remove a workspace root outright. - git-service.ts + routes-git.ts: status/diff/stage/unstage/discard/ commit/log/branches/push/pull/fetch by shelling out to the real git binary via Bun.spawn, parsed through `--porcelain=v1 -z` (NUL-delimited, the only safe way to handle filenames with spaces or newlines). No git library dependency — git's own porcelain output is more complete and battle-tested than a JS reimplementation would be. GIT_TERMINAL_PROMPT=0 throughout so a missing credential fails fast instead of hanging. - github-service.ts + routes-github.ts: list the token owner's pushable repos and clone one into a workspace root. Reuses GITHUB_TOKEN/GITHUB_PERSONAL_ACCESS_TOKEN — the latter is already what agent-defaults/mcp.json.tmpl's github MCP server expects, so one token lights up both surfaces. The embedded-token clone URL is stripped from .git/config immediately after clone; push/pull/fetch instead inject the token as a single-invocation `-c http.extraheader` for github.com remotes, so it's never written to disk. - agent-config-service.ts + routes-agent-config.ts: browse/edit OMP_AGENT_DIR, plus a stage -> review -> apply import flow for replacing it wholesale (e.g. bringing in a ~/.omp/agent from another machine). An uploaded archive extracts into a sibling staging directory — the live directory is never touched until the operator applies a plan they've already seen. Applying is a two-rename atomic swap: the live directory moves aside to a timestamped backup (never deleted, restorable) and the staged tree moves into its place; a same-filesystem rename can't be interrupted halfway. This gets the "turn it off, wipe the folder, drop in new files, turn it back on" outcome without doing any of those steps by hand around a live process — the caller restarts the server *after* the swap so the SDK opens the new agent.db/models.db fresh rather than continuing to hold the old ones' file handles. Merge mode overlays the archive on the current directory (untouched files, including databases, survive byte-for-byte); full-reset mode treats the archive as the complete replacement. Dockerfile: installs git, zip, and unzip in the runtime image — git for the cockpit and GitHub clone flow, zip/unzip for agent-config import/export, all shelling out to the real binaries rather than reaching for JS reimplementations. 61 new tests, including a symlink-escape rejection, git status/diff/ rename parsing (verified against real `git status --porcelain=v1 -z` output — the rename record order is new-path-then-old-path, easy to get backwards and initially wrong here), and the full agent-config stage/apply/backup/restore lifecycle in both merge and full-reset modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
…kpit Aurora: a fourth theme (paper/slate/horizon/aurora) following the existing token contract exactly — every component that reads rgb(var(--accent)) etc. lights up with zero per-component changes. Deep near-black canvas, a genuine two-stop violet-to-cyan accent gradient (--accent-2 is a new token; on the three existing themes it equals --accent, so they render byte-identical to before), glass panels via a .glass utility, and spring/rise-in motion primitives gated behind prefers-reduced-motion. Aurora is now what a fresh dark-mode visitor sees by default — the existing themes are unaffected and still selectable from Settings -> Appearance. The cockpit (new /explorer route): a three-pane workbench built on the existing Layout shell. Sidebar is a lazy-loading file tree (children fetch only on expand, so a big monorepo doesn't pay for a recursive walk to render the root); main is a tab strip over a CodeMirror 6 editor or a unified diff viewer, per tab; inspector is a Git panel (status, stage/unstage/discard, commit, branch switch, push/pull/fetch) and a GitHub panel (list your pushable repos, clone one in a click, open it) on a toggle. Ctrl/Cmd+S saves the active editor tab from anywhere in the view, not just while it has focus. Agent Config (new /agent-config route): browse and edit OMP_AGENT_DIR with the same FileTree component — generalized to accept a pluggable `list` function so the workspace explorer and this browser share every bit of expand/lazy-load/error-state logic — plus the import UX for agent-config-service's stage/review/apply flow. The plan is shown line-by-line (added/changed/removed, database files flagged) before an explicit confirmation checkbox unlocks Apply; a backups panel lists every prior import with one-click restore. That reuse caught a real bug before it shipped: agent-config-service's listAgentDir treated its argument as a path relative to the agent dir, but FileTree always re-lists using the absolute `path` a previous entry returned — same contract as the workspace explorer's filesApi.list. Every second-level expand would have 404'd. Fixed to match files-service's absolute-path contract, with a test that lists a nested directory via the path a prior listing actually returned (not a hand-picked relative string), so a regression here fails the same way a real client would hit it. CodeMirror (~600KB minified) is not in the main bundle: Explorer and Agent Config are React.lazy + Suspense, so the editor's weight loads only when one of those routes opens. The chat view's own bundle is unchanged from before this commit — worth calling out since it's also the phone PWA's first paint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
Durable mobile coding needs two things a browser tab alone can't give: an icon on the home screen that launches full-screen, and notifications that reach the phone with the tab closed. Both land here. Installable: a Web App Manifest (icon.svg for crisp scaling plus the existing apple-touch-icon.png as a fallback — no new image assets, no rasterization pipeline) and a service worker registered unconditionally at boot. The service worker does NOT cache the app shell — this is an actively-developed app on one origin, and a caching SW risks serving yesterday's JS against today's API after a deploy, which is worse than "needs network," which a web app always has anyway. Its only jobs are the two things a foreground tab structurally cannot do: receive push events, and satisfy Chrome/Android's installability requirement (a registered SW with a fetch handler, even a passthrough one). Settings surfaces an Install button wherever the browser offers beforeinstallprompt, manual Safari instructions on iOS (which never fires that event), and a live installed/not-installed state. Push: VAPID keypair generated once and persisted in the deck data directory — regenerating would silently invalidate every existing browser subscription, so this reads-then-generates exactly like the API token in auth/bootstrap.ts. A new WebPushChannel registers into the existing NotificationChannel seam alongside the WS-broadcast browser channel, so every existing notify() call site gets push delivery for free with zero call-site changes. Dead subscriptions (404/410 from the push service — uninstalled, permission revoked, browser data cleared) are pruned automatically on send rather than failing forever. Settings has Enable/Disable and a Test button that sends a real push end to end. GITHUB_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN are now registered in the env schema for Settings -> Env visibility — the GitHub cockpit landed in the previous commit already reading them, but they were undocumented there. 10 new tests: VAPID generation/persistence/caching and the subscription store's upsert-by-endpoint and delete semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
Files/Git/GitHub cockpit, agent-config manager, Aurora theme, installable PWA + push
Settings → Appearance already labels the Aurora card "The new default," but the resolver contradicted its own copy: a first-time visitor only landed on Aurora when their OS reported a dark color-scheme preference, and fell back to the older Paper theme otherwise. Most default/corporate OS installs are light, so most visitors never saw the redesign. Verified visually — booted the built bundle locally, screenshotted login/explorer/agent-config/appearance with Playwright before and after. Before: light Paper theme throughout, no Aurora anywhere in reach without digging into Settings. After: Aurora renders by default across every surface, Settings confirms it ACTIVE, and a saved choice (including an explicit pick of Paper) still overrides it as before — this only changes what a first visit looks like. Two call sites carried the same logic and both needed the fix: the pre-paint script in index.html (authoritative first apply, avoids a flash of the wrong palette) and its React-side mirror in theme.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdpbguegHwnxtct51NuYbQ
Make Aurora the actual default theme, not an OS-gated one
Bump @oh-my-pi/pi-ai and @oh-my-pi/pi-coding-agent from 15.1.7 to 17.2.15
and absorb the v17 API drift in the bridge plan-mode code
(runResolveInvocation -> queueResolveHandler, ResolveToolDetails ->
ResolveDetails, finalPlanFilePath removed, prompt() returns
Promise<void>, addAutocompleteProvider stubbed).
Add a new harness layer on top:
- Custom provider registry. ~/.omp/agent/models.yml is hot-reloaded into
the live ModelRegistry via registerProvider/unregisterProvider. New
/api/providers/custom CRUD with masked-key GETs.
- /api/auth/login accepts a provider + subscriptionKey (+ optional
baseUrl) and materializes a custom provider on the fly for OmniRoute /
MiniMax / custom OpenAI-compatible endpoints, or writes through
AuthStorage.set for known providers.
- Marketplace extras: SSL CA bundle auto-resolved at boot (win32 / darwin /
linux + openssl probe), wired into GIT_SSL_CAINFO and
NODE_EXTRA_CA_CERTS so plugin clones no longer fail with 'Problem with
the SSL CA cert'. /api/marketplace/search, /featured, /popular add a
scoring layer; Featured + Popular strips render on the /marketplace
view.
- SkillsMP-backed skill marketplace at /api/skills/marketplace/* with a
graceful fallback to locally-installed skills.
- /api/system/lifecycle (status / restart / shutdown) and per-session
/api/sessions/:id/{kill, archive, pin, title}. AI title regen derives
a short label from the first user message via a tiny client-side
heuristic.
- Workflowz / orchestrate multi-agent dispatcher at /api/workflows with
four modes (parallel, ultrathink, workflowz fan-out, orchestrate).
Each task runs in its own ephemeral session. /workflows view shows
per-task status cards. Magic words /ultrathink, /workflowz,
/orchestrate, /login, /restart intercepted in the composer.
- Gholam digital-twin sidecar at apps/gholam: standalone Bun process
with heartbeat loop, KB walker, WebSocket bridge, user-controlled
priority queue. GholamOverlay floats bottom-right on every page
with a pulsing dot; clicking expands a chat surface. /gholam route
exposes start/stop, heartbeat tuning, priority CRUD.
- NavRail extended with Workflows and Gholam entries; Featured/Popular
strips fill the marketplace's empty space; workflowz-grid and
Gholam pill styles appended to styles.css.
All four packages typecheck clean; harness routes verified end-to-end
against a live server boot (health, gholam/status, system/lifecycle,
providers/custom CRUD, marketplace/search, skills/marketplace/search).
Install the omp-deck monorepo at the workspace root so apps/gholam resolves hono via the workspace graph, then start the sidecar on 47900 with a HEALTHCHECK against /health.
Lockfile now reflects apps/gholam (hono peer), the @oh-my-pi/pi-ai + @oh-my-pi/pi-coding-agent bump from 15.1.7 to 17.2.15, and the bumped transitive dep graph (hono 4.12, etc.) that bun install resolved after the workspace edit.
The monorepo lockfile references the gholam workspace, so bun install --frozen-lockfile in the Dockerfile needs to see apps/gholam/package.json or the resolver aborts. Stage 1 (web-build) and stage 2 (runtime) both copy the manifest now.
…storefront, prompts, chats, LLM registry, genui, generative UI - Gholam: per-process token mint/WS handshake auth, mcp_call/mcp_reply WS frames, sidecar supervises github+openship+parallel+exa+tavily docker children, gholam-permissions gate. - Fast loop: POST /api/gholam/edit over mcp_call, dist watcher pings reload_available, soft-reload path preserves WS. - Offline: full SW rewrite with IDB queue for prompts, manifest enriched, deploy_state broadcast + DeployStatusBadge. - Studio: tooltip + contextmenu singletons, ~80-entry catalog, StudioProvider, StudioView, 6 panes, /studio + /studio/gholam routes, data-read-only propagation via React 19 inert. - Storefront: Apple/Play grid, sections, detail, search, install round-trip via marketplaceApi.install, MCP health strip, live pulse. - Prompts: library CRUD + share + import/export, recommendation engine (TF-IDF cosine over project/history/usage), composer integration. - Chats: gholam chat persistence migration + service + REST + runtime loop + 3 UI views. - LLM: DeckLLMRegistry over SDK ModelRegistry + CustomProviders, MiniMax provider stanza + adapter, real LLM round-trip via pi-ai streamSimple, LlmMessage.toolCallId typed, LlmChunk.thinking variant, systemPrompt threaded, gholamDeckLLM.complete() preserves provider/model on prior-turn rows. - Generative UI: @ai-sdk/react + ai added, GenComponent allowlist + renderer + useChatThread, /api/genui/stream + /api/preview/render routes, PreviewView + PreviewPane. - Server endpoints: POST /api/mcp/install, POST /api/skills/install, GET /api/storefront/installed. - Codebase cleanup: orphaned livePulseIds slice removed, StoreCard migrated to global pulse, data-read-only propagation wired. - All four workspaces typecheck clean (tsc --noEmit, exit 0).
…tplace (A+B) - apps/server/src/index.ts: seed anthropics/claude-plugins-official on first boot if the marketplace registry is empty; idempotent, no-op on subsequent boots and after the user adds their own marketplaces. - apps/server/src/marketplace-service.ts: addMarketplace() now calls ensureSslFix() before invoking the SDK clone, mirroring install()/dryRun(). Without this, POST /api/marketplaces bypassed the per-call CA wiring and only relied on the boot-time applySslFix(); SDK git env propagation through process.env is intact (git.ts:404-414). Authorization: YES-ASSUME from user (2026-08-14 turn). Fix C (Dockerfile ca-certificates) deferred — only ship if A+B doesn't resolve the 500 on the next redeploy. SDK temp-dir cleanup leak in @oh-my-pi/pi-coding-agent fetcher.ts:288-303 (failed clone leaves .tmp-clone-* behind) is vendored; follow-up TODO, not patchable from this repo.
…ix C) oven/bun:1.3.14 base installs git via apt-get but the prior Dockerfile did not include ca-certificates, leaving the runtime image without a CA bundle that Git could use for HTTPS verification. marketplace add + plugin install both hit 'Problem with the SSL CA cert' even after Fix B wired ensureSslFix() on the per-call path (commit cf6dab0). Authorization: empirical evidence from user-reported 500 reproducer on 2026-08-14 (twice post-cf6dab0). Fix C trigger condition met per prior session guidance. Followup: SDK temp-dir cleanup leak in @oh-my-pi/pi-coding-agent fetcher.ts:288-303 (failed clone leaves .tmp-clone-* behind) is vendored; tracked separately.
Mirror GHOLAM_PERMISSIONS keys from the protocol package into the sidecar (apps/gholam/src/permissions.ts) and the server gate (apps/server/src/auth/gholam-permissions.ts). The sidecar previously had no file of its own; the gate was already wired but its seed included `mcp.invoke` (execute) which should require explicit user approval per §5 of docs/STOREFRONT.md. Reads auto-granted on first boot; writes and execute are off until the user toggles them in Settings → Gholam. Files: - apps/gholam/src/permissions.ts (new) — re-exports from @omp-deck/protocol - apps/server/src/auth/gholam-permissions.ts — remove mcp.invoke from DEFAULT_GRANT
…gent Three issues prevented the probe from driving the live UI: 1. getMcpHealthProbe() was never started at boot — the lazy mount in routes.ts only constructed the singleton on first HTTP hit to /api/mcp/health, so the WS broadcast loop never fired before the user manually probed. index.ts now starts the probe next to the routines runner. 2. The probe had no broadcast wire. McpHealthProbe accepts an onUpdate hook in its constructor but no caller passed one, and the existing code path didn't broadcast directly. runOnce() now emits a single mcp_health WS frame per probe cycle carrying the full snapshot — per-server frames would be dropped silently by the 30s floor in WsHub.broadcast under multi-server configs. 3. resolveConfigPath() returned undefined when OMP_AGENT_DIR was unset, silently no-op'ing the probe. The user's mcp.json lives at ~/.omp/agent/mcp.json (the same fallback used by routes-mcp- install, routes-skills-install, routes-storefront-installed, onboarding-state, session-lifecycle, skillsmp, custom-providers). Added the same fallback so the probe reads the user's actual config on a fresh cold-boot. Files: - apps/server/src/index.ts — getMcpHealthProbe().start() at boot - apps/server/src/mcp-health.ts — snapshot broadcast, agentDir fallback
The BroadcastFrame variant for mcp_health was declared with a single McpHealthStatus. After the probe started broadcasting a per-cycle snapshot (see prior commit), the wire had to match. status is now McpHealthStatus[] so the chrome McpHealthBadge and studio McpPillRow can derive the worst-current state across all servers on a single frame instead of per-server frames that would be dropped under the 30s throttle. File: packages/protocol/src/index.ts
Reducer update to match the new array-shaped mcp_health frame (see prior protocol commit). Iterates frame.status (the array), unions each entry by id into the cached McpHealthResponse, and stamps probedAt as the max across the batch so the relative-time tooltip stays coherent under partial loss. Chromium chrome StatusBar now hosts <McpHealthBadge />: small dot that reports the worst current MCP-server state across all servers, hovers reveal per-server breakdown + last-checked time, marks stale after 60s without a frame. Files: - apps/web/src/lib/store.ts — union-by-id reducer - apps/web/src/components/chrome/StatusBar.tsx — McpHealthBadge inline
Two long-standing bugs that broke Gholam in the deck:
1. `process.cwd()` in two browser-bundle files crashed the moment the
user added a priority from the chat overlay or the Gholam view:
- apps/web/src/components/GholamOverlay.tsx — overlay's submit
- apps/web/src/views/GholamView.tsx — control panel's addPriority
Replaced both with useStore.getState().defaultCwd; the server's
/gholam/priorities route already resolves empty cwd to
OMP_DECK_DEFAULT_CWD / process.cwd() server-side, so the user-facing
behavior is preserved.
2. Server restart via POST /api/system/lifecycle { action: "restart" }
called lifecycle.scheduleSelf which just `process.exit(0)`. That
works only under a supervisor wrapper (Start-OMP-Deck.* script);
in plain `bun src/index.ts` dev mode the process exited and
nothing restarted.
Converged on the same spawn-detached pattern as
index.ts:scheduleRestart(server): Bun.spawn the same argv detached
with inherited env/cwd, unref, then exit. Works in dev and prod
without a supervisor.
Files:
- apps/web/src/components/GholamOverlay.tsx
- apps/web/src/views/GholamView.tsx
- apps/server/src/lifecycle.ts
Adds an interactive shell pane for build/test/git/cleanup operations
the agent can't run safely itself, plus narrows the default nav to
only what a remote workstation needs.
What:
- apps/server/src/shell-service.ts (NEW)
Shell service. Bun.spawn + stdio pipe + bounded tail buffer
(64KB) + SSE chunks. cwd path-guard through workspace / agent-dir
roots. Loopback-only by convention; SIGTERM on beforeExit / SIGTERM.
Log-tail only — vitest --watch / bun run dev need a real PTY
(multi-day native-build work on Windows ConPTY, deferred).
- apps/server/src/routes-shell.ts (NEW)
Seven endpoints: list / create / get / tail / input / kill / stream.
SSE /stream emits 'data: <chunk>\n\n', replays the captured tail
on connect, server-side heartbeat ping every 15s, closes cleanly
when the shell exits.
- apps/server/src/routes.ts
Mount buildShellRouter alongside buildFilesRouter.
- apps/web/src/lib/shell-api.ts (NEW)
Typed fetch client + ShellApiError(status, path, isGone) for the
re-attach auto-unsub path.
- apps/web/src/views/ShellView.tsx (NEW)
Two-pane: 240px left rail (newest first, status dot, relative
time, click to focus) + right pane (toolbar cwd/status/kill,
stdin form, body <pre> auto-scrolls only near the bottom).
EventSource live tail; GET /tail on re-attach for replay.
Toolbar disables when exited.
- apps/web/src/router.tsx
Register /shell route.
- apps/web/src/components/NavRail.tsx
Trim rail to remote-workstation essentials: Chat, Shell, Explorer,
Tasks, Routines, Workflows, Skills, Gholam, Prompts. Dropped
Inbox/Marketplace/Agent Config/KB/Integrations from the rail —
still reachable by URL. Shell moved to second item.
- apps/web/src/styles.css
.shell-body, .shell-list, .shell-row, .shell-row.active — minimal,
matches existing paper-2 / line / font-mono tokens.
Gates: bunx tsc --noEmit clean across protocol/server/web (exit 0).
Smoke (live bun boot on a fresh data dir, 8790-8793):
GET /api/shell -> 200 []
POST /api/shell (echo+node) -> 201 + exit 0 captured
GET /api/shell/:id/tail -> captured stderr verbatim
GET /api/shell/:id/stream -> SSE data: frames emitted
DELETE /api/shell/:id -> {ok}
…marketplace CRUD, offline drafts * Overview dashboard: news (HN/TC/Verge) + trending repos + local stats; / -> OverviewView, /chat -> ChatView * OpenShip REST bridge: MCP JSON-RPC client + 5 routes + panel; 503 when MCP_OPENSHIP_TOKEN unset * GitHub: listBranches/listCommits/createRepo + full PR CRUD via protocol-typed client * Marketplace CRUD parity: plugins/MCP/skills all support search/install/enable/disable/remove/update * Offline-first drafts: debounced IDB + synchronous localStorage mirror on every write + pagehide flush; per-key hydration gate fixes the composer global->session race * Storefront: install chips now seed from server-installed endpoint; install endpoint whitelist dropped * Sessions: activeId persisted across reload, dead ids cleared on refresh * Tests: news-service parseRss + stale-cache fallback (5 pass) Verified: bun run typecheck green across all 4 workspaces.
The typed-during-hydration overwrite is a real bug: if a user typed during the in-flight IDB read, the resolved saved value would silently clobber their live input. Now track typing with a ref so the hydration callback defers to live input when present, then synchronously push the live snapshot to both IDB and the localStorage mirror once the read settles. Also add apps/web/src/lib/drafts.test.ts covering the IDB round-trip, overwrite, clear, structured-clone, and unknown-key cases against an in-memory store. 5/5 pass. The localStorage mirror and the hook-level race are out of scope for this fixture (mirror is one-line, race needs jsdom + hook driver). bun run typecheck: green. bun test apps/web/src/lib/drafts.test.ts: 5/5 pass. Pre-existing server-side test failures: untouched, out of scope.
The contract assertions the offline-autosave promise depends on: 1. mirrorToLocalStorage is synchronous — keystrokes written before the debounce window elapses survive tab teardown without IDB having committed yet. 2. Strictly-newer mirror wins over the IDB copy on load — the path that protects the 'huge prompt dies mid-network' scenario. 3. Conversely, the mirror does NOT overwrite a strictly-newer IDB entry — covers the case where IDB wins the race. 4. clearDraft removes both the IDB entry and the mirror, so a sent prompt cannot resurrect on the next session. Adds a Map-backed localStorage shim and stubs globalThis.indexedDB with a sentinel to bypass the production typeof guard. Exports mirrorToLocalStorage/readLocalStorageMirror/clearLocalStorageMirror as the public seam for the hook and for tests. 8/8 pass.
deploy/Dockerfile — multi-stage build (bun web bundle → bun runtime).
deploy/docker-compose.yml — loopback service, named volume, /workspace mount, healthcheck.
deploy/.env.example — MCP_OPENSHIP_TOKEN + provider keys + workspace + port.
deploy/openship.app.yaml — custom-app template (kind: template, custom: true).
deploy/openship-deploy.sh — orchestrates post_apps_custom → post_apps → patch_projects_by_id_env → post_deployments via the OpenShip MCP.
deploy/README.md — operator guide, two paths (bare-metal + OpenShip), MCP_OPENSHIP_TOKEN wiring, smoke tests.
Mirror of ./Dockerfile is intentional: keeps deploy/ self-describing for the
OpenShip catalog and for operators who do not read the repo root. Keep both
in sync when the runtime image shape changes.
Live verification requires MCP_OPENSHIP_TOKEN in the runtime env; the deck's
GET /api/openship/status flips from {configured:false} to {configured:true}
on the next server restart once it is set.
…oduction Production logs showed custom-providers failing to load models.yml once per second with: YAMLParseError: Map keys must be unique at line 40, column 3: inception: code: DUPLICATE_KEY at async reloadFromDisk (apps/server/src/custom-providers.ts:149) Two 'inception:' mappings were declared — the official Inception Labs endpoint (line 12) and a legacy omnirouter proxy route (line 40). A duplicate key makes the entire document unparseable, so NO custom providers loaded at all and the reload loop spammed the log continuously. Renames the legacy proxy route to 'inception-omnirouter' so it stays reachable while the official endpoint keeps the 'inception' name. Verified: yaml.parse() on the template now succeeds with 10 providers.
The seed script's 'never clobber the volume' rule had a failure mode: once a BROKEN config landed in the persistent agent dir, it was preserved forever. Production hit exactly this — a duplicate 'inception' key made models.yml unparseable, so custom-providers retried and failed once per second, and fixing the template could not heal it because the stale file in the volume always won. Now a rendered target is kept only if it actually parses. If it does not, the operator's copy is preserved as <name>.corrupt.<timestamp> for diagnosis and the template is re-rendered so the server boots with a working config. Validation is deliberately narrow: only .json/.yml/.yaml targets we render ourselves, and anything we cannot validate counts as valid. The goal is to heal corruption we shipped, never to police the user's own edits. Verified: sh -n passes; the yaml validator rejects a duplicate-key document and accepts a well-formed one.
…unts + chat route
Three real bugs caught by post-session review:
1. apps/web/src/lib/store.ts: readLastSession() and readModelSelection()
called localStorage.getItem unguarded. Safari private mode + sealed
embedded webviews throw on getItem access itself, not just on writes.
Since both run at module init, the throw unwound into a black-screen
cold start — the exact 'remote workstation' failure the user is
hedging against. writeLastSession already wrapped in try/catch;
mirror that shape in both reads.
2. apps/server/src/routes-overview.ts: news and trending wire counts
varied run-to-run (observed 30 vs 50 on back-to-back identical
GET /api/overview?window=7d). Upstream RSS feeds are
non-deterministic in their per-run content set, and fetchAllNews's
internal 60-item cap didn't pin what the dashboard renders. Cap on
the wire (NEWS_CAP=30, TRENDING_CAP=15) so the UI shape is
deterministic regardless of upstream noise.
3. apps/web/src/views/OnboardingView.tsx: navigate("/") landed on the
new Overview page, but the user's intent (and the toast hint at
line 68-69) is that onboarding finishes into Chat. Repoint to
/chat so the skip-hint surfaces where it was designed to.
Verified: bun run typecheck green across all 4 workspaces; bun test
drafts+news = 13/13; live GET /api/overview?window=30d returns
news=30, trending=15 (deterministic).
Not in this commit:
- hook-level useDraft race test (jsdom + React Test Renderer fixture
is real work; structural guard is correct by inspection)
- ws-outbox drain test (cursor shim against bun:test microtask
scheduler hung unrecoverably; the IDB round-trip path is covered
by drafts.test.ts, the PERSISTED_FRAME_TYPES set is reviewed by
code)
Legacy GHOLAM_WS_URL was misconfigured in production multiple times (pointed at broken-cert external hosts), leaving the deck stuck 'connecting' with 0 priorities. Renaming the opt-in to an OMP_-prefixed key makes any leftover env value inert — production decks always use the in-process spawn at ws://127.0.0.1:47900/ws.
Even after the rename, legacy env values from prior OpenShip deploys can still be inherited on the next container boot. Detect GHOLAM_WS_URL without the new OMP_DECK_GHOLAM_EXTERNAL_URL, log a one-line warning naming the migration path, then proceed with the in-process spawn so the deck stays online.
Collapse landed in c39a999/c32fe70/be6f71a: gholam is now always spawned in-process at ws://127.0.0.1:47900/ws. The legacy GHOLAM_WS_URL key is inert — the server reads OMP_DECK_GHOLAM_EXTERNAL_URL for the opt-in multi-host escape hatch only. The env-schema entry for GHOLAM_WS_URL is no longer wired to any code path; remove it to prevent future confusion and stale validation messages.
…t add .' - data/deck.db* — stray root-level runtime DB (live data lives in apps/server/data); keep on disk, drop from index - data-shell-probe/ — scratch probe dir holding a persisted deck api-token; deleted (token was public on origin/main; stale, matched no live instance) - WATCHDOG.yml — advisor runtime config written to the workspace root, no code references; kept on disk, untracked - __probe__.txt, raw-full.txt, qr-*.svg — session scratch artifacts, deleted .gitignore: cover /data/, /data-shell-probe/, WATCHDOG.yml, probe/QR/test- output scratch, and Drizzle Kit output dirs (drizzle/, apps/*/drizzle/) ahead of adoption.
Ui ux redesign
Wire up all 7 orphan pages into navigation
- New services: repo-service (clone/list/delete GitHub repos as bare clones under ~/omp-repos/<owner>/<repo>.git), worktree-service (create/list/ delete git worktrees under ~/omp-repos/<owner>/<repo>.worktrees/<branch>), session-meta (CRUD layer for archived/urgency/importance/status fields). - New routers: routes-repos (GET/POST/DELETE /api/repos + worktree subroutes), routes-worktrees (mounted in routes.ts). - Migration 009-session-metadata.sql: adds archived, urgency, importance, status, repo_id, worktree columns to the session table. - Extended /api/sessions GET/POST/PATCH to read/write the new fields and accept ?archived= ?groupBy= ?repoId= ?urgency= query params. - New /api/sessions/grouped endpoint returns sessions bucketed by repo/ status/urgency/importance. - /api/sessions/:id/archive now also sets status='archived'; new companion /api/sessions/:id/unarchive clears the flag. - session-lifecycle: archive() persists metadata. Protocol types added: RepoEntry, WorktreeEntry, CreateRepoRequest, CreateWorktreeRequest, extended CreateSessionRequest (repoId/worktreeBranch), extended PatchSessionRequest (archived/urgency/importance/status/title), extended ListSessionsQuery (archived/groupBy/repoId/urgency). Verified: bunx tsc --noEmit passes (server + protocol); boot smoke ran clone octocat/hello-world, create+list+delete worktree, PATCH urgency, grouped-by-urgency, DELETE repo.
- styles.css: new [data-theme="acid"] block with near-black ground,
acid-chartreuse accent (#d4ff00), saturated semantic trio, violet
'thinking' lane. Adds [data-shape="cut"] clip-path primitive and
.acid-{label,section,badge} utilities.
- theme.ts: register Acid Lab in the THEMES catalog, default for new
visitors (replacing Aurora as the design default).
- index.html: pre-paint script knows about 'acid' so first paint matches.
- components/acid/AcidTimeline.tsx: GitHub-changes-in-time-period layout
for the Overview, slotted above the stats strip.
Typecheck: bunx tsc --noEmit passes.
- RichEditor (Tiptap) with rich + monospace mode, Tab-apply suggestions, wavy-underline mark. - VoiceRecorder (MediaRecorder) wired to /api/transcribe with whisper-cpp sidecar. - POST /api/transcribe + Whisper Dockerfile (ggml-base.bin multilingual model). - WS frames gholam_text_suggest / gholam_text_apply appended to ServerFrame/ClientFrame unions. - ADHD focus mode: zustand persist store, FocusStrip pinned to all routes, ScheduleManager drawer. - POST /api/tasks/from-message splits numbered/bullet/heading messages into backlog tasks. - AGENTS.md with YAML frontmatter variables + 8 sections (Mission, Auto-Kanban, ADHD, Voice, Gholam Tab, Schedule, Variables, Editing). - Consumer swap across 10 textareas (Composer, PromptEditor, TaskDrawer, QueuedMessage, PlanApproval, KbView, SettingsView, ContextIndicator, ExtUiDialog, GholamChatNew, RoutineEditor, RoutineEditorPage). - bun run typecheck green across all 4 workspaces.
- Server: GET /api/fs/dialog (sandboxed dir browser), POST /api/workspaces/register - Server: routes-workspaces.ts new router; routes.ts mounts it; GET /workspaces reads shared list - Server: 5 new tests in routes-fs.test.ts (302/302 pass) - Web: listFsDialog + registerWorkspace + listGroupedSessions API wrappers - Web: NewSessionModal section 3 now a real directory browser with Up nav + register - Web: Sidebar group-by selector (Recent / Repo / Urgency / Importance) with localStorage persist
- /studio: collapse duplicate studio routes into a single nested route (kills the React Router 'duplicate param-less route' warning that broke the entire nav rail). - explorer: add data-context-key to TreeRow + file.menu/folder.menu catalog entries (new file, new folder, copy path, open in chat, delete). Right-click on any file/folder now exposes real actions instead of the global fallback. Listeners in ExplorerView dispatch to the existing handlers; refreshWorkspaces() runs after a clone. - clone: OMP_DECK_CLONE_ROOT env lets operators land clones in a dedicated workspace dir (e.g. ~/workspace) instead of HOME. Default dir is mkdir'd on boot. Backward compatible when unset. - gholam: 8-role multi-agent picker (default/smol/slow/vision/plan/ commit/tiny/advisor). New GholamChatModelRole + GholamChatModelUsed in protocol; migration 010 adds gholam_chats.model_used_json; PUT /api/gholam/chats/:id/model validates role + writes both model and model_used_json; POST .../messages accepts per-turn modelId/role. View: 8-pill role row + composer textarea + send button. Verification: bunx tsc --noEmit -p apps/web/tsconfig.json EXIT=0; bunx tsc --noEmit -p apps/server/tsconfig.json EXIT=0.
feat: rich editor + voice + focus mode + auto-kanban + AGENTS.md
…ol filtering + atomic writes - gholam: 3-candidate spawn path resolution; forward all 9 MCP tokens + DATA_DIR + KB_ROOT into sidecar env; surface env-schema entries so settings UI can manage them. - mcp: GET /api/mcp/:name/tools + POST /api/mcp/:name/tools/:tool/toggle; disabledTools[] filter in loadGholamMcpTools; McpToolSpec + ListMcpToolsResponse + mcp_tools_changed broadcast frame. - genui: setGenuiProvider adapter wired to gholamDeckLLM.complete with overview JSON prompt context; loadOverviewForPrompt extracted from routes-overview. - durability: atomicWriteSync (mkdir+tmp+fsync+rename, error cleanup) in env-store; kb-service, session-lifecycle, custom-providers, gholam-token all route through it. - chat: activeChatId localStorage persistence so reload lands on the last chat thread. typecheck: protocol/server/gholam/web all exit 0. server tests 294/0.
…rated Overview - mcp: replace passive chrome dot with <button data-mcp-chip> + portal popover; factor McpServerActions (power/refresh/trash) shared by chrome, storefront strip, /integrations; new McpToolsPopover consumes GET /tools + toggle; /integrations rebuilt as one-screen MCP manager with AddMcpDialog; POST /mcp/probe-now exposes runOnce(). - storefront: NavRail Storefront entry; skills/prompts sections now install live (POST /api/skills/install and /api/prompts/library); claude-sonnet-4 hardcoded seed dropped; POST /api/marketplace/plugins/:id/upgrade + GET /api/marketplace/updates; Update button on detail when updateAvailable; StudioPane readOnly wired (or removed); empty featured row shows empty-state hint instead of fake content. - overview: useOverviewGenui hook consumes /api/genui/stream?route=/?window=; OverviewGenuiSlot renders <GenuiStack> with skeleton/loading/error/retry/fallback paths; legacy News/Trending/Activity sections preserved as offline fallback so the page never blanks; AcidTimeline untouched. typecheck: protocol/server/gholam/web all exit 0. auth-interceptor notify() restored (worker regression). server tests 294/0.
…DB persistence + crash-recovery test
- db: bump synchronous=NORMAL -> FULL; module-level 60s WAL checkpoint(TRUNCATE) interval registered on first openDb, cleared on closeDb + SIGINT/SIGTERM/exit; idempotent guard prevents listener pile-up across test reopens.
- idb-queue: bump v3 -> v4 with storefront-installs object store; public storefrontInstalls.{begin,confirm,revert,list} with fire-and-forget writes; list() returns { id, phase, installedAt? }, listRaw() preserves enqueuedAt for stale-pending detection.
- storefront-store: StorefrontUiState methods now write to IDB before mutating zustand; bootstrap reconciliation reverts stale pending (>30s) and restores confirmed installs.
- chrome: ConnectionIndicator renders 'storefront: N' sky pill alongside 'N queued' amber pill when storefront installs pending.
- test: apps/server/src/db/crash-recovery.test.ts — opens DB in tmpdir, inserts row, closes (no delete), reopens, asserts row survives. 1 pass / 0 fail.
typecheck protocol/server/gholam/web all exit 0.
Storefront/fixes
fix(overview,store,onboarding): sealed-context + non-deterministic co…
… schemas Comprehensive audit-driven security hardening across the omp-deck server, plus schema tightening in the protocol package. Critical server fixes - SERVER-006: /hooks/* now requires auth principal (was: open on non-loopback) - SERVER-005: install-focus-guard cwd path-validated, refuses to mint .git - SERVER-032: skills-install project cwd path-validated through guardWorkspacePath - SERVER-012: kb-service prefix collision — trailing separator in startsWith - SERVER-016: routes-fs isCwdAllowed replaced with shared guardWorkspacePath - SERVER-029: workspace registration admin-gated + audit log - SERVER-014: agent-config import zip entries validated (zip-slip) - SERVER-001/019/027/030/028: spawn env sanitized via shared spawn-env.ts; internal runner secret kept module-scoped (out of process.env) - SERVER-002: skills install fetchSource SSRF guard (loopback/private deny) - SERVER-003: MCP install command allow-list (no shell interpreters, no meta) - SERVER-004: PATCH /api/settings/env admin-gated (was: any authed user) - SERVER-008: custom-providers apiKeyEnv allow-list + sensitive-key deny - SERVER-021: webhook abort log redacts Cookie/Authorization headers Production hardening - ws.ts: 1 MiB maxPayloadLength, per-connection 100 fps throttle, close 1008 - index.ts: uncaughtException + unhandledRejection handlers - ws.ts broadcast: track per-socket buffered amount; close at 256 KB threshold - index.ts safeShutdown: awaits in-flight fetch handlers before exit - db/index.ts: O_EXCL .lock sentinel exits non-zero on collision - routes.ts: Content-Length cap middleware (4 MB default, uploads exempt) - server build: --external omp-legacy-pi-modules (upstream virtual module) Protocol package - step-http.json: drop format: uri-reference (broke templated URLs) - index.ts: GholamPermissionKey narrowed to GholamPermission - index.ts: GholamCommandFrame index signature removed - step-*.json: unevaluatedProperties: false on all step schemas - validate.ts: replaced double-cast with proper Ajv ValidateFunction typing - validate.test.ts: +28 tests across step schemas + triggers Test infrastructure - bunfig.toml: excludes apps/web/src/**/*.test.tsx from root bun test (matches apps/web/bunfig.toml; root was double-discovering vitest-only files)
The bundled server (apps/server/dist/index.js) needs the platform-specific @oh-my-pi/pi-natives-<plat>.node binary and src/db/migrations/ staged next to it at runtime: - Native loader probes <import.meta.dir>/../native/, which from dist/ lands at apps/server/native/pi_natives.<plat>.node. - db/index.ts resolves MIGRATIONS_DIR relative to its own import.meta.url, which becomes apps/server/dist/migrations/ after bundling. Add scripts/copy-native.mjs and wire it as postbuild. The script walks up to find node_modules/.bun/<flat>@<ver>/node_modules/<pkg>/ (Bun's hoisted location) and copies the .node binary alongside. .gitignore ignores apps/*/native (build output).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.