Skip to content

Remote Updates - #14

Open
kaka-sangi wants to merge 67 commits into
bjb2:mainfrom
kaka-sangi:main
Open

Remote Updates#14
kaka-sangi wants to merge 67 commits into
bjb2:mainfrom
kaka-sangi:main

Conversation

@kaka-sangi

Copy link
Copy Markdown

No description provided.

claude and others added 30 commits August 12, 2026 17:33
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)
omp-deck-orchestrator and others added 30 commits August 16, 2026 07:10
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.
- 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.
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants