@agent-native/core 0.228.2-nightly-20261113032548 → 0.228.2

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.
Files changed (42) hide show
  1. package/dist/application-state/handlers.d.ts +2 -2
  2. package/dist/cli/skills-content/an-skill.d.ts +1 -1
  3. package/dist/cli/skills-content/an-skill.js +16 -0
  4. package/dist/collab/routes.d.ts +1 -1
  5. package/dist/db/schema-snapshot.generated.js +2 -1
  6. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  7. package/dist/file-upload/builder-gateway.d.ts +42 -0
  8. package/dist/file-upload/builder-gateway.js +185 -0
  9. package/dist/file-upload/builder.js +8 -11
  10. package/dist/file-upload/index.d.ts +1 -0
  11. package/dist/file-upload/index.js +1 -0
  12. package/dist/framework-package-versions.json +11 -11
  13. package/dist/identity/rekey.js +2 -0
  14. package/dist/mcp/embed-app.js +259 -3
  15. package/dist/mcp/oauth-route.js +7 -0
  16. package/dist/mcp/screen-memory-stdio.d.ts +3 -3
  17. package/dist/notifications/routes.d.ts +3 -3
  18. package/dist/observability/routes.d.ts +5 -5
  19. package/dist/org/index.d.ts +1 -1
  20. package/dist/secrets/api-keys.js +8 -7
  21. package/dist/secrets/index.d.ts +2 -1
  22. package/dist/secrets/index.js +2 -1
  23. package/dist/secrets/routes.d.ts +3 -3
  24. package/dist/secrets/routes.js +7 -8
  25. package/dist/secrets/schema.d.ts +51 -0
  26. package/dist/secrets/schema.js +3 -0
  27. package/dist/secrets/storage.d.ts +83 -1
  28. package/dist/secrets/storage.js +194 -6
  29. package/dist/secrets/usage.js +2 -4
  30. package/dist/secrets/vault-managed.d.ts +17 -0
  31. package/dist/secrets/vault-managed.js +19 -0
  32. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  33. package/dist/server/builder-signup-experiment.d.ts +4 -4
  34. package/dist/server/credential-provider.js +7 -7
  35. package/dist/server/first-touch-route.d.ts +1 -1
  36. package/dist/server/transcribe-voice.d.ts +1 -1
  37. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
  38. package/docs/content/security.mdx +1 -1
  39. package/docs/content/template-dispatch-developers.mdx +7 -7
  40. package/docs/content/template-dispatch-reference.mdx +16 -10
  41. package/package.json +4 -4
  42. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
@@ -36,18 +36,18 @@ export declare const listComposeDrafts: import("h3").EventHandlerWithFetch<impor
36
36
  export declare const getComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | null>>;
37
37
  export declare const putComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<any>>;
38
38
  export declare const saveComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
39
+ errorCode?: undefined;
39
40
  value?: undefined;
40
41
  error: string;
41
42
  revision?: undefined;
42
- errorCode?: undefined;
43
43
  details?: undefined;
44
44
  } | {
45
+ errorCode?: undefined;
45
46
  error?: undefined;
46
47
  value: {
47
48
  id: string;
48
49
  };
49
50
  revision: string;
50
- errorCode?: undefined;
51
51
  details?: undefined;
52
52
  } | {
53
53
  value?: undefined;
@@ -1,2 +1,2 @@
1
- export declare const AN_SKILL_MD = "---\nname: an\ndescription: >-\n Open and operate Agent-Native workspace apps through Dispatch MCP, with\n inline app surfaces, browser-session authentication, and current screen state.\nmetadata:\n visibility: exported\n---\n\n# Agent-Native Apps\n\nUse `/an <app>` to open an Agent-Native app beside the conversation and keep\nthe app's live UI and the agent's actions in the same workflow. The first\nargument is the Dispatch app id. For example, `/an slides` opens the Slides\neditor. After it opens, the user can sign in in that browser surface and ask\nfor new decks or focused edits in ordinary language.\n\n## Open the app\n\nFor a known app, call Dispatch's `open_app` with an inline surface:\n\n```json\n{\n \"app\": \"slides\",\n \"path\": \"/\",\n \"embed\": true,\n \"chrome\": \"full\"\n}\n```\n\nThe MCP App response is the preferred handoff for Claude, Claude Desktop,\nClaude Cowork, ChatGPT, ChatGPT desktop, Codex, and other compatible hosts. Keep\nthe returned open link as the fallback for a host that cannot render MCP Apps\ninline. Do not claim that a browser opened unless the host rendered the inline\nsurface or its browser tool confirmed the returned URL.\n\nIf the app name is missing or uncertain, call `list_apps` first and use the\nexact granted app id. If the requested app is not listed, explain that it must\nbe granted in Dispatch's Agents access settings. Do not invent a URL, use a\nlocalhost URL, or route around the Dispatch grant.\n\n## Work in the opened app\n\nPrefer direct app tools when the host exposes them. After opening an app, call\n`list-host-webmcp-tools` when available, then use `run-host-webmcp-tool`\nwith the exact listed name and origin. Page-local tools reflect current app\nstate and selection, so use `view-screen` before selection-dependent edits\nand read back writes.\n\nYou are the model doing the work: when the user asks for new content \u2014 a\ndesign, a deck, a form, a document \u2014 author it yourself and save it with the\napp's create/update tools. Never hand off authoring to the app's own agent\nwhen those tools exist, and never wait on an in-app question form; it answers\nback through that app's own chat, not to you.\n\nUse `ask_app` only when the host has no direct page tools, the requested\ncapability is not exposed, the task needs the app agent's interpretation or\nmulti-step specialist reasoning, or a direct call fails and needs recovery.\nDispatch's unified `/mcp` endpoint still exposes generic cross-app verbs; a\ndirect app MCP connection or page WebMCP surface is where named app actions\nappear.\n\nThe app's MCP `instructions` name its key tools (generated from the app's own\nlist); follow those names verbatim and use `tool-search` for anything else \u2014\nnever guess a tool name.\n\n1. Call `view-screen` before any selection-dependent edit and use the ids it\n returns; never guess an id from a title, index, or stale context.\n2. If `view-screen` returns a `selection` and `nextRequiredAction`, make\n that call directly with the smallest atomic change; do not regenerate a\n whole artifact for a focused edit.\n3. Read the result back with the app's matching read action and report what\n changed. A queued `ask_app_status` task is not proof an edit completed.\n\n## Authentication and host behavior\n\nThe Dispatch MCP connector identifies the calling agent, while the embedded app\nuses the user's browser session. Let the user complete sign-in in the rendered\nAgent-Native browser surface. Never request, copy, or store passwords, OAuth\ncodes, cookies, or access tokens.\n\nThe generic Agent-Native plugin registers Dispatch at\n`https://dispatch.agent-native.com/mcp` for hosts that support plugin MCP\nconfiguration. ChatGPT custom connectors use the same URL with OAuth. After\ninstalling or changing a connector, reload Claude/Cowork/ChatGPT or reconnect\nand rescan the MCP server if the new tools or inline surface are not visible.\n\n## Failure handling\n\n- If `open_app` succeeds but no inline surface appears, show its returned\n link and say that this host does not expose an embedded browser here.\n- After an app loads, check for `list-host-webmcp-tools` before delegating. If\n it is available, use the exact returned tool name and origin for direct work.\n- If `ask_app` cannot reach Slides, check that Slides is granted in\n Dispatch and that the connector is authenticated, then retry once with the\n exact app id `slides`.\n- If screen state has no deck or selection, describe that state instead of\n fabricating context. Ask the user to open a deck or select an element when\n the requested edit depends on it.\n- Treat a successful open link, a queued task, or a tool acknowledgment as\n navigation/task setup only. Confirm a write with the app's readback action.\n";
1
+ export declare const AN_SKILL_MD = "---\nname: an\ndescription: >-\n Open and operate Agent-Native workspace apps through Dispatch MCP, with\n inline app surfaces, browser-session authentication, and current screen state.\nmetadata:\n visibility: exported\n---\n\n# Agent-Native Apps\n\nUse `/an <app>` to open an Agent-Native app beside the conversation and keep\nthe app's live UI and the agent's actions in the same workflow. The first\nargument is the Dispatch app id. For example, `/an slides` opens the Slides\neditor. After it opens, the user can sign in in that browser surface and ask\nfor new decks or focused edits in ordinary language.\n\n## Open the app\n\nFor a known app, call Dispatch's `open_app` with an inline surface:\n\n```json\n{\n \"app\": \"slides\",\n \"path\": \"/\",\n \"embed\": true,\n \"chrome\": \"full\"\n}\n```\n\nThe MCP App response is the preferred handoff for Claude, Claude Desktop,\nClaude Cowork, ChatGPT, ChatGPT desktop, Codex, and other compatible hosts. Keep\nthe returned open link as the fallback for a host that cannot render MCP Apps\ninline. Do not claim that a browser opened unless the host rendered the inline\nsurface or its browser tool confirmed the returned URL.\n\nIf the app name is missing or uncertain, call `list_apps` first and use the\nexact granted app id. If the requested app is not listed, explain that it must\nbe granted in Dispatch's Agents access settings. Do not invent a URL, use a\nlocalhost URL, or route around the Dispatch grant.\n\n## Work in the opened app\n\nPrefer direct app tools when the host exposes them. After opening an app, call\n`list-host-webmcp-tools` when available, then use `run-host-webmcp-tool`\nwith the exact listed name and origin. Page-local tools reflect current app\nstate and selection, so use `view-screen` before selection-dependent edits\nand read back writes.\n\nYou are the model doing the work: when the user asks for new content \u2014 a\ndesign, a deck, a form, a document \u2014 author it yourself and save it with the\napp's create/update tools. Never hand off authoring to the app's own agent\nwhen those tools exist, and never wait on an in-app question form; it answers\nback through that app's own chat, not to you.\n\nUse `ask_app` only when the host has no direct page tools, the requested\ncapability is not exposed, the task needs the app agent's interpretation or\nmulti-step specialist reasoning, or a direct call fails and needs recovery.\nDispatch's unified `/mcp` endpoint still exposes generic cross-app verbs; a\ndirect app MCP connection or page WebMCP surface is where named app actions\nappear.\n\nThe app's MCP `instructions` name its key tools (generated from the app's own\nlist); follow those names verbatim and use `tool-search` for anything else \u2014\nnever guess a tool name.\n\n1. Call `view-screen` before any selection-dependent edit and use the ids it\n returns; never guess an id from a title, index, or stale context.\n2. If `view-screen` returns a `selection` and `nextRequiredAction`, make\n that call directly with the smallest atomic change; do not regenerate a\n whole artifact for a focused edit.\n3. Read the result back with the app's matching read action and report what\n changed. A queued `ask_app_status` task is not proof an edit completed.\n\n## Keep the open app in step with the chat\n\nClaude opens an embedded app in its side panel. The panel is one live app: an\nedit made through a tool shows there, and an edit the user makes by hand is in\nthe app's data the next time you read it.\n\n- To change what the open panel shows (filter a list, open a record, switch a\n week), call the view tools `read_view` and `navigate_view` on the open app\n instead of calling `open_app` again. Each new `open_app` call opens a new\n view and replaces the panel.\n- Before acting on \"this\", \"it\" or \"the one I opened\", call `read_view`;\n the user may have clicked or typed in the panel since your last turn.\n- Draft and edit with the app's own actions (create or update a draft, an\n event, a task), then read it back. Send, delete or move only when the user\n asks.\n\n## Authentication and host behavior\n\nThe Dispatch MCP connector identifies the calling agent, while the embedded app\nuses the user's browser session. Let the user complete sign-in in the rendered\nAgent-Native browser surface. Never request, copy, or store passwords, OAuth\ncodes, cookies, or access tokens.\n\nThe generic Agent-Native plugin registers Dispatch at\n`https://dispatch.agent-native.com/mcp` for hosts that support plugin MCP\nconfiguration. ChatGPT custom connectors use the same URL with OAuth. After\ninstalling or changing a connector, reload Claude/Cowork/ChatGPT or reconnect\nand rescan the MCP server if the new tools or inline surface are not visible.\n\n## Failure handling\n\n- If `open_app` succeeds but no inline surface appears, show its returned\n link and say that this host does not expose an embedded browser here.\n- After an app loads, check for `list-host-webmcp-tools` before delegating. If\n it is available, use the exact returned tool name and origin for direct work.\n- If `ask_app` cannot reach Slides, check that Slides is granted in\n Dispatch and that the connector is authenticated, then retry once with the\n exact app id `slides`.\n- If screen state has no deck or selection, describe that state instead of\n fabricating context. Ask the user to open a deck or select an element when\n the requested edit depends on it.\n- Treat a successful open link, a queued task, or a tool acknowledgment as\n navigation/task setup only. Confirm a write with the app's readback action.\n";
2
2
  export declare const AN_COMMAND_MD = "---\ndescription: Open and operate a granted Agent-Native app through Dispatch MCP.\nargument-hint: <app> [request]\n---\n\nUse the an skill. Treat the first argument as the Agent-Native Dispatch app id.\nFor `/an slides`, call `open_app` with app `slides`, path \"/\", embed\ntrue, and chrome `full`. Keep the returned link if the host cannot render\nthe inline MCP App. Any remaining arguments are the user's request after the\napp opens:\n\n$ARGUMENTS\n";
@@ -73,6 +73,22 @@ never guess a tool name.
73
73
  3. Read the result back with the app's matching read action and report what
74
74
  changed. A queued \`ask_app_status\` task is not proof an edit completed.
75
75
 
76
+ ## Keep the open app in step with the chat
77
+
78
+ Claude opens an embedded app in its side panel. The panel is one live app: an
79
+ edit made through a tool shows there, and an edit the user makes by hand is in
80
+ the app's data the next time you read it.
81
+
82
+ - To change what the open panel shows (filter a list, open a record, switch a
83
+ week), call the view tools \`read_view\` and \`navigate_view\` on the open app
84
+ instead of calling \`open_app\` again. Each new \`open_app\` call opens a new
85
+ view and replaces the panel.
86
+ - Before acting on "this", "it" or "the one I opened", call \`read_view\`;
87
+ the user may have clicked or typed in the panel since your last turn.
88
+ - Draft and edit with the app's own actions (create or update a draft, an
89
+ event, a task), then read it back. Send, delete or move only when the user
90
+ asks.
91
+
76
92
  ## Authentication and host behavior
77
93
 
78
94
  The Dispatch MCP connector identifies the calling agent, while the embedded app
@@ -26,8 +26,8 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
26
26
  ok: boolean;
27
27
  }>>;
28
28
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  text?: undefined;
30
+ ok?: undefined;
31
31
  error: string;
32
32
  } | {
33
33
  error?: undefined;
@@ -68,7 +68,7 @@ export const SCHEMA_SNAPSHOT = {
68
68
  'CREATE TABLE "app_permission_overrides" (\n "org_id" text NOT NULL,\n "app_id" text NOT NULL,\n "permission" text NOT NULL,\n "roles_json" text NOT NULL,\n "updated_by" text NOT NULL,\n "updated_at" bigint NOT NULL\n);',
69
69
  'CREATE TABLE "app_resource_change_consumers" (\n "app" text NOT NULL,\n "resource_type" text NOT NULL,\n "consumer" text NOT NULL,\n "registered_at" timestamp with time zone DEFAULT now() NOT NULL\n);',
70
70
  'CREATE TABLE "app_resource_changes" (\n "consumer" text NOT NULL,\n "app" text NOT NULL,\n "resource_type" text NOT NULL,\n "resource_id" text NOT NULL,\n "reason" text NOT NULL,\n "seq" bigint NOT NULL,\n "changed_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,\n "available_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,\n "attempts" integer DEFAULT 0 NOT NULL,\n "failed_at" timestamp with time zone\n);',
71
- 'CREATE TABLE "app_secrets" (\n "id" text NOT NULL,\n "scope" text NOT NULL,\n "scope_id" text NOT NULL,\n "key" text NOT NULL,\n "encrypted_value" text NOT NULL,\n "shared_encrypted_value" text,\n "description" text,\n "url_allowlist" text,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL\n);',
71
+ 'CREATE TABLE "app_secrets" (\n "id" text NOT NULL,\n "scope" text NOT NULL,\n "scope_id" text NOT NULL,\n "key" text NOT NULL,\n "encrypted_value" text NOT NULL,\n "shared_encrypted_value" text,\n "description" text,\n "url_allowlist" text,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL,\n "label" text,\n "provider" text,\n "created_by" text\n);',
72
72
  'CREATE TABLE "application_state" (\n "session_id" text NOT NULL,\n "key" text NOT NULL,\n "value" text NOT NULL,\n "updated_at" bigint NOT NULL,\n "writer" text,\n "writer_seq" bigint\n);',
73
73
  'CREATE TABLE "automation_runs" (\n "id" text NOT NULL,\n "owner" text NOT NULL,\n "automation" text NOT NULL,\n "path" text NOT NULL,\n "scope" text,\n "org_id" text,\n "app_id" text,\n "run_id" text,\n "thread_id" text,\n "status" text DEFAULT \'running\'::text NOT NULL,\n "started_at" bigint NOT NULL,\n "finished_at" bigint,\n "error" text,\n "error_code" text,\n "notification_email" text,\n "failure_alerted" bigint DEFAULT 0 NOT NULL,\n "failure_alert_state" text,\n "failure_alert_attempts" bigint DEFAULT 0 NOT NULL,\n "failure_alert_next_attempt_at" bigint,\n "failure_alert_claimed_at" bigint,\n "failure_alert_provider" text,\n "failure_alert_first_attempt_at" bigint,\n "failure_alert_unsubscribe_token" text,\n "claimed_at" bigint,\n "dispatch_pending" bigint DEFAULT 0 NOT NULL\n);',
74
74
  'CREATE TABLE "automation_scheduler_health" (\n "id" text NOT NULL,\n "app_id" text DEFAULT \'default\'::text NOT NULL,\n "org_id" text,\n "last_checked_at" bigint,\n "last_dispatched_at" bigint,\n "last_error" text,\n "runtime" text,\n "updated_at" bigint NOT NULL,\n "lease_owner" text,\n "lease_expires_at" bigint\n);',
@@ -953,6 +953,7 @@ export const SCHEMA_SNAPSHOT = {
953
953
  ["agent_tool_approvals", "tool-revision", null],
954
954
  ["app_resource_changes", "baseline", null],
955
955
  ["app_secrets", "baseline", null],
956
+ ["app_secrets", "vault-metadata-columns", null],
956
957
  ["application_state", "baseline", null],
957
958
  ["application_state", "indexes", null],
958
959
  ["application_state", "widen-bigint", null],
@@ -33,9 +33,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
33
33
  idempotencyKey: string;
34
34
  deleted: boolean;
35
35
  } | {
36
- id?: undefined;
37
36
  error: string;
38
37
  url?: undefined;
38
+ id?: undefined;
39
39
  provider?: undefined;
40
40
  configured?: undefined;
41
41
  } | {
@@ -45,8 +45,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
45
45
  provider: string;
46
46
  configured?: undefined;
47
47
  } | {
48
- id?: undefined;
49
48
  url?: undefined;
49
+ id?: undefined;
50
50
  provider?: undefined;
51
51
  error: string;
52
52
  configured: boolean;
@@ -0,0 +1,42 @@
1
+ export type BuilderGatewayStep = "signed_url" | "session_start" | "complete";
2
+ export interface BuilderGatewayErrorDetails {
3
+ step: BuilderGatewayStep;
4
+ /** HTTP status of the last attempt; 0 when no response arrived. */
5
+ status: number;
6
+ attempts: number;
7
+ /** The last response was an HTML error page, not the gateway's JSON. */
8
+ isHtml: boolean;
9
+ /** One id per attempt sent to Builder; search the upload function's logs by it. */
10
+ traceIds: string[];
11
+ /** Allow-listed response headers of the last failed attempt. */
12
+ headers: Record<string, string>;
13
+ cause?: unknown;
14
+ }
15
+ export declare class BuilderGatewayError extends Error {
16
+ readonly provider = "builder";
17
+ readonly step: BuilderGatewayStep;
18
+ readonly status: number;
19
+ readonly attempts: number;
20
+ readonly isHtml: boolean;
21
+ readonly traceIds: string[];
22
+ readonly headers: Record<string, string>;
23
+ constructor(message: string, details: BuilderGatewayErrorDetails);
24
+ }
25
+ /** Summarize a failed response without keeping a whole HTML error page. */
26
+ export declare function describeErrorBody(res: Response): Promise<{
27
+ detail: string;
28
+ isHtml: boolean;
29
+ }>;
30
+ export interface GatewayRequestInit {
31
+ method: string;
32
+ headers: Record<string, string>;
33
+ body?: BodyInit;
34
+ }
35
+ /**
36
+ * One bounded, observable call to the Builder upload gateway or the storage
37
+ * session it hands out. Retries only failures that say "try again": no
38
+ * response, a gateway 5xx, and the step's own extra statuses. Resolves with
39
+ * the OK response; rejects with `BuilderGatewayError` once the attempts run
40
+ * out or the failure is not retryable.
41
+ */
42
+ export declare function builderGatewayRequest(step: BuilderGatewayStep, url: string, init: GatewayRequestInit): Promise<Response>;
@@ -0,0 +1,185 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export class BuilderGatewayError extends Error {
3
+ provider = "builder";
4
+ step;
5
+ status;
6
+ attempts;
7
+ isHtml;
8
+ traceIds;
9
+ headers;
10
+ constructor(message, details) {
11
+ super(message, details.cause === undefined ? undefined : { cause: details.cause });
12
+ this.name = "BuilderGatewayError";
13
+ this.step = details.step;
14
+ this.status = details.status;
15
+ this.attempts = details.attempts;
16
+ this.isHtml = details.isHtml;
17
+ this.traceIds = details.traceIds;
18
+ this.headers = details.headers;
19
+ }
20
+ }
21
+ const BACKOFF_FACTOR = 3;
22
+ // The signed-URL and session-start requests are tiny and answer in well under
23
+ // a second, so their 15 s cap lets every attempt fit inside one serverless
24
+ // request; a slow answer is retried instead of waited on.
25
+ const GATEWAY_POLICIES = {
26
+ signed_url: {
27
+ label: "Builder.io signed-URL request failed",
28
+ attempts: 3,
29
+ baseDelayMs: 500,
30
+ timeoutMs: 15_000,
31
+ retryStatuses: new Set([502, 503, 504]),
32
+ retryOnTimeout: true,
33
+ traced: true,
34
+ },
35
+ session_start: {
36
+ label: "GCS resumable session initiation failed",
37
+ attempts: 3,
38
+ baseDelayMs: 500,
39
+ timeoutMs: 15_000,
40
+ retryStatuses: new Set([408, 429, 500, 502, 503, 504]),
41
+ retryOnTimeout: true,
42
+ traced: false,
43
+ },
44
+ // Completing a stable video can include server-side compression, so this
45
+ // keeps the long timeout and never retries after our own timeout: that retry
46
+ // would race the first attempt. A 409 is that race from elsewhere (a
47
+ // duplicate in flight); the next call returns the registered asset.
48
+ complete: {
49
+ label: "Builder.io upload complete failed",
50
+ attempts: 4,
51
+ baseDelayMs: 500,
52
+ timeoutMs: 120_000,
53
+ retryStatuses: new Set([409, 502, 503, 504]),
54
+ retryOnTimeout: false,
55
+ traced: true,
56
+ },
57
+ };
58
+ // Response headers that say which layer answered; never request headers,
59
+ // which carry the Authorization token.
60
+ const DIAGNOSTIC_RESPONSE_HEADERS = [
61
+ "server",
62
+ "via",
63
+ "x-served-by",
64
+ "x-cache",
65
+ "x-cloud-trace-context",
66
+ "x-amz-cf-id",
67
+ "x-amz-cf-pop",
68
+ "cf-ray",
69
+ "retry-after",
70
+ "content-type",
71
+ "date",
72
+ ];
73
+ function snapshotResponseHeaders(headers) {
74
+ const snapshot = {};
75
+ for (const name of DIAGNOSTIC_RESPONSE_HEADERS) {
76
+ const value = headers.get(name);
77
+ if (value)
78
+ snapshot[name] = value;
79
+ }
80
+ return snapshot;
81
+ }
82
+ /** Summarize a failed response without keeping a whole HTML error page. */
83
+ export async function describeErrorBody(res) {
84
+ let raw;
85
+ try {
86
+ raw = await res.text();
87
+ }
88
+ catch {
89
+ return {
90
+ detail: `${res.statusText || "no status text"} (response body unreadable)`,
91
+ isHtml: false,
92
+ };
93
+ }
94
+ const isHtml = /text\/html/i.test(res.headers.get("content-type") ?? "") ||
95
+ /^\s*<(?:!doctype\s+html|html)\b/i.test(raw);
96
+ if (isHtml) {
97
+ return { detail: "HTML error page returned instead of JSON", isHtml: true };
98
+ }
99
+ const text = raw.trim();
100
+ return { detail: text ? text.slice(0, 500) : res.statusText, isHtml: false };
101
+ }
102
+ async function sendOnce(url, init, policy, traceId) {
103
+ const controller = new AbortController();
104
+ let timedOut = false;
105
+ const timer = setTimeout(() => {
106
+ timedOut = true;
107
+ controller.abort();
108
+ }, policy.timeoutMs);
109
+ try {
110
+ const response = await fetch(url, {
111
+ ...init,
112
+ headers: traceId
113
+ ? { ...init.headers, "X-Cloud-Trace-Context": `${traceId}/1;o=1` }
114
+ : init.headers,
115
+ signal: controller.signal,
116
+ });
117
+ return { kind: "response", response };
118
+ }
119
+ catch (error) {
120
+ return { kind: "network", error, timedOut };
121
+ }
122
+ finally {
123
+ clearTimeout(timer);
124
+ }
125
+ }
126
+ /**
127
+ * One bounded, observable call to the Builder upload gateway or the storage
128
+ * session it hands out. Retries only failures that say "try again": no
129
+ * response, a gateway 5xx, and the step's own extra statuses. Resolves with
130
+ * the OK response; rejects with `BuilderGatewayError` once the attempts run
131
+ * out or the failure is not retryable.
132
+ */
133
+ export async function builderGatewayRequest(step, url, init) {
134
+ const policy = GATEWAY_POLICIES[step];
135
+ const traceIds = [];
136
+ for (let attempt = 1;; attempt++) {
137
+ const startedAt = Date.now();
138
+ const traceId = policy.traced ? randomUUID().replaceAll("-", "") : null;
139
+ if (traceId)
140
+ traceIds.push(traceId);
141
+ const result = await sendOnce(url, init, policy, traceId);
142
+ const elapsedMs = Date.now() - startedAt;
143
+ if (result.kind === "response" && result.response.ok) {
144
+ if (attempt > 1) {
145
+ console.log(`[builder-upload] ${step} recovered on attempt ${attempt}/${policy.attempts}`, { elapsedMs, traceId });
146
+ }
147
+ return result.response;
148
+ }
149
+ let status = 0;
150
+ let isHtml = false;
151
+ let detail;
152
+ let headers = {};
153
+ let retryable;
154
+ if (result.kind === "response") {
155
+ status = result.response.status;
156
+ headers = snapshotResponseHeaders(result.response.headers);
157
+ ({ detail, isHtml } = await describeErrorBody(result.response));
158
+ retryable = policy.retryStatuses.has(status);
159
+ }
160
+ else {
161
+ detail = result.timedOut
162
+ ? `timed out after ${policy.timeoutMs} ms`
163
+ : result.error instanceof Error
164
+ ? result.error.message
165
+ : String(result.error);
166
+ retryable = result.timedOut ? policy.retryOnTimeout : true;
167
+ }
168
+ const willRetry = retryable && attempt < policy.attempts;
169
+ console.warn(`[builder-upload] ${step} attempt ${attempt}/${policy.attempts} failed${willRetry ? ", retrying" : ""}`, { status: status || "no response", elapsedMs, traceId, headers });
170
+ if (!willRetry) {
171
+ throw new BuilderGatewayError(`${policy.label} (${status || "no response"}): ${detail}`, {
172
+ step,
173
+ status,
174
+ attempts: attempt,
175
+ isHtml,
176
+ traceIds,
177
+ headers,
178
+ cause: result.kind === "network" ? result.error : undefined,
179
+ });
180
+ }
181
+ // Jitter keeps a burst of retrying recordings from landing together.
182
+ const delayMs = policy.baseDelayMs * BACKOFF_FACTOR ** (attempt - 1);
183
+ await new Promise((resolve) => setTimeout(resolve, Math.round(delayMs * (0.75 + Math.random() * 0.5))));
184
+ }
185
+ }
@@ -1,3 +1,4 @@
1
+ import { builderGatewayRequest, describeErrorBody } from "./builder-gateway.js";
1
2
  const DEFAULT_BUILDER_APP_HOST = "https://builder.io";
2
3
  const LARGE_FILE_THRESHOLD_BYTES = 30 * 1024 * 1024;
3
4
  const UPLOAD_TIMEOUT_MS = 120_000;
@@ -44,8 +45,10 @@ function setRecordAssetQueryParam(url, recordAsset) {
44
45
  }
45
46
  async function assertOk(res, label) {
46
47
  if (!res.ok) {
47
- const body = await res.text().catch(() => "");
48
- throw Object.assign(new Error(`${label} (${res.status}): ${body || res.statusText}`), { status: res.status });
48
+ const { detail } = await describeErrorBody(res);
49
+ throw Object.assign(new Error(`${label} (${res.status}): ${detail}`), {
50
+ status: res.status,
51
+ });
49
52
  }
50
53
  }
51
54
  function confirmedBuilderAuthorizationFailure(error) {
@@ -116,7 +119,7 @@ async function requestBuilderSignedUrl(authorization, apiKey, filename, mimeType
116
119
  const url = new URL("/api/v1/upload/signed-url", host);
117
120
  if (apiKey)
118
121
  url.searchParams.set("apiKey", apiKey);
119
- const res = await fetchWithTimeout(url.toString(), {
122
+ const res = await builderGatewayRequest("signed_url", url.toString(), {
120
123
  method: "POST",
121
124
  headers: {
122
125
  Authorization: authorization,
@@ -129,7 +132,6 @@ async function requestBuilderSignedUrl(authorization, apiKey, filename, mimeType
129
132
  resumable,
130
133
  }),
131
134
  });
132
- await assertOk(res, "Builder.io signed-URL request failed");
133
135
  const json = (await res.json());
134
136
  if (!json.uploadUrl || !json.assetId || !json.requiredHeaders) {
135
137
  throw new Error(`Builder.io signed-URL response missing required fields: ${JSON.stringify(Object.keys(json))}`);
@@ -149,7 +151,7 @@ async function completeBuilderUpload(authorization, apiKey, assetId, filename, o
149
151
  setStableUrlQueryParam(url);
150
152
  }
151
153
  setRecordAssetQueryParam(url, options?.recordAsset);
152
- const res = await fetchWithTimeout(url.toString(), {
154
+ const res = await builderGatewayRequest("complete", url.toString(), {
153
155
  method: "POST",
154
156
  headers: {
155
157
  Authorization: authorization,
@@ -161,7 +163,6 @@ async function completeBuilderUpload(authorization, apiKey, assetId, filename, o
161
163
  ...(options?.recordAsset === false ? { record: false } : {}),
162
164
  }),
163
165
  });
164
- await assertOk(res, "Builder.io upload complete failed");
165
166
  const json = (await res.json());
166
167
  if (!json.url)
167
168
  throw new Error("Builder.io upload/complete returned no URL");
@@ -315,15 +316,11 @@ export const builderFileUploadProvider = {
315
316
  if (contentLengthRange)
316
317
  initHeaders["x-goog-content-length-range"] = contentLengthRange;
317
318
  console.log(`[builder-resumable] session step 2: initiating GCS session`);
318
- const initRes = await fetchWithTimeout(uploadUrl, {
319
+ const initRes = await builderGatewayRequest("session_start", uploadUrl, {
319
320
  method: "POST",
320
321
  headers: initHeaders,
321
322
  body: new Uint8Array(0),
322
323
  });
323
- if (!initRes.ok) {
324
- const body = await initRes.text().catch(() => "");
325
- throw new Error(`GCS resumable session initiation failed (${initRes.status}): ${body}`);
326
- }
327
324
  const sessionUri = initRes.headers.get("location");
328
325
  if (!sessionUri)
329
326
  throw new Error("GCS did not return a Location header for the resumable session");
@@ -1,6 +1,7 @@
1
1
  export type { FileUploadDeleteInput, FileUploadInput, FileUploadProvider, FileUploadResult, ResumableUploadSession, ResumableChunkResult, } from "./types.js";
2
2
  export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, deleteUploadedFile, uploadFile, } from "./registry.js";
3
3
  export { builderFileUploadProvider } from "./builder.js";
4
+ export { BuilderGatewayError, type BuilderGatewayErrorDetails, type BuilderGatewayStep, } from "./builder-gateway.js";
4
5
  export { getBuilderUploadCredentialStatus } from "./status.js";
5
6
  export { ensureS3FileUploadProvider, S3RequestError, s3FileUploadProvider, } from "./s3.js";
6
7
  export { cancelS3ResponseBody, fetchS3WithTimeout, presignS3GetUrl, readS3ErrorText, rfc3986, s3ObjectUri, sha256Hex, signS3Request, signedS3Request, trustedDeploymentS3Origin, type PresignS3GetOptions, type S3ClientConfig, type S3Method, type SignedS3RequestOptions, } from "./s3-client.js";
@@ -1,5 +1,6 @@
1
1
  export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, deleteUploadedFile, uploadFile, } from "./registry.js";
2
2
  export { builderFileUploadProvider } from "./builder.js";
3
+ export { BuilderGatewayError, } from "./builder-gateway.js";
3
4
  export { getBuilderUploadCredentialStatus } from "./status.js";
4
5
  export { ensureS3FileUploadProvider, S3RequestError, s3FileUploadProvider, } from "./s3.js";
5
6
  export { cancelS3ResponseBody, fetchS3WithTimeout, presignS3GetUrl, readS3ErrorText, rfc3986, s3ObjectUri, sha256Hex, signS3Request, signedS3Request, trustedDeploymentS3Origin, } from "./s3-client.js";
@@ -1,17 +1,17 @@
1
1
  {
2
- "@agent-native/agentkit": "0.228.2-nightly-20261113032548",
2
+ "@agent-native/agentkit": "0.228.2",
3
3
  "@agent-native/code-agents-ui": "0.1.75",
4
- "@agent-native/core": "0.228.2-nightly-20261113032548",
5
- "@agent-native/core-corpus": "0.228.2-nightly-20261113032548",
6
- "@agent-native/creative-context": "0.8.71",
7
- "@agent-native/dispatch": "0.42.7-nightly-20261113032548",
4
+ "@agent-native/core": "0.228.2",
5
+ "@agent-native/core-corpus": "0.228.2",
6
+ "@agent-native/creative-context": "0.8.72",
7
+ "@agent-native/dispatch": "0.42.7",
8
8
  "@agent-native/embedding": "0.2.0",
9
9
  "@agent-native/migrate": "0.0.7",
10
- "@agent-native/otel": "0.1.38",
11
- "@agent-native/pinpoint": "0.1.114",
12
- "@agent-native/recap-cli": "0.5.102",
13
- "@agent-native/scheduling": "0.2.58",
10
+ "@agent-native/otel": "0.1.39",
11
+ "@agent-native/pinpoint": "0.1.115",
12
+ "@agent-native/recap-cli": "0.5.103",
13
+ "@agent-native/scheduling": "0.2.59",
14
14
  "@agent-native/shared-app-config": "0.1.2",
15
- "@agent-native/skills": "0.4.7-nightly-20261113032548",
16
- "@agent-native/toolkit": "0.228.2-nightly-20261113032548"
15
+ "@agent-native/skills": "0.4.7",
16
+ "@agent-native/toolkit": "0.228.2"
17
17
  }
@@ -239,6 +239,8 @@ export const IDENTITY_REKEY_IGNORED_COLUMNS = new Set([
239
239
  "tool_history.actor_email",
240
240
  "tool_history.owner_email",
241
241
  "agent_resource_versions.created_by",
242
+ // Creator attribution on a Vault key, kept as written; the key is owned by its scope.
243
+ "app_secrets.created_by",
242
244
  "agent_review_comments.created_by",
243
245
  "agent_human_review_summaries.created_by",
244
246
  "organizations.created_by",