Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# GSD workflow state
.planning/

# Conductor / Claude workspace state
.agents/

# Logs
logs
*.log
Expand Down
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ This file provides guidance when working with code in this repository, including

Studio is an open-source control plane for Model Context Protocol (MCP) traffic. It provides a unified layer for authentication, routing, and observability between MCP clients (Cursor, Claude, VS Code) and MCP servers. The system is built as a monorepo using Bun workspaces with TypeScript, Hono (API), and React 19 (UI).

## Working on a feature (the harness contract)

This repo ships a **feature catalog** at `features/<name>/`. Each catalogued feature has:

- `feature.md` — the user-facing story, the happy path, why the feature matters, and a prompt for AI agents extending it.
- `happy-path.test.ts` — the executable contract, runnable end-to-end.

**Before touching code that participates in a documented feature:**

1. `bun run features:list` — find the feature(s) your change touches.
2. `bun run features:test <name>` — verify the harness passes on the current code. If it doesn't, that's your first task; fix the divergence (or fix the test if the contract genuinely changed) before doing anything else.
3. Read `features/<name>/feature.md` end-to-end. The "Prompt for AI agents" section at the bottom is written for you.
4. **Write or extend `happy-path.test.ts` to cover the new behavior FIRST.** RED. Then code until GREEN.
5. Loop on test ↔ code ↔ `feature.md` until the harness cohesively expresses the experience you intend.
6. **Only then ask a human to verify.** If they reject the result, update `feature.md` to capture the new expectation, fail the test against it, and loop.

We do not ship features verified only by humans. The harness is the contract; coding agents that skip it are out of contract.

If your change adds a NEW major feature (something that would degrade the product story if removed, not a refactor), create a feature folder following `features/page-editor/` as the template. The pattern is:

```
features/<your-feature>/
feature.md # story, value, happy path, prompt for AI agents
happy-path.test.ts # the executable contract
```

See `features/README.md` for the catalog invariants.

**For richer browser-driven verification beyond the deterministic Playwright spec**, install [Webwright](https://github.com/microsoft/Webwright) as a Claude Code skill (`/plugin install webwright@webwright`) and feed it the happy path from `feature.md`. It writes a re-runnable Playwright script + screenshots + a self-verification log against the critical points you described. Treat the output as evidence for a human pass — not a substitute for the deterministic contract that lives in `happy-path.test.ts` + `*.browser.spec.ts`.

## Commands

### Development
Expand Down Expand Up @@ -324,6 +354,8 @@ See [`TESTING.md`](./TESTING.md) for the testing philosophy and rules.

If a test needs `vi.mock`, `mock.module`, a stubbed `MeshContext`, or a fake `fetch` — it's not a unit test. Move it to e2e.

**For cross-cutting features** (Page Editor, Brand Manager, Studio Pack install, etc.), the canonical executable contract is `features/<name>/happy-path.test.ts` plus an optional `apps/mesh/e2e/tests/features/<name>.browser.spec.ts`. See **Working on a feature** above — the contract MUST be extended before the implementation, not after.

## Working with Tools

When creating new MCP tools:
Expand Down
108 changes: 108 additions & 0 deletions apps/mesh/e2e/tests/features/page-editor.browser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Page Editor browser leg of the feature harness.
*
* The data-path contract (Studio Pack install, MCP tool sequence,
* storage state, multi-tenant isolation, export bundle) lives in
* features/page-editor/happy-path.test.ts. This spec is the
* browser-visible half: it sits between Better Auth signup and the
* user actually seeing the iframe with the right contract wired in.
*
* What it proves:
* - Fresh signup auto-installs the Studio Pack, including the Page
* Editor agent.
* - The Page Editor agent surfaces in the org's agents-list UI.
* - Clicking it opens a chat with the Page-Preview tab routed
* automatically — i.e. the `defaultMainView: { type: "page-preview" }`
* metadata threads end-to-end into the panel-tab resolver.
* - The iframe at `/api/<org>/page-preview/host` loads and runs its
* preact bootstrap (the welcome quiz mounts).
*
* Run: `PW=1 bun run features:test page-editor`
* (the harness CLI shells out to playwright when PW=1)
*
* Or directly via Playwright:
* bun run --cwd=apps/mesh exec playwright test \
* e2e/tests/features/page-editor.browser.spec.ts
*
* If this spec breaks, follow the Loop in features/page-editor/feature.md
* before patching. The browser leg is part of the contract; don't
* loosen the test to make a green build.
*/

import { expect, test } from "@playwright/test";
import { signUp } from "../../fixtures/auth";

// Studio Pack installation runs via a DBOS workflow on org.afterCreate.
// It's idempotent and fast (<1s in practice) but async — so the agent
// may not appear in the agents list on the first navigation. Polling
// the list with a generous timeout absorbs that race.
const STUDIO_PACK_INSTALL_TIMEOUT = 30_000;

test.describe("Page Editor — browser leg", () => {
test("Studio Pack installs the agent and the preview iframe boots", async ({
page,
}) => {
// 1. Fresh user + auto-created org. signUp lands somewhere under
// /<orgSlug>/...; agents-section route is reliable.
await signUp(page);
await page.waitForURL(
(url) => {
const slug = url.pathname.split("/")[1];
return !!slug && slug !== "login" && slug !== "api";
},
{ timeout: 15_000 },
);
const orgSlug = new URL(page.url()).pathname.split("/")[1]!;

// 2. Navigate to the agents list — Studio Pack agents (including
// Page Editor) live alongside any user-created ones.
await page.goto(`/${orgSlug}/agents`);

// 3. Wait for Page Editor to appear. The DBOS install workflow
// races signup; refreshing once after a brief delay catches the
// case where the page rendered the agents list before the install
// finished.
const pageEditorCard = page
.locator('[data-testid="project-card"]')
.filter({ hasText: "Page Editor" })
.or(page.getByRole("link", { name: /Page Editor/i }))
.or(page.getByRole("button", { name: /Page Editor/i }))
.first();

await expect(pageEditorCard).toBeVisible({
timeout: STUDIO_PACK_INSTALL_TIMEOUT,
});

// 4. Click into the Page Editor agent. The card navigates to
// /<orgSlug>/<taskId>?virtualmcpid=studio-page-editor_<orgId>.
await pageEditorCard.click();
await page.waitForURL(/[?&]virtualmcpid=studio-page-editor_/, {
timeout: 15_000,
});

// 5. The page-preview tab must be the default main view. We don't
// pin to the tab DOM (it can vary across UI revisions); we pin to
// the iframe it owns — that's the only thing that has to exist
// exactly as named for the feature to work.
const previewIframe = page.frameLocator('iframe[title="Page preview"]');

// 6. The iframe is mounted and pointing at the host route. Wait
// for ANY element inside — the welcome quiz root, the empty stage,
// anything that proves the preact bundle executed. Catch most boot
// failures (network, syntax errors in host-html.ts, CSP block).
await expect(previewIframe.locator("body")).toBeVisible({
timeout: 20_000,
});

// 7. The host runtime renders the welcome quiz on first load (no
// active page yet). It shows the "Build a beautiful page" headline
// or similar marketing copy; pin to one stable string from the
// welcome template. If you renamed the welcome quiz, update both
// the template AND this assertion.
const welcomeHeadline = previewIframe
.locator("body")
.getByText(/build|create|start|page/i)
.first();
await expect(welcomeHeadline).toBeVisible({ timeout: 20_000 });
});
});
23 changes: 23 additions & 0 deletions apps/mesh/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,27 @@
transform: translateY(0);
}
}

@keyframes mise-breathe {
0%,
100% {
transform: scale(1) translate3d(0, 0, 0);
opacity: 0.8;
}
50% {
transform: scale(1.04) translate3d(0, -10px, 0);
opacity: 1;
}
}

@keyframes mise-panel-in {
0% {
opacity: 0;
transform: translateY(18px) scale(0.985);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
}
1 change: 1 addition & 0 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"ai-sdk-provider-claude-code": "^3.4.4",
"ai-sdk-provider-codex-cli": "^1.1.0",
"embedded-postgres": "^18.3.0-beta.16",
"fflate": "^0.8.0",
"ink": "^6.8.0",
"kysely": "^0.28.12",
"nats": "^2.29.3",
Expand Down
14 changes: 11 additions & 3 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,10 +1091,18 @@ export async function createApp(options: CreateAppOptions = {}) {
app.use("*", async (c, next) => {
await next();
// Org-scoped /files/* serves user content (HTML pages written by the
// web-developer agent, uploaded images, etc.) that we deliberately
// iframe back into the app. Same-origin only — auth middleware still
// gates access — and consumers are expected to sandbox the iframe.
// web-developer agent, uploaded images, page-preview blocks, etc.)
// that we deliberately iframe back into the app. Same-origin only —
// auth middleware still gates access — and consumers are expected
// to sandbox the iframe. Page-preview's /host endpoint is also
// framable (it serves the host shell that loads /files/* into the
// preview iframe).
if (c.req.path.includes("/files/")) return;
if (/^\/api\/[^/]+\/page-preview\/host(\/|$)/.test(c.req.path)) {
c.header("X-Frame-Options", "SAMEORIGIN");
c.header("Content-Security-Policy", "frame-ancestors 'self'");
return;
}
c.header("X-Frame-Options", "DENY");
c.header("Content-Security-Policy", "frame-ancestors 'none'");
});
Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/api/routes/org-scoped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createDownstreamTokenRoutes } from "./downstream-token";
import { createFileUploadRoutes } from "./file-uploads";
import { createKVRoutes } from "./kv";
import { createOrgScopedWellKnownProtectedResourceRoutes } from "./oauth-proxy";
import { createPagePreviewRoutes } from "./page-preview";
import { createSsoRoutes } from "./org-sso";
import { createProxyRoutes } from "./proxy";
import { createSelfRoutes } from "./self";
Expand Down Expand Up @@ -83,6 +84,7 @@ export const createOrgScopedApi = (deps: OrgScopedDeps) => {
app.route("/sandbox", createSandboxRoutes()); // /api/:org/sandbox/:virtualMcpId/:branch/*
app.route("/", createHomeNextActionsRoutes());
app.route("/deco-sites", createDecoSitesOrgRoutes()); // /api/:org/deco-sites
app.route("/page-preview", createPagePreviewRoutes()); // /api/:org/page-preview
app.route("/sso", createSsoRoutes()); // /api/:org/sso/* (renamed from /api/org-sso)
app.route(
"/",
Expand Down
Loading
Loading