Blame · Line-by-line history
hosted-loop-secrets.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 4c08073 | 1 | /** |
| 2 | * Hosted Claude loops must not hand platform secrets to user code. | |
| 3 | * | |
| 4 | * `loop.sourceCode` is arbitrary user-supplied JavaScript. Creating a loop | |
| 5 | * requires only requireAuth (src/routes/claude-deploy.tsx:54), so any | |
| 6 | * registered account can run whatever it wants, and the run's stdout is | |
| 7 | * captured and returned to the caller. Anything placed in the subprocess env | |
| 8 | * is readable with a single console.log. | |
| 9 | * | |
| 10 | * The env used to carry: | |
| 11 | * - GLUECRON_PAT — the OPERATOR's admin-scoped token | |
| 12 | * - ANTHROPIC_API_KEY — the platform's billing key | |
| 13 | * | |
| 14 | * i.e. any registered user could exfiltrate both. | |
| 15 | */ | |
| 16 | ||
| 17 | import { describe, expect, it } from "bun:test"; | |
| 18 | import { readFileSync } from "fs"; | |
| 19 | ||
| 20 | const SRC = readFileSync("src/lib/hosted-claude-loop.ts", "utf8"); | |
| 21 | const envBlock = SRC.slice( | |
| 22 | SRC.indexOf("const env: Record<string, string> = {"), | |
| 23 | SRC.indexOf("const exec = getExecutor();") | |
| 24 | ); | |
| 25 | ||
| 26 | describe("operator PAT is never exposed", () => { | |
| 27 | it("GLUECRON_PAT is not injected into the snippet env", () => { | |
| 28 | expect(envBlock).not.toMatch(/env\.GLUECRON_PAT\s*=/); | |
| 29 | expect(envBlock).not.toMatch(/GLUECRON_PAT:/); | |
| 30 | }); | |
| 31 | ||
| 32 | it("is gone from the whole module, not just relocated", () => { | |
| 33 | const assignments = SRC.match(/env\.GLUECRON_PAT\s*=/g) ?? []; | |
| 34 | expect(assignments.length).toBe(0); | |
| 35 | }); | |
| 36 | }); | |
| 37 | ||
| 38 | describe("platform Anthropic key is opt-in", () => { | |
| 39 | it("is not unconditionally placed in the env", () => { | |
| 40 | // The original bug: a bare property on the object literal. | |
| 41 | expect(envBlock).not.toMatch(/^\s*ANTHROPIC_API_KEY:/m); | |
| 42 | }); | |
| 43 | ||
| 44 | it("requires an explicit operator opt-in flag", () => { | |
| 45 | expect(envBlock).toContain('process.env.HOSTED_LOOPS_PLATFORM_KEY === "1"'); | |
| 46 | }); | |
| 47 | ||
| 48 | it("defaults to withholding the key", () => { | |
| 49 | // Guard against the flag being inverted later: absence of the env var | |
| 50 | // must mean no key. | |
| 51 | const gate = envBlock.slice(envBlock.indexOf("HOSTED_LOOPS_PLATFORM_KEY")); | |
| 52 | expect(gate).toContain("env.ANTHROPIC_API_KEY ="); | |
| 53 | expect(envBlock.indexOf("HOSTED_LOOPS_PLATFORM_KEY")).toBeLessThan( | |
| 54 | envBlock.indexOf("env.ANTHROPIC_API_KEY =") | |
| 55 | ); | |
| 56 | }); | |
| 57 | }); | |
| 58 | ||
| 59 | describe("what the snippet legitimately still gets", () => { | |
| 60 | it("keeps the non-secret loop identifiers", () => { | |
| 61 | expect(envBlock).toContain("GLUECRON_AGENT_ID"); | |
| 62 | expect(envBlock).toContain("GLUECRON_LOOP_ID"); | |
| 63 | expect(envBlock).toContain("GLUECRON_BASE_URL"); | |
| 64 | }); | |
| 65 | }); |