@tangle-network/agent-app 0.60.195 → 0.60.197

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/capability.ts"],"sourcesContent":["/**\n * Per-user capability token — the sandbox→app auth primitive behind the\n * `verifyToken` seam in {@link authenticateToolRequest}.\n *\n * An app-agent runs inside the sandbox and reaches the host app back over HTTP\n * (the app tools, the integration-invoke bridge). The route must act AS the\n * connecting user without trusting any model-supplied identity, so the turn\n * mints a short HMAC token bound to the user id and bakes it into the per-turn\n * MCP server header; the route verifies it to recover the user.\n *\n * `HMAC-SHA256(secret, \"user:<userId>\")`, base64url, with an app-chosen prefix.\n * The token encodes no scopes — the hub's policy engine authorizes per action.\n * Fail-closed: with no secret, no token is minted (the caller MUST omit the MCP\n * server rather than fake an authorized call). WebCrypto only — runs on\n * Workers, Node, and the browser with no Node `crypto` dependency.\n */\n\nimport {\n base64UrlDecodeText,\n base64UrlEncodeText,\n constantTimeEqual,\n hmacSha256Base64Url,\n} from '../crypto/web-token'\n\n/** Define options for creating and verifying capability tokens including secret and prefix */\nexport interface CapabilityTokenOptions {\n /** Shared HMAC secret. When absent, mint returns undefined / verify returns false. */\n secret?: string\n /** Token prefix (namespaces the credential; lets verify reject foreign tokens\n * cheaply). Default `cap_`. */\n prefix?: string\n}\n\n/** Mint a capability token for `userId`, or `undefined` when no secret is\n * configured (fail-closed — the caller omits the MCP server rather than fake it). */\nexport async function createCapabilityToken(userId: string, opts: CapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n const prefix = opts.prefix ?? 'cap_'\n return `${prefix}${await sign(userId, secret)}`\n}\n\n/** Verify a capability token against `userId`. Returns false (never throws) for\n * an unconfigured secret, a wrong prefix, a malformed token, or a mismatch. */\nexport async function verifyCapabilityToken(userId: string, token: string, opts: CapabilityTokenOptions): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const expected = `${prefix}${await sign(userId, secret)}`\n return constantTimeEqual(token, expected)\n}\n\n/** Define options for capability tokens that expire after a specified lifetime in milliseconds */\nexport interface ExpiringCapabilityTokenOptions extends CapabilityTokenOptions {\n /** Token lifetime. Expired tokens verify false regardless of signature. */\n expiresInMs: number\n /** Clock injection for tests; defaults to Date.now. */\n now?: () => number\n}\n\n/**\n * Mint an EXPIRING capability token: `<prefix><base64url(payload)>.<sig>` where\n * the payload carries `{ sub, exp, n }` (subject, epoch-ms expiry, random\n * nonce) and the signature is HMAC-SHA256 over the encoded payload. Use this\n * for user-initiated scoped channels (e.g. a per-sequence MCP endpoint) where\n * a captured token must not stay valid past its window; the bare\n * {@link createCapabilityToken} remains for turn-scoped tool bridges whose\n * mint+verify happen inside one request cycle. Fail-closed like the bare\n * variant: no secret → no token.\n */\nexport async function createExpiringCapabilityToken(subject: string, opts: ExpiringCapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n if (!Number.isFinite(opts.expiresInMs) || opts.expiresInMs <= 0) throw new Error('expiresInMs must be a positive number')\n const prefix = opts.prefix ?? 'cap_'\n const now = opts.now ?? Date.now\n const payload = base64UrlEncodeText(JSON.stringify({ sub: subject, exp: now() + opts.expiresInMs, n: crypto.randomUUID() }))\n return `${prefix}${payload}.${await hmacSha256Base64Url(payload, secret)}`\n}\n\n/** Verify an expiring token against `subject`: prefix, payload integrity,\n * subject match, and expiry all checked; returns false (never throws) on any\n * failure including a malformed payload. */\nexport async function verifyExpiringCapabilityToken(subject: string, token: string, opts: CapabilityTokenOptions & { now?: () => number }): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const body = token.slice(prefix.length)\n const dot = body.lastIndexOf('.')\n if (dot <= 0 || dot === body.length - 1) return false\n const payload = body.slice(0, dot)\n const sig = body.slice(dot + 1)\n if (!constantTimeEqual(sig, await hmacSha256Base64Url(payload, secret))) return false\n let parsed: { sub?: unknown; exp?: unknown }\n try {\n parsed = JSON.parse(base64UrlDecodeText(payload)) as { sub?: unknown; exp?: unknown }\n } catch {\n return false\n }\n if (parsed.sub !== subject) return false\n if (typeof parsed.exp !== 'number') return false\n const now = opts.now ?? Date.now\n return parsed.exp > now()\n}\n\nasync function sign(userId: string, secret: string): Promise<string> {\n return hmacSha256Base64Url(`user:${userId}`, secret)\n}\n"],"mappings":";;;;;;;;AAmCA,eAAsB,sBAAsB,QAAgB,MAA2D;AACrH,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,KAAK,UAAU;AAC9B,SAAO,GAAG,MAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,CAAC;AAC/C;AAIA,eAAsB,sBAAsB,QAAgB,OAAe,MAAgD;AACzH,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,QAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,EAAG,QAAO;AACjD,QAAM,WAAW,GAAG,MAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,CAAC;AACvD,SAAO,kBAAkB,OAAO,QAAQ;AAC1C;AAoBA,eAAsB,8BAA8B,SAAiB,MAAmE;AACtI,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,CAAC,OAAO,SAAS,KAAK,WAAW,KAAK,KAAK,eAAe,EAAG,OAAM,IAAI,MAAM,uCAAuC;AACxH,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,UAAU,oBAAoB,KAAK,UAAU,EAAE,KAAK,SAAS,KAAK,IAAI,IAAI,KAAK,aAAa,GAAG,OAAO,WAAW,EAAE,CAAC,CAAC;AAC3H,SAAO,GAAG,MAAM,GAAG,OAAO,IAAI,MAAM,oBAAoB,SAAS,MAAM,CAAC;AAC1E;AAKA,eAAsB,8BAA8B,SAAiB,OAAe,MAAyE;AAC3J,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,QAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,EAAG,QAAO;AACjD,QAAM,OAAO,MAAM,MAAM,OAAO,MAAM;AACtC,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAG,QAAO;AAChD,QAAM,UAAU,KAAK,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,KAAK,MAAM,MAAM,CAAC;AAC9B,MAAI,CAAC,kBAAkB,KAAK,MAAM,oBAAoB,SAAS,MAAM,CAAC,EAAG,QAAO;AAChF,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,oBAAoB,OAAO,CAAC;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,QAAS,QAAO;AACnC,MAAI,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC3C,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,SAAO,OAAO,MAAM,IAAI;AAC1B;AAEA,eAAe,KAAK,QAAgB,QAAiC;AACnE,SAAO,oBAAoB,QAAQ,MAAM,IAAI,MAAM;AACrD;","names":[]}
@@ -5,12 +5,6 @@ import {
5
5
  import {
6
6
  assertRenderUiSchema
7
7
  } from "./chunk-2RCPPOQ4.js";
8
- import {
9
- base64UrlDecodeText,
10
- base64UrlEncodeText,
11
- constantTimeEqual,
12
- hmacSha256Base64Url
13
- } from "./chunk-S5SRJJQG.js";
14
8
  import {
15
9
  buildAppToolOpenAITools,
16
10
  findCustomTool,
@@ -20,54 +14,6 @@ import {
20
14
  ToolInputError
21
15
  } from "./chunk-E65YV7VV.js";
22
16
 
23
- // src/tools/capability.ts
24
- async function createCapabilityToken(userId, opts) {
25
- const secret = opts.secret?.trim();
26
- if (!secret) return void 0;
27
- const prefix = opts.prefix ?? "cap_";
28
- return `${prefix}${await sign(userId, secret)}`;
29
- }
30
- async function verifyCapabilityToken(userId, token, opts) {
31
- const secret = opts.secret?.trim();
32
- const prefix = opts.prefix ?? "cap_";
33
- if (!secret || !token.startsWith(prefix)) return false;
34
- const expected = `${prefix}${await sign(userId, secret)}`;
35
- return constantTimeEqual(token, expected);
36
- }
37
- async function createExpiringCapabilityToken(subject, opts) {
38
- const secret = opts.secret?.trim();
39
- if (!secret) return void 0;
40
- if (!Number.isFinite(opts.expiresInMs) || opts.expiresInMs <= 0) throw new Error("expiresInMs must be a positive number");
41
- const prefix = opts.prefix ?? "cap_";
42
- const now = opts.now ?? Date.now;
43
- const payload = base64UrlEncodeText(JSON.stringify({ sub: subject, exp: now() + opts.expiresInMs, n: crypto.randomUUID() }));
44
- return `${prefix}${payload}.${await hmacSha256Base64Url(payload, secret)}`;
45
- }
46
- async function verifyExpiringCapabilityToken(subject, token, opts) {
47
- const secret = opts.secret?.trim();
48
- const prefix = opts.prefix ?? "cap_";
49
- if (!secret || !token.startsWith(prefix)) return false;
50
- const body = token.slice(prefix.length);
51
- const dot = body.lastIndexOf(".");
52
- if (dot <= 0 || dot === body.length - 1) return false;
53
- const payload = body.slice(0, dot);
54
- const sig = body.slice(dot + 1);
55
- if (!constantTimeEqual(sig, await hmacSha256Base64Url(payload, secret))) return false;
56
- let parsed;
57
- try {
58
- parsed = JSON.parse(base64UrlDecodeText(payload));
59
- } catch {
60
- return false;
61
- }
62
- if (parsed.sub !== subject) return false;
63
- if (typeof parsed.exp !== "number") return false;
64
- const now = opts.now ?? Date.now;
65
- return parsed.exp > now();
66
- }
67
- async function sign(userId, secret) {
68
- return hmacSha256Base64Url(`user:${userId}`, secret);
69
- }
70
-
71
17
  // src/tools/gating.ts
72
18
  function resolveToolCapabilities(opts) {
73
19
  const { taxonomy, capabilities, enabled } = opts;
@@ -259,10 +205,6 @@ async function handleAppToolRequest(request, opts) {
259
205
  }
260
206
 
261
207
  export {
262
- createCapabilityToken,
263
- verifyCapabilityToken,
264
- createExpiringCapabilityToken,
265
- verifyExpiringCapabilityToken,
266
208
  resolveToolCapabilities,
267
209
  restrictTaxonomy,
268
210
  dispatchAppTool,
@@ -270,4 +212,4 @@ export {
270
212
  createAppToolRuntimeExecutor,
271
213
  handleAppToolRequest
272
214
  };
273
- //# sourceMappingURL=chunk-L5JLOC2F.js.map
215
+ //# sourceMappingURL=chunk-QDKY5TBG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/gating.ts","../src/tools/dispatch.ts","../src/tools/runtime.ts","../src/tools/http.ts"],"sourcesContent":["/**\n * Capability gating — compose an agent session's tool surface from a\n * product-defined capability registry.\n *\n * Products that let users pick what an agent can do (a \"Studio\" agent with the\n * full build toolset vs a clean \"Assistant\" with none) all need the same\n * mechanics: a registry of named capabilities, each unlocking proposal types\n * and/or named tool groups, resolved against the product's\n * {@link AppToolTaxonomy} into the concrete tools to expose. The mechanics are\n * generic and live here; the capability VOCABULARY (ids, labels, which\n * proposal types, which tool groups) is the product's.\n */\n\nimport type { AppToolTaxonomy } from './types'\n\n/** One toggleable tool group in a product's capability registry. */\nexport interface ToolCapability {\n id: string\n label: string\n description: string\n /** Proposal types this capability unlocks (intersected with the taxonomy,\n * so a capability can never widen the product's proposal surface). */\n proposalTypes?: readonly string[]\n /** Unlocks every taxonomy proposal type beyond the base set — the domain's\n * specialized long tail (risk_assessment, vuln_report, …). */\n domainActions?: boolean\n /** Named product tool groups (e.g. 'sandbox', 'integrations') this\n * capability unlocks. The vocabulary is the product's; the resolver only\n * unions them. */\n toolGroups?: readonly string[]\n}\n\n/** Resolve options for determining tool capabilities based on taxonomy, capabilities, and enabled IDs */\nexport interface ResolveToolCapabilitiesOptions {\n taxonomy: AppToolTaxonomy\n /** The product's full capability registry. */\n capabilities: readonly ToolCapability[]\n /** Enabled capability ids. `undefined` means full access (legacy callers\n * that don't send a capability set); an explicit `[]` means a pure chat\n * agent with no tools. Unknown ids are ignored. */\n enabled: readonly string[] | undefined\n /** The shared base proposal types `domainActions` excludes. Defaults to\n * every type some capability names explicitly via `proposalTypes` — i.e.\n * \"domain actions\" are the taxonomy types no capability claims. */\n baseProposalTypes?: readonly string[]\n}\n\n/** Describe resolved capabilities including proposal types and product tool groups to expose */\nexport interface ResolvedToolCapabilities {\n /** Proposal types to keep — feed to {@link restrictTaxonomy}. */\n proposalTypes: string[]\n /** Product tool groups to expose (deduped union across enabled caps). */\n toolGroups: string[]\n}\n\n/**\n * Resolve an enabled capability-id set against a taxonomy into the concrete\n * tool surface. Fail-closed: only types present in the taxonomy survive, and\n * an empty `enabled` set yields no tools at all.\n */\nexport function resolveToolCapabilities(\n opts: ResolveToolCapabilitiesOptions,\n): ResolvedToolCapabilities {\n const { taxonomy, capabilities, enabled } = opts\n if (enabled === undefined) {\n return {\n proposalTypes: [...taxonomy.proposalTypes],\n toolGroups: [...new Set(capabilities.flatMap((c) => c.toolGroups ?? []))],\n }\n }\n const base = new Set(\n opts.baseProposalTypes ?? capabilities.flatMap((c) => c.proposalTypes ?? []),\n )\n const domainTypes = taxonomy.proposalTypes.filter((t) => !base.has(t))\n const byId = new Map(capabilities.map((c) => [c.id, c]))\n\n const proposalTypes = new Set<string>()\n const toolGroups = new Set<string>()\n for (const id of enabled) {\n const cap = byId.get(id)\n if (!cap) continue\n for (const t of cap.proposalTypes ?? []) {\n if (taxonomy.proposalTypes.includes(t)) proposalTypes.add(t)\n }\n if (cap.domainActions) for (const t of domainTypes) proposalTypes.add(t)\n for (const g of cap.toolGroups ?? []) toolGroups.add(g)\n }\n return { proposalTypes: [...proposalTypes], toolGroups: [...toolGroups] }\n}\n\n/**\n * Restrict a taxonomy to a subset of proposal types, intersecting the\n * regulated subset too — the regulated label survives restriction, so a\n * narrowed agent can never launder a regulated type into an unregulated one.\n */\nexport function restrictTaxonomy(\n taxonomy: AppToolTaxonomy,\n allowed: readonly string[],\n): AppToolTaxonomy {\n const allow = new Set(allowed)\n return {\n proposalTypes: taxonomy.proposalTypes.filter((t) => allow.has(t)),\n regulatedTypes: taxonomy.regulatedTypes.filter((t) => allow.has(t)),\n }\n}\n","import { ToolInputError } from './errors'\nimport { isAppToolName } from './openai'\nimport { findCustomTool, type AppToolDefinition } from './registry'\nimport { assertRenderUiSchema } from './render-ui-schema'\nimport type {\n AppToolContext,\n AppToolHandlers,\n AppToolOutcome,\n AppToolProducedEvent,\n AppToolTaxonomy,\n} from './types'\n\n/** Define options for dispatching tools including handlers, taxonomy, custom tools, and approval policies */\nexport interface DispatchOptions {\n handlers: AppToolHandlers\n taxonomy: AppToolTaxonomy\n /** Product-registered tools beyond the four built-ins. A called name that is\n * not a built-in is dispatched to the matching {@link AppToolDefinition.execute}\n * through this same validation/outcome path. */\n customTools?: readonly AppToolDefinition[]\n /** Per-call approval policy. When provided it OVERRIDES the static\n * `taxonomy.regulatedTypes` membership check, so products can gate by\n * cost threshold, environment, or first-use instead of always/never.\n * Fail-closed: a predicate that throws counts as \"approval required\". */\n needsApproval?: (type: string, args: { title: string; description: string | null }, ctx: AppToolContext) => boolean | Promise<boolean>\n /** Called at the real side-effect site for proposals (proposal_created) and\n * generated views (artifact) so a consumer's completion oracle credits\n * persisted state. Omit when produced state isn't tracked. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\n/**\n * The ONE place an app-tool call is validated, dispatched to the product's\n * handler, and turned into an {@link AppToolOutcome} + produced events. Shared\n * by the HTTP route layer and the agent-runtime executor so both paths apply\n * identical validation and identical side effects. A {@link ToolInputError}\n * (bad input the agent can correct) and any other throw both become\n * `{ ok: false }` — a tool call never silently \"succeeds\" without its effect.\n */\nexport async function dispatchAppTool(\n toolName: string,\n rawArgs: Record<string, unknown>,\n ctx: AppToolContext,\n opts: DispatchOptions,\n): Promise<AppToolOutcome> {\n try {\n if (!isAppToolName(toolName)) {\n const custom = findCustomTool(toolName, opts.customTools)\n if (!custom) return { ok: false, code: 'unknown_tool', message: `${toolName} is not an app tool.` }\n // Custom tools own their own arg validation (their execute throws\n // ToolInputError for correctable input); the outer try/catch maps it.\n const result = await custom.execute(rawArgs, ctx)\n return { ok: true, result }\n }\n\n if (toolName === 'submit_proposal') {\n const type = String(rawArgs.type ?? '').trim()\n const title = String(rawArgs.title ?? '').trim()\n if (!type || !opts.taxonomy.proposalTypes.includes(type)) {\n return { ok: false, code: 'invalid_type', message: `type must be one of: ${opts.taxonomy.proposalTypes.join(', ')}.` }\n }\n if (!title) return { ok: false, code: 'missing_title', message: 'title is required.' }\n const description = rawArgs.description == null ? null : String(rawArgs.description)\n // Approval policy runs BEFORE the handler so the decision can gate the\n // side effect itself, not merely re-label it afterwards.\n let regulated = opts.taxonomy.regulatedTypes.includes(type)\n if (opts.needsApproval) {\n try {\n regulated = await opts.needsApproval(type, { title, description }, ctx)\n } catch {\n regulated = true // fail-closed: a broken policy means approval required\n }\n }\n const r = await opts.handlers.submitProposal({ type, title, description, regulated }, ctx)\n // Pass the handler's result through: products with immediate-execute\n // proposal types return status 'executed' plus their own fields\n // (e.g. datasetId) — the model must see what actually happened, not a\n // hard-coded \"queued for approval\".\n const { proposalId, deduped, status, ...extra } = r\n const effectiveStatus = status ?? 'queued_for_approval'\n opts.onProduced?.({\n type: 'proposal_created',\n proposalId,\n title,\n status: effectiveStatus === 'executed' ? 'executed' : 'pending',\n content: description ?? undefined,\n })\n return { ok: true, result: { ...extra, status: effectiveStatus, proposalId, deduped, regulated } }\n }\n\n if (toolName === 'schedule_followup') {\n const r = await opts.handlers.scheduleFollowup(\n { title: String(rawArgs.title ?? ''), dueDate: String(rawArgs.dueDate ?? ''), priority: rawArgs.priority as string | undefined },\n ctx,\n )\n return { ok: true, result: { followupId: r.id, dueDate: r.dueDate, deduped: r.deduped } }\n }\n\n if (toolName === 'render_ui') {\n assertRenderUiSchema(rawArgs.schema)\n const r = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ''), schema: rawArgs.schema }, ctx)\n opts.onProduced?.({ type: 'artifact', path: r.path, content: r.content })\n return { ok: true, result: { path: r.path } }\n }\n\n // add_citation\n const r = await opts.handlers.addCitation(\n { path: String(rawArgs.path ?? ''), quote: String(rawArgs.quote ?? ''), label: rawArgs.label as string | undefined },\n ctx,\n )\n return { ok: true, result: { citationId: r.citationId, path: r.path } }\n } catch (err) {\n if (err instanceof ToolInputError) return { ok: false, code: err.code, message: err.message, status: err.status }\n return { ok: false, code: 'app_tool_error', message: err instanceof Error ? err.message : String(err), status: 500 }\n }\n}\n\n/** HTTP status for a failed outcome — the handler's `ToolInputError.status`\n * when present, else 400 for a validation reject. */\nexport function outcomeStatus(outcome: Extract<AppToolOutcome, { ok: false }>): number {\n return outcome.status ?? 400\n}\n","import { dispatchAppTool, type DispatchOptions } from './dispatch'\nimport type { AppToolContext, AppToolOutcome } from './types'\n\n/** Executes an app-tool call the model emits on the agent-runtime chat path.\n * Plug into `runChatThroughRuntime({ appToolExecutor })` (or any loop that\n * dispatches function tool_calls). */\nexport type AppToolRuntimeExecutor = (call: {\n toolName: string\n args: Record<string, unknown>\n}) => Promise<AppToolOutcome>\n\n/** Define options for executing runtime tasks with a trusted per-turn context */\nexport interface RuntimeExecutorOptions extends DispatchOptions {\n /** The trusted per-turn context — supplied directly (not from headers), since\n * the runtime path has no HTTP request. */\n ctx: AppToolContext\n}\n\n/**\n * Build the runtime executor for one turn. The agent-runtime backend must also\n * advertise the tools (`buildAppToolOpenAITools(taxonomy)` on the backend's\n * `tools`) for the model to call them; this executor fulfils each call against\n * the product's handlers and emits produced events via `opts.onProduced`.\n */\nexport function createAppToolRuntimeExecutor(opts: RuntimeExecutorOptions): AppToolRuntimeExecutor {\n return ({ toolName, args }) => dispatchAppTool(toolName, args, opts.ctx, opts)\n}\n","import { authenticateToolRequest, type ToolHeaderNames } from './auth'\nimport { dispatchAppTool, outcomeStatus, type DispatchOptions } from './dispatch'\nimport { buildAppToolOpenAITools, type AppToolName } from './openai'\nimport type { AppToolDefinition } from './registry'\nimport { createMcpToolHandler, type McpToolCallContent } from './mcp-rpc'\nimport type { AppToolOutcome } from './types'\n\n/** Define options for handling tool requests including tool identification and token verification */\nexport interface HandleToolRequestOptions extends DispatchOptions {\n /** Which app tool this route serves — a built-in name or a product-registered\n * {@link AppToolDefinition} (auto-added to `customTools` for dispatch). */\n tool: AppToolName | AppToolDefinition\n /** Verify the bearer capability token belongs to the header user. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** Optional model-facing description used by the MCP tools/list response. */\n description?: string\n /** Product-specific values for the schedule_followup.priority schema. */\n priorityValues?: readonly string[]\n /** Optional success-message builder for a friendlier tool result. */\n message?: (result: unknown) => string\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction isMcpRequest(request: Request, value: unknown): boolean {\n // A valid request declares jsonrpc. An incomplete envelope has no reliable\n // body marker, so use the Streamable HTTP Accept contract as the explicit\n // transport discriminator before falling back to direct tool arguments.\n const acceptsMcp = request.headers\n .get('accept')\n ?.split(',')\n .some((part) => part.trim().toLowerCase() === 'text/event-stream')\n return (\n isRecord(value)\n && (\n 'jsonrpc' in value\n || acceptsMcp === true\n || request.headers.has('mcp-session-id')\n || request.headers.has('mcp-protocol-version')\n )\n )\n}\n\nfunction toolName(tool: AppToolName | AppToolDefinition): string {\n return typeof tool === 'string' ? tool : tool.name\n}\n\nfunction dispatchOptions(opts: HandleToolRequestOptions): DispatchOptions {\n return {\n ...opts,\n customTools:\n typeof opts.tool === 'string' ? opts.customTools : [...(opts.customTools ?? []), opts.tool],\n }\n}\n\nfunction mcpToolManifest(opts: HandleToolRequestOptions): {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n} {\n const name = toolName(opts.tool)\n const tools = buildAppToolOpenAITools(opts.taxonomy, {\n ...(opts.description && typeof opts.tool === 'string'\n ? { descriptions: { [opts.tool]: opts.description } }\n : {}),\n ...(opts.priorityValues ? { priorityValues: opts.priorityValues } : {}),\n ...(typeof opts.tool === 'string' ? {} : { customTools: [opts.tool] }),\n })\n const definition = tools.find((entry) => entry.function.name === name)\n if (!definition) {\n throw new Error(`No MCP manifest exists for app tool ${name}`)\n }\n return {\n name,\n description: definition.function.description,\n inputSchema: definition.function.parameters,\n }\n}\n\nfunction mcpToolResult(outcome: AppToolOutcome, opts: HandleToolRequestOptions): McpToolCallContent {\n if (!outcome.ok) {\n return {\n content: [{ type: 'text', text: `${outcome.code}: ${outcome.message}` }],\n isError: true,\n }\n }\n\n const value = outcome.result\n const payload = value !== null && typeof value === 'object' && !Array.isArray(value)\n ? { ok: true, ...value }\n : { ok: true, value }\n return {\n content: [{ type: 'text', text: JSON.stringify({\n ...payload,\n ...(opts.message ? { message: opts.message(value) } : {}),\n }) }],\n }\n}\n\nfunction createAppToolMcpHandler(\n authContext: Parameters<DispatchOptions['handlers']['submitProposal']>[1],\n opts: HandleToolRequestOptions,\n): (request: Request) => Promise<Response> {\n const manifest = mcpToolManifest(opts)\n return createMcpToolHandler({\n serverInfo: { name: 'agent-app-tool', version: '1' },\n tools: [{\n ...manifest,\n run: (args) => dispatchAppTool(manifest.name, args, authContext, dispatchOptions(opts)),\n }],\n buildEnv: () => ({}),\n formatResult: (result) => mcpToolResult(result as AppToolOutcome, opts),\n })\n}\n\n/**\n * Handle one app-tool HTTP request end to end — the sandbox MCP path. The\n * agent's per-turn HTTP MCP server POSTs here; this authenticates (header user\n * + capability token), reads the args (MCP-alias tolerant), dispatches to the\n * product handler, and returns a JSON Response. A product's route file becomes\n * a one-liner: `export const action = ({ request }) => handleAppToolRequest(request, cfg)`.\n */\nexport async function handleAppToolRequest(request: Request, opts: HandleToolRequestOptions): Promise<Response> {\n if (request.method !== 'POST') return Response.json({ error: 'Method not allowed' }, { status: 405 })\n\n const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames })\n if (!auth.ok) return auth.response\n\n let body: unknown\n try {\n body = await request.clone().json()\n } catch {\n return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n // The same route supports the direct app-tool HTTP shape used by the\n // OpenAI/runtime surfaces and stateless Streamable HTTP MCP JSON-RPC used by\n // OpenCode. Keeping both at this boundary prevents every consumer from\n // writing a second route just to translate protocol envelopes.\n if (isMcpRequest(request, body)) {\n return createAppToolMcpHandler(auth.ctx, opts)(request)\n }\n\n if (!isRecord(body)) return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n const args = (body.args ?? body.arguments ?? body) as Record<string, unknown>\n\n // A custom tool passed as `tool` is registered for this dispatch so the route\n // file stays a one-liner (no separate customTools wiring needed).\n const toolName = typeof opts.tool === 'string' ? opts.tool : opts.tool.name\n const outcome = await dispatchAppTool(toolName, args, auth.ctx, dispatchOptions(opts))\n if (!outcome.ok) {\n return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) })\n }\n const payload = outcome.result as Record<string, unknown>\n return Response.json({ ok: true, ...payload, ...(opts.message ? { message: opts.message(outcome.result) } : {}) })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4DO,SAAS,wBACd,MAC0B;AAC1B,QAAM,EAAE,UAAU,cAAc,QAAQ,IAAI;AAC5C,MAAI,YAAY,QAAW;AACzB,WAAO;AAAA,MACL,eAAe,CAAC,GAAG,SAAS,aAAa;AAAA,MACzC,YAAY,CAAC,GAAG,IAAI,IAAI,aAAa,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;AAAA,IAC1E;AAAA,EACF;AACA,QAAM,OAAO,IAAI;AAAA,IACf,KAAK,qBAAqB,aAAa,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAAA,EAC7E;AACA,QAAM,cAAc,SAAS,cAAc,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;AACrE,QAAM,OAAO,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAEvD,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,aAAa,oBAAI,IAAY;AACnC,aAAW,MAAM,SAAS;AACxB,UAAM,MAAM,KAAK,IAAI,EAAE;AACvB,QAAI,CAAC,IAAK;AACV,eAAW,KAAK,IAAI,iBAAiB,CAAC,GAAG;AACvC,UAAI,SAAS,cAAc,SAAS,CAAC,EAAG,eAAc,IAAI,CAAC;AAAA,IAC7D;AACA,QAAI,IAAI,cAAe,YAAW,KAAK,YAAa,eAAc,IAAI,CAAC;AACvE,eAAW,KAAK,IAAI,cAAc,CAAC,EAAG,YAAW,IAAI,CAAC;AAAA,EACxD;AACA,SAAO,EAAE,eAAe,CAAC,GAAG,aAAa,GAAG,YAAY,CAAC,GAAG,UAAU,EAAE;AAC1E;AAOO,SAAS,iBACd,UACA,SACiB;AACjB,QAAM,QAAQ,IAAI,IAAI,OAAO;AAC7B,SAAO;AAAA,IACL,eAAe,SAAS,cAAc,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAAA,IAChE,gBAAgB,SAAS,eAAe,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAAA,EACpE;AACF;;;ACjEA,eAAsB,gBACpBA,WACA,SACA,KACA,MACyB;AACzB,MAAI;AACF,QAAI,CAAC,cAAcA,SAAQ,GAAG;AAC5B,YAAM,SAAS,eAAeA,WAAU,KAAK,WAAW;AACxD,UAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,MAAM,gBAAgB,SAAS,GAAGA,SAAQ,uBAAuB;AAGlG,YAAM,SAAS,MAAM,OAAO,QAAQ,SAAS,GAAG;AAChD,aAAO,EAAE,IAAI,MAAM,OAAO;AAAA,IAC5B;AAEA,QAAIA,cAAa,mBAAmB;AAClC,YAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE,EAAE,KAAK;AAC7C,YAAM,QAAQ,OAAO,QAAQ,SAAS,EAAE,EAAE,KAAK;AAC/C,UAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,cAAc,SAAS,IAAI,GAAG;AACxD,eAAO,EAAE,IAAI,OAAO,MAAM,gBAAgB,SAAS,wBAAwB,KAAK,SAAS,cAAc,KAAK,IAAI,CAAC,IAAI;AAAA,MACvH;AACA,UAAI,CAAC,MAAO,QAAO,EAAE,IAAI,OAAO,MAAM,iBAAiB,SAAS,qBAAqB;AACrF,YAAM,cAAc,QAAQ,eAAe,OAAO,OAAO,OAAO,QAAQ,WAAW;AAGnF,UAAI,YAAY,KAAK,SAAS,eAAe,SAAS,IAAI;AAC1D,UAAI,KAAK,eAAe;AACtB,YAAI;AACF,sBAAY,MAAM,KAAK,cAAc,MAAM,EAAE,OAAO,YAAY,GAAG,GAAG;AAAA,QACxE,QAAQ;AACN,sBAAY;AAAA,QACd;AAAA,MACF;AACA,YAAMC,KAAI,MAAM,KAAK,SAAS,eAAe,EAAE,MAAM,OAAO,aAAa,UAAU,GAAG,GAAG;AAKzF,YAAM,EAAE,YAAY,SAAS,QAAQ,GAAG,MAAM,IAAIA;AAClD,YAAM,kBAAkB,UAAU;AAClC,WAAK,aAAa;AAAA,QAChB,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,QAAQ,oBAAoB,aAAa,aAAa;AAAA,QACtD,SAAS,eAAe;AAAA,MAC1B,CAAC;AACD,aAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,GAAG,OAAO,QAAQ,iBAAiB,YAAY,SAAS,UAAU,EAAE;AAAA,IACnG;AAEA,QAAID,cAAa,qBAAqB;AACpC,YAAMC,KAAI,MAAM,KAAK,SAAS;AAAA,QAC5B,EAAE,OAAO,OAAO,QAAQ,SAAS,EAAE,GAAG,SAAS,OAAO,QAAQ,WAAW,EAAE,GAAG,UAAU,QAAQ,SAA+B;AAAA,QAC/H;AAAA,MACF;AACA,aAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,YAAYA,GAAE,IAAI,SAASA,GAAE,SAAS,SAASA,GAAE,QAAQ,EAAE;AAAA,IAC1F;AAEA,QAAID,cAAa,aAAa;AAC5B,2BAAqB,QAAQ,MAAM;AACnC,YAAMC,KAAI,MAAM,KAAK,SAAS,SAAS,EAAE,OAAO,OAAO,QAAQ,SAAS,EAAE,GAAG,QAAQ,QAAQ,OAAO,GAAG,GAAG;AAC1G,WAAK,aAAa,EAAE,MAAM,YAAY,MAAMA,GAAE,MAAM,SAASA,GAAE,QAAQ,CAAC;AACxE,aAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,MAAMA,GAAE,KAAK,EAAE;AAAA,IAC9C;AAGA,UAAM,IAAI,MAAM,KAAK,SAAS;AAAA,MAC5B,EAAE,MAAM,OAAO,QAAQ,QAAQ,EAAE,GAAG,OAAO,OAAO,QAAQ,SAAS,EAAE,GAAG,OAAO,QAAQ,MAA4B;AAAA,MACnH;AAAA,IACF;AACA,WAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,YAAY,EAAE,YAAY,MAAM,EAAE,KAAK,EAAE;AAAA,EACxE,SAAS,KAAK;AACZ,QAAI,eAAe,eAAgB,QAAO,EAAE,IAAI,OAAO,MAAM,IAAI,MAAM,SAAS,IAAI,SAAS,QAAQ,IAAI,OAAO;AAChH,WAAO,EAAE,IAAI,OAAO,MAAM,kBAAkB,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG,QAAQ,IAAI;AAAA,EACrH;AACF;AAIO,SAAS,cAAc,SAAyD;AACrF,SAAO,QAAQ,UAAU;AAC3B;;;ACjGO,SAAS,6BAA6B,MAAsD;AACjG,SAAO,CAAC,EAAE,UAAAC,WAAU,KAAK,MAAM,gBAAgBA,WAAU,MAAM,KAAK,KAAK,IAAI;AAC/E;;;ACHA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,aAAa,SAAkB,OAAyB;AAI/D,QAAM,aAAa,QAAQ,QACxB,IAAI,QAAQ,GACX,MAAM,GAAG,EACV,KAAK,CAAC,SAAS,KAAK,KAAK,EAAE,YAAY,MAAM,mBAAmB;AACnE,SACE,SAAS,KAAK,MAEZ,aAAa,SACV,eAAe,QACf,QAAQ,QAAQ,IAAI,gBAAgB,KACpC,QAAQ,QAAQ,IAAI,sBAAsB;AAGnD;AAEA,SAAS,SAAS,MAA+C;AAC/D,SAAO,OAAO,SAAS,WAAW,OAAO,KAAK;AAChD;AAEA,SAAS,gBAAgB,MAAiD;AACxE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,aACE,OAAO,KAAK,SAAS,WAAW,KAAK,cAAc,CAAC,GAAI,KAAK,eAAe,CAAC,GAAI,KAAK,IAAI;AAAA,EAC9F;AACF;AAEA,SAAS,gBAAgB,MAIvB;AACA,QAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,QAAM,QAAQ,wBAAwB,KAAK,UAAU;AAAA,IACnD,GAAI,KAAK,eAAe,OAAO,KAAK,SAAS,WACzC,EAAE,cAAc,EAAE,CAAC,KAAK,IAAI,GAAG,KAAK,YAAY,EAAE,IAClD,CAAC;AAAA,IACL,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;AAAA,IACrE,GAAI,OAAO,KAAK,SAAS,WAAW,CAAC,IAAI,EAAE,aAAa,CAAC,KAAK,IAAI,EAAE;AAAA,EACtE,CAAC;AACD,QAAM,aAAa,MAAM,KAAK,CAAC,UAAU,MAAM,SAAS,SAAS,IAAI;AACrE,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,uCAAuC,IAAI,EAAE;AAAA,EAC/D;AACA,SAAO;AAAA,IACL;AAAA,IACA,aAAa,WAAW,SAAS;AAAA,IACjC,aAAa,WAAW,SAAS;AAAA,EACnC;AACF;AAEA,SAAS,cAAc,SAAyB,MAAoD;AAClG,MAAI,CAAC,QAAQ,IAAI;AACf,WAAO;AAAA,MACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,GAAG,QAAQ,IAAI,KAAK,QAAQ,OAAO,GAAG,CAAC;AAAA,MACvE,SAAS;AAAA,IACX;AAAA,EACF;AAEA,QAAM,QAAQ,QAAQ;AACtB,QAAM,UAAU,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC/E,EAAE,IAAI,MAAM,GAAG,MAAM,IACrB,EAAE,IAAI,MAAM,MAAM;AACtB,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU;AAAA,MAC7C,GAAG;AAAA,MACH,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,IACzD,CAAC,EAAE,CAAC;AAAA,EACN;AACF;AAEA,SAAS,wBACP,aACA,MACyC;AACzC,QAAM,WAAW,gBAAgB,IAAI;AACrC,SAAO,qBAAqB;AAAA,IAC1B,YAAY,EAAE,MAAM,kBAAkB,SAAS,IAAI;AAAA,IACnD,OAAO,CAAC;AAAA,MACN,GAAG;AAAA,MACH,KAAK,CAAC,SAAS,gBAAgB,SAAS,MAAM,MAAM,aAAa,gBAAgB,IAAI,CAAC;AAAA,IACxF,CAAC;AAAA,IACD,UAAU,OAAO,CAAC;AAAA,IAClB,cAAc,CAAC,WAAW,cAAc,QAA0B,IAAI;AAAA,EACxE,CAAC;AACH;AASA,eAAsB,qBAAqB,SAAkB,MAAmD;AAC9G,MAAI,QAAQ,WAAW,OAAQ,QAAO,SAAS,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEpG,QAAM,OAAO,MAAM,wBAAwB,SAAS,EAAE,aAAa,KAAK,aAAa,aAAa,KAAK,YAAY,CAAC;AACpH,MAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAE1B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,QAAQ,MAAM,EAAE,KAAK;AAAA,EACpC,QAAQ;AACN,WAAO,SAAS,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjE;AAMA,MAAI,aAAa,SAAS,IAAI,GAAG;AAC/B,WAAO,wBAAwB,KAAK,KAAK,IAAI,EAAE,OAAO;AAAA,EACxD;AAEA,MAAI,CAAC,SAAS,IAAI,EAAG,QAAO,SAAS,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AACpF,QAAM,OAAQ,KAAK,QAAQ,KAAK,aAAa;AAI7C,QAAMC,YAAW,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,KAAK,KAAK;AACvE,QAAM,UAAU,MAAM,gBAAgBA,WAAU,MAAM,KAAK,KAAK,gBAAgB,IAAI,CAAC;AACrF,MAAI,CAAC,QAAQ,IAAI;AACf,WAAO,SAAS,KAAK,EAAE,OAAO,QAAQ,MAAM,SAAS,QAAQ,QAAQ,GAAG,EAAE,QAAQ,cAAc,OAAO,EAAE,CAAC;AAAA,EAC5G;AACA,QAAM,UAAU,QAAQ;AACxB,SAAO,SAAS,KAAK,EAAE,IAAI,MAAM,GAAG,SAAS,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,QAAQ,MAAM,EAAE,IAAI,CAAC,EAAG,CAAC;AACnH;","names":["toolName","r","toolName","toolName"]}
@@ -43,6 +43,16 @@ export interface OperatorClient {
43
43
  waitSeconds?: number;
44
44
  signal?: AbortSignal;
45
45
  }): Promise<OperatorResult<OperatorTurn>>;
46
+ /**
47
+ * Stop a turn. A settled turn comes back as it settled; otherwise the app
48
+ * has been asked to stop it, and the server holds up to `waitSeconds` for
49
+ * the turn to settle `failed` with `OPERATOR_TURN_CANCELLED_CODE`. Read it
50
+ * with `getTurn` or `waitForTurn` until it settles.
51
+ */
52
+ cancelTurn(workspaceId: string, threadId: string, turnId: string, options?: {
53
+ waitSeconds?: number;
54
+ signal?: AbortSignal;
55
+ }): Promise<OperatorResult<OperatorTurn>>;
46
56
  /** Follow until the turn settles, waits on a decision, or the timeout passes; the last read is returned. */
47
57
  waitForTurn(workspaceId: string, threadId: string, turnId: string, options: WaitForTurnOptions): Promise<OperatorResult<OperatorTurn>>;
48
58
  listThreads(workspaceId: string, query?: {
@@ -43,7 +43,7 @@ export interface OperatorAppInfo {
43
43
  /** Optional operations this app implements; absent ones answer 501. */
44
44
  capabilities: OperatorCapability[];
45
45
  }
46
- export type OperatorCapability = 'turns.attachments' | 'workspaces.create' | 'threads.read' | 'files.read' | 'journal.read' | 'assets.read' | 'scorecard.read';
46
+ export type OperatorCapability = 'turns.attachments' | 'turns.cancel' | 'workspaces.create' | 'threads.read' | 'files.read' | 'journal.read' | 'assets.read' | 'scorecard.read';
47
47
  export interface OperatorPrincipalInfo {
48
48
  keyId: string;
49
49
  scopes: string[];
@@ -71,6 +71,8 @@ export interface OperatorThread {
71
71
  */
72
72
  export type OperatorTurnState = 'queued' | 'working' | 'input-required' | 'succeeded' | 'failed' | 'unknown';
73
73
  export declare const OPERATOR_SETTLED_STATES: readonly OperatorTurnState[];
74
+ /** `failure.code` of a turn that settled because it was cancelled before it finished. */
75
+ export declare const OPERATOR_TURN_CANCELLED_CODE = "turn.cancelled";
74
76
  export interface OperatorAssetRef {
75
77
  id: string;
76
78
  /** Root-relative operator route that returns the bytes. */
@@ -227,6 +229,11 @@ export declare const OPERATOR_ROUTES: readonly [{
227
229
  readonly method: 'GET';
228
230
  readonly path: '/workspaces/:workspaceId/threads/:threadId/turns/:turnId';
229
231
  readonly scopes: readonly ["operator:read"];
232
+ }, {
233
+ readonly id: 'turns.cancel';
234
+ readonly method: 'POST';
235
+ readonly path: '/workspaces/:workspaceId/threads/:threadId/turns/:turnId/cancel';
236
+ readonly scopes: readonly ["operator:read", "operator:run"];
230
237
  }, {
231
238
  readonly id: 'approvals.list';
232
239
  readonly method: 'GET';
@@ -52,6 +52,7 @@ function operatorKeyWorkspaces(scopes) {
52
52
  return restricted.map((scope) => scope.slice(OPERATOR_WORKSPACE_SCOPE_PREFIX.length)).filter((id) => WORKSPACE_ID.test(id));
53
53
  }
54
54
  var OPERATOR_SETTLED_STATES = ["succeeded", "failed"];
55
+ var OPERATOR_TURN_CANCELLED_CODE = "turn.cancelled";
55
56
  var OPERATOR_ROUTES = [
56
57
  { id: "describe", method: "GET", path: "", scopes: ["operator:read"] },
57
58
  { id: "workspaces.list", method: "GET", path: "/workspaces", scopes: ["operator:read"] },
@@ -61,6 +62,7 @@ var OPERATOR_ROUTES = [
61
62
  { id: "threads.list", method: "GET", path: "/workspaces/:workspaceId/threads", scopes: ["operator:read"] },
62
63
  { id: "threads.get", method: "GET", path: "/workspaces/:workspaceId/threads/:threadId", scopes: ["operator:read"] },
63
64
  { id: "turns.get", method: "GET", path: "/workspaces/:workspaceId/threads/:threadId/turns/:turnId", scopes: ["operator:read"] },
65
+ { id: "turns.cancel", method: "POST", path: "/workspaces/:workspaceId/threads/:threadId/turns/:turnId/cancel", scopes: ["operator:read", "operator:run"] },
64
66
  { id: "approvals.list", method: "GET", path: "/workspaces/:workspaceId/approvals", scopes: ["operator:read"] },
65
67
  { id: "journal.list", method: "GET", path: "/workspaces/:workspaceId/journal", scopes: ["operator:read"] },
66
68
  { id: "files.list", method: "GET", path: "/workspaces/:workspaceId/files", scopes: ["operator:read"] },
@@ -210,6 +212,7 @@ function parseStartTurn(body, maxAttachments) {
210
212
  }
211
213
  function capabilities(adapter) {
212
214
  const present = [
215
+ ["turns.cancel", adapter.cancelTurn],
213
216
  ["workspaces.create", adapter.createWorkspace],
214
217
  ["threads.read", adapter.getThread ?? adapter.listThreads],
215
218
  ["files.read", adapter.readFile ?? adapter.listFiles],
@@ -223,8 +226,11 @@ function capabilities(adapter) {
223
226
  function unsupported() {
224
227
  throw new OperatorError("operator.unsupported", 501, "This app does not implement this operation");
225
228
  }
229
+ function settled(turn) {
230
+ return OPERATOR_SETTLED_STATES.includes(turn.state);
231
+ }
226
232
  function needsAttention(turn) {
227
- return turn.state === "succeeded" || turn.state === "failed" || turn.state === "input-required" || turn.approvals.length > 0;
233
+ return settled(turn) || turn.state === "input-required" || turn.approvals.length > 0;
228
234
  }
229
235
  var sleep = (ms, signal) => new Promise((resolve) => {
230
236
  if (signal.aborted) return resolve();
@@ -308,8 +314,19 @@ function createOperatorApi(options) {
308
314
  if (restriction && !restriction.includes(workspaceId)) {
309
315
  throw new OperatorError("operator.workspace_not_found", 404, "Workspace not found");
310
316
  }
311
- const workspace = await adapter.authorizeWorkspace(ctx, workspaceId, route.id === "turns.start" ? "run" : "read");
317
+ const runs = route.id === "turns.start" || route.id === "turns.cancel";
318
+ const workspace = await adapter.authorizeWorkspace(ctx, workspaceId, runs ? "run" : "read");
312
319
  if (!workspace) throw new OperatorError("operator.workspace_not_found", 404, "Workspace not found");
320
+ const hold = async (first, target, wait, done) => {
321
+ const deadline = Date.now() + wait * 1e3;
322
+ let turn = first;
323
+ while (turn && !done(turn) && Date.now() + pollIntervalMs <= deadline && !request.signal.aborted) {
324
+ await sleep(pollIntervalMs, request.signal);
325
+ turn = await adapter.getTurn(ctx, workspace, target);
326
+ }
327
+ if (!turn) throw new OperatorError("operator.turn_not_found", 404, "Turn not found");
328
+ return turn;
329
+ };
313
330
  switch (route.id) {
314
331
  case "workspaces.get":
315
332
  return json({ workspace });
@@ -338,14 +355,17 @@ function createOperatorApi(options) {
338
355
  case "turns.get": {
339
356
  const target = { threadId: params.threadId, turnId: params.turnId };
340
357
  const wait = boundedInteger(url.searchParams.get("wait"), 0, 0, maxWaitSeconds, "wait");
341
- const deadline = Date.now() + wait * 1e3;
342
- let turn = await adapter.getTurn(ctx, workspace, target);
343
- while (turn && !needsAttention(turn) && Date.now() + pollIntervalMs <= deadline && !request.signal.aborted) {
344
- await sleep(pollIntervalMs, request.signal);
345
- turn = await adapter.getTurn(ctx, workspace, target);
346
- }
347
- if (!turn) throw new OperatorError("operator.turn_not_found", 404, "Turn not found");
348
- return json({ turn });
358
+ return json({ turn: await hold(await adapter.getTurn(ctx, workspace, target), target, wait, needsAttention) });
359
+ }
360
+ case "turns.cancel": {
361
+ if (!adapter.cancelTurn) unsupported();
362
+ const target = { threadId: params.threadId, turnId: params.turnId };
363
+ const wait = boundedInteger(url.searchParams.get("wait"), 0, 0, maxWaitSeconds, "wait");
364
+ const current = await adapter.getTurn(ctx, workspace, target);
365
+ if (!current) throw new OperatorError("operator.turn_not_found", 404, "Turn not found");
366
+ if (settled(current)) return json({ turn: current });
367
+ const turn = await hold(await adapter.cancelTurn(ctx, workspace, target), target, wait, settled);
368
+ return json({ turn }, settled(turn) ? 200 : 202);
349
369
  }
350
370
  case "approvals.list":
351
371
  return json({ approvals: await adapter.listApprovals(ctx, workspace) });
@@ -470,6 +490,11 @@ function createOperatorClient(options) {
470
490
  (body) => body.turn,
471
491
  opts.signal ? { signal: opts.signal } : {}
472
492
  ),
493
+ cancelTurn: (workspaceId, threadId, turnId, opts = {}) => call(
494
+ `/workspaces/${segment(workspaceId)}/threads/${segment(threadId)}/turns/${segment(turnId)}/cancel${query({ wait: opts.waitSeconds })}`,
495
+ (body) => body.turn,
496
+ { method: "POST", ...opts.signal ? { signal: opts.signal } : {} }
497
+ ),
473
498
  async waitForTurn(workspaceId, threadId, turnId, opts) {
474
499
  const deadline = Date.now() + opts.timeoutMs;
475
500
  const waitSeconds = opts.waitSeconds ?? 25;
@@ -883,6 +908,7 @@ export {
883
908
  OPERATOR_ROUTES,
884
909
  OPERATOR_SCOPE_DEPENDENCIES,
885
910
  OPERATOR_SETTLED_STATES,
911
+ OPERATOR_TURN_CANCELLED_CODE,
886
912
  OPERATOR_WORKSPACE_SCOPE_PREFIX,
887
913
  OperatorError,
888
914
  createChatOperatorAdapter,