Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ Build stores anonymous projects in browser IndexedDB. Projects auto-save after e

Project storage includes source files, selected file, chat history, and project metadata. It does not currently snapshot the preview app's internal PGlite IndexedDB data.

## Publish to scoutos.live

With managed auth on, signed-in users can publish a project to [Scout Live](https://scoutos.live) and get a stable URL at `{subdomain}.scoutos.live`. Republishing the same project updates the same URL with zero downtime.

How it works (see [`docs/scoutos-live-prd.md`](docs/scoutos-live-prd.md)):

1. Add your ScoutOS API key (`sk_live_...`, scopes `build` + `read`) once in the account panel. It is stored server-side, encrypted with AES-256-GCM, and never returned to the client.
2. Click the rocket button in the project toolbar. The first publish prompts for a subdomain (validated against the platform naming rules and reserved list before any network call); later publishes reuse it automatically.
3. The server packages the project file map into a tar.gz (injecting a non-root `Dockerfile` that runs `vite build` and `node server.js`), deploys it through `POST https://scoutos.live/api/build`, and polls status until the app is live.

The starter template runs the same code in both environments: in the WebContainer preview, `zepto-bridge.js` serves `/api/db` with hyper-zepto's local adapters; deployed to Scout Live, `server.js` mounts the same bridge against the platform's managed ports via `SCOUT_PORTS_URL`.

Publishing requires managed auth — non-managed mode shows no publish UI.

## Model settings

Build opens the model settings modal on first load if no model is configured. OpenRouter is the default provider. Use the gear button beside the app title to change provider, model, API key, or Ollama URL.
Expand Down
62 changes: 37 additions & 25 deletions docs/scoutos-live-prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,28 @@ Add a Publish capability to Build: a signed-in user clicks Publish, their projec

Manually deploy a minimal hyper-zepto app to scoutos.live: tiny Node server using `createPorts({ mode: 'remote', baseUrl: <from SCOUT_PORTS_URL> })`, exercising data-port create/find/update/delete against the platform.

**Outcome (2026-06-12):** smoke app built at `smoke/phase0-live-ports/` (hyper-zepto remote adapters, `/verify` endpoint running the full data CRUD round trip, `deploy.sh` handles deploy + status polling + republish-with-code). Wire format statically verified against both sources and exercised end-to-end against a stub mirroring the platform's data-router paths/response shapes — all green. The three live checks below await a `sk_live_` key (none was available locally); each is one command per the smoke README. One platform-side note: the Dockerfile must use a **numeric** `USER` (e.g. `USER 1000`) — the pod securityContext enforces `runAsNonRoot`, which K8s cannot verify for named users.

**Success criteria:**
- [ ] A hand-built tar.gz deploys via `POST /api/build` and reaches `deployed`.
- [ ] The deployed app completes a data-port CRUD round trip through the sidecar (`SCOUT_PORTS_URL`).
- [ ] The exact env mapping needed for hyper-zepto remote mode is documented in this file (replace this line with the finding), including whether `/_ports` path joining matches hyper-zepto's remote adapter and whether a token is needed (sidecar should inject auth).
- [ ] Republish with `publishCode` updates the same subdomain.
- [ ] A hand-built tar.gz deploys via `POST /api/build` and reaches `deployed`. *(pending sk_live key)*
- [ ] The deployed app completes a data-port CRUD round trip through the sidecar (`SCOUT_PORTS_URL`). *(pending sk_live key; verified locally against a platform-shaped stub)*
- [x] **Env mapping (verified against hyper-zepto 0.1.0 `dist/index.js` and scout-live `src/sidecars/ports-proxy.ts` + `src/ports/data/router.ts`, 2026-06-12):** hyper-zepto's remote adapters append `/_ports/...` to `baseUrl` themselves, so the platform's `SCOUT_PORTS_URL` (`http://127.0.0.1:3101/_ports`) must have its trailing `/_ports` stripped before being passed as `baseUrl` (or as `SCOUTOS_PORTS_URL`): `baseUrl = SCOUT_PORTS_URL.replace(/\/_ports\/?$/, '')`. No token is needed — the sidecar injects the internal app auth header on every proxied request; hyper-zepto's optional `SCOUTOS_TOKEN` stays unset. Path joining then resolves to `http://127.0.0.1:3101/_ports/data/...`, which the sidecar rewrites to the gateway's `/ports/data/...` routes. All six data ops the template bridge uses (`create`, `get`, `find`, `update`, `delete`, `count`) match the platform router's paths, request bodies, and response shapes exactly. Known mismatches in ops the template does **not** use: hyper-zepto `createIndex`/`dropIndex` call `/index`//`/index/:name` while the platform serves `/indexes`//`/indexes/:name`, and hyper-zepto `data.health()` (`GET /_ports/data/health`) has no platform route — fix in hyper-zepto if ever needed.
- [ ] Republish with `publishCode` updates the same subdomain. *(pending sk_live key)*

**If this fails** (wire-format mismatch between hyper-zepto remote adapters and the platform): stop and fix hyper-zepto (same org) before proceeding. Do not work around it in the template.

## Phase 1 — Production-deployable template

Add `server.js` to the starter template: serves built `dist/` statically and mounts the same `/api/db` bridge as the Vite plugin, with `createPorts()` resolving local (dev fallback) vs remote (Scout Live, via the Phase 0 env mapping). Refactor so the bridge logic is written once and used by both `vite.config.ts` (dev) and `server.js` (production). Update both template copies (`src/templates.ts` and `src/build/pure/templates.gleam` — Gleam is what the app uses at runtime) and the agent system prompts (client `src/agent.ts` + server `server/src/prompt.ts`) to preserve `server.js`.

**Outcome (2026-06-12):** done. The bridge lives in a new template file `zepto-bridge.js` (written once: `resolvePorts()` + the connect-style `/api/db` handler), mounted by `vite.config.ts` in dev and the new `server.js` (static `dist/` + SPA fallback) in production. `package.json` gained `build`/`start` scripts.

**Success criteria:**
- [ ] In the WebContainer preview, nothing changes: dev server boots, `/api/db` CRUD works (existing behavior).
- [ ] Locally: `vite build && node server.js` serves the app and `/api/db` CRUD works without Vite.
- [ ] `node server.js` with the Scout Live env vars set (simulated) selects remote adapters.
- [ ] Both template copies and both prompt copies updated in sync; template tests assert `server.js` exists and contains the env mapping.
- [ ] All existing tests pass (`npm test` at root and in `server/`).
- [x] In the WebContainer preview, nothing changes: dev server boots, `/api/db` CRUD works (verified against the dev server locally; same code path).
- [x] Locally: `vite build && node server.js` serves the app and `/api/db` CRUD works without Vite (full create/find/update/get/count/delete round trip).
- [x] `node server.js` with the Scout Live env vars set (simulated) selects remote adapters (`db mode=remote`, CRUD against the sidecar stub).
- [x] Both template copies and both prompt copies updated in sync; template tests assert `server.js` exists and contains the env mapping.
- [x] All existing tests pass (`npm test` at root and in `server/`).

## Phase 2 — Server: credentials storage

Expand All @@ -61,11 +65,13 @@ New PGlite migration: `user_credentials` table (`clerk_user_id`, `provider` = 's

Also: commit the pending `PGLITE_DATA_DIR` boot guard in `server/src/index.ts` with this phase.

**Outcome (2026-06-12):** done as specced (`user_credentials` table keyed by `clerk_user_id` + `provider`). The `PGLITE_DATA_DIR` boot guard turned out to be already merged (PR #23), so nothing was pending. One implementation note: PGlite rejects multi-statement prepared statements, so `migrate()` now runs one statement per `query()` call.

**Success criteria:**
- [ ] Key round-trips through encrypt/store/decrypt in tests (PGlite-backed, like `db.test.ts`).
- [ ] No API response ever contains the stored key (test asserts presence-flag only).
- [ ] Unauthenticated requests are 401; malformed keys are 400.
- [ ] Migration is idempotent (server boots cleanly on an existing data dir).
- [x] Key round-trips through encrypt/store/decrypt in tests (PGlite-backed, like `db.test.ts`).
- [x] No API response ever contains the stored key (test asserts presence-flag only).
- [x] Unauthenticated requests are 401; malformed keys are 400.
- [x] Migration is idempotent (server boots cleanly on an existing data dir).

## Phase 3 — Server: packaging + publish proxy

Expand All @@ -76,34 +82,40 @@ Also: commit the pending `PGLITE_DATA_DIR` boot guard in `server/src/index.ts` w
- `GET /api/publish/:buildId` — proxies build status; on terminal `deployed`, persists `publishCode` and `url`.
- A `scoutlive.ts` client module mirroring the `openrouter.ts` pattern (injectable, faked in tests).

**Outcome (2026-06-12):** done. Hand-rolled USTAR writer came in around 70 lines (`server/src/tar.ts`) — no new dependency. Deviations from the spec text, both forced by reality: the injected Dockerfile uses `npm install` rather than `npm ci` (project file maps from the WebContainer carry no `package-lock.json`) and `USER 1000` rather than a named user (runAsNonRoot). It also pre-creates `.zepto` so the local-adapter fallback works in-container. The image was built and CRUD-tested with real Docker locally, and the generated tar.gz round-trips through bsdtar. Also added `GET /api/deployments/:projectId` (presence + subdomain only) so the client can skip the subdomain prompt after a reload.

**Success criteria:**
- [ ] Generated tar.gz is accepted by a real `POST /api/build` (one live integration check, then faked in CI).
- [ ] First publish creates a deployment record; second publish for the same project sends `code` and reuses the subdomain (test with fake client).
- [ ] 409 from the platform (name taken) maps to a distinct error code the UI can render.
- [ ] User key and publishCode never appear in responses or logs.
- [ ] Missing stored key returns a distinct `no_scoutos_key` error (UI routes user to settings).
- [ ] All server tests pass.
- [ ] Generated tar.gz is accepted by a real `POST /api/build` (one live integration check, then faked in CI). *(pending sk_live key; accepted by bsdtar and built/run under real Docker locally)*
- [x] First publish creates a deployment record; second publish for the same project sends `code` and reuses the subdomain (test with fake client).
- [x] 409 from the platform (name taken) maps to a distinct error code the UI can render (`subdomain_taken`; also `publish_code_rejected`, `scoutlive_rate_limited` + Retry-After).
- [x] User key and publishCode never appear in responses or logs (asserted in tests).
- [x] Missing stored key returns a distinct `no_scoutos_key` error (UI routes user to settings).
- [x] All server tests pass (99).

## Phase 4 — Client UI (Gleam app)

- Settings: ScoutOS API key field (managed mode) → `PUT /api/credentials/scoutos`; shows set/not-set state only.
- Publish button + flow: subdomain prompt (pre-validated against naming rules; remembered per project), then status states `queued → running → deployed(url) | failed(logs link)` polling `GET /api/publish/:buildId`.
- Published URL displayed and clickable; republish reuses the stored subdomain without re-prompting.

**Outcome (2026-06-12):** done. New `publish` actor + modal + toolbar rocket button + write-only key field in the account panel. `scripts/verify-publish-ui.mjs` is the headless-Chromium harness (route-intercepted publish backend, faked managed-auth bridge) — rerunnable against any dev server.

**Success criteria:**
- [ ] Full happy path in the browser (verify like the hyper-zepto template run: headless Chromium against the dev server with a faked publish backend or test deploy).
- [ ] Invalid subdomain feedback appears before any network call; 409 and `no_scoutos_key` errors render actionable messages.
- [ ] Gleam tests cover the new msg/update/effect wiring; `gleam test` and `npm test` pass.
- [x] Full happy path in the browser (headless Chromium against the dev server with a faked publish backend — 8/8 checks pass, including republish-without-prompt).
- [x] Invalid subdomain feedback appears before any network call; 409 re-prompts with the message and `no_scoutos_key` routes to settings.
- [x] Gleam tests cover the new msg/update/effect wiring (21 new); `gleam test` and `npm test` pass.

## Phase 5 — Rollout

- Docs: README section + this PRD updated with outcomes.
- **Gating:** publish requires managed auth (server-stored keys). Production has `VITE_MANAGED_AUTH=false`; PR previews have it on. Ship publish behind managed auth — it activates in production when Phase 5 of the managed-auth migration flips the flag. Confirm no publish UI leaks into non-managed mode.

**Outcome (2026-06-12):** README section added; PRD updated with per-phase outcomes. Gating verified: publish UI renders only in managed mode (headless check), and the key/status flows only run on the managed `InitApp` path.

**Success criteria:**
- [ ] End-to-end on a PR preview: sign in → save key → build an app with the agent → publish → live URL works → edit → republish → same URL updated.
- [ ] Non-managed mode shows no publish affordances.
- [ ] PR merged with all 100+ existing tests plus new coverage green.
- [ ] End-to-end on a PR preview: sign in → save key → build an app with the agent → publish → live URL works → edit → republish → same URL updated. *(needs the PR preview + a sk_live key)*
- [x] Non-managed mode shows no publish affordances (asserted headless).
- [ ] PR merged with all 100+ existing tests plus new coverage green. *(PR open; 155 client + 99 server tests green locally)*

## Risks

Expand Down
Loading