feat(gatorwalk-factory): a read-only studio server, started from swamp (swamp-club #2806) #394

Merged
seth merged 6 commits from cue/2806-gatorwalk-factory-studio into main 2026-09-30 21:45:42 +00:00
Owner

Closes swamp-club #2806. Follow-ups: swamp-club #2841.

What

A new model type, @swamp/gatorwalk-factory/studio, whose serve method runs a read-only local studio page on 127.0.0.1 until Ctrl-C:

swamp model create @swamp/gatorwalk-factory/studio studio   # once per repo
swamp model method run studio serve                         # logs the URL

The page lists every factory in the repo (a picker) and shows its definition file and its scenario files (scenarios/<factory>/, as #2805 settled). It reloads them over server-sent events as an agent edits them, and picks up factories created while it is open. It never writes: edits come from the agent (decided 2026-09-30). This is the shell that Design mode (#2807) and Simulate mode (#2808) build on; the page shows the raw YAML until then.

Security

  • It binds to 127.0.0.1 only.
  • Host must be 127.0.0.1:<port> or localhost:<port>, which blocks DNS rebinding.
  • Any Origin must be the server's own.
  • Sec-Fetch-Site must be same-origin or none. The one exception is a top-level document navigation to /, so the page opens from a link in another app.
  • Every route is GET, and anything else gets 405. No CORS headers are ever sent. Every response carries CSP default-src 'self' (plus frame-ancestors 'none'), CORP same-origin and nosniff.
  • A request names a factory and a scenario, never a path. Definition files go through resolveDefinitionPath. Scenario names must match NameSchema, and the real path must stay inside scenarios/<factory>/.
  • Every file watch is one level deep, on a directory whose real path is inside the repo.

Two departures from the issue text (DESIGN.md decision log)

  • The page is embedded, not found beside the module. A source-loaded model runs from .swamp/bundles/<hash>/ (checked with a probe model), so import.meta.url cannot find studio/dist/. And ctx.extensionFile() needs a manifest. deno task build:studio bundles studio/src and writes a generated _lib/engine/studio_assets.ts, the way the starters are embedded, so go-live needs no additionalFiles for it.
  • Freshness is checked by input digest, not by rebuilding. The bundle's bytes depend on the Deno version, and verification only warns on a mismatch. studio_assets_test recomputes a digest of the build inputs, with LF line endings, and fails when the build is stale.

Also

  • work_item_ops.ts: factoryPathArgument is extracted from readFactoryPath, so both read the path the same way.
  • The fonts (Chakra Petch, JetBrains Mono, Orbitron; OFL) are bundled from @fontsource/*@5.3.0.
  • verification/checks.yaml: studio/ is added to the gatorwalk-factory check, lint and fmt paths.
  • README (a Studio section), DESIGN.md ("The studio server"), and one paragraph in the skill's driving.md.

Testing

  • Unit tests drive the handler on an in-memory repo. They cover every route and every refusal (bad Host, Origin and Sec-Fetch-Site, non-GET, bad scenario names, and symlink escapes for definitions, scenario files and the scenario directory), the security headers, SSE delivery and shutdown, and the digest and size of the generated module.
  • Four integration tests run on the real CLI:
    • serve, load the page, an edit on disk arriving over SSE, a raw-socket bad Host, a real symlink escape, and Ctrl-C with an open stream;
    • a definition directory that appears while serving;
    • a factory created while serving;
    • a definition path through a symlink out of the repo is never watched (this test fails on the earlier watcher).
  • Verified with verify-build and verify-reviews: 13/14 passed, with the codegen idempotency step skipped by its guard. All three reviews pass. Attestation d3894aa7-5866-402c-ac64-18a1a80fb9dc.

🤖 Generated with Claude Code

Closes swamp-club #2806. Follow-ups: swamp-club #2841. ## What A new model type, `@swamp/gatorwalk-factory/studio`, whose `serve` method runs a **read-only** local studio page on 127.0.0.1 until Ctrl-C: ``` swamp model create @swamp/gatorwalk-factory/studio studio # once per repo swamp model method run studio serve # logs the URL ``` The page lists every factory in the repo (a picker) and shows its definition file and its scenario files (`scenarios/<factory>/`, as #2805 settled). It reloads them over server-sent events as an agent edits them, and picks up factories created while it is open. It never writes: edits come from the agent (decided 2026-09-30). This is the shell that Design mode (#2807) and Simulate mode (#2808) build on; the page shows the raw YAML until then. ## Security - It binds to 127.0.0.1 only. - `Host` must be `127.0.0.1:<port>` or `localhost:<port>`, which blocks DNS rebinding. - Any `Origin` must be the server's own. - `Sec-Fetch-Site` must be `same-origin` or `none`. The one exception is a top-level document navigation to `/`, so the page opens from a link in another app. - Every route is GET, and anything else gets 405. No CORS headers are ever sent. Every response carries CSP `default-src 'self'` (plus `frame-ancestors 'none'`), CORP `same-origin` and `nosniff`. - A request names a factory and a scenario, never a path. Definition files go through `resolveDefinitionPath`. Scenario names must match `NameSchema`, and the real path must stay inside `scenarios/<factory>/`. - Every file watch is one level deep, on a directory whose real path is inside the repo. ## Two departures from the issue text (DESIGN.md decision log) - **The page is embedded, not found beside the module.** A source-loaded model runs from `.swamp/bundles/<hash>/` (checked with a probe model), so `import.meta.url` cannot find `studio/dist/`. And `ctx.extensionFile()` needs a manifest. `deno task build:studio` bundles `studio/src` and writes a generated `_lib/engine/studio_assets.ts`, the way the starters are embedded, so go-live needs no `additionalFiles` for it. - **Freshness is checked by input digest, not by rebuilding.** The bundle's bytes depend on the Deno version, and verification only warns on a mismatch. `studio_assets_test` recomputes a digest of the build inputs, with LF line endings, and fails when the build is stale. ## Also - `work_item_ops.ts`: `factoryPathArgument` is extracted from `readFactoryPath`, so both read the path the same way. - The fonts (Chakra Petch, JetBrains Mono, Orbitron; OFL) are bundled from `@fontsource/*@5.3.0`. - `verification/checks.yaml`: `studio/` is added to the gatorwalk-factory check, lint and fmt paths. - README (a Studio section), DESIGN.md ("The studio server"), and one paragraph in the skill's `driving.md`. ## Testing - Unit tests drive the handler on an in-memory repo. They cover every route and every refusal (bad Host, Origin and Sec-Fetch-Site, non-GET, bad scenario names, and symlink escapes for definitions, scenario files and the scenario directory), the security headers, SSE delivery and shutdown, and the digest and size of the generated module. - Four integration tests run on the real CLI: - serve, load the page, an edit on disk arriving over SSE, a raw-socket bad Host, a real symlink escape, and Ctrl-C with an open stream; - a definition directory that appears while serving; - a factory created while serving; - a definition path through a symlink out of the repo is never watched (this test fails on the earlier watcher). - Verified with verify-build and verify-reviews: 13/14 passed, with the codegen idempotency step skipped by its guard. All three reviews pass. Attestation `d3894aa7-5866-402c-ac64-18a1a80fb9dc`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A new model type, @swamp/gatorwalk-factory/studio, whose serve method runs
the studio page on 127.0.0.1 until Ctrl-C. The page lists every factory in
the repo and shows its definition file and its scenario files
(scenarios/<factory>/), reloading them over server-sent events as an agent
edits them. It never writes.

Every route is GET. A request is refused unless its Host names the server,
any Origin is the server's own, and any Sec-Fetch-Site is same-origin or
none. No CORS header is sent; every response carries CSP default-src 'self'
and CORP same-origin. Definition paths are resolved with
resolveDefinitionPath, and scenario names must match NameSchema and stay
inside scenarios/<factory>/.

The page (studio/src, grown from the prototype in swamp-club's HUD style,
fonts bundled) is built by deno task build:studio into a generated module,
studio_assets.ts. A source-loaded model runs from swamp's bundle directory,
so dist/ cannot be found beside it. A unit test checks the module against a
digest of its build inputs, rather than rebuilding, since the bundle depends
on the Deno version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the verification reviews. The file watch's refreshes run one at a time
and never reject: a refresh that fails (a directory removed under it) keeps
the old watches and the next one retries. A definition whose directory does
not exist yet is watched through the nearest directory above it and picked
up when it appears. serve closes the watch if the port cannot be bound. On
port 80 the Host and Origin carry no port. An event stream opened as the
server stops closes at once. The page ignores a late error for a factory or
scenario no longer selected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#2805 landed with SCENARIO_DIR and a directory listing on RepoFiles. The
studio now uses both instead of its own constant and injected lister. It
keeps its stricter rule that a served scenario's real path stays inside
scenarios/<factory>/, and it serves the raw text rather than parsed YAML.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the second verification's reviews, fixed at Seth's request before the
PR. serve reads the factory list again every three seconds, and the page
lists factories again when it changed. A scenario event reloads the list as
well as the selected file, so a deleted scenario leaves it. The watch skips a
definition path outside the repo instead of watching there, and the scenario
check uses the same inside() as the server. The build's input digest hashes
text with LF line endings, so a CRLF checkout gives the same digest. On port
80 the Host may carry the port or leave it out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the third verification's adversarial review. The earlier check was
lexical, so a definition path through a symlinked directory (link/sub/x.yaml
with link pointing out of the repo) could still put a watch outside it.
Every watch is now one level deep, on a directory whose real path is inside
the repo: the nearest directory of a missing definition, scenarios/, and each
factory's directory in it (instead of one recursive watch). The integration
test for the symlinked path fails on the previous watcher.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(gatorwalk-factory): the studio page opens from a link in another app (swamp-club #2806)
All checks were successful
CI / Validate Attestation (pull_request) Successful in 1m12s
CI / Review Integrity (pull_request) Successful in 1m35s
5bf594daa6
Found by Seth trying the studio: opening the URL by clicking it in another
app sends Sec-Fetch-Site: cross-site, which the server refused with 403.
A top-level document navigation to / is now let through. The linking page
cannot read it, and frame-ancestors 'none' keeps it out of frames. Every
other cross-site request stays refused, including a navigation to the API
or the assets, and an iframe or script load of the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
seth merged commit 9d5cab3e97 into main 2026-09-30 21:45:42 +00:00
seth deleted branch cue/2806-gatorwalk-factory-studio 2026-09-30 21:45:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
swamp-club/swamp-extensions!394
No description provided.