diff --git a/.github/workflows/deploy-langgraph.yml b/.github/workflows/deploy-langgraph.yml index 33673b68f..28fb032bb 100644 --- a/.github/workflows/deploy-langgraph.yml +++ b/.github/workflows/deploy-langgraph.yml @@ -6,6 +6,7 @@ on: paths: - 'cockpit/langgraph/**/python/**' - 'cockpit/deep-agents/**/python/**' + - 'examples/chat/python/**' - 'apps/cockpit/scripts/capability-registry.ts' - 'scripts/generate-shared-deployment-config.ts' - 'deployments/shared-dev/langgraph.json' diff --git a/deployment-urls.json b/deployment-urls.json index 0d70fb0f5..59105bca8 100644 --- a/deployment-urls.json +++ b/deployment-urls.json @@ -12,5 +12,6 @@ "subagents": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", "da-memory": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", "skills": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", - "sandboxes": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app" + "sandboxes": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "chat": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app" } diff --git a/deployments/shared-dev/langgraph.json b/deployments/shared-dev/langgraph.json index a337ed132..053db71b8 100644 --- a/deployments/shared-dev/langgraph.json +++ b/deployments/shared-dev/langgraph.json @@ -24,13 +24,15 @@ "subagents": "./deps/da-subagents/src/graph.py:graph", "da-memory": "./deps/da-memory/src/graph.py:graph", "skills": "./deps/skills/src/graph.py:graph", - "sandboxes": "./deps/sandboxes/src/graph.py:graph" + "sandboxes": "./deps/sandboxes/src/graph.py:graph", + "chat": "./deps/examples-chat/src/graph.py:graph" }, "dependencies": [ "./deps/da-memory", "./deps/da-subagents", "./deps/deployment-runtime", "./deps/durable-execution", + "./deps/examples-chat", "./deps/filesystem", "./deps/interrupts", "./deps/memory", diff --git a/docs/superpowers/plans/2026-05-13-canonical-demo-deploy-phase-1.md b/docs/superpowers/plans/2026-05-13-canonical-demo-deploy-phase-1.md new file mode 100644 index 000000000..525155932 --- /dev/null +++ b/docs/superpowers/plans/2026-05-13-canonical-demo-deploy-phase-1.md @@ -0,0 +1,315 @@ +# Canonical Demo Deploy — Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add the canonical demo's LangGraph (`examples/chat/python`, graph name `chat`) to the existing shared `cockpit-dev` LangGraph Cloud deployment, so it's reachable on the LangGraph side before the frontend ships in Phase 2. + +**Architecture:** Extend the generator `scripts/generate-shared-deployment-config.ts` to include a small hardcoded list of "non-cockpit" Python dependencies alongside the existing capability-registry-driven cockpit graphs. Add `examples/chat/python/**` to the deploy-langgraph workflow's watched paths so changes there retrigger redeployment. Register `chat` in `deployment-urls.json` (so the shared-URL coherence check still passes) and in `scripts/verify-shared-deployment.ts`'s smoke list (so post-deploy verification confirms the graph is reachable). + +**Tech Stack:** TypeScript (Nx), vitest, GitHub Actions workflow YAML, LangGraph CLI. + +**Reference spec:** `docs/superpowers/specs/2026-05-13-canonical-demo-deploy-design.md` — see "Phase 1 — backend graph addition". + +--- + +## Background for the implementer + +The deploy pipeline today: + +1. `apps/cockpit/scripts/capability-registry.ts` exports an array of capability descriptors. Each has `pythonDir` and `graphName` fields. +2. `scripts/generate-shared-deployment-config.ts` reads that array, filters to `product === 'langgraph' || product === 'deep-agents'`, reads each capability's `langgraph.json` manifest, and aggregates all graphs into `deployments/shared-dev/langgraph.json`. +3. `.github/workflows/deploy-langgraph.yml` runs on pushes to main that touch any cockpit Python graph (or the generator, or the registry) and deploys `deployments/shared-dev/` to the shared `cockpit-dev` LangGraph Cloud assistant. +4. After CI deploy, `scripts/verify-shared-deployment.ts` runs in production smoke and asserts each entry in `SMOKE_ASSISTANT_IDS` is reachable. +5. `deployment-urls.json` is consulted by `verify-shared-deployment.ts` — every capability that targets the shared deployment is listed there, all pointing to the same URL. + +The canonical demo (`examples/chat/python`, graph `chat`) is **not** in any of these. After this phase it will be. + +We deliberately don't add `examples/chat` to `capability-registry.ts`. That registry's entries describe Angular SPAs with ports and project names — adding a phantom `chat-canonical` row would force fake values into multiple fields. Cleaner: put a single dedicated array of "extra Python deployments" in the generator itself. + +--- + +### Task 1: Extend generator to include `examples/chat/python` + +**Files:** +- Modify: `scripts/generate-shared-deployment-config.ts` +- Create: `scripts/generate-shared-deployment-config.spec.ts` + +**Context:** The generator is a top-level script today — no exported functions. We'll refactor lightly to extract the build into a callable function, then test it by running the generator end-to-end and reading the output. Refactoring is mild because the script is 86 lines and self-contained. + +--- + +- [ ] **Step 1: Create the failing test** + +Create `scripts/generate-shared-deployment-config.spec.ts`: + +```ts +// scripts/generate-shared-deployment-config.spec.ts +// SPDX-License-Identifier: MIT +import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { describe, expect, it } from 'vitest'; + +describe('generate-shared-deployment-config', () => { + it('includes the canonical-demo chat graph in the aggregated manifest', () => { + const root = resolve(__dirname, '..'); + execSync('npx tsx scripts/generate-shared-deployment-config.ts', { + cwd: root, + stdio: 'pipe', + }); + const manifestPath = resolve(root, 'deployments/shared-dev/langgraph.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { + graphs: Record; + dependencies: string[]; + }; + expect(manifest.graphs).toHaveProperty('chat'); + expect(manifest.graphs.chat).toMatch(/examples-chat\/.+\.py:graph$/); + expect(manifest.dependencies.some((d) => d.includes('examples-chat'))).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify failure** + +``` +npx vitest run scripts/generate-shared-deployment-config.spec.ts +``` + +Expected: FAIL with `expected { ... } to have property "chat"`. + +- [ ] **Step 3: Modify the generator to stage `examples/chat/python` as an extra dependency** + +In `scripts/generate-shared-deployment-config.ts`, locate the loop that ends on line 68 (`}` closing the `for (const capability of capabilities)` block). Immediately after that closing `}`, before the line `const streamingManifestPath = resolve(rootDir, 'cockpit/langgraph/streaming/python/langgraph.json');`, insert: + +```ts +// Extra Python deployments NOT in the cockpit capability registry. +// These have no Angular project / port — only a backend graph aggregated +// into the shared cockpit-dev assistant. +const extraPythonDeployments: ReadonlyArray<{ pythonDir: string; alias: string }> = [ + { pythonDir: 'examples/chat/python', alias: 'examples-chat' }, +]; + +for (const extra of extraPythonDeployments) { + const manifestPath = resolve(rootDir, extra.pythonDir, 'langgraph.json'); + const extraManifest = readManifest(manifestPath); + if (!extraManifest.graphs) { + throw new Error(`Missing graphs in ${manifestPath}`); + } + const stagedDependencyRoot = stageDependency(extra.pythonDir, extra.alias); + for (const [graphName, entrypoint] of Object.entries(extraManifest.graphs)) { + addGraph(graphName, toDeploymentPath(stagedDependencyRoot, entrypoint)); + } +} +``` + +- [ ] **Step 4: Run test to verify pass** + +``` +npx vitest run scripts/generate-shared-deployment-config.spec.ts +``` + +Expected: PASS. + +- [ ] **Step 5: Inspect the generated manifest manually** + +``` +cat deployments/shared-dev/langgraph.json | head -30 +``` + +Expected: `"chat": "./deps/examples-chat/src/graph.py:graph"` should appear in the `graphs` object. `"./deps/examples-chat"` should appear in `dependencies`. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/generate-shared-deployment-config.ts \ + scripts/generate-shared-deployment-config.spec.ts \ + deployments/shared-dev/langgraph.json +git commit -m "feat(deploy): include examples/chat/python in shared cockpit-dev deployment + +Adds the canonical demo's Python graph to the aggregated manifest +generator. After CI redeploys, the shared cockpit-dev LangGraph +Cloud assistant will expose 'chat' alongside the existing cockpit +capability graphs. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 2: Wire the new graph into the deploy + verify pipeline + +**Files:** +- Modify: `.github/workflows/deploy-langgraph.yml` (watched paths) +- Modify: `deployment-urls.json` (add `chat` entry) +- Modify: `scripts/verify-shared-deployment.ts` (`SMOKE_ASSISTANT_IDS` array) + +**Context:** Three small config edits. No tests — config-only edits don't benefit from unit coverage; the integration check is the production smoke that runs after deploy. + +--- + +- [ ] **Step 1: Extend the deploy-langgraph workflow's watched paths** + +In `.github/workflows/deploy-langgraph.yml`, locate the `paths:` block at the top (lines 5–10): + +```yaml + paths: + - 'cockpit/langgraph/**/python/**' + - 'cockpit/deep-agents/**/python/**' + - 'apps/cockpit/scripts/capability-registry.ts' + - 'scripts/generate-shared-deployment-config.ts' + - 'deployments/shared-dev/langgraph.json' +``` + +Add a new entry for the canonical demo's Python dir. The final block should read: + +```yaml + paths: + - 'cockpit/langgraph/**/python/**' + - 'cockpit/deep-agents/**/python/**' + - 'examples/chat/python/**' + - 'apps/cockpit/scripts/capability-registry.ts' + - 'scripts/generate-shared-deployment-config.ts' + - 'deployments/shared-dev/langgraph.json' +``` + +- [ ] **Step 2: Register `chat` in `deployment-urls.json`** + +`verify-shared-deployment.ts`'s `getSharedUrl()` function requires every entry in `deployment-urls.json` to resolve to the same URL. Append `chat` with the existing shared URL. + +In `deployment-urls.json`, after the existing `"sandboxes"` line, add a `"chat"` entry. The shared URL value to use is the same one every other capability uses today (read the current file before editing — copy that value verbatim): + +```json + "chat": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app" +``` + +Result: every capability + `chat` all point to the same `cockpit-dev` URL. + +- [ ] **Step 3: Add `chat` to the production-smoke assistant list** + +In `scripts/verify-shared-deployment.ts`, locate `SMOKE_ASSISTANT_IDS` (around line 21): + +```ts +const SMOKE_ASSISTANT_IDS = [ + 'streaming', + 'deployment-runtime', + 'planning', + 'filesystem', + 'c-generative-ui', + 'c-a2ui', +] as const; +``` + +Append `'chat'`: + +```ts +const SMOKE_ASSISTANT_IDS = [ + 'streaming', + 'deployment-runtime', + 'planning', + 'filesystem', + 'c-generative-ui', + 'c-a2ui', + 'chat', +] as const; +``` + +- [ ] **Step 4: Re-run the generator test to confirm nothing broke** + +``` +npx vitest run scripts/generate-shared-deployment-config.spec.ts +``` + +Expected: PASS — the test from Task 1 still passes. + +- [ ] **Step 5: Run the verify script in dry-run mode locally** + +``` +npx tsx scripts/verify-shared-deployment.ts --dry-run +``` + +Expected: succeeds, prints a summary listing `chat` as one of the smoke assistants. (The script's full mode requires `LANGSMITH_API_KEY` and live network; `--dry-run` validates config only.) + +- [ ] **Step 6: Commit** + +```bash +git add .github/workflows/deploy-langgraph.yml \ + deployment-urls.json \ + scripts/verify-shared-deployment.ts +git commit -m "feat(deploy): wire 'chat' assistant into deploy + verify pipeline + +- Watch examples/chat/python/** so changes there retrigger deploy +- Register chat in deployment-urls.json (single shared URL) +- Include chat in production-smoke SMOKE_ASSISTANT_IDS + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 3: Open PR and wait for the first deploy run + +**Context:** After merging to main, the deploy-langgraph workflow runs, redeploys `cockpit-dev` with the chat graph included, and the production-smoke job (in ci.yml) calls `verify-shared-deployment.ts`. If the chat assistant is reachable, smoke passes and Phase 1 is done. No frontend exists yet — the chat graph is dormant until Phase 2. + +--- + +- [ ] **Step 1: Push branch + open PR** + +```bash +git push -u origin claude/canonical-demo-deploy +gh pr create --title "feat(deploy): add canonical demo chat graph to shared cockpit-dev deployment (Phase 1)" --body "$(cat <<'EOF' +## Summary + +Phase 1 of the canonical-demo deployment plan. Adds the canonical demo's Python graph (`examples/chat/python`, graph name `chat`) to the aggregated `cockpit-dev` LangGraph Cloud deployment. + +No frontend yet — Phase 2 stands up `demo.cacheplane.ai`. This phase makes the backend graph reachable so Phase 2 has something to consume. + +## Changes + +- `scripts/generate-shared-deployment-config.ts` — stages `examples/chat/python` as an extra Python dependency, aggregating its graphs alongside the cockpit registry. +- `.github/workflows/deploy-langgraph.yml` — watches `examples/chat/python/**` so changes there retrigger redeploy. +- `deployment-urls.json` — `chat` entry pointing at the shared URL. +- `scripts/verify-shared-deployment.ts` — `chat` added to `SMOKE_ASSISTANT_IDS` for post-deploy smoke. +- `scripts/generate-shared-deployment-config.spec.ts` — vitest spec asserting the generator includes the chat graph. + +## Spec & Plan + +- `docs/superpowers/specs/2026-05-13-canonical-demo-deploy-design.md` +- `docs/superpowers/plans/2026-05-13-canonical-demo-deploy-phase-1.md` + +## Test plan + +- [x] vitest generator spec passes (asserts `chat` in manifest + `examples-chat` in dependencies) +- [x] `npx tsx scripts/verify-shared-deployment.ts --dry-run` succeeds +- [ ] After merge: deploy-langgraph workflow runs successfully +- [ ] After deploy: production-smoke job passes (verify-shared-deployment.ts confirms `chat` assistant is reachable) +EOF +)" +``` + +- [ ] **Step 2: Wait for CI to be green on the PR** + +Required green checks before merge: +- `Library — lint / test / build` (the new vitest spec runs in this job) +- `Website — lint / build` +- `Cockpit — build / test`, `e2e`, `representative capability smoke`, `build all examples`, `deploy smoke dry-run`, `secret-gated integration` +- `examples/chat — python smoke` + +The deploy-langgraph workflow does NOT run on PRs — only on pushes to main with the watched paths touched. So the first real-world test of this phase is the post-merge deploy. + +- [ ] **Step 3: After merge, verify deploy + smoke** + +Within ~10 minutes of merge: +1. Confirm `Deploy LangGraph` workflow ran (Actions tab; should be triggered by the changes to `examples/chat/python/**` watched path entry → no, that path doesn't change on first ship, but the changes to `generate-shared-deployment-config.ts` + `deployments/shared-dev/langgraph.json` DO match — confirm the workflow triggered). +2. Confirm it succeeded (`Deploy cockpit-dev` step). +3. Confirm the subsequent CI `production-smoke` job passed (`verify-shared-deployment.ts` reports `chat` reachable). + +If `production-smoke` fails because the chat assistant isn't reachable, the deploy succeeded but the graph didn't register properly. Check the LangGraph Cloud dashboard for the cockpit-dev assistant and look for the `chat` entry. If absent, the manifest needs investigation; if present but unreachable via the SDK, the assistant ID may need to be configured separately. + +--- + +## Self-review notes + +- **Spec coverage:** every Phase 1 requirement from the spec maps to a task. (a) generator extension → Task 1. (b) workflow paths → Task 2 Step 1. (c) deployment-urls → Task 2 Step 2. (d) verify smoke → Task 2 Step 3. (e) deploy run → Task 3. +- **No placeholders:** every code block is final content the implementer pastes verbatim. +- **Type consistency:** all references to `chat`, `examples-chat`, `examples/chat/python` are spelled identically across tasks. +- **Test coverage proportional to value:** the generator gets a vitest spec because it's pure code; the YAML / JSON / TS config edits don't (low-value tests would just duplicate `grep`). diff --git a/docs/superpowers/specs/2026-05-13-canonical-demo-deploy-design.md b/docs/superpowers/specs/2026-05-13-canonical-demo-deploy-design.md new file mode 100644 index 000000000..dc9811493 --- /dev/null +++ b/docs/superpowers/specs/2026-05-13-canonical-demo-deploy-design.md @@ -0,0 +1,285 @@ +# Canonical Demo Deployment — Design + +**Status:** Approved +**Date:** 2026-05-13 +**Goal:** Publicly deploy `examples/chat` (the canonical demo) at `demo.cacheplane.ai`, backed by the existing shared `cockpit-dev` LangGraph Cloud assistant, with rate-limited anonymous access and server-side API key isolation. + +## Audience for this spec + +A future engineer adding capabilities to this demo, or migrating it elsewhere. They know our monorepo conventions; they do NOT know the existing examples deployment pattern (`examples.cacheplane.ai`) in detail. This spec explains both the existing pattern and what we extend from it. + +## Background — what already exists + +The repo already has a production LangGraph Cloud deployment and a production Angular SPA deployment: + +- **`deployments/shared-dev`** is generated by `scripts/generate-shared-deployment-config.ts` and deployed to LangGraph Cloud as the `cockpit-dev` assistant via `.github/workflows/deploy-langgraph.yml`. Every cockpit capability's Python graph is aggregated under one assistant. Triggered by pushes to `main` touching any `cockpit/*/python/` path. + +- **`examples.cacheplane.ai`** is a Vercel project (`cockpit-examples`) that aggregates all 31 cockpit Angular SPAs under one domain. Built by `scripts/assemble-examples.ts`, which assembles a `deploy/examples/` tree plus a Vercel Build Output API structure with one Node serverless function. The function — `scripts/examples-middleware.ts` — proxies `/api/*` to the LangGraph Cloud assistant URL, injecting `x-api-key` server-side from `LANGSMITH_API_KEY`. CI deploy step lives in `.github/workflows/ci.yml`. + +The canonical demo (`examples/chat`) is **not** part of either today. Its Python graph is in `examples/chat/python/langgraph.json` (graph name `chat`); the Angular app is at `examples/chat/angular/`, hardcoded to `http://localhost:2024`. + +## Goal + +Make `https://demo.cacheplane.ai` a public, polished, marketing-grade entry point for the canonical demo — independently deployable, rate-limited, with API keys never exposed to the client. + +## Architecture + +``` + ┌────────────────────────────────────┐ + │ Browser │ + │ demo.cacheplane.ai │ + └──────────────┬─────────────────────┘ + │ same-origin /api/* + ┌──────────────▼─────────────────────┐ + │ Vercel project: demo │ + │ (built from examples/chat/angular │ + │ + scripts/assemble-demo.ts) │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ Static SPA │ │ + │ └──────────────────────────────┘ │ + │ ┌──────────────────────────────┐ │ + │ │ Node serverless function │ │ + │ │ api/[[...path]] (Build Output) │ + │ │ - rate-limit (Upstash) │ │ + │ │ - prompt-length cap │ │ + │ │ - CORS allowlist │ │ + │ │ - inject x-api-key │ │ + │ │ - proxy to LangGraph Cloud │ │ + │ └──────────────┬───────────────┘ │ + └─────────────────┼──────────────────┘ + │ https + x-api-key + ┌─────────────────▼──────────────────┐ + │ LangGraph Cloud │ + │ cockpit-dev assistant │ + │ (graph "chat" newly added) │ + └────────────────────────────────────┘ +``` + +The shape mirrors `examples.cacheplane.ai`. Two surfaces differ: + +1. The deployment is an independent Vercel project, not folded into `cockpit-examples`. URL is `demo.cacheplane.ai`, not `examples.cacheplane.ai/canonical/chat`. +2. The proxy module is refactored out of `scripts/examples-middleware.ts` into `scripts/langgraph-proxy.ts` so both deployments import the same handler. `examples-middleware.ts` keeps its examples-specific `Referer`-based routing as a wrapper; `demo-middleware.ts` uses defaults. + +## Decisions locked + +| Decision | Choice | +|---|---| +| Subdomain | `demo.cacheplane.ai` | +| Vercel project | New, independent (`demo`) | +| Backend | Existing shared `cockpit-dev` LangGraph Cloud assistant | +| Backend graph | `chat` (the existing name in `examples/chat/python/langgraph.json`) | +| Proxy file location | `scripts/langgraph-proxy.ts` (extracted, reusable) | +| Proxy runtime | Node (`nodejs20.x`) — matches existing examples pattern | +| Proxy auth posture (MVP) | Anonymous, no thread-ownership enforcement; rely on UUIDv4 unguessability + rate limiting | +| Hero screenshot | Capture new canonical-demo screenshot, swap from cockpit-code shot | +| CTA disambiguation | Hero "Try the demo →" → `demo.cacheplane.ai`; FinalCTA "See each feature in action →" → `cockpit.cacheplane.ai` | +| Hardening moves (HTTP-only cookie / signed thread IDs / thread expiry) | Out of scope for MVP; logged as future work | + +## Phasing + +Five PRs. Each is independently shippable except Phase 2 (depends on Phase 1). + +### Phase 1 — backend graph addition + +Add `examples/chat/python` to the aggregated `cockpit-dev` deployment. + +**Files touched:** +- `apps/cockpit/scripts/capability-registry.ts` (add a new product type `chat-canonical` OR extend the generator to special-case examples/chat outside the registry — the implementation plan picks one). +- Possibly `scripts/generate-shared-deployment-config.ts` if the registry's `product` filter needs broadening. +- `scripts/verify-shared-deployment.ts` (production smoke) — assert `chat` is discoverable. + +**Trigger:** `.github/workflows/deploy-langgraph.yml` runs automatically because `cockpit/**/python/**` is one of its watched paths. We need to either include `examples/chat/python/**` in the watched paths or rely on a forced rerun on first ship. + +**Visible effect:** None. The `chat` graph appears in the cockpit-dev assistant. No frontend consumes it yet. + +### Phase 2 — Vercel project for `demo.cacheplane.ai` + +Stand up the public deployment. + +**Creates:** +- `scripts/langgraph-proxy.ts` — extracted shared handler. Exports `createProxyHandler(config: ProxyConfig)`. Identical request/response behavior to the existing `examples-middleware.ts`, parameterized. +- `scripts/demo-middleware.ts` — 5-line wrapper that calls `createProxyHandler({})` with defaults. +- `scripts/assemble-demo.ts` — builds `examples/chat/angular`, copies into `deploy/demo/`, writes `.vercel/output/config.json` and the function bundle. Mirrors `assemble-examples.ts` structure but single-app. +- `vercel.demo.json` — Vercel project config (`framework: null`, `outputDirectory: deploy/demo`, rewrites for SPA fallback). +- `examples/chat/angular/src/environments/environment.ts` — production: `langGraphApiUrl: '/api'`, `assistantId: 'chat'`. +- `examples/chat/angular/src/environments/environment.development.ts` — dev: `langGraphApiUrl: 'http://localhost:2024'`, `assistantId: 'chat'`. +- `examples/chat/angular/project.json` `fileReplacements` entry under `production` config. + +**Modifies:** +- `scripts/examples-middleware.ts` → thin wrapper around `createProxyHandler({ resolveBackend: resolveBackendFromReferer })`. Behavior unchanged. +- `examples/chat/angular/src/app/shell/threads.service.ts:6` — replace `const API_URL = 'http://localhost:2024';` with `import { environment } from '../../environments/environment'; const API_URL = environment.langGraphApiUrl;`. +- `examples/chat/angular/src/app/shell/demo-shell.component.ts:330-331` — same replacement, plus `assistantId: environment.assistantId`. +- `.github/workflows/ci.yml` — add a new deploy step mirroring the existing "Deploy Angular examples" step, gated on changes to `examples/chat/**`, `vercel.demo.json`, or `scripts/assemble-demo.ts`. Adds a `production-smoke` URL `EXAMPLES_DEMO_URL=https://demo.cacheplane.ai`. + +**External setup (one-time, NOT in code):** +- Create Vercel project `demo` in the Vercel UI, link to this repo, framework `Other`. +- Add custom domain `demo.cacheplane.ai`. +- Set Vercel env var `LANGSMITH_API_KEY` (same value as in CI secrets) on the project. Scope: Production + Preview. +- Add `VERCEL_DEMO_PROJECT_ID` to the GitHub repo secrets. +- DNS: CNAME `demo` → Vercel's edge (registrar action). + +**Visible effect:** `demo.cacheplane.ai` is live and serves the canonical demo. Anonymous users can chat. No rate limit yet (Phase 3). + +### Phase 3 — rate limiting + +Apply per-IP rate limit at the proxy. + +**Creates / modifies:** +- `scripts/langgraph-proxy.ts` — accept an `Upstash`-compatible rate limiter in the config. If `process.env.UPSTASH_REDIS_REST_URL` is set, instantiate `@upstash/ratelimit` and call `.limit(ip)` before forwarding. 429 + `Retry-After` header on exceeded. +- `scripts/langgraph-proxy.spec.ts` — tests for the rate-limit branch. +- `package.json` — add `@upstash/ratelimit` and `@upstash/redis`. + +**External setup:** +- Create Upstash Redis database (free tier). +- Add `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` to Vercel env vars on the `demo` project (NOT on the `cockpit-examples` project — that one stays unrate-limited unless a follow-up enables it there too). + +**Rate-limit policy (tunable env-driven):** +- Default: 30 requests per minute per IP (configurable via `RATE_LIMIT_PER_MIN`, default 30). +- Sliding window. Counts all `/api/*` requests including OPTIONS preflight (cheap to tune later). + +**Visible effect:** A scraper hitting the demo from one IP gets 429 after the 30th request. Normal users never notice. + +### Phase 4 — prompt-length cap + CORS allowlist + +Last hardening pass before marketing rewire. + +**Modifies:** +- `scripts/langgraph-proxy.ts`: + - Reject POST bodies > `MAX_PROMPT_BYTES` (default 16 KB; configurable via env). 413 response. + - Tighten the CORS `access-control-allow-origin` from `*` to a `ALLOWED_ORIGINS` allowlist (env: comma-separated). Echo the matching origin; deny others with 403 on preflight. +- `scripts/langgraph-proxy.spec.ts` — tests for both branches. + +**External setup:** +- Add `ALLOWED_ORIGINS` Vercel env on the `demo` project: `https://demo.cacheplane.ai`. + +**Visible effect:** A 100 KB prompt body gets 413. A request from `https://malicious.example` gets 403 on preflight. + +### Phase 5 — marketing rewire + +Update the marketing site to surface the new demo. + +**Modifies:** +- `apps/website/src/components/landing/Hero.tsx`: + - Primary CTA: "Try the demo →" → `https://demo.cacheplane.ai`. + - Frame chrome label and screenshot src updated. +- `apps/website/src/components/landing/FinalCTA.tsx`: + - Secondary CTA: "See each feature in action →" → `https://cockpit.cacheplane.ai`. (Already pointed there; relabel only.) +- `apps/website/public/screenshots/` — add new `canonical-demo-*.webp` screenshots (manual capture). + +**External setup:** +- One-time screenshot capture of `demo.cacheplane.ai` (desktop + mobile) into the `screenshots/` directory. + +**Visible effect:** Landing page now distinguishes the two CTAs and routes traffic appropriately. + +## Data flow (full request walkthrough) + +``` +1. User loads https://demo.cacheplane.ai + → Vercel serves examples/chat/angular static SPA. + +2. App boot: provideAgent({ apiUrl: '/api', assistantId: 'chat' }). + → All LangGraph SDK calls hit /api/* on the same origin. + +3. First call: POST /api/threads (creates a new thread). + → Vercel routes to /api/[[...path]] function. + → Proxy: extract IP (from x-forwarded-for), check rate limit (Phase 3). + → Proxy: validate origin against ALLOWED_ORIGINS (Phase 4). + → Proxy: validate body size against MAX_PROMPT_BYTES (Phase 4). + → Proxy: inject x-api-key: $LANGSMITH_API_KEY. + → fetch('https://cockpit-dev-...us.langgraph.app/threads', {...}). + → LangGraph Cloud creates thread, returns { thread_id: 'uuidv4', ... }. + → Proxy: stream response back. + +4. App stores thread_id in localStorage. + +5. User sends a message: POST /api/threads/{id}/runs/stream. + → Same path, but Content-Type: text/event-stream on response. + → Proxy detects SSE, pipes the upstream ReadableStream chunk-by-chunk. + +6. Reload: localStorage thread_id seeds the agent. History reappears. +``` + +## Error handling + +| Failure mode | Proxy behavior | +|---|---| +| `LANGSMITH_API_KEY` env var missing | 500 with `{ error: 'LANGSMITH_API_KEY not configured' }` (today's behavior; preserved). | +| Upstash unreachable | Log + fail-open (allow the request). Marketing demo must not 500 because a rate-limit dep is flaky. | +| Body too large | 413 with `{ error: 'Body too large', max: }`. | +| Origin not allowed | 403 on OPTIONS preflight, 403 on the actual request. | +| Upstream LangGraph 5xx | Stream the upstream response unchanged (status + body). The Angular shell already handles error responses. | +| Upstream timeout / fetch error | 502 with `{ error: 'Proxy error', message: ... }` (today's behavior). | +| Unguessable thread ID guessing | Out of scope — UUIDv4 entropy is the only mitigation in MVP. | + +## Testing strategy + +| Phase | Unit | Manual / Integration | +|---|---|---| +| 1 | None (config-only) | After deploy: hit `cockpit-dev/assistants/search` with `x-api-key`, confirm `chat` present. `verify-shared-deployment.ts` gets a chat row. | +| 2 | `scripts/langgraph-proxy.spec.ts` — header injection, path stripping, SSE streaming, error paths. Mock `fetch`. | Hit `demo.cacheplane.ai/api/_proxy_debug` post-deploy; confirm `hasApiKey: true`. Optional: Playwright e2e — submit a message, assert streamed response. | +| 3 | Rate-limit branch — mock Upstash, assert 429 + `Retry-After`. | Hammer the deploy from one IP; expect 429 after threshold. | +| 4 | Body-size + CORS branches. | curl from a disallowed origin → 403; curl with 100 KB body → 413. | +| 5 | Snapshot/assertion that Hero / FinalCTA hrefs and labels match. | Visual smoke. | + +## File-level change map + +``` +.github/workflows/ + ci.yml (Phase 2: add demo deploy step) + +scripts/ + langgraph-proxy.ts (Phase 2: NEW — shared handler) + langgraph-proxy.spec.ts (Phase 2: NEW — unit tests) + demo-middleware.ts (Phase 2: NEW — 5-line wrapper) + examples-middleware.ts (Phase 2: refactor to wrapper) + assemble-demo.ts (Phase 2: NEW) + verify-shared-deployment.ts (Phase 1: add chat assertion) + +vercel.demo.json (Phase 2: NEW) + +examples/chat/angular/ + src/environments/environment.ts (Phase 2: NEW — production) + src/environments/environment.development.ts (Phase 2: NEW — dev) + src/app/shell/threads.service.ts (Phase 2: replace hardcoded URL) + src/app/shell/demo-shell.component.ts (Phase 2: replace hardcoded URL + assistantId) + project.json (Phase 2: fileReplacements) + +apps/cockpit/scripts/capability-registry.ts (Phase 1: maybe extend) +scripts/generate-shared-deployment-config.ts (Phase 1: maybe broaden filter) + +apps/website/src/components/landing/ + Hero.tsx (Phase 5: CTA + screenshot) + FinalCTA.tsx (Phase 5: secondary CTA label) +apps/website/public/screenshots/ + canonical-demo-*.webp (Phase 5: NEW) + +package.json (Phase 3: @upstash/* deps) +``` + +## Out of scope + +- Auth / sign-in for the demo. +- Conversation persistence across browsers / devices. +- Multi-tenant API key rotation. +- Cost cap / kill-switch beyond rate limit + body cap. +- Analytics on demo conversations beyond what the marketing site already emits. +- Anything beyond `examples/chat` — `examples/chat-agent` left alone. +- Hardening moves (HTTP-only cookie, signed thread IDs, server-side thread expiry) — logged below. + +## Future work (post-MVP) + +- **HTTP-only cookie for thread ID** instead of localStorage. Eliminates trivial devtools extraction. ~20 lines. +- **Signed thread IDs** (HMAC). Makes IDs non-portable across browsers / clients. ~50 lines. +- **Server-side thread expiry** via Vercel cron. Bounds long-term storage cost + leak window. ~30 lines. +- **Daily cost cap / kill-switch** — global daily budget for the proxy; if exceeded, return 503 until next day. Only needed if the rate limit proves insufficient. +- **Folding cockpit-examples proxy onto the shared module** — currently `examples-middleware.ts` keeps its own routing wrapper. Already importing `createProxyHandler`, but the assemble-examples build still produces a separate function bundle. Could in theory be unified at Phase 2 boundary; left as a follow-up to keep the migration risk low. + +## References + +- `scripts/examples-middleware.ts` — the existing Node serverless proxy this design refactors. +- `scripts/assemble-examples.ts` — the existing assemble pipeline this design mirrors for the demo. +- `scripts/generate-shared-deployment-config.ts` — generator for `deployments/shared-dev/langgraph.json`. +- `.github/workflows/deploy-langgraph.yml` — LangGraph Cloud deploy on push to main. +- `.github/workflows/ci.yml` (lines ~260–305) — examples Vercel deploy step pattern. +- `apps/cockpit/scripts/capability-registry.ts` — source of truth for cockpit capabilities. diff --git a/scripts/generate-shared-deployment-config.spec.ts b/scripts/generate-shared-deployment-config.spec.ts new file mode 100644 index 000000000..0e9c0d08c --- /dev/null +++ b/scripts/generate-shared-deployment-config.spec.ts @@ -0,0 +1,24 @@ +// scripts/generate-shared-deployment-config.spec.ts +// SPDX-License-Identifier: MIT +import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { describe, expect, it } from 'vitest'; + +describe('generate-shared-deployment-config', () => { + it('includes the canonical-demo chat graph in the aggregated manifest', () => { + const root = resolve(__dirname, '..'); + execSync('npx tsx scripts/generate-shared-deployment-config.ts', { + cwd: root, + stdio: 'pipe', + }); + const manifestPath = resolve(root, 'deployments/shared-dev/langgraph.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { + graphs: Record; + dependencies: string[]; + }; + expect(manifest.graphs).toHaveProperty('chat'); + expect(manifest.graphs.chat).toMatch(/examples-chat\/.+\.py:graph$/); + expect(manifest.dependencies.some((d) => d.includes('examples-chat'))).toBe(true); + }); +}); diff --git a/scripts/generate-shared-deployment-config.ts b/scripts/generate-shared-deployment-config.ts index 0527b7bca..858f4803c 100644 --- a/scripts/generate-shared-deployment-config.ts +++ b/scripts/generate-shared-deployment-config.ts @@ -67,6 +67,25 @@ for (const capability of capabilities) { } } +// Extra Python deployments NOT in the cockpit capability registry. +// These have no Angular project / port — only a backend graph aggregated +// into the shared cockpit-dev assistant. +const extraPythonDeployments: ReadonlyArray<{ pythonDir: string; alias: string }> = [ + { pythonDir: 'examples/chat/python', alias: 'examples-chat' }, +]; + +for (const extra of extraPythonDeployments) { + const manifestPath = resolve(rootDir, extra.pythonDir, 'langgraph.json'); + const extraManifest = readManifest(manifestPath); + if (!extraManifest.graphs) { + throw new Error(`Missing graphs in ${manifestPath}`); + } + const stagedDependencyRoot = stageDependency(extra.pythonDir, extra.alias); + for (const [graphName, entrypoint] of Object.entries(extraManifest.graphs)) { + addGraph(graphName, toDeploymentPath(stagedDependencyRoot, entrypoint)); + } +} + const streamingManifestPath = resolve(rootDir, 'cockpit/langgraph/streaming/python/langgraph.json'); const streamingManifest = readManifest(streamingManifestPath); if (!streamingManifest.graphs) { diff --git a/scripts/verify-shared-deployment.ts b/scripts/verify-shared-deployment.ts index 1d8b2e954..83047b15e 100644 --- a/scripts/verify-shared-deployment.ts +++ b/scripts/verify-shared-deployment.ts @@ -25,6 +25,7 @@ const SMOKE_ASSISTANT_IDS = [ 'filesystem', 'c-generative-ui', 'c-a2ui', + 'chat', ] as const; const DEPLOYMENT_URLS_PATH = resolve(__dirname, '../deployment-urls.json');