Public: anyone can read this; the crew can change it.
Confine ANTHROPIC_* and CLAUDE_CODE_* env construction to the runner module #12293
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/12279-confine-anthropic-and-claude-code-env-co → main 10 commits fork
9 files
changed +164
−78
src/core/subscriptions.ts +5 −41
| @@ -28,6 +28,7 @@ import type { Database } from "bun:sqlite"; | ||
| 28 | 28 | import { id } from "../kernel/id.ts"; |
| 29 | 29 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 30 | 30 | import { open, seal } from "../kernel/secret.ts"; |
| 31 | import { accountEnv, envNameOf, hasEnvCredential } from "../crew/runner_env.ts"; | |
| 31 | 32 | import { limitValue } from "../settings/limits.ts"; |
| 32 | 33 | |
| 33 | 34 | export type SubscriptionKind = "oauth" | "api_key"; |
| @@ -192,10 +193,6 @@ const aad = (subscriptionId: string): string => | ||
| 192 | 193 | /** What a sealed account's token is bound to, for opening it elsewhere. */ |
| 193 | 194 | export const subscriptionAad = aad; |
| 194 | 195 | |
| 195 | /** The variable Claude Code reads a token of this kind from. */ | |
| 196 | export const envNameOf = (kind: SubscriptionKind): string => | |
| 197 | kind === "oauth" ? "CLAUDE_CODE_OAUTH_TOKEN" : "ANTHROPIC_API_KEY"; | |
| 198 | ||
| 199 | 196 | /** Longest accepted Anthropic-compatible base URL. */ |
| 200 | 197 | export const API_BASE_MAX = 200; |
| 201 | 198 | |
| @@ -253,45 +250,14 @@ export function parseApiBase(raw: string): string | undefined { | ||
| 253 | 250 | : `https://${url.host}${path}`; |
| 254 | 251 | } |
| 255 | 252 | |
| 256 | /** Hostname a run must be allowed to reach for this base URL, if any. */ | |
| 257 | export function hostOfApiBase(apiBase: string | null | undefined): string | undefined { | |
| 258 | if (apiBase === null || apiBase === undefined || apiBase.trim() === "") | |
| 259 | return undefined; | |
| 260 | try { | |
| 261 | return new URL(apiBase).hostname; | |
| 262 | } catch { | |
| 263 | return undefined; | |
| 264 | } | |
| 265 | } | |
| 266 | ||
| 267 | 253 | /** |
| 268 | * Environment variables a crew run needs for a picked account: the token | |
| 269 | * under the variable Claude Code reads, plus `ANTHROPIC_BASE_URL` (and | |
| 270 | * OpenRouter's `ANTHROPIC_AUTH_TOKEN` dance) when the account has a gateway. | |
| 254 | * Environment variables a crew run needs for a picked account; the names | |
| 255 | * live in `crew/runner_env.ts`. | |
| 271 | 256 | */ |
| 272 | 257 | export function credentialEnvOf( |
| 273 | 258 | picked: Extract<Pick, { kind: "ready" }>, |
| 274 | 259 | ): Record<string, string> { |
| 275 | const base = picked.apiBase; | |
| 276 | if (base === null || base === undefined || base === "") { | |
| 277 | return { [picked.envName]: picked.token }; | |
| 278 | } | |
| 279 | const host = hostOfApiBase(base) ?? ""; | |
| 280 | const openrouter = | |
| 281 | host === "openrouter.ai" || host.endsWith(".openrouter.ai"); | |
| 282 | if (openrouter) { | |
| 283 | // Claude Code + OpenRouter: AUTH_TOKEN carries the key; API_KEY must be | |
| 284 | // an empty string so Claude Code does not fall back to api.anthropic.com. | |
| 285 | return { | |
| 286 | ANTHROPIC_BASE_URL: base, | |
| 287 | ANTHROPIC_AUTH_TOKEN: picked.token, | |
| 288 | ANTHROPIC_API_KEY: "", | |
| 289 | }; | |
| 290 | } | |
| 291 | return { | |
| 292 | ANTHROPIC_BASE_URL: base, | |
| 293 | [picked.envName]: picked.token, | |
| 294 | }; | |
| 260 | return accountEnv(picked); | |
| 295 | 261 | } |
| 296 | 262 | |
| 297 | 263 | export function isSubscriptionName(name: string): boolean { |
| @@ -746,9 +712,7 @@ export function crewHasCredential( | ||
| 746 | 712 | env: Record<string, string | undefined> = process.env, |
| 747 | 713 | ): boolean { |
| 748 | 714 | if (listSubscriptions(db).length > 0) return true; |
| 749 | return ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "FORGE_CLAUDE_CONFIG_DIR"].some( | |
| 750 | (name) => (env[name] ?? "").trim() !== "", | |
| 751 | ); | |
| 715 | return hasEnvCredential(env); | |
| 752 | 716 | } |
| 753 | 717 | |
| 754 | 718 | /** What MCP `subscriptions` and `GET /api/v1/admin/subscriptions` list: each account, never its token. */ |
src/crew/claude.ts +2 −2
| @@ -23,6 +23,7 @@ import { endedLine, liveTree } from "../runs/ended.ts"; | ||
| 23 | 23 | import { CANCEL_GRACE_MS } from "../runs/grace.ts"; |
| 24 | 24 | import { spellings, stripForgeDirs } from "../runs/paths.ts"; |
| 25 | 25 | import type { SandboxPolicy } from "./sandbox.ts"; |
| 26 | import { quietEnv } from "./runner_env.ts"; | |
| 26 | 27 | |
| 27 | 28 | import type { |
| 28 | 29 | AgentOutcome, |
| @@ -155,8 +156,7 @@ export async function runClaude(input: ClaudeInput): Promise<ClaudeOutcome> { | ||
| 155 | 156 | Bun.spawn(present ? backgroundArgv(oomFirst(contained)) : argv, { |
| 156 | 157 | cwd: input.cwd, |
| 157 | 158 | env: childEnv({ |
| 158 | CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", | |
| 159 | DISABLE_TELEMETRY: "1", | |
| 159 | ...quietEnv(), | |
| 160 | 160 | ...input.env, |
| 161 | 161 | }), |
| 162 | 162 | stdin: "pipe", |
src/crew/plan.ts +3 −10
| @@ -16,6 +16,7 @@ | ||
| 16 | 16 | |
| 17 | 17 | import { tmpdir } from "node:os"; |
| 18 | 18 | import { childEnv } from "../apps/process.ts"; |
| 19 | import { oneShotEnv, quietEnv } from "./runner_env.ts"; | |
| 19 | 20 | import { clip } from "../text/clip.ts"; |
| 20 | 21 | |
| 21 | 22 | export interface PlanAgent { |
| @@ -190,16 +191,8 @@ export async function plan( | ||
| 190 | 191 | { |
| 191 | 192 | cwd: tmpdir(), |
| 192 | 193 | env: childEnv({ |
| 193 | CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", | |
| 194 | DISABLE_TELEMETRY: "1", | |
| 195 | ...(process.env["ANTHROPIC_API_KEY"] === undefined | |
| 196 | ? {} | |
| 197 | : { ANTHROPIC_API_KEY: process.env["ANTHROPIC_API_KEY"] }), | |
| 198 | ...(process.env["CLAUDE_CODE_OAUTH_TOKEN"] === undefined | |
| 199 | ? {} | |
| 200 | : { | |
| 201 | CLAUDE_CODE_OAUTH_TOKEN: process.env["CLAUDE_CODE_OAUTH_TOKEN"], | |
| 202 | }), | |
| 194 | ...quietEnv(), | |
| 195 | ...oneShotEnv(), | |
| 203 | 196 | }), |
| 204 | 197 | stdin: "ignore", |
| 205 | 198 | stdout: "pipe", |
src/crew/runner.ts +4 −14
| @@ -167,7 +167,6 @@ import { grantLeft } from "../core/model_grant.ts"; | ||
| 167 | 167 | import { dailyLeft, reserveCeiling } from "../core/account_ceiling.ts"; |
| 168 | 168 | import { |
| 169 | 169 | credentialEnvOf, |
| 170 | hostOfApiBase, | |
| 171 | 170 | MIN_RUN_USD, |
| 172 | 171 | pickSubscription, |
| 173 | 172 | PLATFORM, |
| @@ -201,6 +200,7 @@ import { | ||
| 201 | 200 | skipsFeedback, |
| 202 | 201 | } from "./guard.ts"; |
| 203 | 202 | import { passportEnv } from "./passport_env.ts"; |
| 203 | import { hostOfApiBase, isGatewayAddress, ownEnv, sandboxTmpEnv } from "./runner_env.ts"; | |
| 204 | 204 | import { cappedByLent, fundedEnv, lentCreditLeft, runEnded } from "../spawn/funding.ts"; |
| 205 | 205 | |
| 206 | 206 | const log = createLog("crew"); |
| @@ -1522,7 +1522,7 @@ async function work( | ||
| 1522 | 1522 | runTmp = sandbox.tmpDir; |
| 1523 | 1523 | if (runTmp !== undefined) { |
| 1524 | 1524 | mkdirSync(runTmp, { recursive: true }); |
| 1525 | env["CLAUDE_CODE_TMPDIR"] = runTmp; | |
| 1525 | Object.assign(env, sandboxTmpEnv(runTmp)); | |
| 1526 | 1526 | env["TMPDIR"] = runTmp; |
| 1527 | 1527 | } |
| 1528 | 1528 | // Bun's install cache is the repository's, shared by its runs, so a |
| @@ -2425,7 +2425,7 @@ function redactResult<T extends { result: string }>(r: T): T { | ||
| 2425 | 2425 | */ |
| 2426 | 2426 | export function holdModelCredential(key: string, env: Record<string, string>): string[] { |
| 2427 | 2427 | const secrets = Object.entries(env) |
| 2428 | .filter(([name, value]) => name !== "ANTHROPIC_BASE_URL" && value.length >= 4) | |
| 2428 | .filter(([name, value]) => !isGatewayAddress(name) && value.length >= 4) | |
| 2429 | 2429 | .map(([, value]) => value); |
| 2430 | 2430 | holdValues(key, secrets); |
| 2431 | 2431 | return secrets; |
| @@ -2443,15 +2443,5 @@ export function credentialEnv(picked: Pick, dataDir?: string): Record<string, st | ||
| 2443 | 2443 | // "none" reaches here only under an owner's grant (an org's, #10991), which is the forge's account too. |
| 2444 | 2444 | const lent = (picked.kind === "env" || picked.kind === "none") && dataDir !== undefined ? fundedEnv(dataDir) : undefined; |
| 2445 | 2445 | if (lent !== undefined) return lent; |
| 2446 | const own: Record<string, string> = {}; | |
| 2447 | for (const name of [ | |
| 2448 | "ANTHROPIC_API_KEY", | |
| 2449 | "CLAUDE_CODE_OAUTH_TOKEN", | |
| 2450 | "ANTHROPIC_BASE_URL", | |
| 2451 | "ANTHROPIC_AUTH_TOKEN", | |
| 2452 | ]) { | |
| 2453 | const value = process.env[name]; | |
| 2454 | if (value !== undefined) own[name] = value; | |
| 2455 | } | |
| 2456 | return own; | |
| 2446 | return ownEnv(); | |
| 2457 | 2447 | } |
src/crew/runner_env.ts added +108
| @@ -0,0 +1,108 @@ | ||
| 1 | /** | |
| 2 | * Every environment variable a crew run's model reads, named in one place. | |
| 3 | * Claude Code takes its credential and gateway from `ANTHROPIC_*` and | |
| 4 | * `CLAUDE_CODE_*`; no other module spells those names. Plain data in, plain | |
| 5 | * data out: nothing here reads the process environment on its own account. | |
| 6 | */ | |
| 7 | import type { SubscriptionKind } from "../core/subscriptions.ts"; | |
| 8 | ||
| 9 | type Env = Record<string, string | undefined>; | |
| 10 | ||
| 11 | const DEFAULT_BASE = "https://api.anthropic.com"; | |
| 12 | ||
| 13 | /** The variable Claude Code reads a token of this kind from. */ | |
| 14 | export const envNameOf = (kind: SubscriptionKind): string => | |
| 15 | kind === "oauth" ? "CLAUDE_CODE_OAUTH_TOKEN" : "ANTHROPIC_API_KEY"; | |
| 16 | ||
| 17 | /** Hostname a run must be allowed to reach for this base URL, if any. */ | |
| 18 | export function hostOfApiBase(apiBase: string | null | undefined): string | undefined { | |
| 19 | if (apiBase === null || apiBase === undefined || apiBase.trim() === "") return undefined; | |
| 20 | try { | |
| 21 | return new URL(apiBase).hostname; | |
| 22 | } catch { | |
| 23 | return undefined; | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Variables for a picked account: the token under the variable Claude Code | |
| 29 | * reads, plus the base URL (and OpenRouter's auth-token dance) when the | |
| 30 | * account has a gateway. | |
| 31 | */ | |
| 32 | export function accountEnv(a: { | |
| 33 | readonly envName: string; | |
| 34 | readonly token: string; | |
| 35 | readonly apiBase?: string | null | undefined; | |
| 36 | }): Record<string, string> { | |
| 37 | const base = a.apiBase; | |
| 38 | if (base === null || base === undefined || base === "") return { [a.envName]: a.token }; | |
| 39 | const host = hostOfApiBase(base) ?? ""; | |
| 40 | if (host === "openrouter.ai" || host.endsWith(".openrouter.ai")) { | |
| 41 | // AUTH_TOKEN carries the key; API_KEY must be empty so Claude Code does | |
| 42 | // not fall back to api.anthropic.com. | |
| 43 | return { ANTHROPIC_BASE_URL: base, ANTHROPIC_AUTH_TOKEN: a.token, ANTHROPIC_API_KEY: "" }; | |
| 44 | } | |
| 45 | return { ANTHROPIC_BASE_URL: base, [a.envName]: a.token }; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * A forge's own credential as it was started with, whole: what a run with | |
| 50 | * no account of its own carries. | |
| 51 | */ | |
| 52 | export function ownEnv(env: Env = process.env): Record<string, string> { | |
| 53 | const own: Record<string, string> = {}; | |
| 54 | for (const name of ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN"]) { | |
| 55 | const value = env[name]; | |
| 56 | if (value !== undefined) own[name] = value; | |
| 57 | } | |
| 58 | return own; | |
| 59 | } | |
| 60 | ||
| 61 | /** Whether the environment holds a model token or a signed-in Claude config. */ | |
| 62 | export function hasEnvCredential(env: Env): boolean { | |
| 63 | return ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "FORGE_CLAUDE_CONFIG_DIR"].some( | |
| 64 | (name) => (env[name] ?? "").trim() !== "", | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | /** Points a run at a parent's door, paying with the credit token. */ | |
| 69 | export function lentEnv(door: string, token: string): Record<string, string> { | |
| 70 | return { ANTHROPIC_BASE_URL: door, ANTHROPIC_API_KEY: token }; | |
| 71 | } | |
| 72 | ||
| 73 | /** A variable that holds an address, not a secret: left readable in a run's log. */ | |
| 74 | export const isGatewayAddress = (name: string): boolean => name === "ANTHROPIC_BASE_URL"; | |
| 75 | ||
| 76 | /** The two variables that quiet Claude Code's background traffic. */ | |
| 77 | export const quietEnv = (): Record<string, string> => ({ | |
| 78 | CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", | |
| 79 | DISABLE_TELEMETRY: "1", | |
| 80 | }); | |
| 81 | ||
| 82 | /** Where Claude Code's sandbox keeps its temp files. */ | |
| 83 | export const sandboxTmpEnv = (dir: string): Record<string, string> => ({ CLAUDE_CODE_TMPDIR: dir }); | |
| 84 | ||
| 85 | /** The credential a one-shot call carries: the key or token the forge holds, nothing else. */ | |
| 86 | export function oneShotEnv(env: Env = process.env): Record<string, string> { | |
| 87 | const out: Record<string, string> = {}; | |
| 88 | for (const name of ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]) { | |
| 89 | const value = env[name]; | |
| 90 | if (value !== undefined) out[name] = value; | |
| 91 | } | |
| 92 | return out; | |
| 93 | } | |
| 94 | ||
| 95 | /** Where to send a model call made with the forge's own credential, and with which headers. */ | |
| 96 | export function upstreamOf(env: Env = process.env): | |
| 97 | | { readonly base: string; readonly headers: Record<string, string> } | |
| 98 | | undefined { | |
| 99 | const base = (env["ANTHROPIC_BASE_URL"] ?? DEFAULT_BASE).replace(/\/+$/, ""); | |
| 100 | const key = env["ANTHROPIC_API_KEY"]; | |
| 101 | const auth = env["ANTHROPIC_AUTH_TOKEN"]; | |
| 102 | const oauth = env["CLAUDE_CODE_OAUTH_TOKEN"]; | |
| 103 | if (key !== undefined && key !== "") return { base, headers: { "x-api-key": key } }; | |
| 104 | if (auth !== undefined && auth !== "") return { base, headers: { authorization: `Bearer ${auth}` } }; | |
| 105 | if (oauth !== undefined && oauth !== "") | |
| 106 | return { base, headers: { authorization: `Bearer ${oauth}`, "anthropic-beta": "oauth-2025-04-20" } }; | |
| 107 | return undefined; | |
| 108 | } |
src/spawn/funding.ts +2 −1
| @@ -11,6 +11,7 @@ | ||
| 11 | 11 | import { join } from "node:path"; |
| 12 | 12 | import type { App } from "../app.ts"; |
| 13 | 13 | import { readPrivate, writePrivate } from "../kernel/file_modes.ts"; |
| 14 | import { lentEnv } from "../crew/runner_env.ts"; | |
| 14 | 15 | import { leftUsd, usdWords } from "./credit.ts"; |
| 15 | 16 | import { isParentLocal } from "./beat-door.ts"; |
| 16 | 17 | |
| @@ -66,7 +67,7 @@ export function fundedEnv(dataDir: string): Record<string, string> | undefined { | ||
| 66 | 67 | const local = process.env["FORGE_PARENT_LOCAL"] ?? ""; |
| 67 | 68 | const f = fundingOf(dataDir); |
| 68 | 69 | if (f === undefined || !isParentLocal(local)) return undefined; |
| 69 | return { ANTHROPIC_BASE_URL: local, ANTHROPIC_API_KEY: f.token }; | |
| 70 | return lentEnv(local, f.token); | |
| 70 | 71 | } |
| 71 | 72 | |
| 72 | 73 | type FetchLike = (url: string, init?: RequestInit) => Promise<Response>; |
src/spawn/model_door.ts +2 −9
| @@ -15,6 +15,7 @@ | ||
| 15 | 15 | import type { App } from "../app.ts"; |
| 16 | 16 | import { addSpend, creditOfToken, leftUsd, readCredit, spentWords, type Credit } from "./credit.ts"; |
| 17 | 17 | import { listPriceUsd, NO_USAGE, type Usage } from "./list_price.ts"; |
| 18 | import { upstreamOf } from "../crew/runner_env.ts"; | |
| 18 | 19 | import { readCapped } from "./bounded_read.ts"; |
| 19 | 20 | import { enter, MAX_IN_FLIGHT, noteBad } from "./door_guard.ts"; |
| 20 | 21 | |
| @@ -33,15 +34,7 @@ export interface ModelDeps { | ||
| 33 | 34 | |
| 34 | 35 | /** The parent's own credential from its environment, never from a request. */ |
| 35 | 36 | export function ownUpstream(): ModelUpstream | undefined { |
| 36 | const base = (process.env["ANTHROPIC_BASE_URL"] ?? "https://api.anthropic.com").replace(/\/+$/, ""); | |
| 37 | const key = process.env["ANTHROPIC_API_KEY"]; | |
| 38 | const auth = process.env["ANTHROPIC_AUTH_TOKEN"]; | |
| 39 | const oauth = process.env["CLAUDE_CODE_OAUTH_TOKEN"]; | |
| 40 | if (key !== undefined && key !== "") return { base, headers: { "x-api-key": key } }; | |
| 41 | if (auth !== undefined && auth !== "") return { base, headers: { authorization: `Bearer ${auth}` } }; | |
| 42 | if (oauth !== undefined && oauth !== "") | |
| 43 | return { base, headers: { authorization: `Bearer ${oauth}`, "anthropic-beta": "oauth-2025-04-20" } }; | |
| 44 | return undefined; | |
| 37 | return upstreamOf(); | |
| 45 | 38 | } |
| 46 | 39 | |
| 47 | 40 | export const MODEL_BODY_MAX = 16 * 1024 * 1024; |
test/runner-env-confined.test.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { Glob } from "bun"; | |
| 3 | import { accountEnv, hasEnvCredential, lentEnv, oneShotEnv, ownEnv, upstreamOf } from "../src/crew/runner_env.ts"; | |
| 4 | ||
| 5 | test("only runner_env.ts names a vendor variable in src", async () => { | |
| 6 | const re = /\b(ANTHROPIC_[A-Z_]+|CLAUDE_CODE_[A-Z_]+)\b/; | |
| 7 | const allowed = new Set(["src/crew/runner_env.ts", "src/packs/up.ts" /* a sentence telling a person which variable to set */]); | |
| 8 | const bad: string[] = []; | |
| 9 | for await (const f of new Glob("src/**/*.ts").scan(".")) { | |
| 10 | if (allowed.has(f)) continue; | |
| 11 | const code = (await Bun.file(f).text()) | |
| 12 | .split("\n") | |
| 13 | .filter((l) => !/^\s*(\/\/|\/?\*)/.test(l) && re.test(l)); | |
| 14 | if (code.length > 0) bad.push(`${f}: ${code[0]!.trim()}`); | |
| 15 | } | |
| 16 | expect(bad).toEqual([]); | |
| 17 | }); | |
| 18 | ||
| 19 | test("runner_env builds each environment", () => { | |
| 20 | expect(accountEnv({ envName: "ANTHROPIC_API_KEY", token: "t" })).toEqual({ ANTHROPIC_API_KEY: "t" }); | |
| 21 | expect(accountEnv({ envName: "ANTHROPIC_API_KEY", token: "t", apiBase: "https://openrouter.ai/api" })).toEqual({ | |
| 22 | ANTHROPIC_BASE_URL: "https://openrouter.ai/api", | |
| 23 | ANTHROPIC_AUTH_TOKEN: "t", | |
| 24 | ANTHROPIC_API_KEY: "", | |
| 25 | }); | |
| 26 | expect(lentEnv("http://x", "k")).toEqual({ ANTHROPIC_BASE_URL: "http://x", ANTHROPIC_API_KEY: "k" }); | |
| 27 | expect(ownEnv({ ANTHROPIC_API_KEY: "a", PATH: "/bin" })).toEqual({ ANTHROPIC_API_KEY: "a" }); | |
| 28 | expect(oneShotEnv({ CLAUDE_CODE_OAUTH_TOKEN: "o", ANTHROPIC_BASE_URL: "u" })).toEqual({ CLAUDE_CODE_OAUTH_TOKEN: "o" }); | |
| 29 | expect(hasEnvCredential({})).toBe(false); | |
| 30 | expect(hasEnvCredential({ FORGE_CLAUDE_CONFIG_DIR: "/x" })).toBe(true); | |
| 31 | expect(upstreamOf({})).toBeUndefined(); | |
| 32 | expect(upstreamOf({ ANTHROPIC_API_KEY: "k", ANTHROPIC_BASE_URL: "https://g/" })).toEqual({ | |
| 33 | base: "https://g", | |
| 34 | headers: { "x-api-key": "k" }, | |
| 35 | }); | |
| 36 | expect(upstreamOf({ CLAUDE_CODE_OAUTH_TOKEN: "o" })?.headers["anthropic-beta"]).toBe("oauth-2025-04-20"); | |
| 37 | }); |
test/subscriptions.test.ts +1 −1
| @@ -17,7 +17,6 @@ import { | ||
| 17 | 17 | deleteSubscription, |
| 18 | 18 | EVERYONE, |
| 19 | 19 | getSubscription, |
| 20 | hostOfApiBase, | |
| 21 | 20 | kindOfToken, |
| 22 | 21 | listGrants, |
| 23 | 22 | listSubscriptions, |
| @@ -28,6 +27,7 @@ import { | ||
| 28 | 27 | setGrants, |
| 29 | 28 | wakeSubscription, |
| 30 | 29 | } from "../src/core/subscriptions.ts"; |
| 30 | import { hostOfApiBase } from "../src/crew/runner_env.ts"; | |
| 31 | 31 | import { createUser, type User } from "../src/core/users.ts"; |
| 32 | 32 | import { startCrew, type CrewHandle } from "../src/crew/dispatch.ts"; |
| 33 | 33 | import { isRateLimited, resumeAt } from "../src/crew/limits.ts"; |
new above
offline