@tangle-network/agent-app 0.47.5 → 0.47.7
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.
- package/dist/billing/identity-bound.d.ts +48 -4
- package/dist/billing/index.d.ts +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/chat-routes/index.d.ts +1 -0
- package/dist/chat-routes/index.js +182 -0
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-routes/protected-runtime-producer.d.ts +22 -0
- package/dist/{chunk-TS2ZIB53.js → chunk-4ZJYNI53.js} +112 -30
- package/dist/chunk-4ZJYNI53.js.map +1 -0
- package/dist/chunk-KHRIPDW5.js +135 -0
- package/dist/chunk-KHRIPDW5.js.map +1 -0
- package/dist/preset-cloudflare/index.js +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/protected-model.d.ts +29 -0
- package/package.json +1 -1
- package/dist/chunk-TS2ZIB53.js.map +0 -1
package/dist/runtime/index.d.ts
CHANGED
package/dist/runtime/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProtectedModelSettlementError,
|
|
3
|
+
createRouterProtectedModelPort
|
|
4
|
+
} from "../chunk-KHRIPDW5.js";
|
|
1
5
|
import {
|
|
2
6
|
DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
|
|
3
7
|
DEFAULT_TANGLE_ROUTER_BASE_URL,
|
|
@@ -308,12 +312,14 @@ export {
|
|
|
308
312
|
DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
|
|
309
313
|
DEFAULT_TANGLE_ROUTER_BASE_URL,
|
|
310
314
|
MAX_RECOMMENDED_MODELS,
|
|
315
|
+
ProtectedModelSettlementError,
|
|
311
316
|
TangleExecutionKeyError,
|
|
312
317
|
__resetCatalogCache,
|
|
313
318
|
buildCatalog,
|
|
314
319
|
catalogModelForId,
|
|
315
320
|
createCertifiedDelivery,
|
|
316
321
|
createOpenAICompatStreamTurn,
|
|
322
|
+
createRouterProtectedModelPort,
|
|
317
323
|
createSurfaceRegistry,
|
|
318
324
|
createTangleRouterModelConfig,
|
|
319
325
|
defineSurfaceKind,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/openai-stream.ts","../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts","../../src/runtime/loop.ts"],"sourcesContent":["/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopMessage, LoopToolCall } from './loop'\nimport { normalizeModelId } from './model-catalog'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n /** The model that produced this chunk. The Tangle Router reports the DATED\n * upstream id here (`gpt-5-2025-08-07`) whether or not it substituted, so\n * this is a served-model source only after id folding — see\n * {@link OpenAICompatServedModel}. */\n model?: string\n choices?: Array<{\n delta?: {\n content?: string | null\n /** Reasoning deltas — DeepSeek/router use `reasoning_content`; some proxies use `thinking`. */\n reasoning_content?: string | null\n thinking?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n /** Final-chunk token accounting (requires `stream_options.include_usage`). */\n usage?: {\n prompt_tokens?: number\n completion_tokens?: number\n } | null\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\n }\n }\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n const reasoning = choice.delta?.reasoning_content ?? choice.delta?.thinking\n if (reasoning) yield { type: 'reasoning', text: reasoning }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\n/**\n * Which model actually served one direct-router turn.\n *\n * The router substitutes models on purpose — a quota-walled primary comes back\n * `200` answered by a different model — and says so in response headers. This\n * lane used to drop the whole `Response` after taking `.body`, so a turn\n * requested as `claude-sonnet-4-6` and answered by `openai/gpt-5` was recorded\n * by its caller as Claude: per-model quality scoring blamed the wrong model and\n * cost used the wrong price basis.\n *\n * Map this onto the shell's existing attribution contract rather than inventing\n * a second channel — `ChatTurnRouteProducer.modelAttribution()` (`/chat-routes`):\n *\n * modelAttribution: () => ({ requestedModel, servedModel, echoReceived: true })\n *\n * Leave that contract's `servedSource` unset: its union is sandbox\n * profile-resolution vocabulary with no router analogue.\n */\nexport interface OpenAICompatServedModel {\n /** The model id this turn asked for (`OpenAICompatStreamTurnOptions.model`). */\n requestedModel: string\n /** The model the router/provider reports having actually served. */\n servedModel: string\n /** Where `servedModel` was read from. The header is the router's own\n * substitution signal; the body is the backstop that survives CORS. */\n source: 'router_header' | 'response_body'\n /** True when served differs from requested after id folding. Folded, not\n * compared raw: the body reports a dated id on EVERY turn, so `!==` would\n * claim a substitution every time. */\n substituted: boolean\n /** `x-tangle-failover` `trigger=` — why the router swapped. Absent when the\n * router did not inject the substitute (a caller-supplied fallback chain\n * sets the served-model header without the failover one). */\n trigger?: string\n /** `x-tangle-failover` `degraded=`. */\n degraded?: boolean\n}\n\n/** Parse `from=…; to=…; trigger=…; degraded=…` down to the fields we report.\n * Absent/garbled segments are simply omitted — attribution is best-effort and\n * must never fail a turn that is otherwise streaming fine. */\nfunction parseFailoverHeader(raw: string | null): { trigger?: string; degraded?: boolean } {\n if (!raw) return {}\n const fields = new Map<string, string>()\n for (const segment of raw.split(';')) {\n const eq = segment.indexOf('=')\n if (eq === -1) continue\n fields.set(segment.slice(0, eq).trim().toLowerCase(), segment.slice(eq + 1).trim())\n }\n const trigger = fields.get('trigger')\n const degraded = fields.get('degraded')\n return {\n ...(trigger ? { trigger } : {}),\n ...(degraded != null ? { degraded: degraded === 'true' } : {}),\n }\n}\n\n/** Define options for configuring an OpenAI-compatible streaming chat turn including API details and tools */\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n /**\n * Called at most ONCE per turn, as soon as the serving model is\n * determinable, with what actually answered. Never called when neither the\n * header nor the body names a model — silence means \"learned nothing\", not\n * \"nothing was substituted\".\n *\n * `streamTurn` runs once per TOOL turn, so a multi-turn `runAppToolLoop`\n * fires this once per turn; take the last for row attribution.\n */\n onServedModel?: (served: OpenAICompatServedModel) => void\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }, opts.onServedModel ? { requestedModel: opts.model, report: opts.onServedModel } : undefined),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n attribution?: { requestedModel: string; report: (served: OpenAICompatServedModel) => void },\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n const error = new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n // Stamp the NUMERIC status. `isUpstreamUnavailable` (`/model-resolution`)\n // reads a numeric field first and prose only as a backstop, and a\n // Cloudflare EDGE 502 gives it nothing else to read: `content-type:\n // text/plain`, a body of `error code: 502`, and none of the router's own\n // `x-tangle-*` headers, because the origin never ran. Carrying the status\n // as data keeps THIS path — the browser/edge copilot lane, which calls the\n // router directly — out of the string-matching business entirely.\n Object.assign(error, { status: res.status })\n throw error\n }\n // Served-model attribution, fired at most once, as early as it is knowable.\n //\n // The header comes first because it is the router's OWN substitution signal:\n // `X-Tangle-Served-Model` is set only when the served model differs from the\n // requested one, so its presence is authoritative and it arrives before the\n // first byte of content. `X-Tangle-Failover` rides along only when the router\n // itself picked the substitute (a caller-supplied fallback chain sets the\n // former without the latter), which is why `trigger`/`degraded` are optional.\n //\n // The body is the backstop for when the header cannot reach us. In a BROWSER,\n // headers are invisible to JS unless the server lists them in\n // `Access-Control-Expose-Headers` — tangle-router#324 added both, but that\n // only helps once it is DEPLOYED, and any other OpenAI-compat endpoint\n // pointed at this client exposes nothing. The chunk `model` field is not\n // subject to CORS, so it keeps the browser lane attributable regardless.\n let reported = false\n const report = (served: OpenAICompatServedModel): void => {\n if (reported || !attribution) return\n reported = true\n attribution.report(served)\n }\n if (attribution) {\n const servedHeader = res.headers.get('x-tangle-served-model')\n if (servedHeader) {\n report({\n requestedModel: attribution.requestedModel,\n servedModel: servedHeader,\n source: 'router_header',\n substituted: normalizeModelId(servedHeader) !== normalizeModelId(attribution.requestedModel),\n ...parseFailoverHeader(res.headers.get('x-tangle-failover')),\n })\n }\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n let chunk: OpenAIStreamChunk\n try {\n chunk = JSON.parse(data) as OpenAIStreamChunk\n } catch {\n continue /* skip a partial/garbled SSE frame */\n }\n // Body fallback: only when the header said nothing. Fold both ids before\n // comparing — the router reports the dated upstream id (`gpt-5-2025-08-07`)\n // for a request of `openai/gpt-5` even with NO substitution, so a raw\n // `!==` would report a swap on literally every turn.\n if (attribution && !reported && chunk.model) {\n report({\n requestedModel: attribution.requestedModel,\n servedModel: chunk.model,\n source: 'response_body',\n substituted: normalizeModelId(chunk.model) !== normalizeModelId(attribution.requestedModel),\n })\n }\n yield chunk\n }\n }\n}\n","/**\n * `createCertifiedDelivery` — the delivery truck for Tangle Intelligence.\n *\n * Pulls a tenant's CERTIFIED `AgentProfile` from the deployed plane\n * (`GET /v1/profiles/:target/composed`) and applies it to the agent's resolved\n * surfaces each turn, so an approved improvement reaches the running agent with\n * NO redeploy. This is the `composeProfile` transform `createAgentRuntime`\n * accepts — opt-in per product, fail-closed, cached + refreshed.\n *\n * Profile-WIDE by design (not prompt-only): the composed profile carries every\n * promoted artifact type keyed by kind. What's folded where:\n * - `prompt-surface` + `skill` → the system prompt (via `composeCertifiedPrompt`).\n * - `tool` artifacts that carry an OpenAI tool definition → `extraTools`\n * (advertised to the model; the matching executor is supplied by the product\n * via `executeOtherTool`). Until a `tool` artifact carries a runnable def,\n * it is surfaced (see `current()`) but not advertised — advertising a tool\n * with no executor would make the model call into a dead end.\n * - `mcp` / `memory` / `rag` artifacts materialize as servers/files and deliver\n * through the SANDBOX-provisioning seam, not this in-process one. The full\n * certified profile is exposed via `current()` so that seam can consume it.\n *\n * Substrate boundary: THIS module imports `@tangle-network/agent-runtime`; the\n * `createAgentRuntime` core does not (it only consumes the generic transform).\n */\n\nimport {\n composeCertifiedPrompt,\n type CertifiedProfile,\n pullCertified,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\nconst defaultRefreshMs = 300_000\n\n/** Define configuration options for delivering certified artifacts to a specified tenant target */\nexport interface CertifiedDeliveryConfig {\n /** The tenant target whose certified artifacts to deliver (the agent id). */\n target: string\n /** Bearer for the plane. Reads `TANGLE_API_KEY` when omitted. */\n apiKey?: string\n /** Plane base URL. Reads `TANGLE_INTELLIGENCE_URL` then the public plane. */\n baseUrl?: string\n /** Min interval between certified-profile pulls. Default 5m. */\n refreshMs?: number\n /** fetch impl (tests / non-global-fetch runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** Resolve and manage certified profiles with refresh and composition capabilities */\nexport interface CertifiedDelivery {\n /** The `composeProfile` transform to pass to `createAgentRuntime`. Applies the\n * cached certified profile to the base surfaces; refreshes on the cadence. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now (ignores the refresh window). Best-effort. */\n refresh(): Promise<void>\n /** The certified profile currently in effect (null = none promoted / pull\n * failed). Lets the sandbox-provisioning seam deliver the file/server\n * artifact types this in-process seam doesn't. */\n current(): CertifiedProfile | null\n}\n\n/**\n * Build a certified-delivery transform for one agent target. Fail-closed: a pull\n * error or 404 keeps the last-known certified profile (or null), and the agent\n * runs on its base surfaces — it never breaks because Intelligence is down.\n */\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const refreshMs = config.refreshMs ?? defaultRefreshMs\n let certified: CertifiedProfile | null = null\n let lastPullAt = 0\n let inflight: Promise<void> | null = null\n\n async function refresh(force = false): Promise<void> {\n if (!force && Date.now() - lastPullAt < refreshMs) return\n if (inflight) return inflight\n inflight = (async () => {\n const outcome = await pullCertified({\n target: config.target,\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n fetchImpl: config.fetchImpl,\n })\n lastPullAt = Date.now()\n // Only replace on a real pull; a 404/error keeps the last-known profile.\n if (outcome.succeeded) certified = outcome.value\n })()\n try {\n await inflight\n } finally {\n inflight = null\n }\n }\n\n return {\n async composeProfile(base) {\n await refresh()\n return {\n // prompt-surface + skill fold into the system prompt.\n systemPrompt: composeCertifiedPrompt(base.systemPrompt, certified),\n // Certified `tool` artifacts deliver here once they carry a runnable\n // OpenAI def + the product wires the executor; until then pass through.\n extraTools: base.extraTools,\n }\n },\n refresh: () => refresh(true),\n current: () => certified,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs from server configuration\n * (`buildHttpMcpServer` in ../tools). A client can therefore never inject an\n * arbitrary MCP url, header, or credential into the agent profile: the\n * overlay's `mcp` values are typed as {@link SurfaceMcpServer} (= the\n * server-built `AppToolMcpServer` entry shape), and only build() constructs\n * them.\n *\n * The credential itself never appears in the overlay. Since the tagged-config\n * contract (`agent-interface` 0.38.0) the `Authorization` header is a\n * `secret-ref` naming a box-environment variable, which the sandbox resolves\n * privately; `build()` supplies that key NAME from server configuration and the\n * product writes the value into `SandboxRuntimeConfig.env` at box creation. The\n * value must be deterministic for the box's lifetime (an HMAC over the\n * workspace id, e.g. `createCapabilityToken` in ../tools), because a freshly\n * random per-request mint would never match what the box already carries.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n","/**\n * The bounded agent tool-loop — owned by `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop streams a turn, collects the executable tool\n * calls, dispatches each, appends the results to history in OpenAI\n * function-calling shape, and re-runs so the model reads them — bounded by\n * `maxToolTurns`, a wall-clock `deadlineMs`, and a `maxCostUsd` budget.\n *\n * The history shape is the OpenAI function-calling contract: the assistant turn\n * that emitted tool calls is preserved as an `assistant` message carrying its\n * `tool_calls` array, and each result is its own `{ role: 'tool', tool_call_id,\n * content }` message keyed to the call. A strict model (Claude, and any\n * OpenAI-compatible provider that validates tool history) needs this to read its\n * own tool use back; folding results into a `user` message makes such models\n * re-issue the same call in a loop.\n *\n * The loop is substrate-owned (`runToolLoop` / `streamToolLoop`); the app\n * supplies `streamTurn` (wrapping its model endpoint) and `executeToolCall`\n * (routing to its integration + app-tool executors). The app-facing names below\n * are 1:1 aliases of the canonical symbols, kept so this package's consumers and\n * the in-package `createAgentRuntime` read against a single, stable vocabulary.\n *\n * This is the LEAF the runtime barrel and its children both import — keeping the\n * tool-loop vocabulary out of any import cycle. The barrel re-exports it.\n */\n\nexport {\n runToolLoop as runAppToolLoop,\n streamToolLoop as streamAppToolLoop,\n} from '@tangle-network/agent-runtime/tool-loop'\nexport type {\n ToolLoopCall as LoopToolCall,\n ToolLoopAssistantToolCall as LoopAssistantToolCall,\n ToolLoopMessage as LoopMessage,\n ToolLoopEvent,\n ToolLoopStopReason,\n ToolLoopResult,\n RunToolLoopOptions as AppToolLoopOptions,\n StreamToolLoopOptions as StreamAppToolLoopOptions,\n StreamToolLoopYield as StreamLoopYield,\n} from '@tangle-network/agent-runtime/tool-loop'\n\n/**\n * Events the app's OpenAI-compat stream adapter ({@link toLoopEvents}) yields.\n *\n * This is the app's own `Raw` event type for the streaming loop — the canonical\n * `streamToolLoop<Raw>` is generic over it. It widens the substrate's\n * tool-loop event with `reasoning` (DeepSeek/router `reasoning_content` /\n * `thinking` deltas, rendered as thinking sections) and `usage` (per-message\n * token accounting) — neither belongs in the substrate's loop contract, so they\n * stay here. The adapter maps each into the `streamTurn` seam; `text` and\n * `tool_call` drive the loop, `reasoning` / `usage` pass through to the UI.\n */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'reasoning'; text: string }\n | {\n type: 'tool_call'\n call: import('@tangle-network/agent-runtime/tool-loop').ToolLoopCall\n }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/openai-stream.ts","../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts","../../src/runtime/loop.ts"],"sourcesContent":["/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopMessage, LoopToolCall } from './loop'\nimport { normalizeModelId } from './model-catalog'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n /** The model that produced this chunk. The Tangle Router reports the DATED\n * upstream id here (`gpt-5-2025-08-07`) whether or not it substituted, so\n * this is a served-model source only after id folding — see\n * {@link OpenAICompatServedModel}. */\n model?: string\n choices?: Array<{\n delta?: {\n content?: string | null\n /** Reasoning deltas — DeepSeek/router use `reasoning_content`; some proxies use `thinking`. */\n reasoning_content?: string | null\n thinking?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n /** Final-chunk token accounting (requires `stream_options.include_usage`). */\n usage?: {\n prompt_tokens?: number\n completion_tokens?: number\n } | null\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\n }\n }\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n const reasoning = choice.delta?.reasoning_content ?? choice.delta?.thinking\n if (reasoning) yield { type: 'reasoning', text: reasoning }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\n/**\n * Which model actually served one direct-router turn.\n *\n * The router substitutes models on purpose — a quota-walled primary comes back\n * `200` answered by a different model — and says so in response headers. This\n * lane used to drop the whole `Response` after taking `.body`, so a turn\n * requested as `claude-sonnet-4-6` and answered by `openai/gpt-5` was recorded\n * by its caller as Claude: per-model quality scoring blamed the wrong model and\n * cost used the wrong price basis.\n *\n * Map this onto the shell's existing attribution contract rather than inventing\n * a second channel — `ChatTurnRouteProducer.modelAttribution()` (`/chat-routes`):\n *\n * modelAttribution: () => ({ requestedModel, servedModel, echoReceived: true })\n *\n * Leave that contract's `servedSource` unset: its union is sandbox\n * profile-resolution vocabulary with no router analogue.\n */\nexport interface OpenAICompatServedModel {\n /** The model id this turn asked for (`OpenAICompatStreamTurnOptions.model`). */\n requestedModel: string\n /** The model the router/provider reports having actually served. */\n servedModel: string\n /** Where `servedModel` was read from. The header is the router's own\n * substitution signal; the body is the backstop that survives CORS. */\n source: 'router_header' | 'response_body'\n /** True when served differs from requested after id folding. Folded, not\n * compared raw: the body reports a dated id on EVERY turn, so `!==` would\n * claim a substitution every time. */\n substituted: boolean\n /** `x-tangle-failover` `trigger=` — why the router swapped. Absent when the\n * router did not inject the substitute (a caller-supplied fallback chain\n * sets the served-model header without the failover one). */\n trigger?: string\n /** `x-tangle-failover` `degraded=`. */\n degraded?: boolean\n}\n\n/** Parse `from=…; to=…; trigger=…; degraded=…` down to the fields we report.\n * Absent/garbled segments are simply omitted — attribution is best-effort and\n * must never fail a turn that is otherwise streaming fine. */\nfunction parseFailoverHeader(raw: string | null): { trigger?: string; degraded?: boolean } {\n if (!raw) return {}\n const fields = new Map<string, string>()\n for (const segment of raw.split(';')) {\n const eq = segment.indexOf('=')\n if (eq === -1) continue\n fields.set(segment.slice(0, eq).trim().toLowerCase(), segment.slice(eq + 1).trim())\n }\n const trigger = fields.get('trigger')\n const degraded = fields.get('degraded')\n return {\n ...(trigger ? { trigger } : {}),\n ...(degraded != null ? { degraded: degraded === 'true' } : {}),\n }\n}\n\n/** Define options for configuring an OpenAI-compatible streaming chat turn including API details and tools */\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n /**\n * Called at most ONCE per turn, as soon as the serving model is\n * determinable, with what actually answered. Never called when neither the\n * header nor the body names a model — silence means \"learned nothing\", not\n * \"nothing was substituted\".\n *\n * `streamTurn` runs once per TOOL turn, so a multi-turn `runAppToolLoop`\n * fires this once per turn; take the last for row attribution.\n */\n onServedModel?: (served: OpenAICompatServedModel) => void\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }, opts.onServedModel ? { requestedModel: opts.model, report: opts.onServedModel } : undefined),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n attribution?: { requestedModel: string; report: (served: OpenAICompatServedModel) => void },\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n const error = new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n // Stamp the NUMERIC status. `isUpstreamUnavailable` (`/model-resolution`)\n // reads a numeric field first and prose only as a backstop, and a\n // Cloudflare EDGE 502 gives it nothing else to read: `content-type:\n // text/plain`, a body of `error code: 502`, and none of the router's own\n // `x-tangle-*` headers, because the origin never ran. Carrying the status\n // as data keeps THIS path — the browser/edge copilot lane, which calls the\n // router directly — out of the string-matching business entirely.\n Object.assign(error, { status: res.status })\n throw error\n }\n // Served-model attribution, fired at most once, as early as it is knowable.\n //\n // The header comes first because it is the router's OWN substitution signal:\n // `X-Tangle-Served-Model` is set only when the served model differs from the\n // requested one, so its presence is authoritative and it arrives before the\n // first byte of content. `X-Tangle-Failover` rides along only when the router\n // itself picked the substitute (a caller-supplied fallback chain sets the\n // former without the latter), which is why `trigger`/`degraded` are optional.\n //\n // The body is the backstop for when the header cannot reach us. In a BROWSER,\n // headers are invisible to JS unless the server lists them in\n // `Access-Control-Expose-Headers` — tangle-router#324 added both, but that\n // only helps once it is DEPLOYED, and any other OpenAI-compat endpoint\n // pointed at this client exposes nothing. The chunk `model` field is not\n // subject to CORS, so it keeps the browser lane attributable regardless.\n let reported = false\n const report = (served: OpenAICompatServedModel): void => {\n if (reported || !attribution) return\n reported = true\n attribution.report(served)\n }\n if (attribution) {\n const servedHeader = res.headers.get('x-tangle-served-model')\n if (servedHeader) {\n report({\n requestedModel: attribution.requestedModel,\n servedModel: servedHeader,\n source: 'router_header',\n substituted: normalizeModelId(servedHeader) !== normalizeModelId(attribution.requestedModel),\n ...parseFailoverHeader(res.headers.get('x-tangle-failover')),\n })\n }\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n let chunk: OpenAIStreamChunk\n try {\n chunk = JSON.parse(data) as OpenAIStreamChunk\n } catch {\n continue /* skip a partial/garbled SSE frame */\n }\n // Body fallback: only when the header said nothing. Fold both ids before\n // comparing — the router reports the dated upstream id (`gpt-5-2025-08-07`)\n // for a request of `openai/gpt-5` even with NO substitution, so a raw\n // `!==` would report a swap on literally every turn.\n if (attribution && !reported && chunk.model) {\n report({\n requestedModel: attribution.requestedModel,\n servedModel: chunk.model,\n source: 'response_body',\n substituted: normalizeModelId(chunk.model) !== normalizeModelId(attribution.requestedModel),\n })\n }\n yield chunk\n }\n }\n}\n","/**\n * `createCertifiedDelivery` — the delivery truck for Tangle Intelligence.\n *\n * Pulls a tenant's CERTIFIED `AgentProfile` from the deployed plane\n * (`GET /v1/profiles/:target/composed`) and applies it to the agent's resolved\n * surfaces each turn, so an approved improvement reaches the running agent with\n * NO redeploy. This is the `composeProfile` transform `createAgentRuntime`\n * accepts — opt-in per product, fail-closed, cached + refreshed.\n *\n * Profile-WIDE by design (not prompt-only): the composed profile carries every\n * promoted artifact type keyed by kind. What's folded where:\n * - `prompt-surface` + `skill` → the system prompt (via `composeCertifiedPrompt`).\n * - `tool` artifacts that carry an OpenAI tool definition → `extraTools`\n * (advertised to the model; the matching executor is supplied by the product\n * via `executeOtherTool`). Until a `tool` artifact carries a runnable def,\n * it is surfaced (see `current()`) but not advertised — advertising a tool\n * with no executor would make the model call into a dead end.\n * - `mcp` / `memory` / `rag` artifacts materialize as servers/files and deliver\n * through the SANDBOX-provisioning seam, not this in-process one. The full\n * certified profile is exposed via `current()` so that seam can consume it.\n *\n * Substrate boundary: THIS module imports `@tangle-network/agent-runtime`; the\n * `createAgentRuntime` core does not (it only consumes the generic transform).\n */\n\nimport {\n composeCertifiedPrompt,\n type CertifiedProfile,\n pullCertified,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\nconst defaultRefreshMs = 300_000\n\n/** Define configuration options for delivering certified artifacts to a specified tenant target */\nexport interface CertifiedDeliveryConfig {\n /** The tenant target whose certified artifacts to deliver (the agent id). */\n target: string\n /** Bearer for the plane. Reads `TANGLE_API_KEY` when omitted. */\n apiKey?: string\n /** Plane base URL. Reads `TANGLE_INTELLIGENCE_URL` then the public plane. */\n baseUrl?: string\n /** Min interval between certified-profile pulls. Default 5m. */\n refreshMs?: number\n /** fetch impl (tests / non-global-fetch runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** Resolve and manage certified profiles with refresh and composition capabilities */\nexport interface CertifiedDelivery {\n /** The `composeProfile` transform to pass to `createAgentRuntime`. Applies the\n * cached certified profile to the base surfaces; refreshes on the cadence. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now (ignores the refresh window). Best-effort. */\n refresh(): Promise<void>\n /** The certified profile currently in effect (null = none promoted / pull\n * failed). Lets the sandbox-provisioning seam deliver the file/server\n * artifact types this in-process seam doesn't. */\n current(): CertifiedProfile | null\n}\n\n/**\n * Build a certified-delivery transform for one agent target. Fail-closed: a pull\n * error or 404 keeps the last-known certified profile (or null), and the agent\n * runs on its base surfaces — it never breaks because Intelligence is down.\n */\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const refreshMs = config.refreshMs ?? defaultRefreshMs\n let certified: CertifiedProfile | null = null\n let lastPullAt = 0\n let inflight: Promise<void> | null = null\n\n async function refresh(force = false): Promise<void> {\n if (!force && Date.now() - lastPullAt < refreshMs) return\n if (inflight) return inflight\n inflight = (async () => {\n const outcome = await pullCertified({\n target: config.target,\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n fetchImpl: config.fetchImpl,\n })\n lastPullAt = Date.now()\n // Only replace on a real pull; a 404/error keeps the last-known profile.\n if (outcome.succeeded) certified = outcome.value\n })()\n try {\n await inflight\n } finally {\n inflight = null\n }\n }\n\n return {\n async composeProfile(base) {\n await refresh()\n return {\n // prompt-surface + skill fold into the system prompt.\n systemPrompt: composeCertifiedPrompt(base.systemPrompt, certified),\n // Certified `tool` artifacts deliver here once they carry a runnable\n // OpenAI def + the product wires the executor; until then pass through.\n extraTools: base.extraTools,\n }\n },\n refresh: () => refresh(true),\n current: () => certified,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs from server configuration\n * (`buildHttpMcpServer` in ../tools). A client can therefore never inject an\n * arbitrary MCP url, header, or credential into the agent profile: the\n * overlay's `mcp` values are typed as {@link SurfaceMcpServer} (= the\n * server-built `AppToolMcpServer` entry shape), and only build() constructs\n * them.\n *\n * The credential itself never appears in the overlay. Since the tagged-config\n * contract (`agent-interface` 0.38.0) the `Authorization` header is a\n * `secret-ref` naming a box-environment variable, which the sandbox resolves\n * privately; `build()` supplies that key NAME from server configuration and the\n * product writes the value into `SandboxRuntimeConfig.env` at box creation. The\n * value must be deterministic for the box's lifetime (an HMAC over the\n * workspace id, e.g. `createCapabilityToken` in ../tools), because a freshly\n * random per-request mint would never match what the box already carries.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n","/**\n * The bounded agent tool-loop — owned by `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop streams a turn, collects the executable tool\n * calls, dispatches each, appends the results to history in OpenAI\n * function-calling shape, and re-runs so the model reads them — bounded by\n * `maxToolTurns`, a wall-clock `deadlineMs`, and a `maxCostUsd` budget.\n *\n * The history shape is the OpenAI function-calling contract: the assistant turn\n * that emitted tool calls is preserved as an `assistant` message carrying its\n * `tool_calls` array, and each result is its own `{ role: 'tool', tool_call_id,\n * content }` message keyed to the call. A strict model (Claude, and any\n * OpenAI-compatible provider that validates tool history) needs this to read its\n * own tool use back; folding results into a `user` message makes such models\n * re-issue the same call in a loop.\n *\n * The loop is substrate-owned (`runToolLoop` / `streamToolLoop`); the app\n * supplies `streamTurn` (wrapping its model endpoint) and `executeToolCall`\n * (routing to its integration + app-tool executors). The app-facing names below\n * are 1:1 aliases of the canonical symbols, kept so this package's consumers and\n * the in-package `createAgentRuntime` read against a single, stable vocabulary.\n *\n * This is the LEAF the runtime barrel and its children both import — keeping the\n * tool-loop vocabulary out of any import cycle. The barrel re-exports it.\n */\n\nexport {\n runToolLoop as runAppToolLoop,\n streamToolLoop as streamAppToolLoop,\n} from '@tangle-network/agent-runtime/tool-loop'\nexport type {\n ToolLoopCall as LoopToolCall,\n ToolLoopAssistantToolCall as LoopAssistantToolCall,\n ToolLoopMessage as LoopMessage,\n ToolLoopEvent,\n ToolLoopStopReason,\n ToolLoopResult,\n RunToolLoopOptions as AppToolLoopOptions,\n StreamToolLoopOptions as StreamAppToolLoopOptions,\n StreamToolLoopYield as StreamLoopYield,\n} from '@tangle-network/agent-runtime/tool-loop'\n\n/**\n * Events the app's OpenAI-compat stream adapter ({@link toLoopEvents}) yields.\n *\n * This is the app's own `Raw` event type for the streaming loop — the canonical\n * `streamToolLoop<Raw>` is generic over it. It widens the substrate's\n * tool-loop event with `reasoning` (DeepSeek/router `reasoning_content` /\n * `thinking` deltas, rendered as thinking sections) and `usage` (per-message\n * token accounting) — neither belongs in the substrate's loop contract, so they\n * stay here. The adapter maps each into the `streamTurn` seam; `text` and\n * `tool_call` drive the loop, `reasoning` / `usage` pass through to the UI.\n */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'reasoning'; text: string }\n | {\n type: 'tool_call'\n call: import('@tangle-network/agent-runtime/tool-loop').ToolLoopCall\n }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AgentCandidateModelGrantClient, type AgentCandidateModelPort } from '@tangle-network/agent-runtime/candidate-execution';
|
|
2
|
+
export interface RouterProtectedModelPortOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
maxCostUsd: number;
|
|
5
|
+
clientName?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
onSettlement?(receipt: RouterProtectedModelSettlement): Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
/** A validated charge remains authoritative when the product cannot save its audit record. */
|
|
10
|
+
export declare class ProtectedModelSettlementError extends Error {
|
|
11
|
+
readonly settlement: Awaited<ReturnType<AgentCandidateModelPort['settleGrant']>>;
|
|
12
|
+
constructor(settlement: Awaited<ReturnType<AgentCandidateModelPort['settleGrant']>>, cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
/** Bind one turn to Router's atomic grant ledger without exposing its parent key. */
|
|
15
|
+
export declare function createRouterProtectedModelPort(options: RouterProtectedModelPortOptions): AgentCandidateModelPort;
|
|
16
|
+
export interface RouterProtectedModelSettlement extends Omit<Awaited<ReturnType<AgentCandidateModelGrantClient['settle']>>, 'calls'> {
|
|
17
|
+
billing: {
|
|
18
|
+
status: string;
|
|
19
|
+
authorizationId?: string;
|
|
20
|
+
transactionId?: string;
|
|
21
|
+
reservedCostUsdNanos: number;
|
|
22
|
+
settledCostUsdNanos: number;
|
|
23
|
+
};
|
|
24
|
+
calls: Array<Omit<Awaited<ReturnType<AgentCandidateModelGrantClient['settle']>>['calls'][number], 'costProvenance'> & {
|
|
25
|
+
cacheWriteTokens: number;
|
|
26
|
+
cacheWrite5mTokens: number;
|
|
27
|
+
cacheWrite1hTokens: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/billing/crypto-probe.ts","../src/billing/identity-bound.ts","../src/billing/index.ts"],"sourcesContent":["import type { KeyCrypto } from './index'\n\n/** Prove the at-rest crypto seam before a remote key is minted. */\nexport async function assertWorkspaceKeyCryptoUsable(crypto: KeyCrypto): Promise<void> {\n const probe = 'agent-app:key-manager:crypto-probe'\n let roundTripped: string\n try {\n roundTripped = await crypto.decrypt(await crypto.encrypt(probe))\n } catch (error) {\n throw new Error(\n 'Key encryption is misconfigured: the crypto seam threw before minting. ' +\n 'Validate FIELD_ENCRYPTION_KEY (64-char hex) at startup. No platform key was minted.',\n { cause: error },\n )\n }\n if (roundTripped !== probe) {\n throw new Error(\n 'Key encryption is misconfigured: encrypt/decrypt round-trip did not preserve the plaintext. ' +\n 'No platform key was minted.',\n )\n }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\nimport type { KeyCrypto, KeyProvisioner } from './index'\n\n/** Lifecycle state for a durable delegated key row. */\nexport type WorkspaceKeyStatus =\n | 'legacy'\n | 'provisioning'\n | 'active'\n | 'revocation_pending'\n | 'revoked'\n | 'orphaned'\n\n/** Product partition used by the key store and remote provisioner. */\nexport type WorkspaceKeyProduct = string\n\n/** The identity fields that bind a child key to its paying owner and source. */\nexport interface WorkspaceKeyIdentity {\n workspaceId: string\n ownerUserId: string\n platformUserId: string\n sourceKeyId?: string | null\n /** A non-secret digest of the source credential. */\n sourceKeyFingerprint: string\n}\n\n/** A product/workspace/owner lookup scope. */\nexport interface DurableWorkspaceKeyScope {\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n}\n\n/** A persisted child-key row owned by this manager. */\nexport interface DurableWorkspaceKeyRecord {\n id: string\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n platformUserId: string\n sourceKeyId: string | null\n sourceKeyFingerprint: string\n /** The persisted name used to recover a remote create after a crash. Null only for pre-name rows. */\n name: string | null\n keyId: string\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date\n status: WorkspaceKeyStatus\n revocationAttempts: number\n nextRevocationAt: Date | null\n lastRevocationError: string | null\n createdAt: Date\n}\n\n/** A row that has been written before its remote child key exists. */\nexport interface DurableWorkspaceKeyProvisioningRecord extends DurableWorkspaceKeyRecord {\n status: 'provisioning'\n}\n\ntype DurableWorkspaceKeyCreateResult = Awaited<ReturnType<KeyProvisioner['createKey']>> & {\n budgetUsd?: number | null\n budgetRemaining?: number\n expiresAt?: string | null\n}\n\n/** The remote operations required by the durable manager. */\nexport interface DurableWorkspaceKeyProvisioner {\n createKey(input: Parameters<KeyProvisioner['createKey']>[0]): Promise<DurableWorkspaceKeyCreateResult>\n getKey(id: string): Promise<{\n budgetUsd?: number | null\n budgetSpent?: number\n expiresAt?: string | null\n }>\n revokeKey: KeyProvisioner['revokeKey']\n /** Find remote keys for a specific persisted provisioning attempt. */\n findCreatedKeys(input: {\n name: string\n product: string\n sourceKeyId?: string | null\n }): Promise<Array<{ id: string }>>\n}\n\n/** Persistence operations for identity-bound key lifecycle state. */\nexport interface DurableWorkspaceKeyStore {\n /** Return the active row for this exact product scope. */\n getActive(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyRecord | null>\n /** Return every unfinished provisioning row for this exact product scope. */\n listProvisioning(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyProvisioningRecord[]>\n /** Insert before the remote create so a crashed create can be recovered. */\n insertProvisioning(record: DurableWorkspaceKeyProvisioningRecord): Promise<void>\n /** Save the remote id while the row remains in provisioning state. */\n markProvisioningRemote(input: { id: string; keyId: string }): Promise<void>\n /** Promote a fully encrypted row to active state. */\n markActive(input: {\n id: string\n keyId: string\n keyEncrypted: string\n expiresAt: Date\n budgetUsd: number\n }): Promise<void>\n /** Keep a failed cleanup visible and schedule a later retry. */\n markRevocationPending(input: {\n id: string\n error?: string | null\n nextAttemptAt: Date\n incrementAttempts: boolean\n }): Promise<void>\n /** Mark a remote key as revoked. Missing remote keys count as revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n /** Mark a provisioning row terminal when no remote key remains to clean. */\n markOrphaned(id: string, error: string): Promise<void>\n /** Query due cleanup rows. The implementation should filter by the supplied scope. */\n listPendingRevocations(input: {\n product: WorkspaceKeyProduct\n workspaceId?: string\n ownerUserId?: string\n now: Date\n limit: number\n /** Include rows scheduled for a later retry when checking before issuance. */\n includeFuture?: boolean\n }): Promise<DurableWorkspaceKeyRecord[]>\n /** Acquire a cross-process lease for one product/workspace/owner scope. */\n acquireLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Renew an acquired lease before its expiry. */\n renewLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Release an acquired lease. */\n releaseLease(scope: string, operationId: string): Promise<void>\n}\n\n/** Live budget information for a child key. */\nexport interface WorkspaceKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string\n exhausted: boolean\n}\n\n/** The decrypted key plus the usage snapshot used to issue it. */\nexport interface WorkspaceRuntimeKey {\n key: string\n usage: WorkspaceKeyUsage\n /** True when this call minted a new remote key. */\n refreshed: boolean\n}\n\n/** Identity-bound durable key manager API. */\nexport interface DurableWorkspaceKeyManager {\n /** Reuse a matching active key or reconcile and mint one under a lease. */\n ensureKey(identity: WorkspaceKeyIdentity, options?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey>\n /** Read usage without returning the child secret. */\n getUsage(identity: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null>\n /** Retry due cleanup rows. A supplied owner/workspace limits the retry to its scope. */\n retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number>\n}\n\n/** Configuration for {@link createIdentityBoundWorkspaceKeyManager}. */\nexport interface DurableWorkspaceKeyManagerOptions {\n store: DurableWorkspaceKeyStore\n provisioner: DurableWorkspaceKeyProvisioner\n crypto: KeyCrypto\n /** Product partition. Products must use separate values. */\n product: WorkspaceKeyProduct\n defaultBudgetUsd: number\n now?: () => Date\n /** Local lifetime used when the remote response omits an expiry. */\n keyLifetimeMs?: number\n /** Lease lifetime for remote operations. */\n leaseMs?: number\n /** Maximum time to wait for another issuer to release the scope lease. */\n leaseWaitMs?: number\n /** Poll delay while waiting for a scope lease. */\n leasePollMs?: number\n /** Renewal interval. It must be shorter than `leaseMs`. */\n leaseRenewIntervalMs?: number\n /** Initial delay for a failed remote revoke. */\n revocationRetryBaseMs?: number\n /** Maximum delay for a failed remote revoke. */\n revocationRetryMaxMs?: number\n /** Stable remote name. The operation id makes the default crash-safe. */\n nameForIdentity?: (identity: WorkspaceKeyIdentity, operationId: string) => string\n /** Resolve names for rows written before `name` became durable. Null blocks issuance safely. */\n legacyNameForRecord?: (record: DurableWorkspaceKeyRecord) => string | null | undefined\n /** Recognize a missing remote key without depending on a provider SDK. */\n isRemoteMissing?: (error: unknown) => boolean\n}\n\nconst DEFAULT_KEY_LIFETIME_MS = 55 * 60_000\nconst DEFAULT_LEASE_MS = 60_000\nconst DEFAULT_LEASE_WAIT_MS = 15_000\nconst DEFAULT_LEASE_POLL_MS = 50\nconst DEFAULT_LEASE_RENEW_INTERVAL_MS = Math.floor(DEFAULT_LEASE_MS / 3)\nconst DEFAULT_REVOCATION_RETRY_BASE_MS = 2_000\nconst DEFAULT_REVOCATION_RETRY_MAX_MS = 60_000\nconst PENDING_REVOCATION_BATCH_SIZE = 20\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction operationId(): string {\n return globalThis.crypto.randomUUID()\n}\n\nfunction requirePositive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number`)\n return value\n}\n\nfunction requireNonNegative(value: number, label: string): number {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative finite number`)\n return value\n}\n\nfunction normalizeIdentity(identity: WorkspaceKeyIdentity): WorkspaceKeyIdentity {\n const workspaceId = identity.workspaceId.trim()\n const ownerUserId = identity.ownerUserId.trim()\n const platformUserId = identity.platformUserId.trim()\n const sourceKeyFingerprint = identity.sourceKeyFingerprint.trim()\n const sourceKeyId = identity.sourceKeyId?.trim() || null\n if (!workspaceId || !ownerUserId || !platformUserId || !sourceKeyFingerprint) {\n throw new Error('workspace child key identity is incomplete')\n }\n return { workspaceId, ownerUserId, platformUserId, sourceKeyId, sourceKeyFingerprint }\n}\n\nfunction scopeFor(identity: Pick<WorkspaceKeyIdentity, 'workspaceId' | 'ownerUserId'>, product: string): DurableWorkspaceKeyScope {\n return { workspaceId: identity.workspaceId, ownerUserId: identity.ownerUserId, product }\n}\n\nfunction scopeKey(scope: DurableWorkspaceKeyScope): string {\n return JSON.stringify([scope.product, scope.workspaceId, scope.ownerUserId])\n}\n\nfunction safeNamePart(value: string): string {\n const normalized = value.replace(/[^A-Za-z0-9._-]/g, '-').replace(/-+/g, '-')\n return normalized.slice(0, 80) || 'unknown'\n}\n\nfunction defaultKeyName(identity: WorkspaceKeyIdentity, product: string, operationId: string): string {\n return `agent-app:${safeNamePart(product)}:${safeNamePart(identity.workspaceId)}:${safeNamePart(identity.platformUserId)}:${operationId}`\n}\n\nfunction sourceKeyId(identity: WorkspaceKeyIdentity): string | null {\n return identity.sourceKeyId?.trim() || null\n}\n\nfunction sameIdentity(row: DurableWorkspaceKeyRecord, identity: WorkspaceKeyIdentity, product: string): boolean {\n return row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId\n && row.product === product\n && row.platformUserId === identity.platformUserId\n && row.sourceKeyId === sourceKeyId(identity)\n && row.sourceKeyFingerprint === identity.sourceKeyFingerprint\n}\n\nfunction sameRowIdentity(a: DurableWorkspaceKeyRecord, b: DurableWorkspaceKeyRecord): boolean {\n return a.workspaceId === b.workspaceId\n && a.ownerUserId === b.ownerUserId\n && a.product === b.product\n && a.platformUserId === b.platformUserId\n && a.sourceKeyId === b.sourceKeyId\n && a.sourceKeyFingerprint === b.sourceKeyFingerprint\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return (error instanceof Error ? error.message : fallback)\n .replace(/Bearer\\s+[^\\s,;]+/gi, 'Bearer [redacted]')\n .replace(/((?:authorization|api[-_ ]?key|token|secret|password)\\s*[:=]\\s*)(?!Bearer\\b)[^\\s,;]+/gi, '$1[redacted]')\n .slice(0, 240)\n}\n\nfunction isProvisioningId(value: string): boolean {\n return value.startsWith('provisioning:')\n}\n\nfunction usageFromRemote(\n row: DurableWorkspaceKeyRecord,\n remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>,\n now: Date,\n): WorkspaceKeyUsage {\n const budgetUsd = remote.budgetUsd ?? row.budgetUsd\n const budgetSpent = remote.budgetSpent ?? 0\n if (!Number.isFinite(budgetUsd) || budgetUsd < 0) throw new Error('workspace child key budget is malformed')\n if (!Number.isFinite(budgetSpent) || budgetSpent < 0) throw new Error('workspace child key usage is malformed')\n const expiresAt = remote.expiresAt ?? row.expiresAt.toISOString()\n const expiryMs = Date.parse(expiresAt)\n if (!Number.isFinite(expiryMs)) throw new Error('workspace child key expiry is malformed')\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: row.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt,\n exhausted: budgetRemaining <= 0 || expiryMs <= now.getTime(),\n }\n}\n\nfunction defaultRemoteMissing(error: unknown): boolean {\n const value = error as { status?: unknown; code?: unknown } | null\n return value?.status === 404 || value?.code === 'not_found'\n}\n\n/**\n * Create a product-neutral manager for child-key issuance and cleanup.\n *\n * The provisioner must already be authenticated for the current source\n * credential. The manager persists only the source id and its non-secret\n * fingerprint, then refuses to reuse a row from another identity.\n */\nexport function createIdentityBoundWorkspaceKeyManager(\n options: DurableWorkspaceKeyManagerOptions,\n): DurableWorkspaceKeyManager {\n const product = options.product.trim()\n if (!product) throw new Error('workspace child key product is required')\n const defaultBudgetUsd = requirePositive(options.defaultBudgetUsd, 'defaultBudgetUsd')\n const keyLifetimeMs = requirePositive(options.keyLifetimeMs ?? DEFAULT_KEY_LIFETIME_MS, 'keyLifetimeMs')\n const leaseMs = requirePositive(options.leaseMs ?? DEFAULT_LEASE_MS, 'leaseMs')\n const leaseWaitMs = requireNonNegative(options.leaseWaitMs ?? DEFAULT_LEASE_WAIT_MS, 'leaseWaitMs')\n const leasePollMs = requirePositive(options.leasePollMs ?? DEFAULT_LEASE_POLL_MS, 'leasePollMs')\n const leaseRenewIntervalMs = requirePositive(\n options.leaseRenewIntervalMs ?? Math.min(DEFAULT_LEASE_RENEW_INTERVAL_MS, Math.floor(leaseMs / 3)),\n 'leaseRenewIntervalMs',\n )\n if (leaseRenewIntervalMs >= leaseMs) throw new Error('leaseRenewIntervalMs must be shorter than leaseMs')\n const revocationRetryBaseMs = requirePositive(\n options.revocationRetryBaseMs ?? DEFAULT_REVOCATION_RETRY_BASE_MS,\n 'revocationRetryBaseMs',\n )\n const revocationRetryMaxMs = requirePositive(\n options.revocationRetryMaxMs ?? DEFAULT_REVOCATION_RETRY_MAX_MS,\n 'revocationRetryMaxMs',\n )\n if (revocationRetryMaxMs < revocationRetryBaseMs) throw new Error('revocationRetryMaxMs must cover the base delay')\n\n const now = options.now ?? (() => new Date())\n const isRemoteMissing = options.isRemoteMissing ?? defaultRemoteMissing\n const localLocks = new Map<string, Promise<void>>()\n\n async function withLocalLock<T>(scope: string, work: () => Promise<T>): Promise<T> {\n const previous = localLocks.get(scope) ?? Promise.resolve()\n let release!: () => void\n const current = new Promise<void>((resolve) => { release = resolve })\n localLocks.set(scope, current)\n await previous\n try {\n return await work()\n } finally {\n release()\n if (localLocks.get(scope) === current) localLocks.delete(scope)\n }\n }\n\n async function withDurableLease<T>(\n scope: string,\n work: () => Promise<T>,\n onLeaseLost?: () => Promise<void>,\n ): Promise<T> {\n const leaseOperationId = operationId()\n const startedAt = Date.now()\n let acquired = false\n while (!acquired && Date.now() - startedAt <= leaseWaitMs) {\n acquired = await options.store.acquireLease(scope, leaseOperationId, now(), leaseMs)\n if (!acquired) await sleep(leasePollMs)\n }\n if (!acquired) throw new Error('workspace child-key issuance is busy; retry the request')\n\n let stopped = false\n let leaseLost: Error | null = null\n let leaseTimer: ReturnType<typeof setTimeout> | undefined\n let renewalPromise: Promise<void> | undefined\n const scheduleRenewal = () => {\n if (stopped || leaseLost) return\n leaseTimer = setTimeout(() => {\n leaseTimer = undefined\n const renewal = (async () => {\n try {\n if (!await options.store.renewLease(scope, leaseOperationId, now(), leaseMs)) {\n leaseLost = new Error('workspace child-key lease was lost during reconciliation')\n return\n }\n scheduleRenewal()\n } catch (error) {\n leaseLost = error instanceof Error ? error : new Error('workspace child-key lease renewal failed')\n }\n })()\n renewalPromise = renewal\n void renewal.finally(() => {\n if (renewalPromise === renewal) renewalPromise = undefined\n }).catch(() => undefined)\n }, leaseRenewIntervalMs)\n }\n scheduleRenewal()\n\n let workError: unknown\n let leaseLossCleanupError: unknown\n try {\n const result = await work()\n if (leaseLost) throw leaseLost\n return result\n } catch (error) {\n workError = error\n throw error\n } finally {\n stopped = true\n if (leaseTimer) clearTimeout(leaseTimer)\n if (renewalPromise) await renewalPromise\n if (leaseLost && onLeaseLost) {\n try {\n await onLeaseLost()\n } catch (error) {\n leaseLossCleanupError = error\n }\n }\n try {\n await options.store.releaseLease(scope, leaseOperationId)\n } catch (error) {\n if (workError === undefined) throw error\n }\n if (workError === undefined && leaseLossCleanupError) {\n throw new Error('workspace child-key lease was lost and cleanup failed', { cause: leaseLossCleanupError })\n }\n if (workError === undefined && leaseLost) throw leaseLost\n }\n }\n\n function retryDelay(attempts: number): number {\n return Math.min(revocationRetryMaxMs, revocationRetryBaseMs * 2 ** Math.min(Math.max(attempts - 1, 0), 5))\n }\n\n async function markPending(id: string, error: unknown, incrementAttempts: boolean): Promise<void> {\n const delay = incrementAttempts ? retryDelay(1) : 0\n await options.store.markRevocationPending({\n id,\n error: errorMessage(error, 'remote key cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + delay),\n incrementAttempts,\n })\n }\n\n async function revokePending(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n if (!row.keyId || isProvisioningId(row.keyId)) return cleanupProvisioning(row, identity)\n const active = await options.store.getActive({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n product: row.product,\n })\n if (active && active.id !== row.id && active.keyId === row.keyId) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n try {\n await options.provisioner.revokeKey(row.keyId)\n await options.store.markRevoked(row.id, now())\n return true\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote key revocation failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n async function cleanupProvisioning(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n let candidates: Array<{ id: string }>\n if (row.keyId && !isProvisioningId(row.keyId)) {\n candidates = [{ id: row.keyId }]\n } else {\n const name = row.name?.trim() || options.legacyNameForRecord?.(row)?.trim()\n if (!name) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'provisioning row has no recoverable remote name; migrate the row or configure legacyNameForRecord',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return false\n }\n candidates = await options.provisioner.findCreatedKeys({\n name,\n product: row.product,\n sourceKeyId: row.sourceKeyId,\n })\n }\n\n const scope = { workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product: row.product }\n const active = await options.store.getActive(scope)\n const activeMatches = active && (identity ? sameIdentity(active, identity, product) : sameRowIdentity(active, row))\n const activeKeyId = activeMatches ? active.keyId : null\n const toRevoke = candidates\n .map((candidate) => candidate.id.trim())\n .filter((candidateId) => candidateId && candidateId !== activeKeyId)\n\n for (const keyId of toRevoke) {\n try {\n await options.provisioner.revokeKey(keyId)\n } catch (error) {\n if (isRemoteMissing(error)) continue\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote provisioning cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n if (toRevoke.length > 0 || candidates.length === 0 || activeKeyId !== null) {\n await options.store.markRevoked(row.id, now())\n } else {\n await options.store.markOrphaned(row.id, 'no unowned remote child matched the durable provisioning record')\n }\n return true\n }\n\n async function reconcileProvisioning(identity: WorkspaceKeyIdentity): Promise<void> {\n const scope = scopeFor(identity, product)\n const rows = await options.store.listProvisioning(scope)\n for (const row of rows) {\n if (!(await cleanupProvisioning(row, identity))) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n }\n\n async function retryPendingRevocationsUnlocked(\n scopeFilter?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>,\n activeIdentity?: WorkspaceKeyIdentity,\n ): Promise<{ completed: number; pending: number }> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: scopeFilter?.workspaceId,\n ownerUserId: scopeFilter?.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n })\n let completed = 0\n let pending = 0\n for (const row of rows) {\n if (row.product !== product) continue\n if (scopeFilter && (row.workspaceId !== scopeFilter.workspaceId || row.ownerUserId !== scopeFilter.ownerUserId)) continue\n if (await revokePending(row, activeIdentity)) completed += 1\n else pending += 1\n }\n return { completed, pending }\n }\n\n async function assertNoPendingRevocations(identity: WorkspaceKeyIdentity): Promise<void> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n includeFuture: true,\n })\n if (rows.some((row) => row.product === product\n && row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId)) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n\n async function retryPendingRevocations(scopeInput?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number> {\n if (scopeInput) {\n const workspaceId = scopeInput.workspaceId.trim()\n const ownerUserId = scopeInput.ownerUserId.trim()\n if (!workspaceId || !ownerUserId) throw new Error('workspace child key retry scope is incomplete')\n const scope = scopeKey({ workspaceId, ownerUserId, product })\n return withLocalLock(scope, () => withDurableLease(scope, async () => (\n await retryPendingRevocationsUnlocked({ workspaceId, ownerUserId })).completed))\n }\n\n const rows = await options.store.listPendingRevocations({ product, now: now(), limit: PENDING_REVOCATION_BATCH_SIZE })\n let completed = 0\n const scopes = new Set<string>()\n for (const row of rows) {\n if (row.product !== product) continue\n const scope = scopeKey({ workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product })\n if (scopes.has(scope)) continue\n scopes.add(scope)\n completed += await withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n })).completed))\n }\n return completed\n }\n\n async function readActive(identity: WorkspaceKeyIdentity): Promise<{ row: DurableWorkspaceKeyRecord; usage: WorkspaceKeyUsage } | null> {\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row) return null\n if (!sameIdentity(row, identity, product)) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key identity does not match the authenticated owner and source',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n\n let remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>\n try {\n remote = await options.provisioner.getKey(row.keyId)\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return null\n }\n throw error\n }\n const usage = usageFromRemote(row, remote, now())\n if (usage.exhausted) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key is expired or exhausted',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n return { row, usage }\n }\n\n async function mint(\n identity: WorkspaceKeyIdentity,\n budgetUsd: number,\n onRemoteKey?: (row: DurableWorkspaceKeyProvisioningRecord, remoteId: string) => void,\n ): Promise<WorkspaceRuntimeKey> {\n await assertWorkspaceKeyCryptoUsable(options.crypto)\n const mintOperationId = operationId()\n const createdAt = now()\n const expiresAt = new Date(createdAt.getTime() + keyLifetimeMs)\n const name = (options.nameForIdentity?.(identity, mintOperationId) ?? defaultKeyName(identity, product, mintOperationId)).trim()\n if (!name) throw new Error('workspace child key remote name is required')\n const rowId = operationId()\n const provisioningRow: DurableWorkspaceKeyProvisioningRecord = {\n id: rowId,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n product,\n platformUserId: identity.platformUserId,\n sourceKeyId: sourceKeyId(identity),\n sourceKeyFingerprint: identity.sourceKeyFingerprint,\n name,\n keyId: `provisioning:${mintOperationId}`,\n keyEncrypted: '',\n budgetUsd,\n expiresAt,\n status: 'provisioning',\n revocationAttempts: 0,\n nextRevocationAt: null,\n lastRevocationError: null,\n createdAt,\n }\n await options.store.insertProvisioning(provisioningRow)\n\n let created: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['createKey']>>\n try {\n created = await options.provisioner.createKey({\n name,\n product,\n budgetUsd,\n expiresAt: expiresAt.toISOString(),\n })\n } catch (error) {\n // Keep cleanup running even when the state write fails. The remote\n // request may have committed before its response failed.\n try {\n await markPending(rowId, error, false)\n } catch {\n // cleanupProvisioning below can still find the remote name.\n }\n try {\n await cleanupProvisioning({ ...provisioningRow, status: 'revocation_pending' }, identity)\n } catch (cleanupError) {\n throw new Error('remote create failed and its cleanup could not be completed', { cause: cleanupError })\n }\n throw error\n }\n\n const remoteId = created.id?.trim() ?? ''\n const secret = created.key?.trim() ?? ''\n if (!remoteId || !secret) {\n if (remoteId) {\n try {\n await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId })\n } catch {\n // revokePending uses the returned id even when this state write fails.\n }\n }\n try {\n await markPending(rowId, new Error('remote create returned no usable child key'), false)\n } catch {\n // Always attempt remote cleanup below.\n }\n const pendingRow = { ...provisioningRow, keyId: remoteId || provisioningRow.keyId, status: 'revocation_pending' as const }\n await revokePending(pendingRow, identity)\n throw new Error('remote create returned no usable child key')\n }\n\n onRemoteKey?.(provisioningRow, remoteId)\n\n try {\n await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId })\n const keyEncrypted = await options.crypto.encrypt(secret)\n const remoteExpiresAt = created.expiresAt?.trim()\n const activeExpiresAt = remoteExpiresAt ? new Date(remoteExpiresAt) : expiresAt\n if (!Number.isFinite(activeExpiresAt.getTime())) throw new Error('remote child key expiry is malformed')\n const activeBudgetUsd = created.budgetUsd ?? budgetUsd\n if (!Number.isFinite(activeBudgetUsd) || activeBudgetUsd < 0) throw new Error('remote child key budget is malformed')\n await options.store.markActive({\n id: rowId,\n keyId: remoteId,\n keyEncrypted,\n expiresAt: activeExpiresAt,\n budgetUsd: activeBudgetUsd,\n })\n } catch (error) {\n // A persistence failure must not strand the already-created remote key.\n try {\n await markPending(rowId, error, false)\n } catch {\n // revokePending below still has the remote id and attempts cleanup.\n }\n await revokePending({ ...provisioningRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row || row.keyId !== remoteId || !sameIdentity(row, identity, product)) {\n const error = new Error('workspace child key was not persisted as active')\n try {\n await options.store.markRevocationPending({\n id: rowId,\n error: error.message,\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n } catch {\n // The remote id is still available for the cleanup attempt below.\n }\n await revokePending({ ...provisioningRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n const usage = usageFromRemote(row, {\n budgetUsd: created.budgetUsd,\n budgetSpent: 0,\n expiresAt: created.expiresAt ?? expiresAt.toISOString(),\n }, now())\n return { key: secret, usage, refreshed: true }\n }\n\n async function ensureKey(identityInput: WorkspaceKeyIdentity, keyOptions?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey> {\n const identity = normalizeIdentity(identityInput)\n const budgetUsd = keyOptions?.budgetUsd ?? defaultBudgetUsd\n requirePositive(budgetUsd, 'budgetUsd')\n const scope = scopeKey(scopeFor(identity, product))\n let mintedRemote: { row: DurableWorkspaceKeyProvisioningRecord; remoteId: string } | null = null\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const initialCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (initialCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await reconcileProvisioning(identity)\n const active = await readActive(identity)\n if (active) {\n const key = await options.crypto.decrypt(active.row.keyEncrypted)\n if (!key.trim()) throw new Error('workspace child key secret is empty')\n return { key, usage: active.usage, refreshed: false }\n }\n const finalCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (finalCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await assertNoPendingRevocations(identity)\n return mint(identity, budgetUsd, (row, remoteId) => {\n mintedRemote = { row, remoteId }\n })\n }, async () => {\n const remote = mintedRemote\n if (!remote) return\n const cleaned = await revokePending({\n ...remote.row,\n keyId: remote.remoteId,\n status: 'revocation_pending',\n }, identity)\n if (!cleaned) throw new Error('lease-loss child-key cleanup is pending')\n }))\n }\n\n async function getUsage(identityInput: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null> {\n const identity = normalizeIdentity(identityInput)\n const scope = scopeKey(scopeFor(identity, product))\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const active = await readActive(identity)\n return active?.usage ?? null\n }))\n }\n\n return { ensureKey, getUsage, retryPendingRevocations }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\n\n/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations the key manager needs. Wire it from the\n * platform via {@link createTcloudKeyProvisioner} rather than casting. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/**\n * The subset of the `@tangle-network/tcloud` `TCloudClient` the provisioner uses\n * — declared with METHOD syntax so the real client (whose `product` is a narrow\n * union and whose budgets are `number | null`) is assignable bivariantly. The\n * real SDK client satisfies this; pass it straight in.\n */\nexport interface TcloudKeyClient {\n createKey(opts: {\n name: string\n product?: string\n budgetUsd?: number\n expiresAt?: string\n parentKeyId?: string\n allowedModels?: string[]\n rpmLimit?: number\n }): Promise<{ id: string; key: string }>\n getKey(id: string): Promise<{ budgetUsd?: number | null; budgetSpent?: number; expiresAt?: string | null }>\n revokeKey(id: string): Promise<unknown>\n}\n\n/**\n * Adapt the tcloud SDK client to {@link KeyProvisioner} — the typed seam that\n * replaces the `as unknown as KeyProvisioner` cast every consumer otherwise\n * repeats. The platform already exposes child-key minting (parent→child key,\n * per-key USD budget, expiry); this maps its shapes (`product` union,\n * `number | null` budgets) onto the manager's contract (`null → undefined`).\n */\nexport function createTcloudKeyProvisioner(client: TcloudKeyClient): KeyProvisioner {\n return {\n createKey: async (input) => {\n const created = await client.createKey(input)\n return { id: created.id, key: created.key }\n },\n revokeKey: (keyId) => client.revokeKey(keyId),\n getKey: async (keyId) => {\n const info = await client.getKey(keyId)\n return {\n budgetUsd: info.budgetUsd ?? undefined,\n budgetSpent: info.budgetSpent ?? undefined,\n expiresAt: info.expiresAt ?? null,\n }\n },\n }\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\n/** Define configuration options for managing workspace keys including provisioning, storage, and cryptography */\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/** Describe usage and budget details for a workspace model key including expiration and exhaustion status */\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\n/** Manage workspace keys by ensuring, rotating, and tracking usage of active child-key secrets */\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\n// ---------------------------------------------------------------------------\n// Shared-platform-balance billing\n//\n// A DIFFERENT model from the per-workspace child-key manager above: here every\n// user runs against a SHARED platform balance (id.tangle.tools), keyed by the\n// user's platform identity. The app owns no key minting — it reads the balance,\n// gates a billable turn, and deducts spend through the platform billing API.\n// Plan limits, the platform transport, and identity resolution are SEAMS the\n// product supplies; this module imports no DB and no HTTP client.\n// ---------------------------------------------------------------------------\n\n/** A user's resolved platform identity (from the app's SSO account store). */\nexport interface PlatformIdentity {\n platformUserId: string\n /** The user's per-user platform API key (reads), or null when unlinked. */\n apiKey: string | null\n}\n\n/** Spendable balance for a platform user. */\nexport interface PlatformBalanceInfo {\n balance: number\n lifetimeSpent: number\n}\n\n/** Per-product spend aggregate. */\nexport interface PlatformProductUsage {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,\n * overage policy). Never baked into the framework. */\nexport interface PlanLimit {\n monthlyBalanceUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * The platform billing transport — the product wires these to id.tangle.tools\n * (or any balance backend). Reads authenticate as the user (their `apiKey`);\n * the deduct write is a service-token call naming the target user. This module\n * never touches HTTP — it only sequences these calls.\n */\nexport interface PlatformBillingClient<Plan extends string> {\n /** Resolve the user's platform identity, or null when there is no SSO account. */\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n /** Subscription plan for the user (via their platform key). */\n getPlan(apiKey: string): Promise<Plan>\n /** Spendable balance for the user (via their platform key). */\n getBalance(apiKey: string): Promise<PlatformBalanceInfo>\n /** Per-product usage rows for the user (via their platform key). */\n getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>\n /** Deduct spend against the user's balance (service-token write). */\n deduct(input: { platformUserId: string; amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n}\n\n/** Define shared billing state including user ID, plan, balances, concurrency, and overage permission */\nexport interface SharedBillingState<Plan extends string> {\n /** Platform user id, or null when the user has no Tangle SSO account. */\n platformUserId: string | null\n plan: Plan\n monthlyBalanceUsd: number\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define configuration options for managing platform balance based on billing plans */\nexport interface PlatformBalanceManagerOptions<Plan extends string> {\n client: PlatformBillingClient<Plan>\n /** Plan → limits map (the product's pricing). */\n planLimits: Record<Plan, PlanLimit>\n /** The plan an unlinked / outage user falls to (fails CLOSED). */\n freePlan: Plan\n /** The product slug to attribute usage to (for `getProductUsage`). */\n productSlug: string\n}\n\n/** Manage user plans and balances including state retrieval, billing authorization, deduction, and usage tracking */\nexport interface PlatformBalanceManager<Plan extends string> {\n /** Resolve the user's plan + balance. Unlinked or platform-outage users fail\n * CLOSED: free plan, zero remaining balance — a billable run is never started\n * against an unknown balance. */\n getState(userId: string): Promise<SharedBillingState<Plan>>\n /** Gate a billable turn: allowed when the plan permits overage or remaining\n * balance is positive. Returns the state so the caller deducts against it. */\n canStartBillableTurn(userId: string): Promise<{ allowed: boolean; state: SharedBillingState<Plan> }>\n /** Deduct `amountUsd` against the user's platform balance. Throws when the\n * user is not platform-linked. */\n deduct(userId: string, params: { amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n /** This product's spend for the user (drives a usage panel). */\n getProductUsage(userId: string): Promise<{ spentUsd: number; transactionCount: number }>\n}\n\n/** Create a platform balance manager to handle user plan limits and state based on provided options */\nexport function createPlatformBalanceManager<Plan extends string>(\n opts: PlatformBalanceManagerOptions<Plan>,\n): PlatformBalanceManager<Plan> {\n const { client, planLimits, freePlan, productSlug } = opts\n\n const getState: PlatformBalanceManager<Plan>['getState'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n // No SSO account, or linked without a platform key: unlinked free tier with\n // zero balance. Reads require the user's key — never call them empty.\n if (!identity || !identity.apiKey) {\n const limits = planLimits[freePlan]\n return {\n platformUserId: identity?.platformUserId ?? null,\n plan: freePlan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)])\n const limits = planLimits[plan]\n return {\n platformUserId: identity.platformUserId,\n plan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n\n const canStartBillableTurn: PlatformBalanceManager<Plan>['canStartBillableTurn'] = async (userId) => {\n const state = await getState(userId)\n if (!state.platformUserId) return { allowed: false, state }\n const allowed = state.overageAllowed || state.remainingBalanceUsd > 0\n return { allowed, state }\n }\n\n const deduct: PlatformBalanceManager<Plan>['deduct'] = async (userId, params) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity) throw new Error('Shared billing requires a platform-linked user')\n await client.deduct({\n platformUserId: identity.platformUserId,\n amountUsd: params.amountUsd,\n type: params.type,\n description: params.description,\n referenceId: params.referenceId,\n })\n }\n\n const getProductUsage: PlatformBalanceManager<Plan>['getProductUsage'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 }\n const rows = await client.getUsageByProduct(identity.apiKey)\n const product = rows.find((row) => row.product === productSlug)\n return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 }\n }\n\n return { getState, canStartBillableTurn, deduct, getProductUsage }\n}\n\n/** Create a workspace key manager that handles key provisioning and budget tracking */\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n // Prove the at-rest crypto seam is usable BEFORE spending a platform mint.\n // The `KeyCrypto` impl may close over an env-resolved key (createFieldCrypto)\n // that is empty/invalid and only throws at encrypt time — after the mint —\n // orphaning the child key. A round-trip probe makes a misconfigured consumer\n // fail loud with ZERO platform spend.\n await assertWorkspaceKeyCryptoUsable(opts.crypto)\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const mintedKeyId = created.id\n\n // Past this point the platform has minted a budget-bearing child key. ANY\n // failure before the row is persisted (encrypt, insert) MUST revoke the\n // just-minted key, else it leaks as a zombie holding parent-key headroom —\n // a missing/invalid encryption key would otherwise burn the child budget\n // per mint attempt until the parent can no longer mint (fleet-wide outage).\n let keyEncrypted: string\n let priors: Array<{ id: string; keyId: string }>\n try {\n keyEncrypted = await opts.crypto.encrypt(created.key)\n priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: mintedKeyId, keyEncrypted, budgetUsd, expiresAt })\n } catch (err) {\n // Best-effort revoke of the orphan. A revoke failure must NOT mask the\n // original cause — log it loudly and rethrow the original error.\n try {\n await opts.provisioner.revokeKey(mintedKeyId)\n } catch (revokeErr) {\n console.error(\n `[workspace-key-manager] FAILED to revoke orphaned child key ${mintedKeyId} ` +\n `for workspace ${workspaceId} after a post-mint failure — it now leaks parent-key budget. ` +\n `Revoke error:`,\n revokeErr,\n )\n }\n throw err\n }\n\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n\nexport {\n createIdentityBoundWorkspaceKeyManager,\n type DurableWorkspaceKeyManager,\n type DurableWorkspaceKeyManagerOptions,\n type DurableWorkspaceKeyProvisioner,\n type DurableWorkspaceKeyProvisioningRecord,\n type DurableWorkspaceKeyRecord,\n type DurableWorkspaceKeyScope,\n type DurableWorkspaceKeyStore,\n type WorkspaceKeyIdentity,\n type WorkspaceKeyProduct,\n type WorkspaceKeyStatus,\n type WorkspaceKeyUsage,\n type WorkspaceRuntimeKey,\n} from './identity-bound'\n"],"mappings":"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","names":["sourceKeyId","operationId","limits"]}
|