Skip to content

feat(studio): server/security tier — trusted proxy, request-trust gate, managed-mode controller - #968

Closed
jtenniswood wants to merge 1 commit into
studio/S02from
studio/S03
Closed

feat(studio): server/security tier — trusted proxy, request-trust gate, managed-mode controller#968
jtenniswood wants to merge 1 commit into
studio/S02from
studio/S03

Conversation

@jtenniswood

Copy link
Copy Markdown

Stack 3/21 — Studio: server/security tier

The Next server tier and the managed-mode controller core — the layer between the browser and the daemon. The browser never holds a daemon address or credential; everything below is server-side.

In this PR:

  • src/lib/server-proxy.ts — the trusted proxy: Host/Origin/CSRF pinning (MECATL_STUDIO_PUBLIC_ORIGIN), header allowlists both directions, server-side bearer injection, server-side workspace injection into session-create bodies (the browser never learns or chooses paths), external-mode 409 synthesis for every control surface. Lands minus two later, narrowly-scoped touches: the OIDC authorization swap (PR 12) and the no-fs/debug workspace skip (PR 18) — the static-bearer hunk here is byte-for-byte the pre-OIDC shape, so PR 12's diff reproduces the real refactor.
  • src/lib/request-trust.ts (+ its CSRF/DNS-rebinding truth-table test) and src/lib/controller-security.mjs (+test: isLoopbackHost / requestIsAllowed / validateGatewayURL / validSkillName) — the shared browser+controller gates.
  • src/app/api/mecatl/[...path] + src/app/api/mecatl-control/[...path] — the only two routes the browser talks to.
  • scripts/local-controller.mjs (core) — spawns mecated from ../bin/mecated on a random loopback port with a generated bearer (--ready-file + --lifetime-pipe-fd FIFO, both flags already on main), auto-restart with rollback, authenticated relay, /status, /restart, MCP gateway set + OAuth-injecting proxy. The /skills* and /providers*+/model-router route families arrive with the surfaces that call them (PRs 9/11). src/lib/provider-auth.mjs (+29-test suite) rides here because the controller's startup path (provider-kind validation, /status configuredProviders) imports it — booleans and snippets only, never key material.
  • scripts/dev-local.mjs, .env.example (both modes; the OIDC vars arrive with OIDC in PR 12).
  • The hermetic server-tier suite (tests/rendered-html.test.mjs): a real next build + next start in external mode against a fake in-process recording daemon — no Go binary, no network. Proves bearer + workspace injection, the full external-mode 409 table, CSRF/DNS-rebinding, offline → honest 503, gateway HTTPS-only egress. The SSR assertion targets root branding until the Chats route exists (PR 7). CI's build step upgrades to npm run test:server.

Demo at this stack point: npm run dev spawns controller + daemon (managed) or attaches to MECATL_BASE_URL (external); kill the daemon and the proxy answers an honest 503.

Part of the stacked landing of #618 — see PR 1 of the series for the structure.

🤖 Generated with Claude Code

…e, managed-mode controller core

The layer between the browser and the daemon. server-proxy.ts pins
Host/Origin/CSRF against MECATL_STUDIO_PUBLIC_ORIGIN, allowlists headers in
both directions, injects the bearer and the session workspace server-side
(the browser never learns or chooses paths), and synthesizes 409
deployment-owned answers for every control surface in external mode.
request-trust.ts carries the CSRF/DNS-rebinding truth table with its test;
controller-security.mjs is the shared browser+controller gate
(isLoopbackHost, requestIsAllowed, validateGatewayURL, validSkillName).

scripts/local-controller.mjs lands its core: spawn mecated from
../bin/mecated on a random loopback port with a generated bearer
(--ready-file + --lifetime-pipe-fd FIFO), auto-restart with rollback, the
authenticated relay, /status, /restart, and the MCP gateway set +
OAuth-injecting proxy. provider-auth.mjs rides with it (startup provider
validation and /status fields import it; booleans and key-shape probes only,
never key material). The /skills* and /providers*+/model-router route
families arrive with the surfaces that call them later in the series.

tests/rendered-html.test.mjs is the hermetic proof: a real next build +
next start in external mode against a fake in-process daemon — bearer +
workspace injection, the full external-mode 409 table, CSRF/DNS-rebinding,
offline → honest 503, HTTPS-only gateway egress. CI's build step upgrades to
npm run test:server. The SSR assertion targets root branding until the Chats
route lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jtenniswood jtenniswood added enhancement New feature or request config Operator settings.yaml / YAML config surface, flags, and config UX (init, reference, enable models) WebUI Applies to the WebUI interface labels Sep 2, 2026
@jhrozek jhrozek closed this Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Operator settings.yaml / YAML config surface, flags, and config UX (init, reference, enable models) enhancement New feature or request WebUI Applies to the WebUI interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants