diff --git a/.claude/rules/studio-bff.md b/.claude/rules/studio-bff.md new file mode 100644 index 0000000000..74e12c248c --- /dev/null +++ b/.claude/rules/studio-bff.md @@ -0,0 +1,25 @@ +--- +matlatl: orphan-intentional +paths: + - "apps/server/**" + - "apps/contracts/**" +--- +# Studio BFF and contracts + +A BFF route is a `createRoute` from `@hono/zod-openapi` in `apps/server/src/routes/.ts`, with an `operationId` and an `/api/v1/` path, registered in `app.ts`. For an SDK-backed surface, the SDK calls live in `apps/server/src/mecatl/.ts`. Route tests build `createApp()` with the fakes in `apps/server/src/testing/fakes.ts` and send mutations with its `csrfHeaders()`. Body and response schemas are Zod schemas in `apps/contracts/src/schemas/`, exported from `apps/contracts/src/index.ts`; path and query parameter schemas stay in the route file. + +Errors are RFC 9457 problem details. Return `problem()` from `apps/server/src/http/problem.ts`, declare each error status with `problemDetailsSchema` as `application/problem+json`, and pass upstream error text through `sanitizeUpstreamDetail`, which redacts bearer tokens and upstream addresses. + +A schema or route change also regenerates the contracts artifacts, as [the Studio agent contract](../../apps/AGENTS.md#generated-files) describes. + +Keep these security invariants (`http/security.ts`, `auth/service.ts`, `app.ts`, and `bootstrap.ts` under `apps/server/src/`): + +- Session cookies are sealed and HttpOnly, with the `Secure` flag derived from `STUDIO_PUBLIC_URL` (the request scheme when it is unset). +- Requests must name the `STUDIO_PUBLIC_URL` host, or a loopback host when it is unset; only `/api/health` is exempt, for probes. +- State-changing `/api/v1` requests must be same-origin (`Origin` or `Sec-Fetch-Site`) and carry the double-submit token: the `X-Studio-CSRF` header equals the `studio_csrf` cookie. +- `/api/v1/auth/*` (except the session check), `/oauth/callback`, and `GET /api/v1/status` are rate-limited per client address, trusting `X-Forwarded-For` only through `STUDIO_TRUSTED_PROXY_HOPS`. +- Concurrent requests holding one refresh token share a single refresh. +- With interactive login, `/api/v1` answers `401` without a session, except the status and auth routes. +- With `STUDIO_IMAGE=1`, spawn and mock modes are refused, and a static-token or no-auth target needs `STUDIO_ALLOW_UNAUTHENTICATED=1`. + +The [Studio guide](../../user-docs/operating/studio.md) describes the security model for operators. diff --git a/.claude/rules/studio-tests.md b/.claude/rules/studio-tests.md new file mode 100644 index 0000000000..03be3da1f6 --- /dev/null +++ b/.claude/rules/studio-tests.md @@ -0,0 +1,22 @@ +--- +matlatl: orphan-intentional +paths: + - "apps/**/*.test.ts" + - "apps/**/*.test.tsx" + - "apps/web/src/test-*.ts" + - "apps/web/e2e/**" + - "apps/server/test/**" +--- +# Studio tests + +Studio tests are offline. Vitest runs both apps with `isolate: false`, so the files in a worker share one module cache, Web Storage, and the happy-dom `window`. Web tests run in Node unless the file starts with `// @vitest-environment happy-dom`. The NO-ISOLATE RULES in `apps/web/src/test-setup.ts` give the fixes, and Biome fails on the first two hazards: + +- Render the real module inside real providers, with a fresh `QueryClient` and memory router per test, and fake the network with `apiClient.setConfig({ baseUrl: window.location.origin, fetch })`, where `apiClient` is `client` from `@mecatl-studio/contracts/client`; `vi.mock()` and `vi.doMock()` are banned. +- Spy on storage with `spyOnLocalStorage()` from `apps/web/src/test-storage.ts`; `vi.spyOn` on Web Storage is banned. +- Replace globals with `vi.stubGlobal()`, and give module-level state a reset that the shared hook in `test-setup.ts` calls. + +Browser journeys in `apps/web/e2e/` import `test` and `expect` from `./fixtures`. Its automatic `offlineBff` fixture answers `/api/` calls only from the handlers a test registers, and fails the test on an unregistered BFF call or on network access outside its fixtures. Playwright runs each journey in the `chromium-desktop` (1280x800) and `chromium-mobile` (390x844) projects. + +The approval journey and `apps/server/test/integration/` drive a real `mecated --mock` from `bin/mecated`; [the Studio agent contract](../../apps/AGENTS.md#commands) lists the commands. + +Before changing behavior, pin the current behavior in its own commit, with `*.pinning.test.tsx` tests that pass on the base commit and on the branch. diff --git a/.claude/rules/studio-web.md b/.claude/rules/studio-web.md new file mode 100644 index 0000000000..8378418c6a --- /dev/null +++ b/.claude/rules/studio-web.md @@ -0,0 +1,18 @@ +--- +matlatl: orphan-intentional +paths: + - "apps/web/src/**" +--- +# Studio web app + +Routes are TanStack file routes in `apps/web/src/routes/`. A dot nests a route in its parent's layout (`workspace.chat.tsx` renders inside `workspace.tsx`), `$name` is a path parameter, and a trailing `_` on a segment leaves the parent's layout (`workspace.schedules_.$scheduleName.tsx`). A route file validates search and path parameters, redirects, and renders a component from `features/` or `components/shell/`. `routeTree.gen.ts` is [generated](../../apps/AGENTS.md#generated-files) from these files. + +Each surface keeps its components and helpers in `features//`. `components/shell/` holds the app frame, and `lib/` and `hooks/` hold shared helpers. Some features import each other today; #1777 owns the layout decision. + +Request data only through the generated client (`@mecatl-studio/contracts/generated`, `/query`, and `/client`), configured once in `lib/api-client.ts` with the CSRF header and auth recovery. The only hand-built BFF URLs are browser navigations: the login URL in `features/auth/auth-gate.tsx` and the authorization presentation in `features/chat/authorization-review.tsx`. + +Build controls from the shadcn/ui ports in `components/ui/`. When shadcn/ui provides a control that is missing there, port the primitive into `components/ui/` instead of hand-rolling it (#2201). + +Layouts pivot at 500px, like the prototype: `min-[500px]:` and `max-[499px]:` in classes, and `useIsMobile()` from `lib/use-mobile.ts` in code. The chat list and side panels also pivot at 760px (`min-[760px]:`). `features/breakpoints.test.ts` fails on a new viewport `sm:` or `md:` class until the test lists it with a reason (it skips `features/schedules/` for now); `components/ui/` keeps its upstream shadcn classes, and container queries pass. + +A visual change needs the screenshots described in [the Studio agent contract](../../apps/AGENTS.md#ui-pull-requests). diff --git a/AGENTS.md b/AGENTS.md index bfc7ef66f1..aacc08d9de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,10 +32,11 @@ go run ./cmd/mecademo # offline session smoke test ## Module layout The root is one Go module; `engine/` is its own. Root `go test ./...` does not cross -the module boundary; `task test` does. [`engine/`](engine/AGENTS.md), -[`internal/`](internal/AGENTS.md), and [`website/`](website/AGENTS.md) have their own -`AGENTS.md`. When the code doesn't explain how parts fit together, read the chapter -for that area from [`docs/READING.md`](docs/READING.md). +the module boundary; `task test` does. [`apps/`](apps/AGENTS.md) (Mecatl Studio), +[`engine/`](engine/AGENTS.md), [`internal/`](internal/AGENTS.md), and +[`website/`](website/AGENTS.md) have their own `AGENTS.md`. When the code doesn't +explain how parts fit together, read the chapter for that area from +[`docs/READING.md`](docs/READING.md). ## Implementation boundaries diff --git a/apps/AGENTS.md b/apps/AGENTS.md new file mode 100644 index 0000000000..4d66b748a5 --- /dev/null +++ b/apps/AGENTS.md @@ -0,0 +1,94 @@ +# AGENTS.md: Studio workspace + +`apps/` holds Mecatl Studio, a self-contained pnpm workspace outside the Go build: +`contracts/` (Zod schemas and the generated client), `server/` (the Hono backend for +frontend, or BFF), and `web/` (the Vite and React app). The [README](README.md) +covers local runtime modes. The public [Studio guide](../user-docs/operating/studio.md) +owns deployment, configuration, and the security model. + +## Commands + +Run these from the repository root: + +```sh +task studio:check # local gate: Biome, typecheck, Vitest, contracts drift +task studio:browser # offline Playwright journeys, desktop and phone +task studio:test:integration # BFF over the real SDK against a spawned mecated --mock +task studio:generate # regenerate the contracts artifacts +task studio:format # apply Biome formatting and import order +``` + +- `task studio:check` must pass for any change under `apps/`. `task test` and + `task lint` cover only the Go modules. Biome formats and lints all three packages + from `biome.json`, so run `task studio:format` before committing. CI also runs + `task studio:audit`, which fails on a production dependency with an advisory. +- The approval journey in `task studio:browser` spawns `mecated --mock`, so run + `task build` first. Install Chromium once with `task studio:browser:install`. +- To run one journey, use `pnpm run test:browser --project=chromium-mobile` + from `apps/`. Keep the `=`, because `--project` accepts several names. Leave out + `--`, because Playwright ignores the arguments after it and runs every journey. +- `task studio:test:integration` runs the full `task build` first. To rerun the + suite against an existing `bin/mecated`, run + `cd apps && STUDIO_REQUIRE_MECATED=1 STUDIO_MECATED_BIN=$PWD/../bin/mecated pnpm --filter @mecatl-studio/server run test:integration`. + Without `STUDIO_REQUIRE_MECATED=1`, a missing binary skips the suite. + +## Generated files + +Never edit these by hand; Biome skips them. + +- `contracts/openapi.json` and `contracts/src/generated/` (the Hey API client and + its TanStack Query helpers) come from the Hono routes. Regenerate them with + `task studio:generate` (`pnpm run generate` in `apps/`) and commit them with the + route or schema change; `task studio:generated-check` fails CI when they drift. +- `web/src/routeTree.gen.ts` comes from the files in `web/src/routes/`. The + TanStack Router Vite plugin rewrites it during `dev` and `build`, and the web + `typecheck` script runs `tsr generate`. Commit it with the route change. + +## Boundaries + +- The BFF is the single source of truth for the data the UI shows. `web/` requests + data only from the BFF's `/api/v1`, through the generated client, and only labels + or formats what comes back. It never re-derives state or eligibility; when the UI + needs a value the contract lacks, add it to the schema and the BFF route. +- The design prototype, branch `stack/08-tooling` of `stacklok/mecatl-prototypes`, + is the source of truth for components, layout, interaction, and copy. #2210 lists + what Studio keeps, including the BFF, contracts, security, and contrast tokens. +- `server/` builds the checked-out `sdk/typescript` through a frozen `file:` + dependency and imports it only as `@stacklok-oss/mecatl-sdk`. + `server/src/sdk-source.test.ts` guards the dependency, and Biome rejects relative + imports of `sdk/`. `web/` imports neither the SDK nor its protocol types. +- `MECATL_*` variables describe the target (`server/src/mecatl/runtime.ts`), and + `STUDIO_*` variables configure Studio itself (`server/src/config.ts`). The Studio + guide's [configuration reference](../user-docs/operating/studio.md#configuration-reference) + owns the deployment variables. + +## Path-scoped rules + +These load with the files they cover: + +- [`studio-bff.md`](../.claude/rules/studio-bff.md): `server/` and `contracts/` + routes, problem details, and security invariants. +- [`studio-web.md`](../.claude/rules/studio-web.md): `web/src/` routes, features, + data access, UI primitives, and the 500px breakpoint. +- [`studio-tests.md`](../.claude/rules/studio-tests.md): Vitest without isolation, + the offline Playwright fixtures, and pinning tests. + +## UI pull requests + +A PR that changes what Studio renders shows screenshots in Before, After, and +Prototype columns at desktop (1280x800) and phone (390x844) sizes, in light and +dark, with the changed regions highlighted. + +Capture Before and After with a throwaway `.spec.ts` in `web/e2e/` that you +don't commit. Import `test` from `./fixtures`, answer each JSON call with +`offlineBff.json()` and each event stream with `offlineBff.on()` returning +`text/event-stream`, and fix the clock with +`page.clock.install({ time: new Date("") })`. Save each shot with +`page.screenshot({ path: testInfo.outputPath(".png") })`. The +`chromium-desktop` and `chromium-mobile` projects give both sizes, and +`page.emulateMedia({ colorScheme: "dark" })` switches the theme. Run the spec on +the base commit for Before and on the branch for After, copying the shots out of +`.scratch/studio-browser-playwright/` between runs, because Playwright empties it +at the start of each run. Take the Prototype column from the prototype branch +above. Recent PRs host the images on the `pr-assets` branch and link them as +`raw.githubusercontent.com` URLs. diff --git a/apps/CLAUDE.md b/apps/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/apps/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file