From 879556097cd925608d7417658da6cdbef4b82bd5 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:00:24 -0500 Subject: [PATCH 1/8] feat(shared): add run-token mint/parse/hash and bearer helpers Add an opaque per-run bearer credential used by workers when calling the API: - `scope_rt__` with 32 random bytes of secret and a base62 CRC32 checksum so malformed tokens are rejected without a DB read. - Only a hex SHA-256 digest is meant to be persisted; digests are compared in constant time. - `BearerProvider`, `bearerHeaders` and `fetchWithBearer` let API clients add `Authorization: Bearer` only when a token is available, leaving existing no-token calls unchanged. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/shared/src/auth/index.ts | 1 + packages/shared/src/auth/run-token.test.ts | 66 +++++++++++ packages/shared/src/auth/run-token.ts | 123 +++++++++++++++++++++ packages/shared/src/http/bearer.test.ts | 47 ++++++++ packages/shared/src/http/bearer.ts | 40 +++++++ packages/shared/src/index.ts | 1 + 6 files changed, 278 insertions(+) create mode 100644 packages/shared/src/auth/run-token.test.ts create mode 100644 packages/shared/src/auth/run-token.ts create mode 100644 packages/shared/src/http/bearer.test.ts create mode 100644 packages/shared/src/http/bearer.ts diff --git a/packages/shared/src/auth/index.ts b/packages/shared/src/auth/index.ts index 16d70d859..af8eaf1af 100644 --- a/packages/shared/src/auth/index.ts +++ b/packages/shared/src/auth/index.ts @@ -6,3 +6,4 @@ export * from "./entra.js"; export * from "./claims-enricher.js"; export * from "./user-schema.js"; export * from "./config.js"; +export * from "./run-token.js"; diff --git a/packages/shared/src/auth/run-token.test.ts b/packages/shared/src/auth/run-token.test.ts new file mode 100644 index 000000000..4f7282ae7 --- /dev/null +++ b/packages/shared/src/auth/run-token.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { + RUN_TOKEN_PREFIX, + hashRunToken, + isRunToken, + mintRunToken, + parseRunToken, + runTokenHashPrefix, + runTokenHashesEqual, +} from "./run-token.js"; + +const REQUEST_ID = "6f1c2f1e-1d2b-4c3a-9e8f-0a1b2c3d4e5f"; + +describe("run-token", () => { + it("mints an opaque prefixed token whose hash is persisted, not the token", () => { + const { token, hash } = mintRunToken(REQUEST_ID); + expect(token.startsWith(`${RUN_TOKEN_PREFIX}${REQUEST_ID}_`)).toBe(true); + expect(hash).toMatch(/^[0-9a-f]{64}$/); + expect(hash).toBe(hashRunToken(token)); + expect(hash).not.toContain(token); + }); + + it("mints unique tokens per call", () => { + const a = mintRunToken(REQUEST_ID); + const b = mintRunToken(REQUEST_ID); + expect(a.token).not.toBe(b.token); + expect(a.hash).not.toBe(b.hash); + }); + + it("parses the request id from a well-formed token", () => { + const { token } = mintRunToken(REQUEST_ID); + expect(isRunToken(token)).toBe(true); + expect(parseRunToken(token)).toEqual({ requestId: REQUEST_ID }); + }); + + it("rejects tokens with a bad checksum, truncation, or wrong prefix", () => { + const { token } = mintRunToken(REQUEST_ID); + const flipped = token.slice(0, -10) + (token.at(-10) === "A" ? "B" : "A") + token.slice(-9); + expect(parseRunToken(flipped)).toBeNull(); + expect(parseRunToken(token.slice(0, -1))).toBeNull(); + expect(parseRunToken(token.replace(RUN_TOKEN_PREFIX, "scope_xx_"))).toBeNull(); + expect(parseRunToken("scope_rt_")).toBeNull(); + expect(parseRunToken("eyJhbGciOi.jwt.value")).toBeNull(); + }); + + it("refuses to mint for an unsafe request id", () => { + expect(() => mintRunToken("bad id/../")).toThrow(); + expect(() => mintRunToken("")).toThrow(); + }); + + it("compares digests in constant time and rejects malformed digests", () => { + const { token, hash } = mintRunToken(REQUEST_ID); + expect(runTokenHashesEqual(hash, hashRunToken(token))).toBe(true); + expect(runTokenHashesEqual(hash, mintRunToken(REQUEST_ID).hash)).toBe(false); + expect(runTokenHashesEqual(hash, "short")).toBe(false); + expect(runTokenHashesEqual("z".repeat(64), hash)).toBe(false); + }); + + it("exposes only an 8-char digest prefix for logs", () => { + const { hash } = mintRunToken(REQUEST_ID); + expect(runTokenHashPrefix(hash)).toBe(hash.slice(0, 8)); + }); +}); diff --git a/packages/shared/src/auth/run-token.ts b/packages/shared/src/auth/run-token.ts new file mode 100644 index 000000000..862df0d0e --- /dev/null +++ b/packages/shared/src/auth/run-token.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; + +/** + * Run tokens are opaque, per-run bearer credentials minted by the worker that + * claims a run. Only the SHA-256 digest is persisted (`run.tokenHash`); the raw + * token lives in worker memory for the lifetime of the run. + * + * Format: `scope_rt__` + * - `requestId` is a routing hint for a point read; it is never trusted alone. + * - `secret` is 32 random bytes, base64url-encoded (43 chars). + * - `checksum` is a CRC32 of everything before it, base62-encoded (6 chars), so + * malformed or truncated tokens can be rejected without a database read. + */ +export const RUN_TOKEN_PREFIX = "scope_rt_"; + +const SECRET_BYTES = 32; +const SECRET_LENGTH = 43; +const CHECKSUM_LENGTH = 6; +const REQUEST_ID_PATTERN = /^[A-Za-z0-9-]{1,128}$/; +const SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/; +const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const HASH_HEX_LENGTH = 64; + +/** A freshly minted run token and the digest to persist on the claimed run. */ +export interface MintedRunToken { + /** Raw bearer token. Keep in memory only; never log or persist. */ + token: string; + /** Hex SHA-256 digest stored as `run.tokenHash`. */ + hash: string; +} + +/** The non-secret routing data extracted from a well-formed run token. */ +export interface ParsedRunToken { + requestId: string; +} + +let crcTable: Uint32Array | undefined; + +/** Standard CRC-32 (IEEE 802.3) over a UTF-8 string. */ +function crc32(input: string): number { + if (!crcTable) { + crcTable = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + crcTable[n] = c >>> 0; + } + } + let crc = 0xffffffff; + for (const byte of Buffer.from(input, "utf-8")) { + crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +/** Fixed-width base62 encoding of a 32-bit checksum. */ +function checksum(body: string): string { + let value = crc32(body); + let out = ""; + for (let i = 0; i < CHECKSUM_LENGTH; i++) { + out = BASE62[value % 62] + out; + value = Math.floor(value / 62); + } + return out; +} + +/** Cheap prefix test used to route a bearer to run-token verification. */ +export function isRunToken(token: string): boolean { + return token.startsWith(RUN_TOKEN_PREFIX); +} + +/** Mint a new run token for a request. Call once per claim, outside any retry loop. */ +export function mintRunToken(requestId: string): MintedRunToken { + if (!REQUEST_ID_PATTERN.test(requestId)) { + throw new Error("Cannot mint a run token for an invalid request id"); + } + const secret = randomBytes(SECRET_BYTES).toString("base64url"); + const body = `${RUN_TOKEN_PREFIX}${requestId}_${secret}`; + const token = `${body}${checksum(body)}`; + return { token, hash: hashRunToken(token) }; +} + +/** Hex SHA-256 digest of a run token (high-entropy secret, so no slow hash needed). */ +export function hashRunToken(token: string): string { + return createHash("sha256").update(token, "utf-8").digest("hex"); +} + +/** + * Validate a run token's shape and checksum offline. Returns the request id it + * claims to belong to, or `null` when malformed. Does not prove validity. + */ +export function parseRunToken(token: string): ParsedRunToken | null { + if (!isRunToken(token)) return null; + const tail = SECRET_LENGTH + CHECKSUM_LENGTH; + // prefix + at least one id char + "_" separator + secret + checksum + if (token.length < RUN_TOKEN_PREFIX.length + 2 + tail) return null; + const body = token.slice(0, -CHECKSUM_LENGTH); + if (checksum(body) !== token.slice(-CHECKSUM_LENGTH)) return null; + const secret = body.slice(-SECRET_LENGTH); + const separator = body.slice(-SECRET_LENGTH - 1, -SECRET_LENGTH); + const requestId = body.slice(RUN_TOKEN_PREFIX.length, -SECRET_LENGTH - 1); + if (separator !== "_" || !SECRET_PATTERN.test(secret) || !REQUEST_ID_PATTERN.test(requestId)) { + return null; + } + return { requestId }; +} + +/** Constant-time comparison of two hex SHA-256 digests. */ +export function runTokenHashesEqual(a: string, b: string): boolean { + if (a.length !== HASH_HEX_LENGTH || b.length !== HASH_HEX_LENGTH) return false; + const left = Buffer.from(a, "hex"); + const right = Buffer.from(b, "hex"); + if (left.length !== 32 || right.length !== 32) return false; + return timingSafeEqual(left, right); +} + +/** Non-secret 8-char digest prefix for log correlation. */ +export function runTokenHashPrefix(hash: string): string { + return hash.slice(0, 8); +} diff --git a/packages/shared/src/http/bearer.test.ts b/packages/shared/src/http/bearer.test.ts new file mode 100644 index 000000000..96e8406f8 --- /dev/null +++ b/packages/shared/src/http/bearer.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { bearerHeaders, fetchWithBearer } from "./bearer.js"; + +describe("bearer helpers", () => { + const fetchMock = vi.fn(async () => new Response("ok")); + + afterEach(() => { + vi.unstubAllGlobals(); + fetchMock.mockClear(); + }); + + it("returns no headers without a token", () => { + expect(bearerHeaders()).toEqual({}); + expect(bearerHeaders(() => undefined)).toEqual({}); + }); + + it("returns an Authorization header with a token", () => { + expect(bearerHeaders(() => "abc")).toEqual({ Authorization: "Bearer abc" }); + }); + + it("forwards the original arguments when no token is available", async () => { + vi.stubGlobal("fetch", fetchMock); + await fetchWithBearer("http://api/x"); + expect(fetchMock).toHaveBeenCalledWith("http://api/x"); + await fetchWithBearer("http://api/y", () => undefined, { method: "GET" }); + expect(fetchMock).toHaveBeenLastCalledWith("http://api/y", { method: "GET" }); + }); + + it("merges the bearer into plain-object headers", async () => { + vi.stubGlobal("fetch", fetchMock); + await fetchWithBearer("http://api/x", () => "tok", { headers: { Accept: "application/json" } }); + expect(fetchMock).toHaveBeenCalledWith("http://api/x", { + headers: { Accept: "application/json", Authorization: "Bearer tok" }, + }); + }); + + it("merges the bearer into Headers instances", async () => { + vi.stubGlobal("fetch", fetchMock); + await fetchWithBearer("http://api/x", () => "tok", { headers: new Headers({ Accept: "a" }) }); + const init = (fetchMock.mock.calls[0] as unknown as [string, RequestInit])[1]; + expect(new Headers(init.headers).get("authorization")).toBe("Bearer tok"); + expect(new Headers(init.headers).get("accept")).toBe("a"); + }); +}); diff --git a/packages/shared/src/http/bearer.ts b/packages/shared/src/http/bearer.ts new file mode 100644 index 000000000..8082bfa1d --- /dev/null +++ b/packages/shared/src/http/bearer.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Returns the bearer credential a client should present to the API, or + * `undefined` to send the request without an `Authorization` header. + */ +export type BearerProvider = () => string | undefined; + +/** Build an `Authorization` header object when a bearer is available. */ +export function bearerHeaders(bearer?: BearerProvider): Record { + const token = bearer?.(); + return token ? { Authorization: `Bearer ${token}` } : {}; +} + +/** + * `fetch` that adds `Authorization: Bearer ` when the provider returns a + * token. Without a token, it forwards the original arguments unchanged. + */ +export function fetchWithBearer( + url: string, + bearer?: BearerProvider, + init?: RequestInit, +): Promise { + const auth = bearerHeaders(bearer); + if (Object.keys(auth).length === 0) { + return init === undefined ? fetch(url) : fetch(url, init); + } + const base = init?.headers; + // Keep plain-object headers plain so callers and tests see a simple record. + const headers: HeadersInit = + base === undefined || (typeof base === "object" && !(base instanceof Headers) && !Array.isArray(base)) + ? { ...(base as Record | undefined), ...auth } + : (() => { + const merged = new Headers(base); + merged.set("Authorization", auth.Authorization); + return merged; + })(); + return fetch(url, { ...init, headers }); +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a6febdece..62fad66d0 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -28,5 +28,6 @@ export * from "./har/index.js"; export * from "./utils/index.js"; export * from "./schemas/index.js"; export * from "./auth/index.js"; +export * from "./http/bearer.js"; export * from "./cursor.js"; export * from "./run-duration.js"; From 445367e94131180e156d4ce9058a4a58a0d29e50 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:02:40 -0500 Subject: [PATCH 2/8] feat(api): authenticate run tokens and service keys in auth middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recognize two non-user credentials before IdP verification and attach them as a separate `req.principal`, leaving `req.auth`/`req.user` semantics unchanged: - `Authorization: Bearer scope_rt_…` is verified with a point read on the request: hash must match `run.tokenHash`, the run must be `processing`, and the claim must be younger than RUN_TOKEN_MAX_AGE_MS (default 24h). Forged, inactive or expired tokens return 401; a DB failure returns 503. The IdP is never called for run tokens. - `X-Service-Name` + `X-Internal-Key` are verified against per-service keys (INTERNAL_API_KEY_REGISTER_AGENTS, INTERNAL_API_KEY_MODEL_SCANNER). Missing or short keys disable that service (fail closed). Scopes are recorded on the principal only; enforcement is left to the authorization layer. - Sending both service headers and a bearer returns 400 ambiguous_credentials. - When auth is disabled, all credentials are ignored as before. Also adds `getPrincipal(req)`, server-only `RunState.tokenHash` (kept out of the zod schema) and a metadata-only `runToken` route flag that emits `x-scope-run-token` in OpenAPI. Requests without credentials behave exactly as before. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/api/src/auth/error-handler.ts | 5 ++ apps/api/src/auth/middleware.ts | 71 +++++++++++++++++- apps/api/src/auth/run-token-verifier.ts | 97 +++++++++++++++++++++++++ apps/api/src/auth/service-principals.ts | 79 ++++++++++++++++++++ apps/api/src/auth/types.ts | 23 ++++++ apps/api/src/index.ts | 9 +++ apps/api/src/openapi/api-route.test.ts | 19 +++++ apps/api/src/openapi/api-route.ts | 10 +++ packages/shared/src/auth/types.ts | 6 +- packages/shared/src/types/types.ts | 4 + 10 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/auth/run-token-verifier.ts create mode 100644 apps/api/src/auth/service-principals.ts diff --git a/apps/api/src/auth/error-handler.ts b/apps/api/src/auth/error-handler.ts index 1f937ec72..0af335df3 100644 --- a/apps/api/src/auth/error-handler.ts +++ b/apps/api/src/auth/error-handler.ts @@ -3,6 +3,7 @@ import type { ErrorRequestHandler } from "express"; import { AuthError } from "shared"; +import { AmbiguousCredentialsError } from "./middleware.js"; import { UserAccessError } from "./user-access-resolver.js"; /** Keep login and normal-request authentication failures on the same HTTP contract. */ @@ -17,6 +18,10 @@ export const authErrorHandler: ErrorRequestHandler = ( }); return; } + if (err instanceof AmbiguousCredentialsError) { + res.status(err.status).json({ error: err.message, code: err.code }); + return; + } if (err instanceof UserAccessError) { res.status(err.status).json({ error: err.message, code: err.code }); return; diff --git a/apps/api/src/auth/middleware.ts b/apps/api/src/auth/middleware.ts index d1a753c49..439dacb29 100644 --- a/apps/api/src/auth/middleware.ts +++ b/apps/api/src/auth/middleware.ts @@ -2,7 +2,13 @@ // Licensed under the MIT License. import type { Request, RequestHandler } from "express"; -import { AuthError, type AuthProvider } from "shared"; +import { AuthError, isRunToken, runTokenHashPrefix, type AuthProvider } from "shared"; +import type { RunTokenVerifier } from "./run-token-verifier.js"; +import { + SERVICE_KEY_HEADER, + SERVICE_NAME_HEADER, + type ServiceKeyVerifier, +} from "./service-principals.js"; import { ANONYMOUS_USER } from "./types.js"; import type { UserAccessService } from "./user-access-resolver.js"; @@ -27,11 +33,40 @@ function extractBearerToken(req: Request): string | null { return match[1]; } +/** Raised when a request carries both service-key headers and a bearer token. */ +export class AmbiguousCredentialsError extends Error { + readonly status = 400; + readonly code = "ambiguous_credentials"; + + constructor() { + super("Send either service credentials or a bearer token, not both"); + this.name = "AmbiguousCredentialsError"; + } +} + +/** Read a single-valued header, treating empty values as absent. */ +function headerValue(req: Request, name: string): string | undefined { + const value = req.headers[name]; + const first = Array.isArray(value) ? value[0] : value; + return first ? first : undefined; +} + export interface AuthMiddlewareDeps { getProvider: () => AuthProvider | null; + /** Verifies `scope_rt_` bearer tokens. Absent → run tokens are rejected. */ + getRunTokenVerifier?: () => RunTokenVerifier | null; + /** Verifies `X-Service-Name`/`X-Internal-Key`. Absent → service keys are rejected. */ + getServiceKeyVerifier?: () => ServiceKeyVerifier | null; } -/** Verify credentials before either login enrollment or cached access resolution. */ +/** + * Verify credentials before either login enrollment or cached access resolution. + * + * Order: public path → auth disabled (all credentials ignored) → service key + * headers → `scope_rt_` run token → IdP bearer. Run and service callers are + * attached as `req.principal` with an anonymous `req.user` and no `req.auth`, so + * they never reach user enrollment or user-access resolution. + */ export function createAuthMiddleware(deps: AuthMiddlewareDeps): RequestHandler { return async (req, _res, next): Promise => { try { @@ -45,7 +80,39 @@ export function createAuthMiddleware(deps: AuthMiddlewareDeps): RequestHandler { next(); return; } + const serviceName = headerValue(req, SERVICE_NAME_HEADER); + const serviceKey = headerValue(req, SERVICE_KEY_HEADER); + if (serviceName !== undefined || serviceKey !== undefined) { + if (req.headers.authorization !== undefined) throw new AmbiguousCredentialsError(); + const verify = deps.getServiceKeyVerifier?.() ?? null; + if (!serviceName || !serviceKey || !verify) { + throw new AuthError("invalid_service_key", "Invalid service credentials"); + } + const service = verify(serviceName, serviceKey); + req.principal = { kind: "service", name: service.name, scopes: service.scopes }; + req.user = ANONYMOUS_USER; + console.debug(`[auth] service caller "${service.name}" ${req.method} ${req.path}`); + next(); + return; + } const token = extractBearerToken(req); + if (token && isRunToken(token)) { + const verify = deps.getRunTokenVerifier?.() ?? null; + if (!verify) throw new AuthError("invalid_run_token", "Invalid run token"); + const run = await verify(token); + req.principal = { + kind: "run", + requestId: run.requestId, + runId: run.runId, + projectId: run.projectId, + }; + req.user = ANONYMOUS_USER; + console.debug( + `[auth] run caller request=${run.requestId} run=${run.runId} token=${runTokenHashPrefix(run.tokenHash)} ${req.method} ${req.path}`, + ); + next(); + return; + } if (!token) { req.user = ANONYMOUS_USER; next(); diff --git a/apps/api/src/auth/run-token-verifier.ts b/apps/api/src/auth/run-token-verifier.ts new file mode 100644 index 000000000..8cc81f817 --- /dev/null +++ b/apps/api/src/auth/run-token-verifier.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Collection } from "mongodb"; +import { + AuthError, + hashRunToken, + parseRunToken, + runTokenHashesEqual, + type RequestDocument, +} from "shared"; + +/** Default absolute lifetime of a run token, measured from the run's claim. */ +export const DEFAULT_RUN_TOKEN_MAX_AGE_MS = 24 * 60 * 60 * 1000; + +/** Verified run caller derived from a run token. */ +export interface VerifiedRun { + requestId: string; + runId: string; + projectId: string; + /** Hex digest of the presented token (for 8-char log correlation only). */ + tokenHash: string; +} + +/** Verifies a raw run token, throwing {@link AuthError} when it is not accepted. */ +export type RunTokenVerifier = (token: string) => Promise; + +/** Options for {@link createRunTokenVerifier}. */ +export interface RunTokenVerifierOptions { + /** Absolute max age from `run.startedAt`. Defaults to {@link DEFAULT_RUN_TOKEN_MAX_AGE_MS}. */ + maxAgeMs?: number; + /** Injectable clock for tests. */ + now?: () => number; +} + +/** Parse `RUN_TOKEN_MAX_AGE_MS`, falling back to the default on missing/invalid input. */ +export function resolveRunTokenMaxAgeMs(raw = process.env.RUN_TOKEN_MAX_AGE_MS): number { + if (!raw) return DEFAULT_RUN_TOKEN_MAX_AGE_MS; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_RUN_TOKEN_MAX_AGE_MS; +} + +/** + * Build a run-token verifier backed by a point read on the requests collection. + * A token is valid only while its run is `processing` under the claim that + * minted it and within the absolute max age. + */ +export function createRunTokenVerifier( + getCollection: () => Collection | undefined, + options: RunTokenVerifierOptions = {}, +): RunTokenVerifier { + const maxAgeMs = options.maxAgeMs ?? resolveRunTokenMaxAgeMs(); + const now = options.now ?? Date.now; + + return async (token) => { + const parsed = parseRunToken(token); + if (!parsed) throw new AuthError("invalid_run_token", "Malformed run token"); + + const collection = getCollection(); + if (!collection) { + throw new AuthError("service_unavailable", "Run token verification unavailable"); + } + + let doc: Pick | null; + try { + doc = await collection.findOne( + { _id: parsed.requestId }, + { + projection: { + projectId: 1, + deletedAt: 1, + "run._id": 1, + "run.status": 1, + "run.tokenHash": 1, + "run.startedAt": 1, + }, + }, + ); + } catch { + throw new AuthError("service_unavailable", "Run token verification unavailable"); + } + + const tokenHash = hashRunToken(token); + const run = doc?.run; + if (!doc || doc.deletedAt || !run?.tokenHash || !runTokenHashesEqual(run.tokenHash, tokenHash)) { + throw new AuthError("invalid_run_token", "Invalid run token"); + } + if (run.status !== "processing") { + throw new AuthError("run_not_active", "Run is not active"); + } + const startedAt = run.startedAt ? new Date(run.startedAt).getTime() : NaN; + if (!Number.isFinite(startedAt) || now() - startedAt > maxAgeMs) { + throw new AuthError("run_token_expired", "Run token expired"); + } + return { requestId: doc._id, runId: run._id, projectId: doc.projectId, tokenHash }; + }; +} diff --git a/apps/api/src/auth/service-principals.ts b/apps/api/src/auth/service-principals.ts new file mode 100644 index 000000000..b2031bba4 --- /dev/null +++ b/apps/api/src/auth/service-principals.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createHash, timingSafeEqual } from "node:crypto"; +import { AuthError } from "shared"; + +/** Header carrying the calling service's name. */ +export const SERVICE_NAME_HEADER = "x-service-name"; +/** Header carrying the calling service's shared key. */ +export const SERVICE_KEY_HEADER = "x-internal-key"; + +/** Minimum accepted key length; shorter configured keys disable the service. */ +export const MIN_SERVICE_KEY_LENGTH = 32; + +/** Static definition of an internal service caller. */ +export interface ServicePrincipalDefinition { + /** Value expected in `X-Service-Name`. */ + name: string; + /** API env var holding this service's key. */ + keyEnv: string; + /** + * Scopes this service is identified with. Recorded on the principal only; + * enforcement is added by the authorization layer. + */ + scopes: readonly string[]; +} + +/** Known internal services. Each has its own key; there is no shared key. */ +export const SERVICE_PRINCIPALS: readonly ServicePrincipalDefinition[] = Object.freeze([ + { name: "register-agents", keyEnv: "INTERNAL_API_KEY_REGISTER_AGENTS", scopes: ["agents:register"] }, + { + name: "model-scanner", + keyEnv: "INTERNAL_API_KEY_MODEL_SCANNER", + scopes: ["agents:read", "agents:register", "models:sync"], + }, +]); + +/** Verified service caller. */ +export interface VerifiedService { + name: string; + scopes: readonly string[]; +} + +/** Verifies a service name/key pair, throwing {@link AuthError} when rejected. */ +export type ServiceKeyVerifier = (name: string, key: string) => VerifiedService; + +function digest(value: string): Buffer { + return createHash("sha256").update(value, "utf-8").digest(); +} + +/** + * Build a verifier from env. Services whose key is missing or shorter than + * {@link MIN_SERVICE_KEY_LENGTH} are disabled (fail closed). + */ +export function createServiceKeyVerifier( + env: NodeJS.ProcessEnv = process.env, + definitions: readonly ServicePrincipalDefinition[] = SERVICE_PRINCIPALS, +): ServiceKeyVerifier { + const keys = new Map(); + for (const def of definitions) { + const key = env[def.keyEnv]; + if (!key) continue; + if (key.length < MIN_SERVICE_KEY_LENGTH) { + console.warn(`[auth] ${def.keyEnv} is shorter than ${MIN_SERVICE_KEY_LENGTH} characters; service "${def.name}" is disabled`); + continue; + } + keys.set(def.name, { digest: digest(key), scopes: def.scopes }); + } + + return (name, key) => { + const entry = keys.get(name); + // Always hash the presented key so unknown and known services take similar time. + const presented = digest(key); + if (!entry || !timingSafeEqual(entry.digest, presented)) { + throw new AuthError("invalid_service_key", "Invalid service credentials"); + } + return { name, scopes: entry.scopes }; + }; +} diff --git a/apps/api/src/auth/types.ts b/apps/api/src/auth/types.ts index 16d570499..72806a7bb 100644 --- a/apps/api/src/auth/types.ts +++ b/apps/api/src/auth/types.ts @@ -33,6 +33,20 @@ export interface AuthenticatedUser { isService?: boolean; } +/** + * Non-user caller authenticated by a run token or a service key. Kept separate + * from `req.user`/`req.auth` so user identity semantics stay unchanged. + */ +export type CallerPrincipal = + | { kind: "run"; requestId: string; runId: string; projectId: string } + | { kind: "service"; name: string; scopes: readonly string[] }; + +/** Caller classification for logging and (later) authorization decisions. */ +export type Principal = + | { kind: "anonymous" } + | { kind: "user"; user: AuthenticatedUser } + | CallerPrincipal; + // Augment Express's Request so handlers can read `req.user`. declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -40,6 +54,8 @@ declare global { interface Request { auth?: VerifiedAuthContext; user?: AuthenticatedUser; + /** Set only for run-token or service-key callers. */ + principal?: CallerPrincipal; } } } @@ -54,3 +70,10 @@ export const ANONYMOUS_USER: AuthenticatedUser = Object.freeze({ export function getUser(req: Request): AuthenticatedUser { return req.user ?? ANONYMOUS_USER; } + +/** Classify the caller: authenticated user, run/service caller, or anonymous. */ +export function getPrincipal(req: Request): Principal { + const user = getUser(req); + if (user.isAuthenticated) return { kind: "user", user }; + return req.principal ?? { kind: "anonymous" }; +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index f35f6d0de..c81eb89d8 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -14,6 +14,8 @@ import { initTelemetry } from "telemetry"; import type { TaskPromptDocument, SkillDocument, SkillRevisionDocument, CodebaseDocument, CodebaseRevisionDocument, ResourceDocument, ResourceRevisionDocument, ProfileDocument, ProfileVersionDocument, ProjectDocument, HeartbeatStore, AuthProvider, ProfileEnricher, UserDocument } from "shared"; import { UserStore } from "./auth/user-store.js"; import { createAuthMiddleware, createUserAccessMiddleware } from "./auth/middleware.js"; +import { createRunTokenVerifier } from "./auth/run-token-verifier.js"; +import { createServiceKeyVerifier } from "./auth/service-principals.js"; import { authErrorHandler } from "./auth/error-handler.js"; import { RedisUserAccessCache, type UserAccessCache } from "./auth/user-access-cache.js"; import { UserAccessResolver, type UserAccessService } from "./auth/user-access-resolver.js"; @@ -320,6 +322,11 @@ const routeCtx: RouteContext = { storageAccountName, }; +// Run tokens are checked against the live request document; service keys are +// read once from env at startup (missing keys disable that service). +const runTokenVerifier = createRunTokenVerifier(() => collection); +const serviceKeyVerifier = createServiceKeyVerifier(); + // ─── Route registration ─────────────────────────────────────────────────────── app.use("/api/v1/users/me", (_req, res, next) => { res.setHeader("Cache-Control", "no-store"); @@ -328,6 +335,8 @@ app.use("/api/v1/users/me", (_req, res, next) => { app.use( createAuthMiddleware({ getProvider: () => authProvider, + getRunTokenVerifier: () => runTokenVerifier, + getServiceKeyVerifier: () => serviceKeyVerifier, }), ); registerUsersRoutes(routeCtx); diff --git a/apps/api/src/openapi/api-route.test.ts b/apps/api/src/openapi/api-route.test.ts index d4b80d3c0..c5cc5a7b8 100644 --- a/apps/api/src/openapi/api-route.test.ts +++ b/apps/api/src/openapi/api-route.test.ts @@ -226,6 +226,25 @@ describe("apiRoute — OpenAPI registration", () => { expect(resp.description).toBe("Server-sent event stream"); }); + it("marks runToken routes with the x-scope-run-token extension only when set", () => { + apiRoute(app, registry, { + method: "get", path: "/api/v1/flagged/:id", tags: ["Items"], summary: "Flagged", + params: z.object({ id: z.string() }), response: z.any(), runToken: true, + handler: async (_req, res) => { res.end(); }, + }); + apiRoute(app, registry, { + method: "get", path: "/api/v1/plain/:id", tags: ["Items"], summary: "Plain", + params: z.object({ id: z.string() }), response: z.any(), + handler: async (_req, res) => { res.end(); }, + }); + + const doc = generateDoc(registry); + const flagged = doc.paths?.["/api/v1/flagged/{id}"]?.get as Record; + const plain = doc.paths?.["/api/v1/plain/{id}"]?.get as Record; + expect(flagged["x-scope-run-token"]).toBe(true); + expect(plain["x-scope-run-token"]).toBeUndefined(); + }); + it("registers errorResponses with description only", () => { apiRoute(app, registry, { method: "get", diff --git a/apps/api/src/openapi/api-route.ts b/apps/api/src/openapi/api-route.ts index 584e09bad..a9b3e7f14 100644 --- a/apps/api/src/openapi/api-route.ts +++ b/apps/api/src/openapi/api-route.ts @@ -37,6 +37,14 @@ export interface ApiRouteConfig< /** OpenAPI-only security requirements; enforcement remains in auth middleware/handlers. */ security?: RouteConfig["security"]; + /** + * Metadata only: marks a read route that workers call with a run token + * during a run. Authentication accepts run tokens on every route; this flag + * documents the intended run-token surface (`x-scope-run-token` in OpenAPI) + * for the authorization layer to enforce. + */ + runToken?: boolean; + // Schemas (all optional) body?: TBody; query?: TQuery; @@ -138,6 +146,7 @@ export function apiRoute< summary, description, security, + runToken, body, query, params, @@ -195,6 +204,7 @@ export function apiRoute< summary, ...(description ? { description } : {}), ...(security !== undefined ? { security } : {}), + ...(runToken ? { "x-scope-run-token": true } : {}), ...(Object.keys(request).length > 0 ? { request } : {}), responses, } as Parameters[0]); diff --git a/packages/shared/src/auth/types.ts b/packages/shared/src/auth/types.ts index 41a00ea87..bd6cc00a6 100644 --- a/packages/shared/src/auth/types.ts +++ b/packages/shared/src/auth/types.ts @@ -54,7 +54,11 @@ export type AuthErrorCode = | "invalid_issuer" | "missing_claim" | "not_configured" - | "service_unavailable"; + | "service_unavailable" + | "invalid_run_token" + | "run_not_active" + | "run_token_expired" + | "invalid_service_key"; /** Error thrown for any authentication failure. */ export class AuthError extends Error { diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index d30d5cb29..501b8fccb 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -408,6 +408,10 @@ export interface RunState { logsUrl?: string; updatedAt?: Date; startedAt?: Date; // When worker picked up this attempt + /** Server-only SHA-256 digest of the run token minted at claim. Never + * returned by the API, copied to history, or archived; cleared when the + * attempt finishes. Intentionally absent from the zod RunStateSchema. */ + tokenHash?: string; finishedAt?: Date; // When this attempt reached "done" /** When this run was paused (if status = "paused") */ pausedAt?: Date; From cf4071e79d1ca0089e76215088195a28d31c89a5 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:04:00 -0500 Subject: [PATCH 3/8] feat(api): mark worker read routes for run tokens and redact run.tokenHash - Flag the 14 GET routes workers and the judge call during a run with `runToken: true` (MCP server, skill revision by ref + archive, extension, resource + revisions, task prompt + content, codebase revision + archive, criteria list + by id). An inventory test pins the exact list. - Never expose the run-token digest: strip `run.tokenHash` from request get/list, attempt list/detail, history snapshots and run archives. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../openapi-snapshot.test.ts.snap | 14 ++++++ apps/api/src/archive-har.ts | 2 + apps/api/src/routes/codebases.ts | 2 + apps/api/src/routes/criteria.ts | 2 + apps/api/src/routes/extensions.ts | 1 + apps/api/src/routes/mcp-servers.ts | 1 + apps/api/src/routes/requests/index.ts | 9 ++-- apps/api/src/routes/resources.ts | 4 ++ apps/api/src/routes/skills.ts | 2 + apps/api/src/routes/task-prompts.ts | 2 + apps/api/src/run-token-routes.test.ts | 48 +++++++++++++++++++ apps/api/src/runs-repo.ts | 4 +- apps/api/src/utils/redact-run.test.ts | 27 +++++++++++ apps/api/src/utils/redact-run.ts | 18 +++++++ website/src/openapi/scope-openapi.json | 2 +- 15 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/run-token-routes.test.ts create mode 100644 apps/api/src/utils/redact-run.test.ts create mode 100644 apps/api/src/utils/redact-run.ts diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 49b9f80b3..bc4df1284 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -4646,6 +4646,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Codebase Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/codebase-revisions/{id}/archive": { @@ -4665,6 +4666,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Codebase Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/codebases": { @@ -4957,6 +4959,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Criteria", ], + "x-scope-run-token": true, }, "post": { "parameters": [ @@ -5378,6 +5381,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Criteria", ], + "x-scope-run-token": true, }, "put": { "parameters": [ @@ -5657,6 +5661,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Extensions", ], + "x-scope-run-token": true, }, "put": { "parameters": [ @@ -6673,6 +6678,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "MCP Servers", ], + "x-scope-run-token": true, }, "put": { "parameters": [ @@ -10583,6 +10589,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Resource Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/resources/{id}": { @@ -10664,6 +10671,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Resources", ], + "x-scope-run-token": true, }, "patch": { "parameters": [ @@ -10865,6 +10873,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Resource Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/resources/{id}/revisions/{revisionNumber}": { @@ -10918,6 +10927,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Resource Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/runs/upload": { @@ -11012,6 +11022,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Skill Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/skill-revisions/by-ref/{ref}(*)/archive": { @@ -11045,6 +11056,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Skill Revisions", ], + "x-scope-run-token": true, }, }, "/api/v1/skill-revisions/{id}": { @@ -11726,6 +11738,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Task Prompts", ], + "x-scope-run-token": true, }, }, "/api/v1/task-prompts/{id}/content": { @@ -11771,6 +11784,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "tags": [ "Task Prompts", ], + "x-scope-run-token": true, }, }, "/api/v1/task-prompts/{id}/extract-features": { diff --git a/apps/api/src/archive-har.ts b/apps/api/src/archive-har.ts index 204eaf124..a2ce2fcd8 100644 --- a/apps/api/src/archive-har.ts +++ b/apps/api/src/archive-har.ts @@ -60,6 +60,8 @@ export function rewriteHarUrlsForArchive(resource: T): T { const copy = JSON.parse(JSON.stringify(resource)); if (copy.run) { + // Archives are user-facing; drop the server-only run-token digest. + delete copy.run.tokenHash; if (copy.run.harUrl) { copy.run.harUrl = "run.har"; } diff --git a/apps/api/src/routes/codebases.ts b/apps/api/src/routes/codebases.ts index 920a6d5a5..60c8a8571 100644 --- a/apps/api/src/routes/codebases.ts +++ b/apps/api/src/routes/codebases.ts @@ -423,6 +423,7 @@ export function registerCodebasesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/codebase-revisions/:id/archive", + runToken: true, tags: ["Codebase Revisions"], summary: "Download codebase revision archive", response: z.any(), @@ -461,6 +462,7 @@ export function registerCodebasesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/codebase-revisions/:id", + runToken: true, tags: ["Codebase Revisions"], summary: "Get codebase revision by id", response: CodebaseRevisionResponseSchema, diff --git a/apps/api/src/routes/criteria.ts b/apps/api/src/routes/criteria.ts index 36564769e..1ffd28563 100644 --- a/apps/api/src/routes/criteria.ts +++ b/apps/api/src/routes/criteria.ts @@ -217,6 +217,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/criteria", + runToken: true, tags: ["Criteria"], summary: "List criteria", query: z.object({ @@ -399,6 +400,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/criteria/:id", + runToken: true, tags: ["Criteria"], summary: "Get criterion", params: z.object({ id: z.string() }), diff --git a/apps/api/src/routes/extensions.ts b/apps/api/src/routes/extensions.ts index f25e75a2f..aaf5e9352 100644 --- a/apps/api/src/routes/extensions.ts +++ b/apps/api/src/routes/extensions.ts @@ -159,6 +159,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/extensions/:id", + runToken: true, tags: ["Extensions"], summary: "Get extension by ID", params: z.object({ id: z.string() }), diff --git a/apps/api/src/routes/mcp-servers.ts b/apps/api/src/routes/mcp-servers.ts index 985a38f87..8a7a607e7 100644 --- a/apps/api/src/routes/mcp-servers.ts +++ b/apps/api/src/routes/mcp-servers.ts @@ -103,6 +103,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/mcp/servers/:id", + runToken: true, tags: ["MCP Servers"], summary: "Get MCP server", params: z.object({ id: z.string() }), diff --git a/apps/api/src/routes/requests/index.ts b/apps/api/src/routes/requests/index.ts index 0d719ffbb..b31789d81 100644 --- a/apps/api/src/routes/requests/index.ts +++ b/apps/api/src/routes/requests/index.ts @@ -59,6 +59,7 @@ import { computeAnalysis, capRunsToLimit } from "../../analysis.js"; import type { AnalysisResponse, AnalyzableRun } from "../../analysis.js"; import { parseStateKey } from "../../criteria-mdp.js"; import { buildGroupingPipeline } from "../../grouping.js"; +import { redactRunSecrets, redactRunState } from "../../utils/redact-run.js"; import { buildMultiClause, buildSearchClause, @@ -1410,7 +1411,7 @@ apiRoute(ctx.app, ctx.registry, { } // Map _id back to id for API response - res.json({ ...resource, id: resource._id }); + res.json({ ...redactRunSecrets(resource), id: resource._id }); }, }); @@ -1754,7 +1755,7 @@ apiRoute(ctx.app, ctx.registry, { resources.reverse(); } - const data = resources.map((r) => ({ ...r, id: r._id })); + const data = resources.map((r) => ({ ...redactRunSecrets(r), id: r._id })); // Enrich `processing` runs with the latest liveness heartbeat from // Redis. The store's mget is cluster-safe (a pipeline of single-key GETs, @@ -3020,7 +3021,7 @@ apiRoute(ctx.app, ctx.registry, { return; } const history = await listHistoricalRuns({ runsCollection: ctx.runsCollection }, id); - const current = request.run ? [request.run] : []; + const current = request.run ? [redactRunState(request.run)] : []; // Combine current + history; current is always the highest attemptNumber res.json([...current, ...history]); }, @@ -3043,7 +3044,7 @@ apiRoute(ctx.app, ctx.registry, { return; } if (request.run?._id === runId) { - res.json(request.run); + res.json(redactRunState(request.run)); return; } const historical = await getHistoricalRun({ runsCollection: ctx.runsCollection }, runId); diff --git a/apps/api/src/routes/resources.ts b/apps/api/src/routes/resources.ts index f8c666e0d..b456b8ef7 100644 --- a/apps/api/src/routes/resources.ts +++ b/apps/api/src/routes/resources.ts @@ -154,6 +154,7 @@ export function registerResourcesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/resources/:id", + runToken: true, tags: ["Resources"], summary: "Get a resource", params: z.object({ id: z.string() }), @@ -329,6 +330,7 @@ export function registerResourcesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/resources/:id/revisions/latest", + runToken: true, tags: ["Resource Revisions"], summary: "Get the latest resource revision", params: z.object({ id: z.string() }), @@ -357,6 +359,7 @@ export function registerResourcesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/resources/:id/revisions/:revisionNumber", + runToken: true, tags: ["Resource Revisions"], summary: "Get a resource revision by resource and revision number", params: z.object({ id: z.string(), revisionNumber: z.string() }), @@ -390,6 +393,7 @@ export function registerResourcesRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/resources/revisions/:id", + runToken: true, tags: ["Resource Revisions"], summary: "Get resource revision by id", params: z.object({ id: z.string() }), diff --git a/apps/api/src/routes/skills.ts b/apps/api/src/routes/skills.ts index 59ca38434..733692ada 100644 --- a/apps/api/src/routes/skills.ts +++ b/apps/api/src/routes/skills.ts @@ -560,6 +560,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/skill-revisions/by-ref/:ref(*)/archive", + runToken: true, tags: ["Skill Revisions"], summary: "Download skill revision archive", query: ProjectIdQuerySchema, @@ -643,6 +644,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/skill-revisions/by-ref/:ref(*)", + runToken: true, tags: ["Skill Revisions"], summary: "Get skill revision by ref", query: ProjectIdQuerySchema, diff --git a/apps/api/src/routes/task-prompts.ts b/apps/api/src/routes/task-prompts.ts index f626ea079..6ecaebc7c 100644 --- a/apps/api/src/routes/task-prompts.ts +++ b/apps/api/src/routes/task-prompts.ts @@ -103,6 +103,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/task-prompts/:id", + runToken: true, tags: ["Task Prompts"], summary: "Get task prompt", params: z.object({ id: z.string() }), @@ -134,6 +135,7 @@ apiRoute(ctx.app, ctx.registry, { apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/task-prompts/:id/content", + runToken: true, tags: ["Task Prompts"], summary: "Get resolved task prompt content", params: z.object({ id: z.string() }), diff --git a/apps/api/src/run-token-routes.test.ts b/apps/api/src/run-token-routes.test.ts new file mode 100644 index 000000000..529ead262 --- /dev/null +++ b/apps/api/src/run-token-routes.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("db-migrations/check-migrations", () => ({ + checkMigrations: vi.fn().mockResolvedValue(undefined), +})); + +const { generateOpenAPIDocument } = await import("./openapi/registry.js"); +await import("./index.js"); + +/** + * Worker/judge read routes that are expected to be called with a run token. + * Changing this list changes the documented run-token surface; keep it in sync + * with the worker clients and the authorization layer. + */ +const EXPECTED_RUN_TOKEN_ROUTES = [ + "GET /api/v1/codebase-revisions/{id}", + "GET /api/v1/codebase-revisions/{id}/archive", + "GET /api/v1/criteria", + "GET /api/v1/criteria/{id}", + "GET /api/v1/extensions/{id}", + "GET /api/v1/mcp/servers/{id}", + "GET /api/v1/resources/revisions/{id}", + "GET /api/v1/resources/{id}", + "GET /api/v1/resources/{id}/revisions/latest", + "GET /api/v1/resources/{id}/revisions/{revisionNumber}", + "GET /api/v1/skill-revisions/by-ref/{ref}", + "GET /api/v1/skill-revisions/by-ref/{ref}/archive", + "GET /api/v1/task-prompts/{id}", + "GET /api/v1/task-prompts/{id}/content", +].sort(); + +describe("run-token route inventory", () => { + it("flags exactly the worker read routes", () => { + const doc = generateOpenAPIDocument(); + const flagged: string[] = []; + for (const [path, item] of Object.entries(doc.paths ?? {})) { + for (const [method, op] of Object.entries(item as Record)) { + if ((op as Record | undefined)?.["x-scope-run-token"] === true) { + flagged.push(`${method.toUpperCase()} ${path.replace("(*)", "")}`); + } + } + } + expect(flagged.sort()).toEqual(EXPECTED_RUN_TOKEN_ROUTES); + }); +}); diff --git a/apps/api/src/runs-repo.ts b/apps/api/src/runs-repo.ts index a56f07b0a..469152d79 100644 --- a/apps/api/src/runs-repo.ts +++ b/apps/api/src/runs-repo.ts @@ -22,6 +22,7 @@ import type { Collection } from "mongodb"; import type { RunState, RunHistoryDocument } from "shared"; +import { redactRunState } from "./utils/redact-run.js"; export interface RunsRepoOptions { runsCollection: Collection; @@ -40,7 +41,8 @@ export async function insertHistoricalRun( run: RunState, projectId: string, ): Promise { - const doc: RunHistoryDocument = { ...run, requestId, projectId }; + // History never carries the claim's run-token digest. + const doc: RunHistoryDocument = { ...redactRunState(run), requestId, projectId }; await opts.runsCollection.insertOne(doc as any); } diff --git a/apps/api/src/utils/redact-run.test.ts b/apps/api/src/utils/redact-run.test.ts new file mode 100644 index 000000000..862733387 --- /dev/null +++ b/apps/api/src/utils/redact-run.test.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { redactRunSecrets, redactRunState } from "./redact-run.js"; + +describe("redact-run", () => { + it("removes tokenHash from a run", () => { + const run = { _id: "r1", status: "processing" as const, tokenHash: "a".repeat(64) }; + expect(redactRunState(run)).toEqual({ _id: "r1", status: "processing" }); + expect(run.tokenHash).toBeDefined(); + }); + + it("removes tokenHash from an embedded run without mutating the source", () => { + const doc = { _id: "q1", run: { _id: "r1", tokenHash: "b".repeat(64) } }; + const redacted = redactRunSecrets(doc); + expect(redacted.run).toEqual({ _id: "r1" }); + expect(doc.run.tokenHash).toBeDefined(); + }); + + it("returns documents without secrets unchanged", () => { + const doc = { _id: "q1", run: { _id: "r1" } }; + expect(redactRunSecrets(doc)).toBe(doc); + const noRun = { _id: "q2" }; + expect(redactRunSecrets(noRun)).toBe(noRun); + }); +}); diff --git a/apps/api/src/utils/redact-run.ts b/apps/api/src/utils/redact-run.ts new file mode 100644 index 000000000..f9928f4cf --- /dev/null +++ b/apps/api/src/utils/redact-run.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { RunState } from "shared"; + +/** Return a run without server-only secrets (`tokenHash`). */ +export function redactRunState>(run: T): Omit { + if (!("tokenHash" in run)) return run; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { tokenHash: _tokenHash, ...rest } = run; + return rest; +} + +/** Return a request-like document whose embedded `run` has no server-only secrets. */ +export function redactRunSecrets | null }>(doc: T): T { + if (!doc.run || !("tokenHash" in doc.run)) return doc; + return { ...doc, run: redactRunState(doc.run) }; +} diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index a885790ae..5dab98c67 100644 --- a/website/src/openapi/scope-openapi.json +++ b/website/src/openapi/scope-openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","requestId","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","requestId","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","x-scope-run-token":true,"parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","x-scope-run-token":true,"parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","x-scope-run-token":true,"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","x-scope-run-token":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","x-scope-run-token":true,"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file From 479b7002a64f05d98ac1d514a4a0679bc8453f98 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:05:43 -0500 Subject: [PATCH 4/8] feat(shared): mint run token at claim and send it from worker API clients The queue processor mints an opaque run token when it atomically claims a run, persists only its SHA-256 digest in the claim ($set run.tokenHash), and keeps the raw token in memory for the life of the handling. MCP, skill, extension, resource, prompt, codebase, gate-prompt and judge clients send it as a bearer via an optional BearerProvider. The digest is unset on the terminal success and error writes, and the in-memory token is dropped when handling ends. Clients without a bearer behave exactly as before. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../shared/src/codebases/codebase-client.ts | 10 ++-- .../shared/src/extensions/extension-client.ts | 8 ++- packages/shared/src/judge/judge-client.ts | 7 ++- packages/shared/src/mcp/mcp-server-client.ts | 8 ++- .../shared/src/queue/base-queue-processor.ts | 2 + .../shared/src/queue/queue-processor.test.ts | 9 ++++ packages/shared/src/queue/queue-processor.ts | 51 +++++++++++++++---- .../shared/src/resources/resource-client.ts | 8 ++- packages/shared/src/skills/skill-client.ts | 10 ++-- .../shared/src/task-prompts/prompt-client.ts | 8 ++- 10 files changed, 96 insertions(+), 25 deletions(-) diff --git a/packages/shared/src/codebases/codebase-client.ts b/packages/shared/src/codebases/codebase-client.ts index 74ffe17ca..422d39562 100644 --- a/packages/shared/src/codebases/codebase-client.ts +++ b/packages/shared/src/codebases/codebase-client.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { CodebaseConfig, CodebaseRevisionDocument } from "../types/codebase.js"; +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** * Client for resolving codebase revisions via the Scope REST API. @@ -13,9 +14,12 @@ import type { CodebaseConfig, CodebaseRevisionDocument } from "../types/codebase */ export class CodebaseClient { private readonly apiUrl: string; + /** Supplies the run token (when present) for API calls. */ + private readonly bearer?: BearerProvider; - constructor(apiUrl: string) { + constructor(apiUrl: string, bearer?: BearerProvider) { this.apiUrl = apiUrl.replace(/\/+$/, ""); + this.bearer = bearer; } /** @@ -26,7 +30,7 @@ export class CodebaseClient { */ async resolveCodebase(revisionId: string): Promise { const url = `${this.apiUrl}/api/v1/codebase-revisions/${encodeURIComponent(revisionId)}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Codebase revision '${revisionId}' not found via API`); } @@ -52,7 +56,7 @@ export class CodebaseClient { */ async downloadCodebaseArchive(revisionId: string): Promise { const url = `${this.apiUrl}/api/v1/codebase-revisions/${encodeURIComponent(revisionId)}/archive`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Codebase archive for '${revisionId}' not found via API`); } diff --git a/packages/shared/src/extensions/extension-client.ts b/packages/shared/src/extensions/extension-client.ts index 55d6c02cd..127d2f5ce 100644 --- a/packages/shared/src/extensions/extension-client.ts +++ b/packages/shared/src/extensions/extension-client.ts @@ -3,6 +3,7 @@ import type { ExtensionConfig, ExtensionDocument, ExtensionSearchResult, ExtensionVersionInfo } from '../types/extension.js'; import { parseExtensionSpec } from '../types/extension.js'; +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** VS Code marketplace Gallery API response types (subset). */ interface MarketplaceExtension { @@ -25,11 +26,14 @@ interface MarketplaceResponse { */ export class ExtensionClient { private readonly apiUrl: string; + /** Supplies the run token (when present) for API calls. */ + private readonly bearer?: BearerProvider; private static readonly MARKETPLACE_URL = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"; - constructor(apiUrl: string) { + constructor(apiUrl: string, bearer?: BearerProvider) { this.apiUrl = apiUrl.replace(/\/+$/, ''); + this.bearer = bearer; } /** @@ -101,7 +105,7 @@ export class ExtensionClient { for (const spec of specs) { const { id, version } = parseExtensionSpec(spec); const url = `${this.apiUrl}/api/v1/extensions/${encodeURIComponent(id)}${projectQuery}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Extension '${id}' not found via API`); diff --git a/packages/shared/src/judge/judge-client.ts b/packages/shared/src/judge/judge-client.ts index acbf4eb49..1f625af8a 100644 --- a/packages/shared/src/judge/judge-client.ts +++ b/packages/shared/src/judge/judge-client.ts @@ -3,6 +3,7 @@ import { ConversationTurn, CriterionResult, GateId } from "../types/types.js"; import { withRetry } from "../utils/retry.js"; +import { bearerHeaders, type BearerProvider } from "../http/bearer.js"; /** * Request payload for the judge service's /api/v1/evaluate endpoint. @@ -48,6 +49,8 @@ export interface JudgeEvaluateResponse { } export interface JudgeClientOptions { + /** Supplies the run token forwarded to the judge (and on to the API). */ + bearer?: BearerProvider; /** Timeout in ms for evaluate requests (default: JUDGE_CLIENT_TIMEOUT env or 600000 = 10 minutes) */ timeoutMs?: number; /** Maximum retry attempts on timeout/transient errors (default: JUDGE_CLIENT_RETRIES env or 2) */ @@ -147,6 +150,7 @@ export class JudgeClient { private baseUrl: string; private timeoutMs: number; private maxRetries: number; + private bearer?: BearerProvider; constructor(baseUrl: string, options?: JudgeClientOptions) { // Strip trailing slash @@ -155,6 +159,7 @@ export class JudgeClient { ?? parseInt(process.env.JUDGE_CLIENT_TIMEOUT || String(DEFAULT_JUDGE_CLIENT_TIMEOUT)); this.maxRetries = options?.maxRetries ?? parseInt(process.env.JUDGE_CLIENT_RETRIES || String(DEFAULT_JUDGE_CLIENT_RETRIES)); + this.bearer = options?.bearer; } /** @@ -189,7 +194,7 @@ export class JudgeClient { private async doEvaluate(url: string, request: JudgeEvaluateRequest): Promise { const response = await fetch(url, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...bearerHeaders(this.bearer) }, body: JSON.stringify(request), signal: AbortSignal.timeout(this.timeoutMs), }); diff --git a/packages/shared/src/mcp/mcp-server-client.ts b/packages/shared/src/mcp/mcp-server-client.ts index 1ff3e4da5..fec36e597 100644 --- a/packages/shared/src/mcp/mcp-server-client.ts +++ b/packages/shared/src/mcp/mcp-server-client.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { McpServerConfig, McpServerDocument } from '../types/mcp.js'; +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** * Client for resolving MCP server slugs via the Scope REST API. @@ -12,9 +13,12 @@ import type { McpServerConfig, McpServerDocument } from '../types/mcp.js'; */ export class McpServerClient { private readonly apiUrl: string; + /** Supplies the run token (when present) for API calls. */ + private readonly bearer?: BearerProvider; - constructor(apiUrl: string) { + constructor(apiUrl: string, bearer?: BearerProvider) { this.apiUrl = apiUrl.replace(/\/+$/, ''); + this.bearer = bearer; } /** @@ -30,7 +34,7 @@ export class McpServerClient { for (const slug of slugs) { const url = `${this.apiUrl}/api/v1/mcp/servers/${encodeURIComponent(slug)}?projectId=${encodeURIComponent(projectId)}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`MCP server '${slug}' not found via API`); diff --git a/packages/shared/src/queue/base-queue-processor.ts b/packages/shared/src/queue/base-queue-processor.ts index 08bc35d51..7af995edb 100644 --- a/packages/shared/src/queue/base-queue-processor.ts +++ b/packages/shared/src/queue/base-queue-processor.ts @@ -362,6 +362,8 @@ export abstract class BaseQueueProcessor 0) { diff --git a/packages/shared/src/queue/queue-processor.test.ts b/packages/shared/src/queue/queue-processor.test.ts index 6a7ed97da..09ad267e6 100644 --- a/packages/shared/src/queue/queue-processor.test.ts +++ b/packages/shared/src/queue/queue-processor.test.ts @@ -7,6 +7,7 @@ import { CodingAgentQueueProcessor, pairBindingsWithConfigs } from "./queue-proc import type { QueueProcessorConfig, WorkerProcessor, WorkerResult } from "../types/types.js"; import type { VisibilityHeartbeat } from "./visibility-heartbeat.js"; import { InMemoryHeartbeatStore } from "./heartbeat-store.js"; +import { hashRunToken } from "../auth/run-token.js"; const testConfig: QueueProcessorConfig = { mongoUri: "mongodb://localhost:27017", @@ -526,9 +527,15 @@ describe("CodingAgentQueueProcessor pre-processing failures", () => { await (qp as any).processMessage(message); + // The claim persists only the digest; the worker sends the raw run token. + const claimedHash = updateOne.mock.calls[0][1].$set["run.tokenHash"] as string; + expect(claimedHash).toMatch(/^[0-9a-f]{64}$/); expect(fetchMock).toHaveBeenCalledWith( "http://api:80/api/v1/extensions/ms-python.python?projectId=project%20with%20spaces", + { headers: { Authorization: expect.stringMatching(/^Bearer scope_rt_req-extension-failure_/) } }, ); + const sentToken = (fetchMock.mock.calls[0][1].headers.Authorization as string).slice("Bearer ".length); + expect(hashRunToken(sentToken)).toBe(claimedHash); expect(updateOne).toHaveBeenCalledTimes(2); expect(updateOne.mock.calls[0][0]).toEqual({ _id: requestId, @@ -548,6 +555,8 @@ describe("CodingAgentQueueProcessor pre-processing failures", () => { "run.error": expect.stringMatching(/400 Bad Request/), "run.durationMs": expect.any(Number), }); + expect(updateOne.mock.calls[1][1].$unset).toEqual({ "run.tokenHash": "" }); + expect((qp as any).runTokens.size).toBe(0); expect((qp as any).processMultiTurn).not.toHaveBeenCalled(); expect(deleteMessage).toHaveBeenCalledWith("message-1", "receipt-1"); }); diff --git a/packages/shared/src/queue/queue-processor.ts b/packages/shared/src/queue/queue-processor.ts index 2be51bf18..6762f2f03 100644 --- a/packages/shared/src/queue/queue-processor.ts +++ b/packages/shared/src/queue/queue-processor.ts @@ -40,6 +40,8 @@ import { CodebaseClient } from "../codebases/codebase-client.js"; import { ResourceClient } from "../resources/resource-client.js"; import type { ResourceConfig } from "../types/resource.js"; import { seedCodebaseToWorkspace } from "../codebases/codebase-seeder.js"; +import { mintRunToken } from "../auth/run-token.js"; +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** * Pair each resource binding with its resolved config, preserving submission @@ -81,6 +83,8 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor(); constructor(config: QueueProcessorConfig, processor: WorkerProcessor) { super(config, processor.workerName); @@ -152,12 +156,32 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor this.runTokens.get(requestId); + } + protected async handleRequest( requestDoc: RequestDocument, message: DequeuedMessageItem, heartbeat: VisibilityHeartbeat, log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, payload?: Record, + ): Promise { + try { + await this.handleClaimableRequest(requestDoc, message, heartbeat, log, payload); + } finally { + // The run token never outlives this worker's handling of the run. + this.runTokens.delete(requestDoc._id); + } + } + + private async handleClaimableRequest( + requestDoc: RequestDocument, + message: DequeuedMessageItem, + heartbeat: VisibilityHeartbeat, + log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, + payload?: Record, ): Promise { const runtimeAgentVersion = this.runtimeAgentVersion; if (requestDoc.run?.status === "pending") { @@ -403,7 +427,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor s.name).join(", ")}`); @@ -449,7 +473,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor s.name).join(", ")}`); @@ -462,7 +486,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor e.version ? `${e.id}@${e.version}` : e.id).join(", ")}`); @@ -478,7 +502,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor b.ref) }); const resolved = await resourceClient.resolveResources( requestDoc.projectId, @@ -504,6 +528,8 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor this.collection.updateOne( { _id: requestDoc._id, @@ -524,6 +550,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor) => Promise, + bearer?: BearerProvider, ): Promise { if (!promptId) { throw new Error("Gate is missing a promptId"); @@ -657,7 +687,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor(path: string, projectId: string): Promise { const sep = path.includes("?") ? "&" : "?"; const url = `${this.apiUrl}${path}${sep}projectId=${encodeURIComponent(projectId)}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) return null; if (!res.ok) { throw new Error(`[ResourceClient] GET ${url} failed: ${res.status} ${res.statusText}`); diff --git a/packages/shared/src/skills/skill-client.ts b/packages/shared/src/skills/skill-client.ts index 5dd286cad..3622fe970 100644 --- a/packages/shared/src/skills/skill-client.ts +++ b/packages/shared/src/skills/skill-client.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { SkillConfig, SkillRevisionDocument } from '../types/skill.js'; +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** * Client for resolving skill revision refs via the Scope REST API. @@ -12,9 +13,12 @@ import type { SkillConfig, SkillRevisionDocument } from '../types/skill.js'; */ export class SkillClient { private readonly apiUrl: string; + /** Supplies the run token (when present) for API calls. */ + private readonly bearer?: BearerProvider; - constructor(apiUrl: string) { + constructor(apiUrl: string, bearer?: BearerProvider) { this.apiUrl = apiUrl.replace(/\/+$/, ''); + this.bearer = bearer; } /** @@ -36,7 +40,7 @@ export class SkillClient { for (const ref of refs) { const url = `${this.apiUrl}/api/v1/skill-revisions/by-ref/${encodeURIComponent(ref)}${projectQuery}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Skill revision '${ref}' not found via API`); @@ -70,7 +74,7 @@ export class SkillClient { */ async downloadSkillArchive(projectId: string, ref: string): Promise { const url = `${this.apiUrl}/api/v1/skill-revisions/by-ref/${encodeURIComponent(ref)}/archive?projectId=${encodeURIComponent(projectId)}`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Skill archive for '${ref}' not found via API`); diff --git a/packages/shared/src/task-prompts/prompt-client.ts b/packages/shared/src/task-prompts/prompt-client.ts index ade51cc2f..75bb04224 100644 --- a/packages/shared/src/task-prompts/prompt-client.ts +++ b/packages/shared/src/task-prompts/prompt-client.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { fetchWithBearer, type BearerProvider } from "../http/bearer.js"; /** * Client for resolving prompt text (task or AGENTS.md) via the Scope REST API. * @@ -11,9 +12,12 @@ */ export class PromptClient { private readonly apiUrl: string; + /** Supplies the run token (when present) for API calls. */ + private readonly bearer?: BearerProvider; - constructor(apiUrl: string) { + constructor(apiUrl: string, bearer?: BearerProvider) { this.apiUrl = apiUrl.replace(/\/+$/, ""); + this.bearer = bearer; } /** @@ -24,7 +28,7 @@ export class PromptClient { */ async getText(id: string): Promise { const url = `${this.apiUrl}/api/v1/task-prompts/${encodeURIComponent(id)}/content`; - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) { throw new Error(`Prompt '${id}' not found via API`); From 4745c6e91e85437f592c9c60260453e313daeeb0 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:06:41 -0500 Subject: [PATCH 5/8] feat(judge): forward the caller's run token on criteria reads The judge extracts a well-formed run token from the evaluate request's Authorization header and, when present, builds a per-evaluation REST criteria provider that sends it as a bearer. Requests without a run token keep using the cached per-project provider. RestApiCriteriaProvider and createCriteriaProvider accept an optional BearerProvider. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/judge/src/index.ts | 2 + apps/judge/src/judge-agent.ts | 11 ++++- apps/judge/src/run-token-forwarding.test.ts | 22 +++++++++ apps/judge/src/run-token-forwarding.ts | 17 +++++++ .../criteria/criteria-provider-api.test.ts | 49 +++++++++++++++++++ .../src/criteria/criteria-provider-api.ts | 13 +++-- .../src/criteria/criteria-provider-factory.ts | 12 ++++- 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 apps/judge/src/run-token-forwarding.test.ts create mode 100644 apps/judge/src/run-token-forwarding.ts create mode 100644 packages/shared/src/criteria/criteria-provider-api.test.ts diff --git a/apps/judge/src/index.ts b/apps/judge/src/index.ts index 9bf7c967e..7cb64f329 100644 --- a/apps/judge/src/index.ts +++ b/apps/judge/src/index.ts @@ -7,6 +7,7 @@ import { evaluateWorkspace } from "./judge-agent.js"; import { verifyCopilotProtocol } from "./protocol-check.js"; import { resolveCurrentIteration, mapIterationToolCallUrls } from "./tool-call-history.js"; import { BlobStorage, RedisLogPublisher } from "shared"; +import { forwardableRunToken } from "./run-token-forwarding.js"; import { initTelemetry, trackMetric, trackEvent } from "telemetry"; import { mkdtempSync, rmSync } from "fs"; import { tmpdir } from "os"; @@ -179,6 +180,7 @@ app.post( iterationToolCalls, projectId, currentAgentResponse, + runToken: forwardableRunToken(req.headers.authorization), }); const elapsed = Date.now() - startTime; diff --git a/apps/judge/src/judge-agent.ts b/apps/judge/src/judge-agent.ts index ef12ffad5..33353eb77 100644 --- a/apps/judge/src/judge-agent.ts +++ b/apps/judge/src/judge-agent.ts @@ -10,7 +10,7 @@ import { GateId, IterationToolCalls, } from "shared"; -import { getCriteriaProvider } from "shared/criteria-provider-factory"; +import { createCriteriaProvider, getCriteriaProvider } from "shared/criteria-provider-factory"; import { createJudgeStrategy } from "./judge-strategies.js"; import { FeedbackGenerator } from "./feedback-generator.js"; @@ -31,6 +31,8 @@ export interface EvaluationInput { projectId?: string; /** The coding agent's response (prose) for the iteration being judged, exposed to the judge via read_agent_response. */ currentAgentResponse?: string; + /** The calling run's token, forwarded on criteria reads. Never logged. */ + runToken?: string; } export interface EvaluationResult { @@ -65,7 +67,12 @@ export async function evaluateWorkspace( // 3. Resolve criteria IDs from provider (with ancestor dependencies) let normalizedCriteria: CriteriaConfig[]; try { - const provider = getCriteriaProvider(input.projectId); + // A run-token caller gets a per-evaluation provider so the credential is + // never shared through the cached provider. + const runToken = input.runToken; + const provider = runToken + ? createCriteriaProvider(input.projectId, { bearer: () => runToken }) + : getCriteriaProvider(input.projectId); normalizedCriteria = await provider.resolveWithAncestors(input.criteria); console.log( `[judge-agent] Loaded ${normalizedCriteria.length} criteria (including ancestors) from provider` diff --git a/apps/judge/src/run-token-forwarding.test.ts b/apps/judge/src/run-token-forwarding.test.ts new file mode 100644 index 000000000..36b31d228 --- /dev/null +++ b/apps/judge/src/run-token-forwarding.test.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { mintRunToken } from "shared"; +import { forwardableRunToken } from "./run-token-forwarding.js"; + +describe("forwardableRunToken", () => { + it("forwards a well-formed run token", () => { + const { token } = mintRunToken("req-1"); + expect(forwardableRunToken(`Bearer ${token}`)).toBe(token); + expect(forwardableRunToken(`bearer ${token}`)).toBe(token); + }); + + it("does not forward missing, malformed, or non-run credentials", () => { + expect(forwardableRunToken(undefined)).toBeUndefined(); + expect(forwardableRunToken("")).toBeUndefined(); + expect(forwardableRunToken("Bearer eyJhbGciOi.user.jwt")).toBeUndefined(); + expect(forwardableRunToken("Basic abc")).toBeUndefined(); + expect(forwardableRunToken("Bearer scope_rt_req-1_tampered")).toBeUndefined(); + }); +}); diff --git a/apps/judge/src/run-token-forwarding.ts b/apps/judge/src/run-token-forwarding.ts new file mode 100644 index 000000000..34f6d4eff --- /dev/null +++ b/apps/judge/src/run-token-forwarding.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { parseRunToken } from "shared"; + +/** + * Extract the caller's run token from an `Authorization` header so the judge can + * forward it on its own API reads. Only well-formed run tokens are forwarded; + * anything else (missing, other schemes, user tokens) yields `undefined`. + */ +export function forwardableRunToken(authorization: string | undefined): string | undefined { + if (!authorization) return undefined; + const match = /^Bearer\s+(\S+)$/i.exec(authorization.trim()); + if (!match) return undefined; + const token = match[1]; + return parseRunToken(token) ? token : undefined; +} diff --git a/packages/shared/src/criteria/criteria-provider-api.test.ts b/packages/shared/src/criteria/criteria-provider-api.test.ts new file mode 100644 index 000000000..875f6a3bd --- /dev/null +++ b/packages/shared/src/criteria/criteria-provider-api.test.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { RestApiCriteriaProvider } from "./criteria-provider-api.js"; +import { createCriteriaProvider } from "./criteria-provider-factory.js"; + +const criterion = { id: "c1", name: "C1", description: "d", dependsOn: [] }; + +function stubFetch() { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: "OK", + json: async () => [criterion], + }); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +describe("RestApiCriteriaProvider bearer", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + }); + + it("sends no Authorization header without a bearer", async () => { + const fetchMock = stubFetch(); + await new RestApiCriteriaProvider("http://api/", { projectId: "p1" }).getAll(); + expect(fetchMock).toHaveBeenCalledWith("http://api/api/v1/criteria?projectId=p1"); + }); + + it("sends the run token as a bearer when provided", async () => { + const fetchMock = stubFetch(); + await new RestApiCriteriaProvider("http://api", { bearer: () => "scope_rt_x" }).getAll(); + expect(fetchMock).toHaveBeenCalledWith("http://api/api/v1/criteria", { + headers: { Authorization: "Bearer scope_rt_x" }, + }); + }); + + it("createCriteriaProvider wires the bearer into the REST provider", async () => { + vi.stubEnv("CRITERIA_API_URL", "http://api"); + const fetchMock = stubFetch(); + await createCriteriaProvider("p1", { bearer: () => "scope_rt_y" }).getAll(); + expect(fetchMock).toHaveBeenCalledWith("http://api/api/v1/criteria?projectId=p1", { + headers: { Authorization: "Bearer scope_rt_y" }, + }); + }); +}); diff --git a/packages/shared/src/criteria/criteria-provider-api.ts b/packages/shared/src/criteria/criteria-provider-api.ts index 176045c97..2e5eca4ea 100644 --- a/packages/shared/src/criteria/criteria-provider-api.ts +++ b/packages/shared/src/criteria/criteria-provider-api.ts @@ -3,6 +3,7 @@ import { CriteriaConfig } from '../types/types.js'; import { CriteriaProvider } from './criteria-provider.js'; +import { fetchWithBearer, type BearerProvider } from '../http/bearer.js'; interface CacheEntry { value: CriteriaConfig; @@ -34,13 +35,19 @@ export class RestApiCriteriaProvider implements CriteriaProvider { private readonly maxSize: number; private readonly ttlMs: number; private readonly projectId?: string; + /** Supplies the run token (when present) sent with criteria reads. */ + private readonly bearer?: BearerProvider; - constructor(apiUrl: string, options?: { maxSize?: number; ttlMs?: number; projectId?: string }) { + constructor( + apiUrl: string, + options?: { maxSize?: number; ttlMs?: number; projectId?: string; bearer?: BearerProvider }, + ) { // Strip trailing slash this.apiUrl = apiUrl.replace(/\/+$/, ''); this.maxSize = options?.maxSize ?? 200; this.ttlMs = options?.ttlMs ?? 60_000; // 1 minute this.projectId = options?.projectId; + this.bearer = options?.bearer; } /** Append ?projectId= when this provider is bound to a project (per-project isolation). */ @@ -64,7 +71,7 @@ export class RestApiCriteriaProvider implements CriteriaProvider { // 2. Fetch from API const url = this.withProject(`${this.apiUrl}/api/v1/criteria/${encodeURIComponent(id)}`); - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (res.status === 404) return undefined; if (!res.ok) { @@ -86,7 +93,7 @@ export class RestApiCriteriaProvider implements CriteriaProvider { // 2. Fetch from API const url = this.withProject(`${this.apiUrl}/api/v1/criteria`); - const res = await fetch(url); + const res = await fetchWithBearer(url, this.bearer); if (!res.ok) { throw new Error(`[RestApiCriteriaProvider] GET ${url} failed: ${res.status} ${res.statusText}`); diff --git a/packages/shared/src/criteria/criteria-provider-factory.ts b/packages/shared/src/criteria/criteria-provider-factory.ts index 612a2b0aa..6359fb977 100644 --- a/packages/shared/src/criteria/criteria-provider-factory.ts +++ b/packages/shared/src/criteria/criteria-provider-factory.ts @@ -5,6 +5,7 @@ import { join } from 'path'; import { CriteriaProvider } from './criteria-provider.js'; import { FileSystemCriteriaProvider } from './criteria-provider-fs.js'; import { RestApiCriteriaProvider } from './criteria-provider-api.js'; +import type { BearerProvider } from '../http/bearer.js'; /** * Create a CriteriaProvider based on environment variables: @@ -16,12 +17,19 @@ import { RestApiCriteriaProvider } from './criteria-provider-api.js'; * When `projectId` is provided, a REST provider is bound to that project so its * criteria fetches are isolated (`?projectId=`); the filesystem provider ignores * it (single-tenant local config). + * + * `opts.bearer` attaches a caller credential (the run token) to REST reads. + * Providers built with a bearer are per-evaluation and must not be cached. */ -export function createCriteriaProvider(projectId?: string): CriteriaProvider { +export function createCriteriaProvider( + projectId?: string, + opts?: { bearer?: BearerProvider }, +): CriteriaProvider { const apiUrl = process.env.CRITERIA_API_URL; if (apiUrl) { console.log(`[CriteriaProviderFactory] Using REST API provider: ${apiUrl}${projectId ? ` (project ${projectId})` : ''}`); - return new RestApiCriteriaProvider(apiUrl, projectId ? { projectId } : undefined); + const options = { ...(projectId ? { projectId } : {}), ...(opts?.bearer ? { bearer: opts.bearer } : {}) }; + return new RestApiCriteriaProvider(apiUrl, Object.keys(options).length ? options : undefined); } const criteriaDir = process.env.CRITERIA_DIR || join(process.cwd(), 'config', 'criteria'); From 472b6193b50bb7d5deb6c006d664b57182192aa4 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:08:29 -0500 Subject: [PATCH 6/8] feat: send per-service API keys from register-agents and model scanners register-agent.sh sends X-Service-Name: register-agents and X-Internal-Key from a private header file when SCOPE_INTERNAL_API_KEY is set, so the key never appears on the curl command line. Model-scanner fetch/upsert/sync calls send the model-scanner principal headers the same way. Without a key both callers behave exactly as before. Compose passes INTERNAL_API_KEY_REGISTER_AGENTS and INTERNAL_API_KEY_MODEL_SCANNER to the API and the matching key to each caller; .env.example documents the variables. A missing key on the API side disables that service principal (fail closed). Scopes are identified only; enforcement lands with RBAC. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .env.example | 12 ++++++ docker-compose.yml | 6 +++ packages/model-scanning/src/fetch-agents.ts | 3 +- packages/model-scanning/src/index.ts | 1 + packages/model-scanning/src/reconcile.ts | 3 +- .../model-scanning/src/service-auth.test.ts | 40 +++++++++++++++++++ packages/model-scanning/src/service-auth.ts | 19 +++++++++ packages/model-scanning/src/upsert-agent.ts | 3 +- scripts/register-agent.sh | 21 +++++++++- scripts/register-agent.test.ts | 39 ++++++++++++++++++ 10 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 packages/model-scanning/src/service-auth.test.ts create mode 100644 packages/model-scanning/src/service-auth.ts diff --git a/.env.example b/.env.example index 916bc1e40..ecf3d371d 100644 --- a/.env.example +++ b/.env.example @@ -167,3 +167,15 @@ FEEDBACK_DESCENDANT_GUARD=true # Fixed active-user cache lifetime, in seconds (positive integer, default 300). # Cache hits do not extend expiry; Redis outages fall back to MongoDB. # AUTH_USER_CACHE_TTL_SECONDS=300 + +# Service-to-API authentication (only used when AUTH_PROVIDER is set). +# Per-service keys (>= 32 characters each). The API reads them to recognise each +# service principal; Compose passes the same value to the matching caller as +# SCOPE_INTERNAL_API_KEY. Unset = that service's credentials are rejected. +# Generate with: openssl rand -hex 32 +# INTERNAL_API_KEY_REGISTER_AGENTS= +# INTERNAL_API_KEY_MODEL_SCANNER= + +# Absolute maximum age of a run token, in ms, measured from the run's claim +# (default 86400000 = 24h). Run tokens are only valid while the run is processing. +# RUN_TOKEN_MAX_AGE_MS=86400000 diff --git a/docker-compose.yml b/docker-compose.yml index 31b6b191a..5f4b7dbd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -365,6 +365,9 @@ services: AUTH_BOOTSTRAP_ADMINS: "${AUTH_BOOTSTRAP_ADMINS:-entra:11111111-1111-1111-1111-111111111111/aaaaaaaa-0000-0000-0000-000000000001}" AUTH_BOOTSTRAP_TENANTS: "${AUTH_BOOTSTRAP_TENANTS:-11111111-1111-1111-1111-111111111111}" AUTH_USER_CACHE_TTL_SECONDS: "${AUTH_USER_CACHE_TTL_SECONDS:-300}" + # Per-service API keys (unset = that service principal is disabled). + INTERNAL_API_KEY_REGISTER_AGENTS: ${INTERNAL_API_KEY_REGISTER_AGENTS:-} + INTERNAL_API_KEY_MODEL_SCANNER: ${INTERNAL_API_KEY_MODEL_SCANNER:-} NODE_TLS_REJECT_UNAUTHORIZED: "1" NODE_EXTRA_CA_CERTS: "${NODE_EXTRA_CA_CERTS-/ca/rootCA.pem}" volumes: @@ -394,6 +397,7 @@ services: environment: SCOPE_REGISTER_COPILOT_AGENT: ${SCOPE_REGISTER_COPILOT_AGENT:-false} SCOPE_REGISTER_CLAUDE_CODE_AGENT: ${SCOPE_REGISTER_CLAUDE_CODE_AGENT:-false} + SCOPE_INTERNAL_API_KEY: ${INTERNAL_API_KEY_REGISTER_AGENTS:-} restart: "no" entrypoint: - /bin/sh @@ -441,6 +445,7 @@ services: API_URL: http://api:80 TOKEN_MANAGER_URL: http://token-manager:80 GITHUB_TOKEN: ${GITHUB_TOKEN:-} + SCOPE_INTERNAL_API_KEY: ${INTERNAL_API_KEY_MODEL_SCANNER:-} # Model scanner: Anthropic - scans available models and syncs to API model-scanner-anthropic: @@ -465,6 +470,7 @@ services: API_URL: http://api:80 TOKEN_MANAGER_URL: http://token-manager:80 ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + SCOPE_INTERNAL_API_KEY: ${INTERNAL_API_KEY_MODEL_SCANNER:-} # Judge - evaluation service (always starts) judge: diff --git a/packages/model-scanning/src/fetch-agents.ts b/packages/model-scanning/src/fetch-agents.ts index 9336aed10..89c69ebc1 100644 --- a/packages/model-scanning/src/fetch-agents.ts +++ b/packages/model-scanning/src/fetch-agents.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { AgentDefinition } from "./types.js"; +import { serviceAuthHeaders } from "./service-auth.js"; /** * Fetch agents that declare a given model provider. @@ -17,7 +18,7 @@ export async function fetchAgentsByProvider( const response = await fetch(url, { method: "GET", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...serviceAuthHeaders() }, signal: AbortSignal.timeout(10_000), }); diff --git a/packages/model-scanning/src/index.ts b/packages/model-scanning/src/index.ts index 4f0338c56..26c95246a 100644 --- a/packages/model-scanning/src/index.ts +++ b/packages/model-scanning/src/index.ts @@ -7,3 +7,4 @@ export * from "./wait-for-api.js"; export * from "./upsert-agent.js"; export * from "./fetch-agents.js"; export * from "./cli.js"; +export * from "./service-auth.js"; diff --git a/packages/model-scanning/src/reconcile.ts b/packages/model-scanning/src/reconcile.ts index fc0d935ca..51166cb04 100644 --- a/packages/model-scanning/src/reconcile.ts +++ b/packages/model-scanning/src/reconcile.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ModelSyncRequest, ReconcileReport, ScanResult } from "./types.js"; +import { serviceAuthHeaders } from "./service-auth.js"; /** * Send scanned models to the API for lifecycle reconciliation. @@ -41,7 +42,7 @@ export async function reconcileModels( const response = await fetch(url, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...serviceAuthHeaders() }, body: JSON.stringify(body), signal: AbortSignal.timeout(30_000), }); diff --git a/packages/model-scanning/src/service-auth.test.ts b/packages/model-scanning/src/service-auth.test.ts new file mode 100644 index 000000000..4f7bdb675 --- /dev/null +++ b/packages/model-scanning/src/service-auth.test.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { serviceAuthHeaders } from "./service-auth.js"; +import { upsertAgent } from "./upsert-agent.js"; + +describe("serviceAuthHeaders", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + }); + + it("returns no headers when no key is configured", () => { + expect(serviceAuthHeaders({})).toEqual({}); + expect(serviceAuthHeaders({ SCOPE_INTERNAL_API_KEY: "" })).toEqual({}); + }); + + it("identifies the model-scanner principal when a key is configured", () => { + expect(serviceAuthHeaders({ SCOPE_INTERNAL_API_KEY: "secret" })).toEqual({ + "X-Service-Name": "model-scanner", + "X-Internal-Key": "secret", + }); + }); + + it("is sent on API writes", async () => { + vi.stubEnv("SCOPE_INTERNAL_API_KEY", "secret"); + const fetchMock = vi.fn().mockResolvedValue({ ok: true }); + vi.stubGlobal("fetch", fetchMock); + vi.spyOn(console, "log").mockImplementation(() => {}); + + await upsertAgent("http://api", { _id: "a" } as never); + + expect(fetchMock.mock.calls[0][1].headers).toEqual({ + "Content-Type": "application/json", + "X-Service-Name": "model-scanner", + "X-Internal-Key": "secret", + }); + }); +}); diff --git a/packages/model-scanning/src/service-auth.ts b/packages/model-scanning/src/service-auth.ts new file mode 100644 index 000000000..50874a8a3 --- /dev/null +++ b/packages/model-scanning/src/service-auth.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** Service principal name the API maps to the model-scanner key. */ +export const MODEL_SCANNER_SERVICE_NAME = "model-scanner"; + +/** + * Service-auth headers for model-scanner calls to the Scope API. + * + * Returns the `X-Service-Name` / `X-Internal-Key` pair when + * `SCOPE_INTERNAL_API_KEY` is set, otherwise no headers (anonymous, as before). + */ +export function serviceAuthHeaders( + env: NodeJS.ProcessEnv = process.env, +): Record { + const key = env.SCOPE_INTERNAL_API_KEY; + if (!key) return {}; + return { "X-Service-Name": MODEL_SCANNER_SERVICE_NAME, "X-Internal-Key": key }; +} diff --git a/packages/model-scanning/src/upsert-agent.ts b/packages/model-scanning/src/upsert-agent.ts index a5e3107bf..bbf361073 100644 --- a/packages/model-scanning/src/upsert-agent.ts +++ b/packages/model-scanning/src/upsert-agent.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { AgentDefinition } from "./types.js"; +import { serviceAuthHeaders } from "./service-auth.js"; /** * Create or update an agent definition via the API. @@ -18,7 +19,7 @@ export async function upsertAgent( const response = await fetch(url, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...serviceAuthHeaders() }, body: JSON.stringify(agent), signal: AbortSignal.timeout(10_000), }); diff --git a/scripts/register-agent.sh b/scripts/register-agent.sh index ba7db7288..47780e760 100755 --- a/scripts/register-agent.sh +++ b/scripts/register-agent.sh @@ -63,7 +63,19 @@ fi TMP_DIR=$(mktemp -d) RESPONSE_FILE="$TMP_DIR/response" -trap 'rm -f "$RESPONSE_FILE"; rmdir "$TMP_DIR"' EXIT HUP INT TERM +SERVICE_HEADERS_FILE="$TMP_DIR/service-headers" +trap 'rm -f "$RESPONSE_FILE" "$SERVICE_HEADERS_FILE"; rmdir "$TMP_DIR"' EXIT HUP INT TERM + +# Service credential for the register-agents principal. Written to a private +# header file so the key never appears on the curl command line. +if [ -n "${SCOPE_INTERNAL_API_KEY:-}" ]; then + ( + umask 077 + printf 'X-Service-Name: register-agents\nX-Internal-Key: %s\n' "$SCOPE_INTERNAL_API_KEY" > "$SERVICE_HEADERS_FILE" + ) +else + SERVICE_HEADERS_FILE= +fi response_body() { if [ -f "$RESPONSE_FILE" ]; then @@ -158,6 +170,12 @@ post_idempotent() { description=$3 attempt=1 + # Optional service-auth headers, added only when a key is configured. + set -- + if [ -n "$SERVICE_HEADERS_FILE" ]; then + set -- -H "@$SERVICE_HEADERS_FILE" + fi + while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do status=000 if status=$(curl -sS --connect-timeout "$CONNECT_TIMEOUT_SECONDS" \ @@ -165,6 +183,7 @@ post_idempotent() { -o "$RESPONSE_FILE" -w "%{http_code}" \ -X POST "$API_URL$path" \ -H "Content-Type: application/json" \ + "$@" \ --data-binary "$payload"); then : else diff --git a/scripts/register-agent.test.ts b/scripts/register-agent.test.ts index 23de84ed0..a30d6852a 100644 --- a/scripts/register-agent.test.ts +++ b/scripts/register-agent.test.ts @@ -25,11 +25,13 @@ function fixture(statuses: number[], exitCodes: number[] = []) { mkdirSync(bin); const statusFile = join(directory, "statuses"); const callsFile = join(directory, "calls"); + const headersFile = join(directory, "headers"); const exitCodesFile = join(directory, "exit-codes"); const agentManifest = join(directory, "agent.yaml"); const versionManifest = join(directory, "version.yaml"); writeFileSync(statusFile, `${statuses.join("\n")}\n`); writeFileSync(callsFile, ""); + writeFileSync(headersFile, ""); writeFileSync(exitCodesFile, `${exitCodes.join("\n")}\n`); writeFileSync(agentManifest, "_id: test-worker\n"); writeFileSync(versionManifest, "agentVersion: test-v1\n"); @@ -51,6 +53,11 @@ while [ "$#" -gt 0 ]; do if [ "$1" = "-o" ]; then shift output=$1 + elif [ "$1" = "-H" ]; then + case "$2" in + @*) cat "\${2#@}" >> "$FAKE_CURL_HEADERS_FILE" ;; + *) printf '%s\n' "$2" >> "$FAKE_CURL_HEADERS_FILE" ;; + esac fi shift done @@ -93,8 +100,11 @@ esac versionManifest, statusFile, callsFile, + headersFile, env: { ...process.env, + SCOPE_INTERNAL_API_KEY: "", + FAKE_CURL_HEADERS_FILE: headersFile, PATH: `${bin}:${process.env.PATH ?? ""}`, FAKE_CURL_STATUS_FILE: statusFile, FAKE_CURL_CALLS_FILE: callsFile, @@ -132,6 +142,35 @@ describe("register-agent.sh", () => { ); }, SCRIPT_TEST_TIMEOUT_MS); + it("sends register-agents service headers from a file when a key is set", () => { + const test = fixture([200, 201, 200]); + const key = "k".repeat(40); + const result = spawnSync( + "sh", + [script, "http://scope-api", test.agentManifest, test.versionManifest], + { encoding: "utf8", env: { ...test.env, SCOPE_INTERNAL_API_KEY: key } }, + ); + + expect(result.status).toBe(0); + const calls = readFileSync(test.callsFile, "utf8"); + expect(calls).not.toContain(key); + const headers = readFileSync(test.headersFile, "utf8").split("\n"); + expect(headers.filter((h) => h === `X-Internal-Key: ${key}`)).toHaveLength(2); + expect(headers.filter((h) => h === "X-Service-Name: register-agents")).toHaveLength(2); + }, SCRIPT_TEST_TIMEOUT_MS); + + it("sends no service headers when no key is set", () => { + const test = fixture([200, 201, 200]); + const result = spawnSync( + "sh", + [script, "http://scope-api", test.agentManifest, test.versionManifest], + { encoding: "utf8", env: test.env }, + ); + + expect(result.status).toBe(0); + expect(readFileSync(test.headersFile, "utf8")).not.toMatch(/X-Internal-Key|X-Service-Name/); + }, SCRIPT_TEST_TIMEOUT_MS); + it("fails immediately and non-zero on permanent agent errors", () => { const test = fixture([200, 400, 201, 201]); const result = spawnSync( From 04fecb927116738a967680113c4d50eb6648cd46 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:09:42 -0500 Subject: [PATCH 7/8] test(api): cover run-token and service-key authentication Adds unit tests for the run-token verifier (valid claim, malformed, forged, deleted/missing digest, not processing, expired, store unavailable), the per-service key verifier (per-service keys and scopes, wrong/foreign/unknown keys, fail closed when unset or too short), and the auth middleware (run and service callers attached and logged without secrets, IdP and user access skipped, 401/400/503 mappings, no-token requests still anonymous, credentials ignored while auth is disabled). Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/api/src/auth/middleware.test.ts | 181 ++++++++++++++++++- apps/api/src/auth/run-token-verifier.test.ts | 116 ++++++++++++ apps/api/src/auth/service-principals.test.ts | 60 ++++++ 3 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/auth/run-token-verifier.test.ts create mode 100644 apps/api/src/auth/service-principals.test.ts diff --git a/apps/api/src/auth/middleware.test.ts b/apps/api/src/auth/middleware.test.ts index e330a8f70..5e50f8787 100644 --- a/apps/api/src/auth/middleware.test.ts +++ b/apps/api/src/auth/middleware.test.ts @@ -4,10 +4,10 @@ import express from "express"; import request from "supertest"; import { describe, expect, it, vi } from "vitest"; -import { AuthError, type AuthProvider, type VerifiedIdentity } from "shared"; +import { AuthError, mintRunToken, type AuthProvider, type VerifiedIdentity } from "shared"; import { createAuthMiddleware, createUserAccessMiddleware } from "./middleware.js"; import { authErrorHandler } from "./error-handler.js"; -import { getUser } from "./types.js"; +import { getPrincipal, getUser } from "./types.js"; import { UserAccessError, type UserAccessService } from "./user-access-resolver.js"; const identity: VerifiedIdentity = { @@ -156,3 +156,180 @@ describe("IdP verification and application access middleware", () => { expect(res.body.code).toBe(code); }); }); + +describe("run-token and service-key callers", () => { + const runToken = mintRunToken("req-1").token; + const verifiedRun = { requestId: "req-1", runId: "run-1", projectId: "project-1", tokenHash: "abcdef0123456789" }; + + function setupCallers(opts: { providerEnabled?: boolean; withVerifiers?: boolean } = {}) { + const { providerEnabled = true, withVerifiers = true } = opts; + const provider = { + id: "entra", + verifyAccessToken: vi.fn(async () => identity), + } satisfies AuthProvider; + const resolver = { + resolveExisting: vi.fn(async () => principal), + enrollOnLogin: vi.fn(async () => principal), + } satisfies UserAccessService; + const runVerifier = vi.fn(async (token: string) => { + if (token !== runToken) throw new AuthError("invalid_run_token", "Invalid run token"); + return verifiedRun; + }); + const serviceVerifier = vi.fn((name: string, key: string) => { + if (name !== "register-agents" || key !== "k".repeat(40)) { + throw new AuthError("invalid_service_key", "Invalid service credentials"); + } + return { name, scopes: ["agents:register"] }; + }); + const app = express(); + app.use(createAuthMiddleware({ + getProvider: () => providerEnabled ? provider : null, + getRunTokenVerifier: () => withVerifiers ? runVerifier : null, + getServiceKeyVerifier: () => withVerifiers ? serviceVerifier : null, + })); + app.use(createUserAccessMiddleware(() => resolver)); + app.get("/api/v1/private", (req, res) => { + res.json({ principal: getPrincipal(req), user: getUser(req), hasAuth: req.auth !== undefined }); + }); + app.use(authErrorHandler); + return { app, provider, resolver, runVerifier, serviceVerifier }; + } + + it("attaches a verified run caller without IdP verification or user access", async () => { + const debug = vi.spyOn(console, "debug").mockImplementation(() => {}); + const { app, provider, resolver, runVerifier } = setupCallers(); + const res = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${runToken}`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + principal: { kind: "run", requestId: "req-1", runId: "run-1", projectId: "project-1" }, + user: { id: "anonymous", isAuthenticated: false }, + hasAuth: false, + }); + expect(runVerifier).toHaveBeenCalledWith(runToken); + expect(provider.verifyAccessToken).not.toHaveBeenCalled(); + expect(resolver.resolveExisting).not.toHaveBeenCalled(); + // The caller is logged with an 8-char digest prefix, never the raw token. + const logged = debug.mock.calls.map((c) => String(c[0])).join("\n"); + expect(logged).toContain("run caller request=req-1 run=run-1 token=abcdef01"); + expect(logged).not.toContain(runToken); + debug.mockRestore(); + }); + + it.each([ + ["invalid_run_token"], + ["run_not_active"], + ["run_token_expired"], + ] as const)("rejects a run token failing with %s as 401", async (code) => { + const { app, runVerifier, provider } = setupCallers(); + runVerifier.mockRejectedValueOnce(new AuthError(code, "nope")); + const res = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${runToken}`); + expect(res.status).toBe(401); + expect(res.body.code).toBe(code); + expect(provider.verifyAccessToken).not.toHaveBeenCalled(); + }); + + it("rejects a forged run token as 401", async () => { + const { app } = setupCallers(); + const forged = mintRunToken("req-1").token; + const res = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${forged}`); + expect(res.status).toBe(401); + expect(res.body.code).toBe("invalid_run_token"); + }); + + it("returns 503 when run-token verification is unavailable", async () => { + const { app, runVerifier } = setupCallers(); + runVerifier.mockRejectedValueOnce(new AuthError("service_unavailable", "down")); + const res = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${runToken}`); + expect(res.status).toBe(503); + }); + + it("rejects run tokens when no verifier is configured", async () => { + const { app } = setupCallers({ withVerifiers: false }); + const res = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${runToken}`); + expect(res.status).toBe(401); + expect(res.body.code).toBe("invalid_run_token"); + }); + + it("attaches a verified service caller", async () => { + const debug = vi.spyOn(console, "debug").mockImplementation(() => {}); + const { app, provider, resolver } = setupCallers(); + const res = await request(app) + .get("/api/v1/private") + .set("X-Service-Name", "register-agents") + .set("X-Internal-Key", "k".repeat(40)); + expect(res.status).toBe(200); + expect(res.body.principal).toEqual({ kind: "service", name: "register-agents", scopes: ["agents:register"] }); + expect(res.body.user).toEqual({ id: "anonymous", isAuthenticated: false }); + expect(provider.verifyAccessToken).not.toHaveBeenCalled(); + expect(resolver.resolveExisting).not.toHaveBeenCalled(); + expect(debug.mock.calls.map((c) => String(c[0])).join("\n")).not.toContain("k".repeat(40)); + debug.mockRestore(); + }); + + it.each([ + [{ "X-Service-Name": "register-agents", "X-Internal-Key": "wrong" }], + [{ "X-Service-Name": "register-agents" }], + [{ "X-Internal-Key": "k".repeat(40) }], + ])("rejects incomplete or wrong service credentials %j as 401", async (headers) => { + const { app } = setupCallers(); + const res = await request(app).get("/api/v1/private").set(headers); + expect(res.status).toBe(401); + expect(res.body.code).toBe("invalid_service_key"); + }); + + it("fails closed for service headers when no service verifier is configured", async () => { + const { app } = setupCallers({ withVerifiers: false }); + const res = await request(app) + .get("/api/v1/private") + .set("X-Service-Name", "register-agents") + .set("X-Internal-Key", "k".repeat(40)); + expect(res.status).toBe(401); + }); + + it("rejects service headers combined with an Authorization header", async () => { + const { app, serviceVerifier, provider } = setupCallers(); + const res = await request(app) + .get("/api/v1/private") + .set("Authorization", `Bearer ${runToken}`) + .set("X-Service-Name", "register-agents") + .set("X-Internal-Key", "k".repeat(40)); + expect(res.status).toBe(400); + expect(res.body.code).toBe("ambiguous_credentials"); + expect(serviceVerifier).not.toHaveBeenCalled(); + expect(provider.verifyAccessToken).not.toHaveBeenCalled(); + }); + + it("keeps no-credential requests anonymous", async () => { + const { app, runVerifier, serviceVerifier } = setupCallers(); + const res = await request(app).get("/api/v1/private"); + expect(res.status).toBe(200); + expect(res.body.principal).toEqual({ kind: "anonymous" }); + expect(runVerifier).not.toHaveBeenCalled(); + expect(serviceVerifier).not.toHaveBeenCalled(); + }); + + it("still sends IdP bearers to the provider and resolves the user", async () => { + const { app, provider, runVerifier } = setupCallers(); + const res = await request(app).get("/api/v1/private").set("Authorization", "Bearer idp-token"); + expect(res.status).toBe(200); + expect(res.body.principal).toEqual({ kind: "user", user: principal }); + expect(res.body.hasAuth).toBe(true); + expect(provider.verifyAccessToken).toHaveBeenCalledWith("idp-token"); + expect(runVerifier).not.toHaveBeenCalled(); + }); + + it("ignores run and service credentials while auth is disabled", async () => { + const { app, runVerifier, serviceVerifier } = setupCallers({ providerEnabled: false }); + const run = await request(app).get("/api/v1/private").set("Authorization", `Bearer ${runToken}`); + const svc = await request(app) + .get("/api/v1/private") + .set("X-Service-Name", "register-agents") + .set("X-Internal-Key", "wrong"); + for (const res of [run, svc]) { + expect(res.status).toBe(200); + expect(res.body.principal).toEqual({ kind: "anonymous" }); + } + expect(runVerifier).not.toHaveBeenCalled(); + expect(serviceVerifier).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/auth/run-token-verifier.test.ts b/apps/api/src/auth/run-token-verifier.test.ts new file mode 100644 index 000000000..383ff99fa --- /dev/null +++ b/apps/api/src/auth/run-token-verifier.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it, vi } from "vitest"; +import { AuthError, mintRunToken } from "shared"; +import { + DEFAULT_RUN_TOKEN_MAX_AGE_MS, + createRunTokenVerifier, + resolveRunTokenMaxAgeMs, +} from "./run-token-verifier.js"; + +const NOW = Date.parse("2026-01-01T12:00:00Z"); +const HOUR = 60 * 60 * 1000; + +function setup(overrides: { run?: Record; doc?: Record | null } = {}) { + const { token, hash } = mintRunToken("req-1"); + const doc = + overrides.doc === null + ? null + : { + _id: "req-1", + projectId: "project-1", + run: { + _id: "run-1", + status: "processing", + tokenHash: hash, + startedAt: new Date(NOW - HOUR), + ...overrides.run, + }, + ...overrides.doc, + }; + const findOne = vi.fn().mockResolvedValue(doc); + const verify = createRunTokenVerifier(() => ({ findOne }) as never, { + maxAgeMs: 2 * HOUR, + now: () => NOW, + }); + return { token, hash, findOne, verify }; +} + +async function codeOf(promise: Promise): Promise { + const err = await promise.then(() => undefined, (e: unknown) => e); + expect(err).toBeInstanceOf(AuthError); + return (err as AuthError).code; +} + +describe("createRunTokenVerifier", () => { + it("accepts a token whose digest matches the processing run's claim", async () => { + const { token, hash, findOne, verify } = setup(); + await expect(verify(token)).resolves.toEqual({ + requestId: "req-1", + runId: "run-1", + projectId: "project-1", + tokenHash: hash, + }); + // Point read by request id with a projection that never returns the full doc. + expect(findOne).toHaveBeenCalledWith( + { _id: "req-1" }, + expect.objectContaining({ projection: expect.objectContaining({ "run.tokenHash": 1 }) }), + ); + }); + + it("rejects malformed tokens without a database read", async () => { + const { findOne, verify } = setup(); + expect(await codeOf(verify("scope_rt_garbage"))).toBe("invalid_run_token"); + expect(findOne).not.toHaveBeenCalled(); + }); + + it("rejects a forged token for a real run", async () => { + const { verify } = setup(); + const forged = mintRunToken("req-1").token; + expect(await codeOf(verify(forged))).toBe("invalid_run_token"); + }); + + it.each([ + ["missing request", { doc: null }], + ["deleted request", { doc: { deletedAt: new Date(NOW) } }], + ["no stored digest", { run: { tokenHash: undefined } }], + ])("rejects %s as invalid", async (_label, overrides) => { + const { token, verify } = setup(overrides as never); + expect(await codeOf(verify(token))).toBe("invalid_run_token"); + }); + + it.each(["queued", "done", "pending"])("rejects a run in status %s", async (status) => { + const { token, verify } = setup({ run: { status } }); + expect(await codeOf(verify(token))).toBe("run_not_active"); + }); + + it("rejects a claim older than the absolute max age", async () => { + const { token, verify } = setup({ run: { startedAt: new Date(NOW - 3 * HOUR) } }); + expect(await codeOf(verify(token))).toBe("run_token_expired"); + }); + + it("rejects a claim without a start time", async () => { + const { token, verify } = setup({ run: { startedAt: undefined } }); + expect(await codeOf(verify(token))).toBe("run_token_expired"); + }); + + it("fails as service_unavailable when the database is unavailable", async () => { + const { token } = setup(); + const noCollection = createRunTokenVerifier(() => undefined); + expect(await codeOf(noCollection(token))).toBe("service_unavailable"); + + const findOne = vi.fn().mockRejectedValue(new Error("boom")); + const failing = createRunTokenVerifier(() => ({ findOne }) as never); + expect(await codeOf(failing(token))).toBe("service_unavailable"); + }); +}); + +describe("resolveRunTokenMaxAgeMs", () => { + it("parses positive values and falls back otherwise", () => { + expect(resolveRunTokenMaxAgeMs("1000")).toBe(1000); + expect(resolveRunTokenMaxAgeMs(undefined)).toBe(DEFAULT_RUN_TOKEN_MAX_AGE_MS); + expect(resolveRunTokenMaxAgeMs("0")).toBe(DEFAULT_RUN_TOKEN_MAX_AGE_MS); + expect(resolveRunTokenMaxAgeMs("nope")).toBe(DEFAULT_RUN_TOKEN_MAX_AGE_MS); + }); +}); diff --git a/apps/api/src/auth/service-principals.test.ts b/apps/api/src/auth/service-principals.test.ts new file mode 100644 index 000000000..a737f498f --- /dev/null +++ b/apps/api/src/auth/service-principals.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { AuthError } from "shared"; +import { SERVICE_PRINCIPALS, createServiceKeyVerifier } from "./service-principals.js"; + +const REGISTER_KEY = "r".repeat(40); +const SCANNER_KEY = "s".repeat(40); +const env = { + INTERNAL_API_KEY_REGISTER_AGENTS: REGISTER_KEY, + INTERNAL_API_KEY_MODEL_SCANNER: SCANNER_KEY, +}; + +function codeOf(fn: () => unknown): string { + try { + fn(); + } catch (err) { + expect(err).toBeInstanceOf(AuthError); + return (err as AuthError).code; + } + throw new Error("expected rejection"); +} + +describe("createServiceKeyVerifier", () => { + afterEach(() => vi.restoreAllMocks()); + + it("identifies each service with its own key and scopes", () => { + const verify = createServiceKeyVerifier(env); + expect(verify("register-agents", REGISTER_KEY)).toEqual({ + name: "register-agents", + scopes: ["agents:register"], + }); + expect(verify("model-scanner", SCANNER_KEY)).toEqual({ + name: "model-scanner", + scopes: ["agents:read", "agents:register", "models:sync"], + }); + }); + + it("rejects a wrong key, another service's key, and unknown services", () => { + const verify = createServiceKeyVerifier(env); + expect(codeOf(() => verify("register-agents", "x".repeat(40)))).toBe("invalid_service_key"); + expect(codeOf(() => verify("register-agents", SCANNER_KEY))).toBe("invalid_service_key"); + expect(codeOf(() => verify("someone-else", REGISTER_KEY))).toBe("invalid_service_key"); + }); + + it("fails closed when a service's key is not configured", () => { + const verify = createServiceKeyVerifier({}); + for (const def of SERVICE_PRINCIPALS) { + expect(codeOf(() => verify(def.name, ""))).toBe("invalid_service_key"); + } + }); + + it("disables a service whose configured key is too short", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const verify = createServiceKeyVerifier({ INTERNAL_API_KEY_REGISTER_AGENTS: "short" }); + expect(codeOf(() => verify("register-agents", "short"))).toBe("invalid_service_key"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("INTERNAL_API_KEY_REGISTER_AGENTS")); + }); +}); From a6cc74e4e2c7829f151e61b334f2470cf0d8914b Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Fri, 23 Oct 2026 10:10:35 -0500 Subject: [PATCH 8/8] docs: document run-token and per-service key authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auth-rbac.md §6 now describes the implemented run token (mint at claim, digest at rest, verification rules, failure codes, redaction, runToken route metadata) and per-service keys (headers, fail-closed rules, service table), and notes that enforcement arrives with route-level authorization. ENV_VARIABLES.md documents RUN_TOKEN_MAX_AGE_MS, INTERNAL_API_KEY_REGISTER_AGENTS, INTERNAL_API_KEY_MODEL_SCANNER and SCOPE_INTERNAL_API_KEY. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ENV_VARIABLES.md | 39 +++++++++++++++++ docs/architecture/auth-rbac.md | 79 +++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/ENV_VARIABLES.md b/ENV_VARIABLES.md index bc66a4316..6dbd55268 100644 --- a/ENV_VARIABLES.md +++ b/ENV_VARIABLES.md @@ -1095,6 +1095,45 @@ identity is promoted only when its tenant is explicitly listed here. > secret). A later On-Behalf-Of enrichment would introduce > `AUTH_API_CLIENT_SECRET`; it is **not** used now. +### RUN_TOKEN_MAX_AGE_MS +**Default:** `86400000` (24h) +**Type:** positive number (milliseconds) +**Scope:** API + +Absolute maximum age of a worker run token, measured from the run's claim +(`run.startedAt`). A run token is accepted only while its run is `processing` under +the claim that minted it **and** within this age. Missing or invalid values fall back +to the default. See [auth-rbac.md §6](docs/architecture/auth-rbac.md). + +### INTERNAL_API_KEY_REGISTER_AGENTS +**Default:** (not set — service disabled) +**Type:** string, at least 32 characters +**Scope:** API + +Key for the `register-agents` service principal (scope `agents:register`). The API +accepts `X-Service-Name: register-agents` + `X-Internal-Key: ` only when this is +set; unset or shorter keys disable the principal (fail closed). Only used when +`AUTH_PROVIDER` is configured. Generate with `openssl rand -hex 32`. + +### INTERNAL_API_KEY_MODEL_SCANNER +**Default:** (not set — service disabled) +**Type:** string, at least 32 characters +**Scope:** API + +Key for the `model-scanner` service principal (scopes `agents:read`, +`agents:register`, `models:sync`). Same rules as `INTERNAL_API_KEY_REGISTER_AGENTS`. + +### SCOPE_INTERNAL_API_KEY +**Default:** (not set) +**Type:** string +**Scope:** `register-agents` script, model scanners + +The calling service's own key. When set, `scripts/register-agent.sh` sends the +`register-agents` service headers and the model scanners send the `model-scanner` +service headers; when unset they send no service headers (unchanged behavior). Docker +Compose maps `INTERNAL_API_KEY_REGISTER_AGENTS` / `INTERNAL_API_KEY_MODEL_SCANNER` +from `.env` to the matching caller. + ## Token Manager Configuration ### TOKEN_MANAGER_URL diff --git a/docs/architecture/auth-rbac.md b/docs/architecture/auth-rbac.md index 12fb57c2c..ce42fad62 100644 --- a/docs/architecture/auth-rbac.md +++ b/docs/architecture/auth-rbac.md @@ -72,6 +72,7 @@ must be resolved with stakeholders before or during implementation. |-----------|-------|----------------| | API | Verifies the IdP signature and claims before any access-cache lookup. `/users/me` owns explicit enrollment; subsequent middleware resolves existing active users. Public/anonymous rollout is unchanged. | [apps/api/src/index.ts](../../apps/api/src/index.ts), [auth/middleware.ts](../../apps/api/src/auth/middleware.ts), [routes/users.ts](../../apps/api/src/routes/users.ts) | | User access | `UserAccessResolver` reads a validated Redis snapshot or the exact `(idp, idpTenant, idpSubject)` Mongo record. Only explicit login invokes `upsertOnLogin`; no per-request JIT. | [auth/user-access-resolver.ts](../../apps/api/src/auth/user-access-resolver.ts), [auth/user-access-cache.ts](../../apps/api/src/auth/user-access-cache.ts), [auth/user-store.ts](../../apps/api/src/auth/user-store.ts) | +| Service callers | Worker run tokens (`scope_rt_…`) and per-service keys (`X-Service-Name` + `X-Internal-Key`) are verified before the IdP and attached as `req.principal` (`run` / `service`). Authentication only — no route enforcement yet. See §6. | [auth/run-token-verifier.ts](../../apps/api/src/auth/run-token-verifier.ts), [auth/service-principals.ts](../../apps/api/src/auth/service-principals.ts), [shared/src/auth/run-token.ts](../../packages/shared/src/auth/run-token.ts) | | Runs data model | `RequestResponseSchema` + embedded `RunStateSchema`; history in `runs` collection (`RunHistoryDocumentSchema`). **No owner field.** | [packages/shared/src/schemas/request.ts](../../packages/shared/src/schemas/request.ts) | | Run listing | Cursor-paginated `GET /api/v1/requests` with filters; no per-user scoping. | [apps/api/src/routes/requests.ts](../../apps/api/src/routes/requests.ts) | | CLI | Centralized `apiFetch()` injects `SCOPE_TOKEN` as a raw IdP bearer. No CLI interactive-login implementation is added here. New identities must explicitly enroll; ordinary CLI calls never enroll them. | [apps/cli/src/utils/api-client.ts](../../apps/cli/src/utils/api-client.ts), [CLI guidance](../../.agents/skills/scope-cli/SKILL.md) | @@ -666,9 +667,83 @@ that must be settled before that work is scheduled. **v1 ships `ownerId` + fields above are documented intent, not implemented yet. -### 6. Deferred: service-to-service auth +### 6. Service-to-service auth (run tokens and per-service keys implemented; service JWTs and on-behalf-of deferred) -> This section preserves the future ownership/RBAC design. None of its Scope-issued +#### Implemented: run tokens and per-service keys (authentication only) + +Two non-human credentials are recognized by `createAuthMiddleware` **before** the IdP +bearer is verified. Both are authentication only: they identify the caller as +`req.principal` but no route enforces scopes or flags yet (that arrives with +route-level authorization, §4). `req.auth` and `req.user` keep their human meaning — +a run or service caller has **no** `req.auth`, an anonymous `req.user`, and never +reaches enrollment or user-access resolution. + +```mermaid +sequenceDiagram + participant W as Worker (queue processor) + participant DB as MongoDB + participant API + participant J as Judge + W->>DB: atomic claim ($set run.status=processing, run.tokenHash=sha256(token)) + W->>API: GET worker read route (Authorization: Bearer scope_rt_…) + API->>DB: point read {_id: requestId} (projection: run status/hash/startedAt) + API-->>W: 200 (req.principal = run) + W->>J: POST /api/v1/evaluate (same bearer) + J->>API: GET /api/v1/criteria (same bearer, per-evaluation provider) + W->>DB: terminal write ($unset run.tokenHash) +``` + +**Run token.** + +- Minted by the shared queue processor when it atomically claims a run: + `scope_rt__` (opaque, 256-bit random secret, CRC-32 + checksum so malformed tokens are rejected without a database read). Only the + SHA-256 digest is stored, in `run.tokenHash`, as part of the claim update. The raw + token lives in worker memory for the duration of the handling and is sent as a + bearer by the MCP, skill, extension, resource, prompt, codebase, gate-prompt and + judge clients. The judge forwards a well-formed run token on its criteria reads + using a per-evaluation provider (never the cached one). +- Verified by a point read on the request: valid only when the digest matches + (constant-time), the request is not deleted, `run.status` is `processing`, and + `run.startedAt` is within `RUN_TOKEN_MAX_AGE_MS` (default 24h). A new claim + replaces the digest; the worker's terminal success/error writes `$unset` it. + Other terminal transitions (cancel, stuck-run reaper) leave it in place, but it is + rejected because the run is no longer `processing`. +- Failure codes (all 401): `invalid_run_token` (malformed, forged, mismatched, + deleted, no digest), `run_not_active`, `run_token_expired`. A database failure + during verification is `503 service_unavailable`. +- `run.tokenHash` is server-only: it is not part of `RunStateSchema` and is + redacted from request, attempt, and run-history responses and from archived HARs. +- The worker read routes it is intended for carry `runToken: true` in their + `apiRoute()` definition, emitted as `x-scope-run-token: true` in OpenAPI. The flag is + metadata today; an inventory test pins the flagged set. +- The caller is logged at debug level with request id, run id, and the first 8 hex + characters of the digest — never the token. + +**Per-service keys.** + +- Each internal caller has its own key; there is no shared key. The API reads + `INTERNAL_API_KEY_`; the caller sends `X-Service-Name` and + `X-Internal-Key` (both required; sending them together with `Authorization` is + `400 ambiguous_credentials`). Keys are compared as SHA-256 digests in constant + time. A missing key, or one shorter than 32 characters, disables that service + (fail closed, `401 invalid_service_key`). + + | Service | API key variable | Scopes (identified, not yet enforced) | Caller | + |---------|------------------|----------------------------------------|--------| + | `register-agents` | `INTERNAL_API_KEY_REGISTER_AGENTS` | `agents:register` | `scripts/register-agent.sh` | + | `model-scanner` | `INTERNAL_API_KEY_MODEL_SCANNER` | `agents:read`, `agents:register`, `models:sync` | `packages/model-scanning` | + + Callers read their key from `SCOPE_INTERNAL_API_KEY`; without it they send no + service headers and behave exactly as before. The registration script passes the + headers through a private header file so the key is not on the command line. + +**Unchanged behavior.** When no `AUTH_PROVIDER` is configured, run and service +credentials are ignored and every request stays anonymous. With auth enabled, +requests without credentials remain anonymous as before; only a presented run +token or service header is verified (and rejected if invalid). + +> The remainder of this section preserves the future ownership/RBAC design. None of its Scope-issued > JWTs, signing keys, service credentials, minting endpoints, or revocation lists are > introduced by the current explicit-login/cache implementation. Human clients keep > presenting the IdP bearer; there is no human token-exchange endpoint.