From fb45e86bc26fc34454115b179c783c0af7a02e54 Mon Sep 17 00:00:00 2001 From: pietroid Date: Fri, 23 Oct 2026 16:20:54 -0300 Subject: [PATCH 1/5] adding authentication and individual sessions --- .../apps/calaverita_app/lib/app/app.dart | 1 + .../calaverita_app/lib/app/app_config.dart | 29 +++++++++++++++++++ .../apps/calaverita_app/lib/app/app.dart | 1 + .../calaverita_app/lib/app/app_config.dart | 29 +++++++++++++++++++ .../apps/calaverita_app/lib/app/app.dart | 1 + .../calaverita_app/lib/app/app_config.dart | 29 +++++++++++++++++++ 03-custom-server/server/src/index.ts | 2 +- README.md | 20 ++++++++++++- runtime/.env.example | 4 +++ runtime/server.ts | 26 +++++++++++++++-- tool/setup.mjs | 10 +++++-- 11 files changed, 146 insertions(+), 6 deletions(-) diff --git a/01-shared-state/apps/calaverita_app/lib/app/app.dart b/01-shared-state/apps/calaverita_app/lib/app/app.dart index 7a00fc6..b1f379e 100644 --- a/01-shared-state/apps/calaverita_app/lib/app/app.dart +++ b/01-shared-state/apps/calaverita_app/lib/app/app.dart @@ -22,6 +22,7 @@ class App extends StatelessWidget { Widget build(BuildContext context) { return CopilotKit( runtimeUrl: AppConfig.runtimeUrl, + headers: AppConfig.headers, child: MaterialApp( title: 'Calaverita Generator', debugShowCheckedModeBanner: false, diff --git a/01-shared-state/apps/calaverita_app/lib/app/app_config.dart b/01-shared-state/apps/calaverita_app/lib/app/app_config.dart index a202800..cbe99d5 100644 --- a/01-shared-state/apps/calaverita_app/lib/app/app_config.dart +++ b/01-shared-state/apps/calaverita_app/lib/app/app_config.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + /// The one place that says where the agent lives. /// /// To use the shared hosted backend, change [defaultRuntimeUrl] (or run with @@ -24,4 +26,31 @@ class AppConfig { 'AGENT_ID', defaultValue: 'default', ); + + /// The password of the shared backend, when it asks for one. + /// + /// Run with `--dart-define=WORKSHOP_CODE=`. A local runtime asks for + /// nothing, so leave it out there. + static const workshopCode = String.fromEnvironment('WORKSHOP_CODE'); + + /// A random id for this run of the app, so a server can tell sessions apart. + static final String sessionId = _randomId(); + + /// The headers sent with every run. + /// + /// Custom names on purpose: a runtime answers the browser's preflight with + /// `Access-Control-Allow-Headers: *`, which covers them but not + /// `Authorization`. A server that does not know them ignores them. + static Future> headers() async => { + 'X-Workshop-Session': sessionId, + if (workshopCode.isNotEmpty) 'X-Workshop-Code': workshopCode, + }; + + static String _randomId() { + final random = Random.secure(); + return List.generate( + 16, + (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join(); + } } diff --git a/02-generative-ui/apps/calaverita_app/lib/app/app.dart b/02-generative-ui/apps/calaverita_app/lib/app/app.dart index 09733c8..f073c20 100644 --- a/02-generative-ui/apps/calaverita_app/lib/app/app.dart +++ b/02-generative-ui/apps/calaverita_app/lib/app/app.dart @@ -22,6 +22,7 @@ class App extends StatelessWidget { Widget build(BuildContext context) { return CopilotKit( runtimeUrl: AppConfig.runtimeUrl, + headers: AppConfig.headers, child: MaterialApp( title: 'Calaverita Generator', debugShowCheckedModeBanner: false, diff --git a/02-generative-ui/apps/calaverita_app/lib/app/app_config.dart b/02-generative-ui/apps/calaverita_app/lib/app/app_config.dart index a202800..cbe99d5 100644 --- a/02-generative-ui/apps/calaverita_app/lib/app/app_config.dart +++ b/02-generative-ui/apps/calaverita_app/lib/app/app_config.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + /// The one place that says where the agent lives. /// /// To use the shared hosted backend, change [defaultRuntimeUrl] (or run with @@ -24,4 +26,31 @@ class AppConfig { 'AGENT_ID', defaultValue: 'default', ); + + /// The password of the shared backend, when it asks for one. + /// + /// Run with `--dart-define=WORKSHOP_CODE=`. A local runtime asks for + /// nothing, so leave it out there. + static const workshopCode = String.fromEnvironment('WORKSHOP_CODE'); + + /// A random id for this run of the app, so a server can tell sessions apart. + static final String sessionId = _randomId(); + + /// The headers sent with every run. + /// + /// Custom names on purpose: a runtime answers the browser's preflight with + /// `Access-Control-Allow-Headers: *`, which covers them but not + /// `Authorization`. A server that does not know them ignores them. + static Future> headers() async => { + 'X-Workshop-Session': sessionId, + if (workshopCode.isNotEmpty) 'X-Workshop-Code': workshopCode, + }; + + static String _randomId() { + final random = Random.secure(); + return List.generate( + 16, + (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join(); + } } diff --git a/03-custom-server/apps/calaverita_app/lib/app/app.dart b/03-custom-server/apps/calaverita_app/lib/app/app.dart index 09733c8..f073c20 100644 --- a/03-custom-server/apps/calaverita_app/lib/app/app.dart +++ b/03-custom-server/apps/calaverita_app/lib/app/app.dart @@ -22,6 +22,7 @@ class App extends StatelessWidget { Widget build(BuildContext context) { return CopilotKit( runtimeUrl: AppConfig.runtimeUrl, + headers: AppConfig.headers, child: MaterialApp( title: 'Calaverita Generator', debugShowCheckedModeBanner: false, diff --git a/03-custom-server/apps/calaverita_app/lib/app/app_config.dart b/03-custom-server/apps/calaverita_app/lib/app/app_config.dart index 7823881..18b76a2 100644 --- a/03-custom-server/apps/calaverita_app/lib/app/app_config.dart +++ b/03-custom-server/apps/calaverita_app/lib/app/app_config.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + /// The one place that says where the agent lives. /// /// To use a hosted server, change [defaultRuntimeUrl] (or run with @@ -24,4 +26,31 @@ class AppConfig { 'AGENT_ID', defaultValue: 'calaverita', ); + + /// The password of a hosted server, when it asks for one. + /// + /// Run with `--dart-define=WORKSHOP_CODE=`. A local server asks for + /// nothing, so leave it out there. + static const workshopCode = String.fromEnvironment('WORKSHOP_CODE'); + + /// A random id for this run of the app, so a server can tell sessions apart. + static final String sessionId = _randomId(); + + /// The headers sent with every run. + /// + /// Custom names on purpose: a runtime answers the browser's preflight with + /// `Access-Control-Allow-Headers: *`, which covers them but not + /// `Authorization`. A server that does not know them ignores them. + static Future> headers() async => { + 'X-Workshop-Session': sessionId, + if (workshopCode.isNotEmpty) 'X-Workshop-Code': workshopCode, + }; + + static String _randomId() { + final random = Random.secure(); + return List.generate( + 16, + (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join(); + } } diff --git a/03-custom-server/server/src/index.ts b/03-custom-server/server/src/index.ts index 865afe8..f3c073a 100644 --- a/03-custom-server/server/src/index.ts +++ b/03-custom-server/server/src/index.ts @@ -17,7 +17,7 @@ const runtime = new CopilotRuntime({ }); const app = new Hono(); -app.use("*", cors({ origin: "*", allowHeaders: ["authorization", "content-type"] })); +app.use("*", cors({ origin: "*", allowHeaders: ["authorization", "content-type", "x-workshop-code", "x-workshop-session"] })); app.get("/health", (c) => c.text("ok")); app.route("/", createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit" })); diff --git a/README.md b/README.md index c075495..bac856b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ node tool/setup.mjs It installs Flutter 3.47.0 through FVM, checks your machine, asks which step to run and which backend to use, and starts the app in Chrome. The backend is one of: -- **The shared backend:** nothing to set up. +- **The shared backend:** nothing to set up except the workshop password, which + you get at the workshop. It asks for it (hidden as you type), or pass + `--code `. - **Your own runtime:** it asks for your Anthropic API key (hidden as you type) and starts the runtime in `runtime/` for you. - **A custom backend:** any CopilotKit runtime you have a URL for, for example one @@ -187,6 +189,20 @@ The runtime URL is set in **one place per snapshot**: `AppConfig.defaultRuntimeUrl`, or run with `--dart-define=RUNTIME_URL=`. +The shared backend also asks for a password. The app sends it, and a random id +of the run, as the `X-Workshop-Code` and `X-Workshop-Session` headers on every +request: + +```sh +fvm flutter run -d chrome \ + --dart-define=RUNTIME_URL= --dart-define=WORKSHOP_CODE= +``` + +A runtime on your machine ignores both headers, so leave `WORKSHOP_CODE` out +there. To host the shared backend, deploy `runtime/` and set `WORKSHOP_CODE` and +`ANTHROPIC_API_KEY` on the host. The runtime then answers `401` without the +password. `GET /info` and the CORS preflight stay open. + ## How the code is organized Each step is a Dart workspace of small packages: @@ -225,6 +241,8 @@ packages. - **The runtime starts but every message fails**: the model or the key is wrong. Send a run straight to the runtime and read the `RUN_ERROR` event (see the [SDK example README](https://github.com/VGVentures/copilotkit_flutter_sdk/blob/main/example/README.md#troubleshooting)). +- **"The copilot could not answer" on the shared backend**: the password is + missing or wrong (`401`). - **`pub get` can't fetch the SDK**: you need access to the SDK repository. ## License diff --git a/runtime/.env.example b/runtime/.env.example index 26df643..ae75ec6 100644 --- a/runtime/.env.example +++ b/runtime/.env.example @@ -1,3 +1,7 @@ # Copy this file to .env (git-ignored) and fill in your key. COPILOTKIT_MODEL=anthropic:claude-sonnet-5-5 ANTHROPIC_API_KEY= + +# Only for a shared, hosted runtime: the password the participants send. Leave it +# out on your machine. +# WORKSHOP_CODE= diff --git a/runtime/server.ts b/runtime/server.ts index 208cae1..dec4b3b 100644 --- a/runtime/server.ts +++ b/runtime/server.ts @@ -1,7 +1,8 @@ // Copilot Runtime for the Calaverita Generator workshop, based on the // CopilotKit Flutter SDK's example runtime. Start it with `npm start`. import "./env.js"; -import { createServer } from "node:http"; +import { timingSafeEqual } from "node:crypto"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { networkInterfaces } from "node:os"; import { BuiltInAgent, CopilotRuntime } from "@copilotkit/runtime/v2"; import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node"; @@ -57,7 +58,28 @@ function lanAddresses(): string[] { .map((net) => net!.address); } -createServer(createCopilotNodeListener({ runtime, basePath, cors: true })).listen( +const copilot = createCopilotNodeListener({ runtime, basePath, cors: true }); + +// The shared deployment sets WORKSHOP_CODE; a runtime on your machine leaves it +// out and asks for nothing. The app sends the code as X-Workshop-Code (and a +// random id of its run as X-Workshop-Session, which this server ignores). `/info` +// and the CORS preflight stay open, so `tool/doctor.mjs` and the browser work +// without the code. +const workshopCode = process.env.WORKSHOP_CODE; + +function guarded(req: IncomingMessage, res: ServerResponse) { + const path = (req.url ?? "").split("?")[0]; + if (!workshopCode || req.method === "OPTIONS" || path.endsWith("/info")) return copilot(req, res); + + const given = Buffer.from(String(req.headers["x-workshop-code"] ?? "")); + const wanted = Buffer.from(workshopCode); + if (given.length === wanted.length && timingSafeEqual(given, wanted)) return copilot(req, res); + + res.writeHead(401, { "content-type": "application/json", "access-control-allow-origin": "*" }); + res.end(JSON.stringify({ error: "Missing or wrong workshop code." })); +} + +createServer(guarded).listen( port, "0.0.0.0", () => { diff --git a/tool/setup.mjs b/tool/setup.mjs index 8d4f570..bdd8015 100644 --- a/tool/setup.mjs +++ b/tool/setup.mjs @@ -14,6 +14,7 @@ // --backend which backend to use (step 03 always runs its own server) // --url the runtime URL, for a custom backend // --agent the agent id, when the backend has more than one +// --code the workshop password of the shared backend (or set WORKSHOP_CODE) // --no-run set everything up, then print the commands instead of running them // --no-app start the backend and keep it running, without the app // --yes never ask, use the defaults and the options given @@ -166,6 +167,8 @@ const stepDir = `${root}${step.dir}`; /** The backend the app talks to: its URL and agent id, and the folder to start if it is ours. */ let backend = null; +/** The password of the shared backend. The app sends it as a header; a runtime on your machine asks for none. */ +let workshopCode = ""; /** Makes sure `dir` has the model key in its .env, asking for it when it is missing. */ async function ensureKey(dir) { @@ -231,6 +234,7 @@ if (stepKey !== "00" && step.server) { if (kind === "shared") { if (!config.sharedBackendUrl) fail("The shared backend is not configured yet.", "Use --backend local, or --backend custom --url ."); await useExternal(config.sharedBackendUrl, config.sharedAgentId ?? "default"); + workshopCode = option("code") ?? process.env.WORKSHOP_CODE ?? (await askSecret("Workshop password (nothing is shown as you type, empty if there is none):")) ?? ""; } else if (kind === "custom") { say("\nTo make a backend with CopilotKit's tool, run this in another terminal and follow its steps:"); hint("npx copilotkit with-claude my-backend"); @@ -249,8 +253,10 @@ heading(`${stepKey === "00" ? "2" : "3"}. Getting the app ready`); if (!runVisible("fvm", ["flutter", "pub", "get"], stepDir)) fail("`flutter pub get` failed.", "If it can't fetch the CopilotKit SDK, check that you can reach github.com."); const appDir = `${stepDir}/apps/calaverita_app`; -const appArgs = ["flutter", "run", "-d", "chrome", ...(backend ? [`--dart-define=RUNTIME_URL=${backend.url}`, `--dart-define=AGENT_ID=${backend.agent}`] : [])]; +const appArgs = ["flutter", "run", "-d", "chrome", ...(backend ? [`--dart-define=RUNTIME_URL=${backend.url}`, `--dart-define=AGENT_ID=${backend.agent}`, ...(workshopCode ? [`--dart-define=WORKSHOP_CODE=${workshopCode}`] : [])] : [])]; const appCommand = `fvm ${appArgs.join(" ")}`; +/** The same command for the screen: the password is never printed. */ +const shownCommand = workshopCode ? appCommand.replace(workshopCode, "") : appCommand; /** Starts the backend of ours in the background and waits until it answers. Returns a function that stops it. */ async function startBackend() { @@ -289,7 +295,7 @@ async function startBackend() { if (flag("no-run")) { heading("Ready. To run it yourself:"); if (backend?.folder) say(` 1. In one terminal: cd ${backend.folder.replace(root, "")} && npm start`); - say(` ${backend?.folder ? "2. In another:" : "Run:"} cd ${appDir.replace(root, "")} && ${appCommand}`); + say(` ${backend?.folder ? "2. In another:" : "Run:"} cd ${appDir.replace(root, "")} && ${shownCommand}`); process.exit(0); } From 23427cfb71470d2044e4bf32e1c5b4e666f719df Mon Sep 17 00:00:00 2001 From: pietroid Date: Fri, 23 Oct 2026 17:01:40 -0300 Subject: [PATCH 2/5] feat: deploy the shared backend to Cloud Run with GitHub Actions - runtime/Dockerfile and ignore files: the image holds only server.ts and env.ts, and a source deploy uploads nothing else (no .env) - deploy_runtime workflow: builds the image, deploys to Cloud Run through Workload Identity and checks that the password gate is on - runtime_ci workflow: typechecks the runtime and builds the image on pull requests - runtime/deploy: one-time Google Cloud setup script and the steps - point tool/workshop.config.json at the deployed backend --- .github/workflows/deploy_runtime.yaml | 88 ++++++++++++++++++++++++++ .github/workflows/runtime_ci.yaml | 33 ++++++++++ runtime/.dockerignore | 6 ++ runtime/.gcloudignore | 8 +++ runtime/Dockerfile | 16 +++++ runtime/deploy/README.md | 87 +++++++++++++++++++++++++ runtime/deploy/setup_gcp.sh | 91 +++++++++++++++++++++++++++ runtime/package.json | 3 +- tool/workshop.config.json | 2 +- 9 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy_runtime.yaml create mode 100644 .github/workflows/runtime_ci.yaml create mode 100644 runtime/.dockerignore create mode 100644 runtime/.gcloudignore create mode 100644 runtime/Dockerfile create mode 100644 runtime/deploy/README.md create mode 100755 runtime/deploy/setup_gcp.sh diff --git a/.github/workflows/deploy_runtime.yaml b/.github/workflows/deploy_runtime.yaml new file mode 100644 index 0000000..92e6484 --- /dev/null +++ b/.github/workflows/deploy_runtime.yaml @@ -0,0 +1,88 @@ +# Deploys the shared backend (runtime/) to Cloud Run on every change to it on main. +# One-time setup of Google Cloud and of the GitHub secrets: runtime/deploy/README.md +name: deploy_runtime + +on: + push: + branches: [main] + paths: + - "runtime/**" + - ".github/workflows/deploy_runtime.yaml" + workflow_dispatch: + +# Two deploys at once would race on the same service. +concurrency: + group: deploy_runtime + cancel-in-progress: false + +permissions: + contents: read + id-token: write # lets the job sign in to Google Cloud without a key file + +env: + PROJECT_ID: gen-lang-client-0563181971 + REGION: us-central1 + SERVICE: calaverita-runtime + REGISTRY_REPOSITORY: calaverita + RUNTIME_SERVICE_ACCOUNT: calaverita-runtime@gen-lang-client-0563181971.iam.gserviceaccount.com + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Sign in to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + token_format: access_token + + - name: Sign in to Artifact Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.auth.outputs.access_token }} + + - name: Build and push the image + id: image + env: + IMAGE: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REGISTRY_REPOSITORY }}/runtime:${{ github.sha }} + run: | + docker build --tag "$IMAGE" runtime + docker push "$IMAGE" + echo "name=$IMAGE" >> "$GITHUB_OUTPUT" + + # The key and the password are repository secrets. GitHub masks them in the + # logs; Cloud Run keeps them as environment variables of the revision. + - name: Deploy to Cloud Run + id: deploy + uses: google-github-actions/deploy-cloudrun@v2 + with: + project_id: ${{ env.PROJECT_ID }} + region: ${{ env.REGION }} + service: ${{ env.SERVICE }} + image: ${{ steps.image.outputs.name }} + env_vars: | + ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} + WORKSHOP_CODE=${{ secrets.WORKSHOP_CODE }} + flags: >- + --service-account=${{ env.RUNTIME_SERVICE_ACCOUNT }} + --allow-unauthenticated + --max-instances=1 + --memory=512Mi + --timeout=900 + + # The password gate is the point of this deployment, so check it is on. + - name: Smoke test + env: + BASE: ${{ steps.deploy.outputs.url }}/api/copilotkit + run: | + curl --fail --silent --show-error --retry 5 --retry-delay 5 --retry-all-errors "$BASE/info" | jq -e '.agents.default' + status=$(curl --silent --output /dev/null --write-out '%{http_code}' --request POST "$BASE/agent/default/run" --data '{}') + test "$status" = 401 || { echo "Expected 401 without the workshop code, got $status"; exit 1; } + echo "Deployed: $BASE" diff --git a/.github/workflows/runtime_ci.yaml b/.github/workflows/runtime_ci.yaml new file mode 100644 index 0000000..9536469 --- /dev/null +++ b/.github/workflows/runtime_ci.yaml @@ -0,0 +1,33 @@ +# Checks a change to the shared backend before it can reach main. +name: runtime_ci + +on: + pull_request: + paths: + - "runtime/**" + - ".github/workflows/runtime_ci.yaml" + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: runtime/package-lock.json + + - name: Typecheck + working-directory: runtime + run: | + npm ci --no-audit --no-fund + npm run typecheck + + - name: Build the image + run: docker build --tag calaverita-runtime runtime diff --git a/runtime/.dockerignore b/runtime/.dockerignore new file mode 100644 index 0000000..dcbfcff --- /dev/null +++ b/runtime/.dockerignore @@ -0,0 +1,6 @@ +# The key lives on the host, never in an image. +.env +node_modules +deploy +Dockerfile +.dockerignore diff --git a/runtime/.gcloudignore b/runtime/.gcloudignore new file mode 100644 index 0000000..7d84e75 --- /dev/null +++ b/runtime/.gcloudignore @@ -0,0 +1,8 @@ +# What `gcloud run deploy --source runtime` uploads. The key lives on the host, +# never in the upload, and the image installs its own node_modules. +* +!Dockerfile +!package.json +!package-lock.json +!server.ts +!env.ts diff --git a/runtime/Dockerfile b/runtime/Dockerfile new file mode 100644 index 0000000..e116d35 --- /dev/null +++ b/runtime/Dockerfile @@ -0,0 +1,16 @@ +# The image of the shared backend. Build it from this folder: +# docker build -t calaverita-runtime runtime +FROM node:22-slim + +WORKDIR /app +ENV NODE_ENV=production + +# `npm start` runs the TypeScript with tsx, a dev dependency, so install all of them. +COPY package.json package-lock.json ./ +RUN npm ci --include=dev --no-audit --no-fund + +COPY server.ts env.ts ./ + +USER node +# Cloud Run sets PORT; server.ts reads it and listens on 0.0.0.0. +CMD ["npm", "start", "--silent"] diff --git a/runtime/deploy/README.md b/runtime/deploy/README.md new file mode 100644 index 0000000..316cbf8 --- /dev/null +++ b/runtime/deploy/README.md @@ -0,0 +1,87 @@ +# Deploying the shared backend + +The shared backend is `runtime/` on Cloud Run, in the Google Cloud project +`gen-lang-client-0563181971`. GitHub Actions deploys it on every change to +`runtime/` on `main` (`.github/workflows/deploy_runtime.yaml`). Pull requests only +typecheck it and build the image (`.github/workflows/runtime_ci.yaml`). + +``` +push to main ──> GitHub Actions ──(Workload Identity, no key file)──> Google Cloud + │ build image ─> Artifact Registry ─> Cloud Run + │ ▲ ANTHROPIC_API_KEY, WORKSHOP_CODE + │ │ (GitHub repository secrets) + └ smoke test: /info is open, a run without the code is 401 +``` + +## Where each value lives + +| Value | Lives in | Why | +| --- | --- | --- | +| `ANTHROPIC_API_KEY` | GitHub secret | The workflow passes it to Cloud Run as an environment variable. | +| `WORKSHOP_CODE` | GitHub secret | Same. Use letters and digits only: the value is passed in a comma-separated list. | +| `GCP_WORKLOAD_IDENTITY_PROVIDER` | GitHub secret | Tells the job which identity provider to sign in with. | +| `GCP_SERVICE_ACCOUNT` | GitHub secret | The account the job acts as. | + +GitHub masks the secrets in the logs and they are never in the image. Cloud Run +shows environment variables in the revision's settings, though, so anyone with +view access to the Google Cloud project can read them there. Keep that access +small, or move the two values to Secret Manager if it ever grows. + +No Google key file exists anywhere. The provider only accepts tokens from this +repository on `main`. + +## Steps + +**1. Sign in** with an account that owns the project: + +```sh +gcloud auth login +``` + +**2. Run the setup once.** It enables the APIs, creates the registry, the two +service accounts and the Workload Identity provider, and it is safe to run +again. It creates no secret and asks for nothing: + +```sh +bash runtime/deploy/setup_gcp.sh +``` + +**3. Add the four GitHub secrets**, in the repository's *Settings → Secrets and +variables → Actions*, or with the GitHub CLI (`gh auth login` first). The script +prints the first two values. The last two commands ask for the value and hide it: + +```sh +gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER --repo VeryGoodOpenSource/copilotkit_flutter_workshop +gh secret set GCP_SERVICE_ACCOUNT --repo VeryGoodOpenSource/copilotkit_flutter_workshop +gh secret set ANTHROPIC_API_KEY --repo VeryGoodOpenSource/copilotkit_flutter_workshop +gh secret set WORKSHOP_CODE --repo VeryGoodOpenSource/copilotkit_flutter_workshop +``` + +For a password, `openssl rand -hex 5` gives ten letters and digits. + +**4. Deploy.** Merge to `main`, or run it by hand: *Actions → deploy_runtime → +Run workflow* (`gh workflow run deploy_runtime.yaml`). The last step of the job +prints the URL and fails if the password gate is not on. + +**5. Point the workshop at it.** Set `sharedBackendUrl` in +`tool/workshop.config.json` to the URL ending in `/api/copilotkit`, and check it: + +```sh +node tool/doctor.mjs --only backend --backend +``` + +**6. Hand out the password** at the workshop. Participants give it to +`node tool/setup.mjs --code ` (or type it when asked). + +## Operating it + +- **Rotate the password or the key:** `gh secret set WORKSHOP_CODE` (or + `ANTHROPIC_API_KEY`), then run the workflow again so a new revision picks it up. +- **Turn it off after the workshop:** `gcloud run services delete calaverita-runtime --region us-central1`, + or revoke the Anthropic key. The deployment costs nothing while idle. +- **One instance on purpose** (`--max-instances=1`): the runtime keeps threads in + memory, and a second instance would not know them. For a large room, raise the + limit only after moving the threads out of memory. +- **Cold starts:** the first message after a quiet period takes a few seconds. Add + `--min-instances=1` to the `flags` in the workflow during the workshop to avoid it. +- **Logs:** `gcloud run services logs read calaverita-runtime --region us-central1`. diff --git a/runtime/deploy/setup_gcp.sh b/runtime/deploy/setup_gcp.sh new file mode 100755 index 0000000..4138113 --- /dev/null +++ b/runtime/deploy/setup_gcp.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# One-time Google Cloud setup for the shared backend. Safe to run again. +# +# gcloud auth login +# bash runtime/deploy/setup_gcp.sh +# +# It creates no secret: the Anthropic key and the workshop password are GitHub +# repository secrets. See runtime/deploy/README.md for what each step is for. +set -euo pipefail + +PROJECT="${PROJECT:-gen-lang-client-0563181971}" +REGION="${REGION:-us-central1}" +GITHUB_REPO="${GITHUB_REPO:-VeryGoodOpenSource/copilotkit_flutter_workshop}" +SERVICE="calaverita-runtime" +REGISTRY="calaverita" +POOL="github" +PROVIDER="github-oidc" +DEPLOYER="github-deployer" +RUNTIME_SA="calaverita-runtime" + +gc() { gcloud --project "$PROJECT" "$@"; } +step() { printf '\n\033[1m%s\033[0m\n' "$*"; } + +step "1. Enabling the APIs" +gc services enable \ + run.googleapis.com artifactregistry.googleapis.com \ + iam.googleapis.com iamcredentials.googleapis.com sts.googleapis.com \ + cloudresourcemanager.googleapis.com + +PROJECT_NUMBER="$(gc projects describe "$PROJECT" --format='value(projectNumber)')" +DEPLOYER_EMAIL="$DEPLOYER@$PROJECT.iam.gserviceaccount.com" +RUNTIME_EMAIL="$RUNTIME_SA@$PROJECT.iam.gserviceaccount.com" + +step "2. Artifact Registry repository for the image" +gc artifacts repositories describe "$REGISTRY" --location "$REGION" >/dev/null 2>&1 \ + || gc artifacts repositories create "$REGISTRY" --repository-format docker --location "$REGION" \ + --description "Calaverita workshop runtime" + +step "3. Service accounts" +# The one the runtime runs as. It has no roles on purpose: the runtime only calls +# Anthropic, and the default account would carry the project's Editor role. +gc iam service-accounts describe "$RUNTIME_EMAIL" >/dev/null 2>&1 \ + || gc iam service-accounts create "$RUNTIME_SA" --display-name "Calaverita runtime" +# The one GitHub Actions deploys with. +gc iam service-accounts describe "$DEPLOYER_EMAIL" >/dev/null 2>&1 \ + || gc iam service-accounts create "$DEPLOYER" --display-name "GitHub deployer" + +step "4. What the deployer may do" +gc artifacts repositories add-iam-policy-binding "$REGISTRY" --location "$REGION" \ + --member "serviceAccount:$DEPLOYER_EMAIL" --role roles/artifactregistry.writer >/dev/null +# run.admin, not run.developer: the first deploy sets the service public. +gc projects add-iam-policy-binding "$PROJECT" \ + --member "serviceAccount:$DEPLOYER_EMAIL" --role roles/run.admin --condition=None >/dev/null +gc iam service-accounts add-iam-policy-binding "$RUNTIME_EMAIL" \ + --member "serviceAccount:$DEPLOYER_EMAIL" --role roles/iam.serviceAccountUser >/dev/null + +step "5. Workload Identity Federation: GitHub signs in without a key file" +gc iam workload-identity-pools describe "$POOL" --location global >/dev/null 2>&1 \ + || gc iam workload-identity-pools create "$POOL" --location global --display-name "GitHub Actions" +gc iam workload-identity-pools providers describe "$PROVIDER" --workload-identity-pool "$POOL" --location global >/dev/null 2>&1 \ + || gc iam workload-identity-pools providers create-oidc "$PROVIDER" \ + --workload-identity-pool "$POOL" --location global \ + --issuer-uri "https://token.actions.githubusercontent.com" \ + --attribute-mapping "google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref" \ + --attribute-condition "assertion.repository == '$GITHUB_REPO' && assertion.ref == 'refs/heads/main'" +gc iam service-accounts add-iam-policy-binding "$DEPLOYER_EMAIL" \ + --role roles/iam.workloadIdentityUser \ + --member "principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL/attribute.repository/$GITHUB_REPO" >/dev/null + +PROVIDER_NAME="$(gc iam workload-identity-pools providers describe "$PROVIDER" --workload-identity-pool "$POOL" --location global --format='value(name)')" + +step "Done. Add these four secrets to GitHub (Settings > Secrets and variables > Actions):" +cat < Date: Fri, 23 Oct 2026 17:10:57 -0300 Subject: [PATCH 3/5] refactor: name the model key CLAUDE_API_KEY everywhere - the runtime, the step 03 server, setup and doctor read CLAUDE_API_KEY and hand it to the Anthropic SDK as ANTHROPIC_API_KEY - the deploy workflow passes the CLAUDE_API_KEY repository secret to Cloud Run - update the examples, the README and the deploy steps --- .github/workflows/deploy_runtime.yaml | 2 +- 03-custom-server/server/.env.example | 2 +- 03-custom-server/server/src/index.ts | 6 ++++-- README.md | 6 +++--- runtime/.env.example | 2 +- runtime/deploy/README.md | 8 ++++---- runtime/deploy/setup_gcp.sh | 8 ++++---- runtime/env.ts | 4 +++- tool/doctor.mjs | 2 +- tool/setup.mjs | 2 +- 10 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy_runtime.yaml b/.github/workflows/deploy_runtime.yaml index 92e6484..800e551 100644 --- a/.github/workflows/deploy_runtime.yaml +++ b/.github/workflows/deploy_runtime.yaml @@ -68,7 +68,7 @@ jobs: service: ${{ env.SERVICE }} image: ${{ steps.image.outputs.name }} env_vars: | - ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_API_KEY=${{ secrets.CLAUDE_API_KEY }} WORKSHOP_CODE=${{ secrets.WORKSHOP_CODE }} flags: >- --service-account=${{ env.RUNTIME_SERVICE_ACCOUNT }} diff --git a/03-custom-server/server/.env.example b/03-custom-server/server/.env.example index 1ba0f45..b151c03 100644 --- a/03-custom-server/server/.env.example +++ b/03-custom-server/server/.env.example @@ -1,5 +1,5 @@ # Required. Claude API key. Copy this file to .env and fill it in. Never commit .env. -ANTHROPIC_API_KEY= +CLAUDE_API_KEY= # Optional. Model id (default claude-sonnet-5-5). # CLAUDE_MODEL=claude-sonnet-5-5 # Optional. Server port (default 4100). diff --git a/03-custom-server/server/src/index.ts b/03-custom-server/server/src/index.ts index f3c073a..a6902b3 100644 --- a/03-custom-server/server/src/index.ts +++ b/03-custom-server/server/src/index.ts @@ -6,10 +6,12 @@ import { CopilotRuntime, createCopilotHonoHandler, InMemoryAgentRunner } from "@ import { CalaveritaAgent, MODEL } from "./agent.js"; process.env.COPILOTKIT_TELEMETRY_DISABLED ??= "true"; -if (!process.env.ANTHROPIC_API_KEY) { - console.error("ANTHROPIC_API_KEY is not set. Copy .env.example to .env and put your key in it."); +if (!process.env.CLAUDE_API_KEY) { + console.error("CLAUDE_API_KEY is not set. Copy .env.example to .env and put your key in it."); process.exit(1); } +// The Anthropic SDK reads ANTHROPIC_API_KEY, so hand it the key. +process.env.ANTHROPIC_API_KEY = process.env.CLAUDE_API_KEY; const runtime = new CopilotRuntime({ agents: { calaverita: new CalaveritaAgent() }, diff --git a/README.md b/README.md index bac856b..65bd7bd 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ npm install ``` Create your `runtime/.env` from the example, then open it and set -`ANTHROPIC_API_KEY=`: +`CLAUDE_API_KEY=`: ```sh cp .env.example .env # macOS and Linux @@ -169,7 +169,7 @@ Start the server in a first terminal. It uses port 4100, so the runtime of steps ```sh cd 03-custom-server/server -cp .env.example .env # set ANTHROPIC_API_KEY +cp .env.example .env # set CLAUDE_API_KEY npm install npm start # http://localhost:4100/api/copilotkit ``` @@ -200,7 +200,7 @@ fvm flutter run -d chrome \ A runtime on your machine ignores both headers, so leave `WORKSHOP_CODE` out there. To host the shared backend, deploy `runtime/` and set `WORKSHOP_CODE` and -`ANTHROPIC_API_KEY` on the host. The runtime then answers `401` without the +`CLAUDE_API_KEY` on the host. The runtime then answers `401` without the password. `GET /info` and the CORS preflight stay open. ## How the code is organized diff --git a/runtime/.env.example b/runtime/.env.example index ae75ec6..c3b2911 100644 --- a/runtime/.env.example +++ b/runtime/.env.example @@ -1,6 +1,6 @@ # Copy this file to .env (git-ignored) and fill in your key. COPILOTKIT_MODEL=anthropic:claude-sonnet-5-5 -ANTHROPIC_API_KEY= +CLAUDE_API_KEY= # Only for a shared, hosted runtime: the password the participants send. Leave it # out on your machine. diff --git a/runtime/deploy/README.md b/runtime/deploy/README.md index 316cbf8..557530b 100644 --- a/runtime/deploy/README.md +++ b/runtime/deploy/README.md @@ -8,7 +8,7 @@ typecheck it and build the image (`.github/workflows/runtime_ci.yaml`). ``` push to main ──> GitHub Actions ──(Workload Identity, no key file)──> Google Cloud │ build image ─> Artifact Registry ─> Cloud Run - │ ▲ ANTHROPIC_API_KEY, WORKSHOP_CODE + │ ▲ CLAUDE_API_KEY, WORKSHOP_CODE │ │ (GitHub repository secrets) └ smoke test: /info is open, a run without the code is 401 ``` @@ -17,7 +17,7 @@ push to main ──> GitHub Actions ──(Workload Identity, no key file)── | Value | Lives in | Why | | --- | --- | --- | -| `ANTHROPIC_API_KEY` | GitHub secret | The workflow passes it to Cloud Run as an environment variable. | +| `CLAUDE_API_KEY` | GitHub secret | The workflow passes it to Cloud Run as the `CLAUDE_API_KEY` environment variable of the service. | | `WORKSHOP_CODE` | GitHub secret | Same. Use letters and digits only: the value is passed in a comma-separated list. | | `GCP_WORKLOAD_IDENTITY_PROVIDER` | GitHub secret | Tells the job which identity provider to sign in with. | | `GCP_SERVICE_ACCOUNT` | GitHub secret | The account the job acts as. | @@ -53,7 +53,7 @@ prints the first two values. The last two commands ask for the value and hide it ```sh gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER --repo VeryGoodOpenSource/copilotkit_flutter_workshop gh secret set GCP_SERVICE_ACCOUNT --repo VeryGoodOpenSource/copilotkit_flutter_workshop -gh secret set ANTHROPIC_API_KEY --repo VeryGoodOpenSource/copilotkit_flutter_workshop +gh secret set CLAUDE_API_KEY --repo VeryGoodOpenSource/copilotkit_flutter_workshop gh secret set WORKSHOP_CODE --repo VeryGoodOpenSource/copilotkit_flutter_workshop ``` @@ -76,7 +76,7 @@ node tool/doctor.mjs --only backend --backend ## Operating it - **Rotate the password or the key:** `gh secret set WORKSHOP_CODE` (or - `ANTHROPIC_API_KEY`), then run the workflow again so a new revision picks it up. + `CLAUDE_API_KEY`), then run the workflow again so a new revision picks it up. - **Turn it off after the workshop:** `gcloud run services delete calaverita-runtime --region us-central1`, or revoke the Anthropic key. The deployment costs nothing while idle. - **One instance on purpose** (`--max-instances=1`): the runtime keeps threads in diff --git a/runtime/deploy/setup_gcp.sh b/runtime/deploy/setup_gcp.sh index 4138113..e901645 100755 --- a/runtime/deploy/setup_gcp.sh +++ b/runtime/deploy/setup_gcp.sh @@ -4,8 +4,8 @@ # gcloud auth login # bash runtime/deploy/setup_gcp.sh # -# It creates no secret: the Anthropic key and the workshop password are GitHub -# repository secrets. See runtime/deploy/README.md for what each step is for. +# It creates no secret: the Anthropic key (CLAUDE_API_KEY) and the workshop +# password are GitHub repository secrets. See runtime/deploy/README.md for what each step is for. set -euo pipefail PROJECT="${PROJECT:-gen-lang-client-0563181971}" @@ -74,14 +74,14 @@ cat < = { openai: "OPENAI_API_KEY", - anthropic: "ANTHROPIC_API_KEY", + anthropic: "CLAUDE_API_KEY", google: "GOOGLE_API_KEY", gemini: "GOOGLE_API_KEY", "google-gemini": "GOOGLE_API_KEY", @@ -40,3 +40,5 @@ if (keyVariable && !process.env[keyVariable]) { console.error("If runtime/.env does not exist yet, copy runtime/.env.example to runtime/.env first."); process.exit(1); } +// The Anthropic SDK underneath reads ANTHROPIC_API_KEY, so hand it the key. +if (provider === "anthropic") process.env.ANTHROPIC_API_KEY = process.env.CLAUDE_API_KEY; diff --git a/tool/doctor.mjs b/tool/doctor.mjs index 280994a..02e3b63 100644 --- a/tool/doctor.mjs +++ b/tool/doctor.mjs @@ -135,7 +135,7 @@ async function checkMachine() { } const env = readEnvFile(`${runtimeDir}/.env`); const model = process.env.COPILOTKIT_MODEL || env.COPILOTKIT_MODEL || "anthropic:claude-sonnet-5-5"; - const keyName = { openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY", google: "GOOGLE_API_KEY", gemini: "GOOGLE_API_KEY" }[model.split(/[:/]/)[0].toLowerCase()]; + const keyName = { openai: "OPENAI_API_KEY", anthropic: "CLAUDE_API_KEY", google: "GOOGLE_API_KEY", gemini: "GOOGLE_API_KEY" }[model.split(/[:/]/)[0].toLowerCase()]; if (!keyName || process.env[keyName] || env[keyName]) { report("ok", keyName ? `${keyName} is set (the value is not shown)` : `Model ${model} needs no key`); } else { diff --git a/tool/setup.mjs b/tool/setup.mjs index bdd8015..cff98cb 100644 --- a/tool/setup.mjs +++ b/tool/setup.mjs @@ -175,7 +175,7 @@ async function ensureKey(dir) { const envPath = `${dir}/.env`; const env = readEnvFile(envPath); const model = process.env.COPILOTKIT_MODEL || env.COPILOTKIT_MODEL || "anthropic:claude-sonnet-5-5"; - const variable = { openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY", google: "GOOGLE_API_KEY", gemini: "GOOGLE_API_KEY" }[model.split(/[:/]/)[0].toLowerCase()]; + const variable = { openai: "OPENAI_API_KEY", anthropic: "CLAUDE_API_KEY", google: "GOOGLE_API_KEY", gemini: "GOOGLE_API_KEY" }[model.split(/[:/]/)[0].toLowerCase()]; if (!variable) return; if (process.env[variable] || env[variable]) { say(`${paint("32", "✓")} ${variable} is set (the value is not shown).`); From 7dff95feda6ddfb7796bd806220d63d5cf2f979a Mon Sep 17 00:00:00 2001 From: pietroid Date: Fri, 23 Oct 2026 17:13:48 -0300 Subject: [PATCH 4/5] docs: name the shared backend URL in the README --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 65bd7bd..cd05dd0 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ node tool/setup.mjs It installs Flutter 3.47.0 through FVM, checks your machine, asks which step to run and which backend to use, and starts the app in Chrome. The backend is one of: -- **The shared backend:** nothing to set up except the workshop password, which - you get at the workshop. It asks for it (hidden as you type), or pass - `--code `. +- **The shared backend:** the one we host for the workshop, at + `https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit`. + Nothing to set up except the workshop password, which you get at the workshop. + It asks for it (hidden as you type), or pass `--code `. - **Your own runtime:** it asks for your Anthropic API key (hidden as you type) and starts the runtime in `runtime/` for you. - **A custom backend:** any CopilotKit runtime you have a URL for, for example one @@ -189,7 +190,8 @@ The runtime URL is set in **one place per snapshot**: `AppConfig.defaultRuntimeUrl`, or run with `--dart-define=RUNTIME_URL=`. -The shared backend also asks for a password. The app sends it, and a random id +The shared backend (`https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit`) +also asks for a password. The app sends it, and a random id of the run, as the `X-Workshop-Code` and `X-Workshop-Session` headers on every request: From ee962a05dcc800def7bbb64395884f32dd9f88cd Mon Sep 17 00:00:00 2001 From: pietroid Date: Fri, 23 Oct 2026 17:15:03 -0300 Subject: [PATCH 5/5] docs: drop the deploy README, the setup script lists what it needs --- .github/workflows/deploy_runtime.yaml | 3 +- runtime/deploy/README.md | 87 --------------------------- runtime/deploy/setup_gcp.sh | 2 +- 3 files changed, 3 insertions(+), 89 deletions(-) delete mode 100644 runtime/deploy/README.md diff --git a/.github/workflows/deploy_runtime.yaml b/.github/workflows/deploy_runtime.yaml index 800e551..9ea70aa 100644 --- a/.github/workflows/deploy_runtime.yaml +++ b/.github/workflows/deploy_runtime.yaml @@ -1,5 +1,6 @@ # Deploys the shared backend (runtime/) to Cloud Run on every change to it on main. -# One-time setup of Google Cloud and of the GitHub secrets: runtime/deploy/README.md +# One-time setup of Google Cloud: runtime/deploy/setup_gcp.sh, which also lists the +# GitHub secrets this workflow needs. name: deploy_runtime on: diff --git a/runtime/deploy/README.md b/runtime/deploy/README.md deleted file mode 100644 index 557530b..0000000 --- a/runtime/deploy/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Deploying the shared backend - -The shared backend is `runtime/` on Cloud Run, in the Google Cloud project -`gen-lang-client-0563181971`. GitHub Actions deploys it on every change to -`runtime/` on `main` (`.github/workflows/deploy_runtime.yaml`). Pull requests only -typecheck it and build the image (`.github/workflows/runtime_ci.yaml`). - -``` -push to main ──> GitHub Actions ──(Workload Identity, no key file)──> Google Cloud - │ build image ─> Artifact Registry ─> Cloud Run - │ ▲ CLAUDE_API_KEY, WORKSHOP_CODE - │ │ (GitHub repository secrets) - └ smoke test: /info is open, a run without the code is 401 -``` - -## Where each value lives - -| Value | Lives in | Why | -| --- | --- | --- | -| `CLAUDE_API_KEY` | GitHub secret | The workflow passes it to Cloud Run as the `CLAUDE_API_KEY` environment variable of the service. | -| `WORKSHOP_CODE` | GitHub secret | Same. Use letters and digits only: the value is passed in a comma-separated list. | -| `GCP_WORKLOAD_IDENTITY_PROVIDER` | GitHub secret | Tells the job which identity provider to sign in with. | -| `GCP_SERVICE_ACCOUNT` | GitHub secret | The account the job acts as. | - -GitHub masks the secrets in the logs and they are never in the image. Cloud Run -shows environment variables in the revision's settings, though, so anyone with -view access to the Google Cloud project can read them there. Keep that access -small, or move the two values to Secret Manager if it ever grows. - -No Google key file exists anywhere. The provider only accepts tokens from this -repository on `main`. - -## Steps - -**1. Sign in** with an account that owns the project: - -```sh -gcloud auth login -``` - -**2. Run the setup once.** It enables the APIs, creates the registry, the two -service accounts and the Workload Identity provider, and it is safe to run -again. It creates no secret and asks for nothing: - -```sh -bash runtime/deploy/setup_gcp.sh -``` - -**3. Add the four GitHub secrets**, in the repository's *Settings → Secrets and -variables → Actions*, or with the GitHub CLI (`gh auth login` first). The script -prints the first two values. The last two commands ask for the value and hide it: - -```sh -gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER --repo VeryGoodOpenSource/copilotkit_flutter_workshop -gh secret set GCP_SERVICE_ACCOUNT --repo VeryGoodOpenSource/copilotkit_flutter_workshop -gh secret set CLAUDE_API_KEY --repo VeryGoodOpenSource/copilotkit_flutter_workshop -gh secret set WORKSHOP_CODE --repo VeryGoodOpenSource/copilotkit_flutter_workshop -``` - -For a password, `openssl rand -hex 5` gives ten letters and digits. - -**4. Deploy.** Merge to `main`, or run it by hand: *Actions → deploy_runtime → -Run workflow* (`gh workflow run deploy_runtime.yaml`). The last step of the job -prints the URL and fails if the password gate is not on. - -**5. Point the workshop at it.** Set `sharedBackendUrl` in -`tool/workshop.config.json` to the URL ending in `/api/copilotkit`, and check it: - -```sh -node tool/doctor.mjs --only backend --backend -``` - -**6. Hand out the password** at the workshop. Participants give it to -`node tool/setup.mjs --code ` (or type it when asked). - -## Operating it - -- **Rotate the password or the key:** `gh secret set WORKSHOP_CODE` (or - `CLAUDE_API_KEY`), then run the workflow again so a new revision picks it up. -- **Turn it off after the workshop:** `gcloud run services delete calaverita-runtime --region us-central1`, - or revoke the Anthropic key. The deployment costs nothing while idle. -- **One instance on purpose** (`--max-instances=1`): the runtime keeps threads in - memory, and a second instance would not know them. For a large room, raise the - limit only after moving the threads out of memory. -- **Cold starts:** the first message after a quiet period takes a few seconds. Add - `--min-instances=1` to the `flags` in the workflow during the workshop to avoid it. -- **Logs:** `gcloud run services logs read calaverita-runtime --region us-central1`. diff --git a/runtime/deploy/setup_gcp.sh b/runtime/deploy/setup_gcp.sh index e901645..3b11d94 100755 --- a/runtime/deploy/setup_gcp.sh +++ b/runtime/deploy/setup_gcp.sh @@ -5,7 +5,7 @@ # bash runtime/deploy/setup_gcp.sh # # It creates no secret: the Anthropic key (CLAUDE_API_KEY) and the workshop -# password are GitHub repository secrets. See runtime/deploy/README.md for what each step is for. +# password are GitHub repository secrets, listed at the end. set -euo pipefail PROJECT="${PROJECT:-gen-lang-client-0563181971}"