@tangle-network/agent-app 0.47.23 → 0.47.24

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.
@@ -192,7 +192,7 @@ function assertProfileMcpServer(server, label) {
192
192
  }
193
193
  function unresolvableSurfaceCredential(surface) {
194
194
  throw new Error(
195
- `The ${surface} MCP surface cannot be mounted: its capability token is scoped to a single user and resource, and an AgentProfile may only reference a credential the sandbox can resolve from the box environment, which is workspace-wide and fixed at sandbox creation. Mounting it needs a per-session secret channel on the sandbox API, or a route that authenticates the workspace rather than the caller.`
195
+ `The ${surface} MCP surface cannot be mounted: its capability token is scoped to a single user and resource, and an AgentProfile may only reference a credential the sandbox can resolve from the box environment, which is workspace-wide even when runtimeEnv refreshes it. Mounting it needs a per-session secret channel on the sandbox API, or a route that authenticates the workspace rather than the caller.`
196
196
  );
197
197
  }
198
198
  function buildHttpMcpServer(opts) {
@@ -281,4 +281,4 @@ export {
281
281
  buildScopedMcpServerEntry,
282
282
  buildAppToolMcpServer
283
283
  };
284
- //# sourceMappingURL=chunk-KBJL6LIW.js.map
284
+ //# sourceMappingURL=chunk-6A7MYOUI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/auth.ts","../src/tools/mcp-rpc.ts","../src/tools/mcp.ts"],"sourcesContent":["import type { AppToolContext } from './types'\n\n/**\n * Header names carrying the server-set per-turn context + the capability token.\n * Defaults are product-neutral (`X-Agent-App-*`); a product that already ships\n * a header convention (e.g. `X-Acme-User-Id`) passes its own.\n */\nexport interface ToolHeaderNames {\n userId: string\n workspaceId: string\n threadId: string\n}\n\n/** Provide default HTTP header names for user, workspace, and thread identification */\nexport const DEFAULT_HEADER_NAMES: ToolHeaderNames = {\n userId: 'X-Agent-App-User-Id',\n workspaceId: 'X-Agent-App-Workspace-Id',\n threadId: 'X-Agent-App-Thread-Id',\n}\n\n/**\n * Which identity the bearer is bound to.\n *\n * `'userId'` (default) is right when the product mints a token per user and can\n * deliver it per turn.\n *\n * `'workspaceId'` is the only workable choice when the token lives in the BOX\n * ENVIRONMENT. Since agent-interface 0.38 a credential may reach a profile\n * only as a reference the sandbox resolves from that environment. `env` writes\n * at creation and `runtimeEnv` can refresh retained boxes, but the environment\n * remains workspace-wide. A per-user token therefore cannot be delivered as a\n * safe box credential — and a per-user token that IS written there was never\n * per-user in any meaningful sense, because every member of that workspace's\n * box can read it.\n *\n * Binding the bearer to the workspace does NOT collapse the identity: the user\n * header is still required, still server-set, still returned on `ctx`, and is\n * what downstream domain code attributes work to. Only the question \"may this\n * caller act at all\" moves from the user to the workspace — which is what the\n * shared box already implies.\n */\nexport type CapabilitySubject = 'userId' | 'workspaceId'\n\n/** Define options to verify bearer tokens and customize authentication header names */\nexport interface AuthenticateOptions {\n /** Verify the bearer capability token belongs to the subject named by\n * {@link AuthenticateOptions.subject}. The product's HMAC/JWT impl — the\n * seam that keeps token crypto out of this package. */\n verifyToken: (subject: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** What the bearer is bound to. Defaults to `'userId'`, so an existing caller\n * is byte-unchanged. */\n subject?: CapabilitySubject\n}\n\n/** Represent the result of tool authentication with success context or failure response */\nexport type ToolAuthResult =\n | { ok: true; ctx: AppToolContext }\n | { ok: false; response: Response }\n\n/**\n * Recover + verify the trusted context for a tool request.\n *\n * Both the user and the workspace come from server-set headers — never from\n * tool args — so the model can neither forge identity nor target another\n * workspace. The bearer must verify against whichever of the two the product\n * declares as its {@link CapabilitySubject}.\n *\n * Fail-closed, and deliberately in this order: a missing credential or a token\n * minted for another subject yields 401 before anything else is read. When the\n * subject is the workspace, its header is required BEFORE verification rather\n * than after — verifying against an absent subject is not a check at all.\n */\nexport async function authenticateToolRequest(request: Request, opts: AuthenticateOptions): Promise<ToolAuthResult> {\n const h = opts.headerNames ?? DEFAULT_HEADER_NAMES\n const userId = request.headers.get(h.userId)?.trim()\n const workspaceId = request.headers.get(h.workspaceId)?.trim()\n const threadId = request.headers.get(h.threadId)?.trim() || null\n const bearer = request.headers.get('authorization')?.match(/^Bearer\\s+(.+)$/i)?.[1]\n\n if (!userId || !bearer) {\n return { ok: false, response: Response.json({ error: 'Missing capability credentials' }, { status: 401 }) }\n }\n const subject = opts.subject === 'workspaceId' ? workspaceId : userId\n if (!subject) {\n return { ok: false, response: Response.json({ error: 'Missing workspace context' }, { status: 400 }) }\n }\n if (!(await opts.verifyToken(subject, bearer))) {\n return { ok: false, response: Response.json({ error: 'Invalid capability token' }, { status: 401 }) }\n }\n if (!workspaceId) {\n return { ok: false, response: Response.json({ error: 'Missing workspace context' }, { status: 400 }) }\n }\n return { ok: true, ctx: { userId, workspaceId, threadId } }\n}\n\n/** Read a tool's argument object from the request body, tolerant of direct\n * aliases (`args` / `arguments`), Streamable HTTP MCP (`params.arguments`),\n * or a bare body. Returns null on non-JSON. */\nexport async function readToolArgs<T>(request: Request): Promise<T | null> {\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return null\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return body as T\n const record = body as Record<string, unknown>\n if ('jsonrpc' in record) {\n const params = record.params\n if (typeof params === 'object' && params !== null && !Array.isArray(params)) {\n return ((params as Record<string, unknown>).arguments ?? {}) as T\n }\n return {} as T\n }\n return (record.args ?? record.arguments ?? record) as T\n}\n","/**\n * Generic streamable-HTTP JSON-RPC 2.0 envelope for a tools-only MCP server.\n * Stateless, Workers-compatible: no session table, no SSE — every request gets\n * a single `application/json` response, which the streamable-HTTP transport\n * explicitly permits for tools-only servers.\n *\n * Protocol surface:\n * initialize → echo client's protocolVersion if supported, else latest\n * ping → empty result {}\n * notifications/* (no `id`) → 202 with no body\n * tools/list → tool manifest\n * tools/call → run + surface execution failures as isError text results\n * anything else → -32601\n *\n * Execution failures (argument shape, validation, store throws) become `isError`\n * tool results carrying the thrown message verbatim — the model reads WHY and\n * retries. Protocol misuse becomes a JSON-RPC error object.\n */\n\nexport const MCP_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'] as const\n/** Resolve a valid protocol version from the predefined MCP_PROTOCOL_VERSIONS array */\nexport type McpProtocolVersion = (typeof MCP_PROTOCOL_VERSIONS)[number]\n\nconst LATEST_PROTOCOL_VERSION: McpProtocolVersion = MCP_PROTOCOL_VERSIONS[0]\n\n/** Describe the structure of server information including name and version */\nexport interface McpServerInfo {\n name: string\n version: string\n}\n\n/** One tool entry in the registry the handler owns. */\nexport interface McpToolDefinition<TEnv = Record<string, never>> {\n name: string\n description: string\n /** JSON Schema for the `params.arguments` object. */\n inputSchema: Record<string, unknown>\n /** Receive validated (Record) args + the env the handler threaded; throw to\n * surface an isError result — never throw for protocol/framing issues. */\n run(args: Record<string, unknown>, env: TEnv): Promise<unknown>\n}\n\n/** Define options for creating a handler that manages MCP tools with environment support */\nexport interface CreateMcpToolHandlerOptions<TEnv = Record<string, never>> {\n serverInfo: McpServerInfo\n /** Full tool list; order IS the tools/list order. */\n tools: McpToolDefinition<TEnv>[]\n /** Per-request environment threaded into every `run` call. If your tools are\n * stateless (or carry state through closure) pass an empty builder:\n * `() => ({} as TEnv)`. */\n buildEnv(request: Request): TEnv | Promise<TEnv>\n /** Optional result formatter for callers that need structured tool errors. */\n formatResult?: (result: unknown, tool: McpToolDefinition<TEnv>) => McpToolCallContent\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\ntype JsonRpcId = string | number | null\n\nexport interface McpToolCallContent {\n content: Array<{ type: 'text'; text: string }>\n isError?: true\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction rpcResult(id: JsonRpcId, result: unknown): Response {\n return Response.json({ jsonrpc: '2.0', id, result })\n}\n\nfunction rpcError(id: JsonRpcId, code: number, message: string, status = 200): Response {\n return Response.json({ jsonrpc: '2.0', id, error: { code, message } }, { status })\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build a request handler for a tools-only MCP server. The returned function\n * accepts a standard `Request` and resolves to a `Response` — mount it on any\n * Cloudflare Worker route or Remix `loader`.\n *\n * The handler calls `buildEnv` exactly ONCE per `tools/call` request (after\n * the tool is found, before `run`) — non-`tools/call` paths skip it entirely\n * so metadata requests do not pay env-build cost.\n */\nexport function createMcpToolHandler<TEnv = Record<string, never>>(\n opts: CreateMcpToolHandlerOptions<TEnv>,\n): (request: Request) => Promise<Response> {\n const toolMap = new Map<string, McpToolDefinition<TEnv>>()\n for (const tool of opts.tools) {\n if (toolMap.has(tool.name)) throw new Error(`duplicate MCP tool name: ${tool.name}`)\n toolMap.set(tool.name, tool)\n }\n\n return async (request: Request): Promise<Response> => {\n if (request.method !== 'POST') {\n return new Response('MCP server accepts JSON-RPC 2.0 over POST only', {\n status: 405,\n headers: { Allow: 'POST' },\n })\n }\n\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return rpcError(null, -32700, 'Parse error: request body is not valid JSON', 400)\n }\n\n if (Array.isArray(body)) {\n return rpcError(null, -32600, 'Invalid request: JSON-RPC batching is not supported', 400)\n }\n if (!isRecord(body) || body.jsonrpc !== '2.0' || typeof body.method !== 'string') {\n return rpcError(\n null,\n -32600,\n 'Invalid request: expected a JSON-RPC 2.0 object with jsonrpc \"2.0\" and a string method',\n 400,\n )\n }\n\n const method = body.method\n const params = isRecord(body.params) ? body.params : {}\n\n // Notifications have no `id` — acknowledge without a JSON-RPC body.\n if (!('id' in body) || body.id === undefined) {\n return new Response(null, { status: 202 })\n }\n const id = body.id as JsonRpcId\n\n switch (method) {\n case 'initialize': {\n const requested = typeof params.protocolVersion === 'string' ? params.protocolVersion : undefined\n const protocolVersion =\n requested !== undefined && (MCP_PROTOCOL_VERSIONS as readonly string[]).includes(requested)\n ? requested\n : LATEST_PROTOCOL_VERSION\n return rpcResult(id, {\n protocolVersion,\n capabilities: { tools: { listChanged: false } },\n serverInfo: opts.serverInfo,\n })\n }\n\n case 'ping':\n return rpcResult(id, {})\n\n case 'tools/list':\n return rpcResult(id, {\n tools: opts.tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n })),\n })\n\n case 'tools/call': {\n const name = params.name\n if (typeof name !== 'string' || name.length === 0) {\n return rpcError(id, -32602, 'tools/call requires params.name (string)')\n }\n const tool = toolMap.get(name)\n if (!tool) {\n return rpcError(\n id,\n -32602,\n `Unknown tool: ${name}. Available tools: ${opts.tools.map((t) => t.name).join(', ')}`,\n )\n }\n if (params.arguments !== undefined && !isRecord(params.arguments)) {\n return rpcError(id, -32602, 'tools/call params.arguments must be an object when provided')\n }\n const args = isRecord(params.arguments) ? params.arguments : {}\n let env: TEnv\n try {\n env = await opts.buildEnv(request)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n const payload: McpToolCallContent = {\n content: [{ type: 'text', text: `${name} failed to build env: ${message}` }],\n isError: true,\n }\n return rpcResult(id, payload)\n }\n try {\n const result = await tool.run(args, env)\n const payload = opts.formatResult\n ? opts.formatResult(result, tool)\n : { content: [{ type: 'text', text: JSON.stringify(result) }] }\n return rpcResult(id, payload)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n const payload: McpToolCallContent = {\n content: [{ type: 'text', text: `${name} failed: ${message}` }],\n isError: true,\n }\n return rpcResult(id, payload)\n }\n }\n\n default:\n return rpcError(id, -32601, `Method not found: ${method}`)\n }\n }\n}\n","/**\n * The tagged-configuration contract for every MCP server this package emits.\n *\n * An `AgentProfile` is digested, diffed, stored, and logged, so a credential\n * that rides it as plain text is a leak by construction. `@tangle-network/\n * agent-interface` closed that at **0.38.0**: `args`, `env`, and `headers` on\n * an `AgentProfileMcpServer` no longer take strings. Every value is either\n * deliberately public profile material (`defineAgentProfilePublicConfig`) or an\n * opaque reference to a credential (`defineAgentProfileSecretRef`) resolved\n * privately at materialization, validated by a `z.discriminatedUnion('kind')`.\n *\n * The schema also refuses to let a secret hide as public material: a `public`\n * value under a credential-bearing key name (`Authorization`, `*_TOKEN`,\n * `*_API_KEY`, `*_SECRET`, `Cookie`, …) is rejected with \"credential-bearing\n * config names require a secret-ref\", and any value whose bytes look like a\n * credential (`Bearer …`, `sk-…`, `ghp_…`) is rejected outright. So the\n * capability token this package used to inline as `Bearer <token>` cannot be\n * expressed at all — it must become a reference.\n *\n * **A reference resolves ONLY from an environment variable present on the box,\n * named by `key`.** That is why the builders below take a `tokenEnvKey` (the\n * box-env variable NAME) instead of a token VALUE: agent-app cannot guess the\n * name, and a reference to a key nothing places fails the turn at\n * materialization rather than running credential-less. The box env is what\n * `SandboxRuntimeConfig.env` (and the platform secret store via\n * `SandboxRuntimeConfig.secrets`) writes at sandbox creation.\n * `SandboxRuntimeConfig.runtimeEnv` can also supply expiring workspace\n * credentials at creation and refresh them on retained boxes before\n * bootstrap. The key must name a variable one of those places — and it must\n * therefore be a real environment-variable name, which this module enforces.\n *\n * A workspace credential may be renewed through `runtimeEnv`; it still has\n * workspace-wide authority because every member and turn shares the box. A\n * token scoped narrower than the box — per-user, per-document — cannot be\n * referenced safely; {@link unresolvableSurfaceCredential} names that blocker\n * instead of emitting a reference that resolves to nothing.\n */\nimport {\n agentProfileMcpServerSchema,\n defineAgentProfilePublicConfig,\n defineAgentProfileSecretRef,\n type AgentProfileConfigValue,\n} from '@tangle-network/agent-interface'\nimport type { AppToolContext } from './types'\nimport type { AppToolName } from './openai'\nimport type { AppToolDefinition } from './registry'\nimport type { ToolHeaderNames } from './auth'\nimport { DEFAULT_HEADER_NAMES } from './auth'\n\n/** Default route path each app tool is served at. A product mounts its routes\n * at these paths (or supplies its own via {@link BuildMcpServerOptions.paths}). */\nexport const DEFAULT_APP_TOOL_PATHS: Record<AppToolName, string> = {\n submit_proposal: '/api/tools/propose',\n schedule_followup: '/api/tools/followup',\n render_ui: '/api/tools/render-ui',\n add_citation: '/api/tools/citation',\n}\n\n/** The portable MCP server entry the sandbox SDK accepts (transport + url +\n * tagged headers). Assignable to `AgentProfileMcpServer` without a cast —\n * products spread it into their profile's `mcp` map. */\nexport interface AppToolMcpServer {\n transport: 'http'\n url: string\n headers: Record<string, AgentProfileConfigValue>\n enabled: true\n metadata: { description: string }\n}\n\n/** POSIX environment-variable name — the same shape `agent-interface`'s\n * `environmentNameSchema` accepts for an env key. A secret reference resolves\n * from the box environment, so a key outside this shape can never resolve. */\nconst ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/\n\n/**\n * Reject a secret-reference key that names nothing the box can hold.\n *\n * `agent-interface` accepts any non-credential string as a reference key, so a\n * typo or a token VALUE passed where a key belongs validates against the\n * profile schema and fails much later, inside materialization, as an opaque\n * missing-secret error. Failing here names the parameter.\n */\nfunction assertSecretEnvKey(key: string, label: string): string {\n if (!ENV_NAME_PATTERN.test(key)) {\n throw new Error(\n `${label}: tokenEnvKey must be an environment-variable NAME the sandbox box carries ` +\n `(e.g. 'APP_CAPABILITY_TOKEN'), not a token value — got ${JSON.stringify(key)}. ` +\n 'A profile may only REFERENCE a credential; the value is placed on the box by ' +\n 'SandboxRuntimeConfig.env or the platform secret store.',\n )\n }\n return key\n}\n\n/**\n * Validate one emitted entry against the REAL `agentProfileMcpServerSchema`.\n *\n * The contract's rules (which key names demand a reference, which byte patterns\n * read as a credential) live in `agent-interface`. Re-implementing them here\n * would drift the moment that package moves — which it does: 0.36.0 → 0.40.0 in\n * three days. Running the shipped validator instead means a product that passes\n * a credential-bearing custom header name, a relative base URL, or a URL with\n * embedded credentials fails at the builder with the contract's own message.\n *\n * It is also the loud failure for a version skew: if the resolved\n * `@tangle-network/agent-interface` predates 0.38.0, its schema rejects the\n * tagged shape this package now emits, and that surfaces here instead of as a\n * sandbox provisioning error.\n */\nfunction assertProfileMcpServer<T extends AppToolMcpServer>(server: T, label: string): T {\n const result = agentProfileMcpServerSchema.safeParse(server)\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('; ')\n throw new Error(\n `${label} produced an MCP server the AgentProfile contract rejects: ${issues}. ` +\n 'Requires @tangle-network/agent-interface >= 0.38.0 (tagged MCP config values).',\n )\n }\n return server\n}\n\n/**\n * Refuse to mount a surface whose credential is scoped narrower than the box.\n *\n * A per-user or per-resource capability token is minted per request. The box\n * environment is shared by every turn and every member of the workspace;\n * `env` writes at creation and `runtimeEnv` refreshes workspace-scoped values\n * on retained boxes. Neither path provides a per-turn secret channel, so a\n * narrower token cannot be referenced safely. Widening the channel to a\n * workspace-bound token is not a substitute when the route authenticates the\n * CALLER: the agent can read its own box env, so it could forge that identity.\n *\n * Mounting the surface anyway would emit a plain-string `Authorization` header\n * (rejected by the schema) or a reference to a key nothing places (rejected at\n * materialization). Throwing here names the actual blocker. Exported because\n * every product with per-document MCP surfaces hits it and was writing this\n * message itself.\n */\nexport function unresolvableSurfaceCredential(surface: string): never {\n throw new Error(\n `The ${surface} MCP surface cannot be mounted: its capability token is scoped to a single ` +\n 'user and resource, and an AgentProfile may only reference a credential the sandbox can ' +\n 'resolve from the box environment, which is workspace-wide even when runtimeEnv refreshes it. ' +\n 'Mounting it needs a per-session secret channel on the sandbox API, or a route that ' +\n 'authenticates the workspace rather than the caller.',\n )\n}\n\n/** Define configuration options for building an HTTP MCP server including path, baseUrl, token env key, context, and description */\nexport interface BuildHttpMcpServerOptions {\n /** Route path on the app the sandbox POSTs to (e.g. `/api/tools/propose`). */\n path: string\n /** App base URL the sandbox reaches back to (no trailing slash required). */\n baseUrl: string\n /**\n * NAME of the box-environment variable holding the capability token — never\n * the token itself. The emitted `Authorization` header is a `secret-ref` to\n * this key with `format: 'bearer'`, so the sandbox resolves the value\n * privately and the profile carries only the name.\n *\n * The key MUST name a variable the box actually carries (placed by\n * `SandboxRuntimeConfig.env` at creation, refreshed by\n * `SandboxRuntimeConfig.runtimeEnv` before retained-box bootstrap, or\n * injected from the platform secret store via\n * `SandboxRuntimeConfig.secrets`). The value must be a workspace-scoped token\n * this route accepts for the active box; a per-user or per-resource token is\n * not referenceable: see {@link unresolvableSurfaceCredential}.\n */\n tokenEnvKey: string\n ctx: AppToolContext\n /** Tool description the model sees. */\n description: string\n headerNames?: ToolHeaderNames\n}\n\n/**\n * Build ONE HTTP MCP server entry — the generic agent→app bridge. The\n * capability token (as a secret reference) + the user/workspace/thread ids ride\n * in server-set headers (never tool args), so the model can't forge identity or\n * target another workspace. Workspace/thread headers are omitted when their\n * `ctx` value is empty/null (e.g. an integration-invoke bridge that's\n * user-scoped only). Used directly for non-app-tool bridges\n * (integration_invoke) and via {@link buildAppToolMcpServer} for the four app\n * tools.\n */\nexport function buildHttpMcpServer(opts: BuildHttpMcpServerOptions): AppToolMcpServer {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const h = opts.headerNames ?? DEFAULT_HEADER_NAMES\n return assertProfileMcpServer(\n {\n transport: 'http',\n url: `${base}${opts.path}`,\n headers: {\n Authorization: defineAgentProfileSecretRef(\n assertSecretEnvKey(opts.tokenEnvKey, 'buildHttpMcpServer'),\n 'bearer',\n ),\n [h.userId]: defineAgentProfilePublicConfig(opts.ctx.userId),\n ...(opts.ctx.workspaceId\n ? { [h.workspaceId]: defineAgentProfilePublicConfig(opts.ctx.workspaceId) }\n : {}),\n ...(opts.ctx.threadId\n ? { [h.threadId]: defineAgentProfilePublicConfig(opts.ctx.threadId) }\n : {}),\n 'Content-Type': defineAgentProfilePublicConfig('application/json'),\n },\n enabled: true,\n metadata: { description: opts.description },\n },\n 'buildHttpMcpServer',\n )\n}\n\n/** Options for a per-document/scoped MCP channel entry (design-canvas,\n * sequences, …). The capability token + path scope ONE resource; the document\n * id lives in the path, never a tool argument. */\nexport interface ScopedMcpServerEntryOptions {\n /** App base URL the sandbox reaches back to (trailing slash tolerated). */\n baseUrl: string\n /** Product route serving the resource's MCP handler — id is part of the path. */\n path: string\n /**\n * NAME of the box-environment variable holding this channel's capability\n * token — never the token itself. See\n * {@link BuildHttpMcpServerOptions.tokenEnvKey}.\n *\n * A per-(user, resource) token cannot satisfy this: the box environment is\n * workspace-wide; `env` writes at creation and `runtimeEnv` refreshes\n * workspace-scoped values on retained boxes. A product whose channel needs\n * a narrower token calls {@link unresolvableSurfaceCredential} rather than\n * mounting an entry that cannot resolve safely.\n */\n tokenEnvKey: string\n /** Override the channel's default tool-server description. */\n description?: string\n /** Identity headers for products whose route recovers the user via\n * `authenticateToolRequest`. Omit when the bearer token is self-contained. */\n ctx?: AppToolContext\n headerNames?: ToolHeaderNames\n}\n\n/**\n * Build the `AgentProfileMcpServer`-shaped entry for a scoped, per-resource MCP\n * channel. The shared mechanism behind the per-domain entry builders\n * (`buildDesignCanvasMcpServerEntry`, `buildSequencesMcpServerEntry`): same\n * token/path guards, same description default, same ctx-vs-self-contained-token\n * branching. The domain is two parameters — `label` (for guard messages) and\n * `defaultDescription` — never baked.\n *\n * The no-`ctx` branch is a GENUINE behavioral path, not a shortcut: it emits a\n * self-contained-token entry with ONLY `Authorization` + `Content-Type`.\n * Routing it through {@link buildHttpMcpServer} would unconditionally write a\n * `userId` identity header (here `undefined`), so it stays a distinct branch.\n */\nexport function buildScopedMcpServerEntry(\n opts: ScopedMcpServerEntryOptions & { label: string; defaultDescription: string },\n): AppToolMcpServer {\n if (opts.tokenEnvKey.trim().length === 0) {\n throw new Error(`${opts.label} requires a capability token env key — omit the MCP server when none is available`)\n }\n if (!opts.path.startsWith('/')) {\n throw new Error(`${opts.label} path must start with \"/\" (got \"${opts.path}\")`)\n }\n const description = opts.description ?? opts.defaultDescription\n\n if (opts.ctx) {\n return buildHttpMcpServer({\n path: opts.path,\n baseUrl: opts.baseUrl,\n tokenEnvKey: opts.tokenEnvKey,\n ctx: opts.ctx,\n description,\n headerNames: opts.headerNames ?? DEFAULT_HEADER_NAMES,\n })\n }\n\n return assertProfileMcpServer(\n {\n transport: 'http',\n url: `${opts.baseUrl.replace(/\\/+$/, '')}${opts.path}`,\n headers: {\n Authorization: defineAgentProfileSecretRef(\n assertSecretEnvKey(opts.tokenEnvKey, opts.label),\n 'bearer',\n ),\n 'Content-Type': defineAgentProfilePublicConfig('application/json'),\n },\n enabled: true,\n metadata: { description },\n },\n opts.label,\n )\n}\n\n/** Define configuration options required to build an MCP server including tool, baseUrl, token, and context */\nexport interface BuildMcpServerOptions {\n /** A built-in app tool name, or a product-registered {@link AppToolDefinition}.\n * A custom tool supplies its route via `AppToolDefinition.path` (or `paths`). */\n tool: AppToolName | AppToolDefinition\n baseUrl: string\n /** NAME of the box-environment variable holding the capability token — see\n * {@link BuildHttpMcpServerOptions.tokenEnvKey}. */\n tokenEnvKey: string\n ctx: AppToolContext\n description: string\n headerNames?: ToolHeaderNames\n paths?: Partial<Record<string, string>>\n}\n\n/** Build one app-tool MCP server entry — a thin wrapper over\n * {@link buildHttpMcpServer} that resolves the tool's route path. Built-ins map\n * through {@link DEFAULT_APP_TOOL_PATHS}; a custom tool uses its own `path`\n * (or a `paths` override). */\nexport function buildAppToolMcpServer(opts: BuildMcpServerOptions): AppToolMcpServer {\n const path =\n typeof opts.tool === 'string'\n ? opts.paths?.[opts.tool] ?? DEFAULT_APP_TOOL_PATHS[opts.tool]\n : opts.paths?.[opts.tool.name] ?? opts.tool.path\n if (!path) {\n const name = typeof opts.tool === 'string' ? opts.tool : opts.tool.name\n throw new Error(`buildAppToolMcpServer: tool \"${name}\" has no route path — set AppToolDefinition.path or pass it via opts.paths`)\n }\n return buildHttpMcpServer({\n path,\n baseUrl: opts.baseUrl,\n tokenEnvKey: opts.tokenEnvKey,\n ctx: opts.ctx,\n description: opts.description,\n headerNames: opts.headerNames,\n })\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  authenticateToolRequest,
3
3
  createMcpToolHandler
4
- } from "./chunk-KBJL6LIW.js";
4
+ } from "./chunk-6A7MYOUI.js";
5
5
  import {
6
6
  ToolInputError,
7
7
  buildAppToolOpenAITools,
@@ -264,4 +264,4 @@ export {
264
264
  createAppToolRuntimeExecutor,
265
265
  handleAppToolRequest
266
266
  };
267
- //# sourceMappingURL=chunk-P5PIAQVS.js.map
267
+ //# sourceMappingURL=chunk-FZOGDD2E.js.map
@@ -35,7 +35,7 @@ import {
35
35
  import {
36
36
  buildScopedMcpServerEntry,
37
37
  createMcpToolHandler
38
- } from "../chunk-KBJL6LIW.js";
38
+ } from "../chunk-6A7MYOUI.js";
39
39
  import "../chunk-3YUI7WNW.js";
40
40
  import "../chunk-TXD5HXLE.js";
41
41
 
@@ -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 through `SandboxRuntimeConfig.env` at creation or\n * refreshes it through `SandboxRuntimeConfig.runtimeEnv` before retained-box\n * bootstrap. The value remains workspace-scoped because every member and turn\n * shares the box; runtime renewal does not create a per-user secret channel.\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":[]}
@@ -27,10 +27,10 @@
27
27
  * contract (`agent-interface` 0.38.0) the `Authorization` header is a
28
28
  * `secret-ref` naming a box-environment variable, which the sandbox resolves
29
29
  * privately; `build()` supplies that key NAME from server configuration and the
30
- * product writes the value into `SandboxRuntimeConfig.env` at box creation. The
31
- * value must be deterministic for the box's lifetime (an HMAC over the
32
- * workspace id, e.g. `createCapabilityToken` in ../tools), because a freshly
33
- * random per-request mint would never match what the box already carries.
30
+ * product writes the value through `SandboxRuntimeConfig.env` at creation or
31
+ * refreshes it through `SandboxRuntimeConfig.runtimeEnv` before retained-box
32
+ * bootstrap. The value remains workspace-scoped because every member and turn
33
+ * shares the box; runtime renewal does not create a per-user secret channel.
34
34
  */
35
35
  import type { AppToolMcpServer } from '../tools/mcp';
36
36
  /** Sandbox permission posture values, ranked deny > ask > allow for merging. */
@@ -155,26 +155,19 @@ export interface SandboxRuntimeConfig {
155
155
  metadata: (harness: Harness) => Record<string, unknown>;
156
156
  connectedIntegrationIds: (workspaceId: string) => Promise<string[]>;
157
157
  /**
158
- * Raw box environment written once at sandbox CREATION — deliberately plain
159
- * strings, and the one place a credential value legitimately lives.
160
- *
161
- * This is the private side of the tagged-config contract, not profile
162
- * material: an `AgentProfileMcpServer` may only carry a `secret-ref` naming a
163
- * key, and the sandbox resolves that key against THIS map (or against the
164
- * platform secret store fed by {@link SandboxRuntimeConfig.secrets}). So a
165
- * `tokenEnvKey` passed to `buildAppToolMcpServers` must name a variable this
166
- * seam places, or the reference resolves to nothing.
167
- *
168
- * It is NOT widened to tagged values: the sandbox SDK's create payload types
169
- * `env` as `Record<string, string>`, and {@link assertEnvWithinLimits}
170
- * measures those bytes against the kernel's per-entry `MAX_ARG_STRLEN`.
171
- * Tagging it would make the box env reference itself.
172
- *
173
- * Values are workspace-wide and fixed for the box's lifetime, so a per-user
174
- * or per-resource credential cannot be placed here — see
175
- * `unresolvableSurfaceCredential` in `../tools/mcp`.
158
+ * Creation-only environment. Use runtimeEnv for expiring app credentials.
159
+ * Values remain private; profiles reference their names through secret-ref.
160
+ * Workspace-wide values cannot carry per-user or per-resource authority.
176
161
  */
177
162
  env: (ctx: SandboxBuildContext) => Promise<Record<string, string>>;
163
+ /**
164
+ * Workspace app credentials resolved on creation and before every reuse,
165
+ * resume, or recovery. Fresh values override env in the creation payload.
166
+ * The SDK updates retained runtimes before bootstrap and refuses runtime-managed keys.
167
+ * Throw when required credentials cannot be minted; renewal failure preserves
168
+ * the sandbox and prevents dispatch. Requires the matching Sandbox runtime.
169
+ */
170
+ runtimeEnv?: (scope: SandboxScope) => Promise<Record<string, string>>;
178
171
  files: (ctx: SandboxBuildContext) => Promise<AgentProfileFileMount[]>;
179
172
  secrets: (workspaceId: string) => Promise<string[]>;
180
173
  profile: (options: ProfileComposeOptions) => AgentProfile;
@@ -257,8 +250,10 @@ export interface BuildAppToolMcpServersOptions {
257
250
  * `BuildHttpMcpServerOptions.tokenEnvKey` in `../tools/mcp`.
258
251
  *
259
252
  * The key must name a variable the box carries — placed by
260
- * {@link SandboxRuntimeConfig.env} at creation, or injected from the platform
261
- * secret store via {@link SandboxRuntimeConfig.secrets}.
253
+ * {@link SandboxRuntimeConfig.env} at creation, refreshed by
254
+ * {@link SandboxRuntimeConfig.runtimeEnv} before retained-box bootstrap, or
255
+ * injected from the platform secret store via
256
+ * {@link SandboxRuntimeConfig.secrets}.
262
257
  */
263
258
  tokenEnvKey: string;
264
259
  ctx: AppToolContext;
@@ -99,11 +99,11 @@ import {
99
99
  workspaceSandboxRecoveryMessage,
100
100
  workspaceSandboxRecoveryRecommendedActions,
101
101
  writeProfileFilesToBox
102
- } from "../chunk-PMAQSFJS.js";
102
+ } from "../chunk-3SIQ4CDA.js";
103
103
  import "../chunk-MCYJON3F.js";
104
104
  import "../chunk-LWSJK546.js";
105
- import "../chunk-P5PIAQVS.js";
106
- import "../chunk-KBJL6LIW.js";
105
+ import "../chunk-FZOGDD2E.js";
106
+ import "../chunk-6A7MYOUI.js";
107
107
  import "../chunk-TX6S7XXU.js";
108
108
  import "../chunk-UGWQLQDS.js";
109
109
  import "../chunk-S5SRJJQG.js";
@@ -15,7 +15,7 @@ import {
15
15
  MCP_PROTOCOL_VERSIONS,
16
16
  buildScopedMcpServerEntry,
17
17
  createMcpToolHandler
18
- } from "../chunk-KBJL6LIW.js";
18
+ } from "../chunk-6A7MYOUI.js";
19
19
  import {
20
20
  assertMediaUrl
21
21
  } from "../chunk-3YUI7WNW.js";
@@ -17,13 +17,14 @@ export declare const DEFAULT_HEADER_NAMES: ToolHeaderNames;
17
17
  * `'userId'` (default) is right when the product mints a token per user and can
18
18
  * deliver it per turn.
19
19
  *
20
- * `'workspaceId'` is the only workable choice when the token has to survive in
21
- * the BOX ENVIRONMENT. Since agent-interface 0.38 a credential may reach a
22
- * profile only as a reference the sandbox resolves from that environment, and
23
- * the environment is workspace-wide and written once at box creation. A
24
- * per-user token therefore cannot be delivered at all — and a per-user token
25
- * that IS written there was never per-user in any meaningful sense, because
26
- * every member of that workspace's box can read it.
20
+ * `'workspaceId'` is the only workable choice when the token lives in the BOX
21
+ * ENVIRONMENT. Since agent-interface 0.38 a credential may reach a profile
22
+ * only as a reference the sandbox resolves from that environment. `env` writes
23
+ * at creation and `runtimeEnv` can refresh retained boxes, but the environment
24
+ * remains workspace-wide. A per-user token therefore cannot be delivered as a
25
+ * safe box credential — and a per-user token that IS written there was never
26
+ * per-user in any meaningful sense, because every member of that workspace's
27
+ * box can read it.
27
28
  *
28
29
  * Binding the bearer to the workspace does NOT collapse the identity: the user
29
30
  * header is still required, still server-set, still returned on `ctx`, and is
@@ -9,7 +9,7 @@ import {
9
9
  restrictTaxonomy,
10
10
  verifyCapabilityToken,
11
11
  verifyExpiringCapabilityToken
12
- } from "../chunk-P5PIAQVS.js";
12
+ } from "../chunk-FZOGDD2E.js";
13
13
  import {
14
14
  DEFAULT_APP_TOOL_PATHS,
15
15
  DEFAULT_HEADER_NAMES,
@@ -21,7 +21,7 @@ import {
21
21
  createMcpToolHandler,
22
22
  readToolArgs,
23
23
  unresolvableSurfaceCredential
24
- } from "../chunk-KBJL6LIW.js";
24
+ } from "../chunk-6A7MYOUI.js";
25
25
  import {
26
26
  APP_TOOL_NAMES,
27
27
  ToolInputError,
@@ -23,16 +23,17 @@
23
23
  * name, and a reference to a key nothing places fails the turn at
24
24
  * materialization rather than running credential-less. The box env is what
25
25
  * `SandboxRuntimeConfig.env` (and the platform secret store via
26
- * `SandboxRuntimeConfig.secrets`) writes at sandbox creation, so the key must
27
- * name a variable one of those places — and it must therefore be a real
28
- * environment-variable name, which this module enforces.
26
+ * `SandboxRuntimeConfig.secrets`) writes at sandbox creation.
27
+ * `SandboxRuntimeConfig.runtimeEnv` can also supply expiring workspace
28
+ * credentials at creation and refresh them on retained boxes before
29
+ * bootstrap. The key must name a variable one of those places — and it must
30
+ * therefore be a real environment-variable name, which this module enforces.
29
31
  *
30
- * A per-request credential is only referenceable when the value written at box
31
- * creation is byte-identical to the one every later turn would mint (a
32
- * deterministic derivation such as an HMAC over the workspace id). A token
33
- * scoped narrower than the box — per-user, per-document — cannot be referenced
34
- * at all; {@link unresolvableSurfaceCredential} names that blocker instead of
35
- * emitting a reference that resolves to nothing.
32
+ * A workspace credential may be renewed through `runtimeEnv`; it still has
33
+ * workspace-wide authority because every member and turn shares the box. A
34
+ * token scoped narrower than the box — per-user, per-document — cannot be
35
+ * referenced safely; {@link unresolvableSurfaceCredential} names that blocker
36
+ * instead of emitting a reference that resolves to nothing.
36
37
  */
37
38
  import { type AgentProfileConfigValue } from '@tangle-network/agent-interface';
38
39
  import type { AppToolContext } from './types';
@@ -58,10 +59,11 @@ export interface AppToolMcpServer {
58
59
  * Refuse to mount a surface whose credential is scoped narrower than the box.
59
60
  *
60
61
  * A per-user or per-resource capability token is minted per request. The box
61
- * environment is written once at sandbox creation and shared by every turn and
62
- * every member of the workspace, so such a token can neither be placed there
63
- * ahead of time nor referenced from a per-turn profile. Widening the channel to
64
- * a workspace-bound token is not a substitute when the route authenticates the
62
+ * environment is shared by every turn and every member of the workspace;
63
+ * `env` writes at creation and `runtimeEnv` refreshes workspace-scoped values
64
+ * on retained boxes. Neither path provides a per-turn secret channel, so a
65
+ * narrower token cannot be referenced safely. Widening the channel to a
66
+ * workspace-bound token is not a substitute when the route authenticates the
65
67
  * CALLER: the agent can read its own box env, so it could forge that identity.
66
68
  *
67
69
  * Mounting the surface anyway would emit a plain-string `Authorization` header
@@ -84,12 +86,12 @@ export interface BuildHttpMcpServerOptions {
84
86
  * privately and the profile carries only the name.
85
87
  *
86
88
  * The key MUST name a variable the box actually carries (placed by
87
- * `SandboxRuntimeConfig.env` at creation, or injected from the platform
88
- * secret store via `SandboxRuntimeConfig.secrets`), and the value written
89
- * there must be the token this route will accept for every turn — which in
90
- * practice means a deterministic derivation (e.g. an HMAC over the workspace
91
- * id), not a freshly-random per-request mint. A token scoped narrower than
92
- * the box is not referenceable: see {@link unresolvableSurfaceCredential}.
89
+ * `SandboxRuntimeConfig.env` at creation, refreshed by
90
+ * `SandboxRuntimeConfig.runtimeEnv` before retained-box bootstrap, or
91
+ * injected from the platform secret store via
92
+ * `SandboxRuntimeConfig.secrets`). The value must be a workspace-scoped token
93
+ * this route accepts for the active box; a per-user or per-resource token is
94
+ * not referenceable: see {@link unresolvableSurfaceCredential}.
93
95
  */
94
96
  tokenEnvKey: string;
95
97
  ctx: AppToolContext;
@@ -122,9 +124,10 @@ export interface ScopedMcpServerEntryOptions {
122
124
  * {@link BuildHttpMcpServerOptions.tokenEnvKey}.
123
125
  *
124
126
  * A per-(user, resource) token cannot satisfy this: the box environment is
125
- * workspace-wide and fixed at sandbox creation. A product whose channel needs
126
- * one calls {@link unresolvableSurfaceCredential} rather than mounting an
127
- * entry that cannot resolve.
127
+ * workspace-wide; `env` writes at creation and `runtimeEnv` refreshes
128
+ * workspace-scoped values on retained boxes. A product whose channel needs
129
+ * a narrower token calls {@link unresolvableSurfaceCredential} rather than
130
+ * mounting an entry that cannot resolve safely.
128
131
  */
129
132
  tokenEnvKey: string;
130
133
  /** Override the channel's default tool-server description. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.47.23",
3
+ "version": "0.47.24",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -543,7 +543,7 @@
543
543
  "@tangle-network/agent-profile-materialize": "0.19.0",
544
544
  "@tangle-network/agent-runtime": "0.222.1",
545
545
  "@tangle-network/brand": "1.5.0",
546
- "@tangle-network/sandbox": "0.39.2",
546
+ "@tangle-network/sandbox": "0.39.4",
547
547
  "@tangle-network/sandbox-ui": "0.113.3",
548
548
  "@tangle-network/ui": "^11.8.0",
549
549
  "@testing-library/dom": "^10.4.1",
@@ -603,7 +603,7 @@
603
603
  "@tangle-network/agent-profile-materialize": ">=0.19.0 <0.20.0",
604
604
  "@tangle-network/agent-runtime": ">=0.222.1 <0.223.0",
605
605
  "@tangle-network/brand": ">=1.5.0",
606
- "@tangle-network/sandbox": ">=0.38.2 <0.40.0",
606
+ "@tangle-network/sandbox": ">=0.39.4 <0.40.0",
607
607
  "@tangle-network/sandbox-ui": ">=0.113.3 <0.114.0",
608
608
  "@tangle-network/ui": ">=11.6.0 <12.0.0",
609
609
  "@tiptap/core": ">=3.28.0 <4.0.0",