@agent-native/core 0.148.1 → 0.149.1

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 (165) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
  4. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
  5. package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
  6. package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
  8. package/corpus/templates/calendar/vitest.config.ts +6 -1
  9. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
  10. package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
  11. package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
  13. package/corpus/templates/content/actions/_property-utils.ts +11 -1
  14. package/corpus/templates/content/actions/view-screen.ts +62 -12
  15. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  16. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
  17. package/corpus/templates/design/AGENTS.md +3 -0
  18. package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
  19. package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
  20. package/corpus/templates/design/actions/get-design-template.ts +178 -0
  21. package/corpus/templates/design/actions/list-designs.ts +3 -1
  22. package/corpus/templates/design/actions/view-screen.ts +31 -0
  23. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
  24. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  25. package/corpus/templates/design/app/i18n-data.ts +78 -0
  26. package/corpus/templates/design/app/lib/design-authors.ts +64 -0
  27. package/corpus/templates/design/app/pages/Index.tsx +114 -4
  28. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
  29. package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
  30. package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
  31. package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
  32. package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
  33. package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
  34. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
  35. package/corpus/templates/forms/AGENTS.md +2 -2
  36. package/corpus/templates/forms/actions/create-form.ts +1 -1
  37. package/corpus/templates/forms/actions/update-form.ts +5 -5
  38. package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
  39. package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
  40. package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
  41. package/corpus/templates/slides/AGENTS.md +4 -3
  42. package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
  43. package/corpus/templates/slides/actions/patch-deck.ts +18 -2
  44. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  45. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  46. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
  48. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
  49. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
  50. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
  51. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
  52. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  53. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
  54. package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
  57. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  58. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
  59. package/corpus/templates/slides/app/pages/Index.tsx +124 -135
  60. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
  72. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
  73. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  74. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  75. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  76. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
  77. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  78. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  79. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  80. package/dist/agent/engine/registry.d.ts +8 -2
  81. package/dist/agent/engine/registry.js +63 -29
  82. package/dist/agent/production-agent.js +7 -0
  83. package/dist/agent/run-manager.d.ts +5 -4
  84. package/dist/agent/run-manager.js +6 -5
  85. package/dist/cli/claude-code-participant.d.ts +2 -1
  86. package/dist/cli/claude-code-participant.js +19 -0
  87. package/dist/cli/code-agent-executor.js +223 -0
  88. package/dist/cli/create-workspace.js +3 -3
  89. package/dist/cli/create.d.ts +3 -2
  90. package/dist/cli/create.js +20 -24
  91. package/dist/client/AgentPanel.js +8 -4
  92. package/dist/client/AssistantChat.js +34 -1
  93. package/dist/client/agent-chat-adapter.d.ts +1 -0
  94. package/dist/client/agent-chat-adapter.js +10 -2
  95. package/dist/client/chat/markdown-renderer.d.ts +1 -0
  96. package/dist/client/chat/markdown-renderer.js +4 -0
  97. package/dist/client/chat/message-components.d.ts +1 -0
  98. package/dist/client/chat/message-components.js +9 -1
  99. package/dist/client/chat/tool-call-display.d.ts +8 -0
  100. package/dist/client/chat/tool-call-display.js +30 -21
  101. package/dist/client/chat-first/app-pane.js +1 -1
  102. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  103. package/dist/client/chat-first/apps-rail.js +11 -11
  104. package/dist/client/chat-first/chat-history.d.ts +1 -1
  105. package/dist/client/chat-first/chat-history.js +2 -0
  106. package/dist/client/chat-first/primary-nav.js +1 -1
  107. package/dist/client/progress/RunsTray.js +14 -1
  108. package/dist/client/resources/McpConnectionSuggestion.js +2 -2
  109. package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
  110. package/dist/client/settings/SettingsTabsPage.js +2 -2
  111. package/dist/client/sse-event-processor.d.ts +8 -0
  112. package/dist/client/sse-event-processor.js +13 -6
  113. package/dist/collab/awareness.d.ts +2 -2
  114. package/dist/collab/routes.d.ts +1 -1
  115. package/dist/deploy/build.d.ts +5 -7
  116. package/dist/deploy/build.js +19 -12
  117. package/dist/localization/default-messages.js +1 -1
  118. package/dist/notifications/routes.d.ts +3 -3
  119. package/dist/observability/routes.d.ts +6 -6
  120. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  121. package/dist/scripts/call-agent.js +3 -3
  122. package/dist/scripts/describe-workspace-apps.js +1 -1
  123. package/dist/server/agent-capabilities.js +0 -0
  124. package/dist/server/agent-chat/context-tools.js +1 -1
  125. package/dist/server/agent-chat/prompt-resources.js +1 -1
  126. package/dist/server/agent-chat-plugin.js +1 -1
  127. package/dist/server/auth.js +190 -42
  128. package/dist/server/better-auth-migrations.js +53 -0
  129. package/dist/server/http-response-telemetry.js +7 -1
  130. package/dist/server/onboarding-html.js +119 -33
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/shared/password-policy.d.ts +2 -1
  133. package/dist/shared/password-policy.js +2 -1
  134. package/dist/styles/agent-conversation.css +20 -3
  135. package/dist/styles/agent-native.css +14 -2
  136. package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
  137. package/dist/templates/default/app/i18n/de-DE.ts +1 -1
  138. package/dist/templates/default/app/i18n/en-US.ts +1 -1
  139. package/dist/templates/default/app/i18n/es-ES.ts +1 -1
  140. package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
  141. package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
  142. package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
  143. package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
  144. package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
  145. package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
  146. package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
  147. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  148. package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
  149. package/docs/content/cloneable-saas.mdx +6 -6
  150. package/docs/content/deployment.mdx +10 -8
  151. package/docs/content/environment-variables.mdx +4 -3
  152. package/package.json +2 -2
  153. package/src/templates/default/app/i18n/ar-SA.ts +1 -1
  154. package/src/templates/default/app/i18n/de-DE.ts +1 -1
  155. package/src/templates/default/app/i18n/en-US.ts +1 -1
  156. package/src/templates/default/app/i18n/es-ES.ts +1 -1
  157. package/src/templates/default/app/i18n/fr-FR.ts +1 -1
  158. package/src/templates/default/app/i18n/hi-IN.ts +1 -1
  159. package/src/templates/default/app/i18n/ja-JP.ts +1 -1
  160. package/src/templates/default/app/i18n/ko-KR.ts +1 -1
  161. package/src/templates/default/app/i18n/pt-BR.ts +1 -1
  162. package/src/templates/default/app/i18n/zh-CN.ts +1 -1
  163. package/src/templates/default/app/i18n/zh-TW.ts +1 -1
  164. package/src/templates/default/pnpm-workspace.yaml +1 -0
  165. package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,192 @@
1
+ import net from "node:net";
2
+
3
+ /**
4
+ * Hostnames that never belong to a legitimate slide image but are classic
5
+ * SSRF targets. Cloud metadata endpoints resolve to a link-local address
6
+ * that `isPrivateAddress` already rejects; they are listed here so the
7
+ * request is refused before a DNS query is issued.
8
+ */
9
+ const BLOCKED_HOSTNAMES = new Set([
10
+ "localhost",
11
+ "metadata.google.internal",
12
+ "metadata",
13
+ ]);
14
+
15
+ const BLOCKED_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];
16
+
17
+ /** Largest image we are willing to buffer and hand back to the browser. */
18
+ export const MAX_PROXIED_IMAGE_BYTES = 15 * 1024 * 1024;
19
+
20
+ /** Redirect hops to follow. Each hop is re-validated before it is fetched. */
21
+ export const MAX_PROXY_REDIRECTS = 3;
22
+
23
+ /**
24
+ * Whether an IP literal points somewhere inside our own infrastructure.
25
+ * Anything unroutable, loopback, link-local, or RFC1918 is refused, as is
26
+ * any address we cannot parse — unknown means unsafe here.
27
+ */
28
+ export function isPrivateAddress(address: string): boolean {
29
+ const version = net.isIP(address);
30
+
31
+ if (version === 4) {
32
+ const parts = address.split(".").map((part) => Number(part));
33
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
34
+ return true;
35
+ }
36
+ const [a, b] = parts;
37
+ if (a === 0 || a === 10 || a === 127) return true;
38
+ if (a === 169 && b === 254) return true;
39
+ if (a === 172 && b >= 16 && b <= 31) return true;
40
+ if (a === 192 && b === 168) return true;
41
+ if (a === 100 && b >= 64 && b <= 127) return true;
42
+ if (a >= 224) return true;
43
+ return false;
44
+ }
45
+
46
+ if (version === 6) {
47
+ // Classify on the expanded hextets, never on the text. `::ffff:127.0.0.1`
48
+ // and `::ffff:7f00:1` are the same address, and a prefix-matching check
49
+ // sees only the first.
50
+ const hextets = expandIpv6(address);
51
+ if (!hextets) return true;
52
+
53
+ const embedsIpv4 =
54
+ hextets.slice(0, 5).every((part) => part === 0) &&
55
+ (hextets[5] === 0xffff || hextets[5] === 0);
56
+ if (embedsIpv4 && (hextets[6] !== 0 || hextets[7] !== 0)) {
57
+ const ipv4 = [
58
+ hextets[6] >> 8,
59
+ hextets[6] & 0xff,
60
+ hextets[7] >> 8,
61
+ hextets[7] & 0xff,
62
+ ].join(".");
63
+ return isPrivateAddress(ipv4);
64
+ }
65
+
66
+ // Unspecified (::) and loopback (::1).
67
+ if (hextets.every((part) => part === 0)) return true;
68
+ if (hextets.slice(0, 7).every((part) => part === 0) && hextets[7] === 1) {
69
+ return true;
70
+ }
71
+ // Unique-local fc00::/7, link-local fe80::/10, multicast ff00::/8.
72
+ if ((hextets[0] & 0xfe00) === 0xfc00) return true;
73
+ if ((hextets[0] & 0xffc0) === 0xfe80) return true;
74
+ if ((hextets[0] & 0xff00) === 0xff00) return true;
75
+ // NAT64 well-known prefix 64:ff9b::/96 tunnels an IPv4 destination.
76
+ if (hextets[0] === 0x0064 && hextets[1] === 0xff9b) {
77
+ const ipv4 = [
78
+ hextets[6] >> 8,
79
+ hextets[6] & 0xff,
80
+ hextets[7] >> 8,
81
+ hextets[7] & 0xff,
82
+ ].join(".");
83
+ return isPrivateAddress(ipv4);
84
+ }
85
+ return false;
86
+ }
87
+
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Expand any textual IPv6 form into its 8 hextets, including `::` elision and
93
+ * a dotted IPv4 tail. Returns null when the input is not parseable.
94
+ */
95
+ export function expandIpv6(address: string): number[] | null {
96
+ // Zone indices ("fe80::1%eth0") are routing hints, not part of the address.
97
+ const bare = address.toLowerCase().split("%")[0];
98
+ if (!bare) return null;
99
+
100
+ let head = bare;
101
+ let tail = "";
102
+ const elision = bare.indexOf("::");
103
+ if (elision !== -1) {
104
+ head = bare.slice(0, elision);
105
+ tail = bare.slice(elision + 2);
106
+ }
107
+
108
+ const parseGroups = (segment: string): string[] =>
109
+ segment.length === 0 ? [] : segment.split(":");
110
+
111
+ const headGroups = parseGroups(head);
112
+ const tailGroups = parseGroups(tail);
113
+ const all = [...headGroups, ...tailGroups];
114
+
115
+ // A dotted IPv4 tail occupies the final two hextets.
116
+ const last = all[all.length - 1];
117
+ let ipv4Tail: number[] | null = null;
118
+ if (last?.includes(".")) {
119
+ if (net.isIP(last) !== 4) return null;
120
+ const octets = last.split(".").map((part) => Number(part));
121
+ if (
122
+ octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
123
+ )
124
+ return null;
125
+ ipv4Tail = [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
126
+ if (tailGroups.length > 0) tailGroups.pop();
127
+ else headGroups.pop();
128
+ }
129
+
130
+ const toHextets = (groups: string[]): number[] | null => {
131
+ const out: number[] = [];
132
+ for (const group of groups) {
133
+ if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
134
+ out.push(parseInt(group, 16));
135
+ }
136
+ return out;
137
+ };
138
+
139
+ const headParts = toHextets(headGroups);
140
+ const tailParts = toHextets(tailGroups);
141
+ if (!headParts || !tailParts) return null;
142
+
143
+ const explicit = [...headParts, ...tailParts, ...(ipv4Tail ? ipv4Tail : [])]
144
+ .length;
145
+ if (explicit > 8) return null;
146
+
147
+ if (elision === -1) {
148
+ if (explicit !== 8) return null;
149
+ return [...headParts, ...(ipv4Tail ?? [])];
150
+ }
151
+
152
+ const fill = new Array(8 - explicit).fill(0) as number[];
153
+ return [...headParts, ...fill, ...tailParts, ...(ipv4Tail ?? [])];
154
+ }
155
+
156
+ /**
157
+ * Parse a caller-supplied image URL, refusing anything that is not a plain
158
+ * public http(s) resource. Returns null rather than throwing so callers can
159
+ * answer with a single 400.
160
+ */
161
+ export function parseProxyableImageUrl(raw: string): URL | null {
162
+ if (!raw) return null;
163
+
164
+ let url: URL;
165
+ try {
166
+ url = new URL(raw);
167
+ } catch {
168
+ // coercion-ok: null is this function's documented "refused" result and
169
+ // the caller answers 400; an unparseable URL carries no other detail.
170
+ return null;
171
+ }
172
+
173
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
174
+ // Credentials in the URL would be replayed by the server on the user's
175
+ // behalf against a host they may not control.
176
+ if (url.username || url.password) return null;
177
+
178
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
179
+ if (!host) return null;
180
+ if (BLOCKED_HOSTNAMES.has(host)) return null;
181
+ if (BLOCKED_SUFFIXES.some((suffix) => host.endsWith(suffix))) return null;
182
+
183
+ const literal = host.startsWith("[") ? host.slice(1, -1) : host;
184
+ if (net.isIP(literal) && isPrivateAddress(literal)) return null;
185
+
186
+ return url;
187
+ }
188
+
189
+ // A hostname is no longer validated here before fetching. Resolving once for
190
+ // the check and again for the connection let a DNS-rebinding host answer
191
+ // differently each time, so the address check now lives in the socket's
192
+ // `lookup` — see `publicOnlyLookup` in fetch-remote-image.ts.
@@ -66,7 +66,7 @@ Image workflow is strict. For direct insertion, call generate-image-api with ins
66
66
 
67
67
  Provider-specific Slides actions are shortcuts, not limits. If a first-class action cannot express the exact Google Drive endpoint, file metadata field, export format, query, request body, pagination mode, payload shape, or API version needed, call provider-api-catalog and provider-api-docs as needed, then call provider-api-request against the real provider API. Use the raw provider API escape hatch instead of weakening the answer or claiming Slides cannot do something the underlying Google Drive API can do.
68
68
 
69
- Slides' Google Drive provider API uses the user's connected Google Docs OAuth account. The drive.file scope is intentionally limited to files the user selected or the app created. For large Drive file lists or metadata sweeps, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.
69
+ Slides' Google Drive provider API uses the user's connected Google Docs OAuth account. Picker imports keep the per-file drive.file path, while pasted Google Slides links use Drive export access and may ask the user to reconnect Google. For large Drive file lists or metadata sweeps, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.
70
70
 
71
71
  When a Google Drive or Google Slides request needs authentication, tell the user to use the Connect Google button shown in Slides. Do not expose internal routes, API endpoints, OAuth setup instructions, client IDs, or keys in the response, and do not ask the user to configure an API manually.`,
72
72
  mentionProviders: async () => {
@@ -0,0 +1,65 @@
1
+ import { getSession } from "@agent-native/core/server";
2
+ import { defineEventHandler, getQuery, setResponseStatus } from "h3";
3
+
4
+ import {
5
+ fetchRemoteImage,
6
+ type RemoteImageFailure,
7
+ } from "../../lib/fetch-remote-image.js";
8
+
9
+ /**
10
+ * Re-serve a remote image from our own origin.
11
+ *
12
+ * PDF/PPTX export rasterizes the slide DOM through a canvas, and the browser
13
+ * blanks out any image whose host does not send `Access-Control-Allow-Origin`.
14
+ * No client-side flag can override that, so images on hosts without CORS have
15
+ * to come back through us to be same-origin.
16
+ *
17
+ * This is an authenticated, image-only, size-capped fetcher — not a general
18
+ * proxy. See `fetch-remote-image.ts` for the address pinning that keeps it
19
+ * from being turned into an SSRF primitive.
20
+ */
21
+ const FAILURE_STATUS: Record<RemoteImageFailure, number> = {
22
+ "unsupported-url": 400,
23
+ "blocked-address": 400,
24
+ "fetch-failed": 502,
25
+ "too-many-redirects": 502,
26
+ "not-an-image": 415,
27
+ "too-large": 413,
28
+ };
29
+
30
+ const FAILURE_MESSAGE: Record<RemoteImageFailure, string> = {
31
+ "unsupported-url": "Unsupported image URL",
32
+ "blocked-address": "Unsupported image URL",
33
+ "fetch-failed": "Could not fetch image",
34
+ "too-many-redirects": "Too many redirects",
35
+ "not-an-image": "Not an image",
36
+ "too-large": "Image too large",
37
+ };
38
+
39
+ export default defineEventHandler(async (event) => {
40
+ const session = await getSession(event);
41
+ if (!session?.email) {
42
+ setResponseStatus(event, 401);
43
+ return { error: "Unauthorized" };
44
+ }
45
+
46
+ const raw = getQuery(event).url;
47
+ if (typeof raw !== "string") {
48
+ setResponseStatus(event, 400);
49
+ return { error: "Missing url" };
50
+ }
51
+
52
+ const result = await fetchRemoteImage(raw);
53
+ if (!result.ok) {
54
+ setResponseStatus(event, FAILURE_STATUS[result.reason]);
55
+ return { error: FAILURE_MESSAGE[result.reason] };
56
+ }
57
+
58
+ event.node?.res?.setHeader("Content-Type", result.contentType);
59
+ event.node?.res?.setHeader("Content-Length", String(result.body.byteLength));
60
+ event.node?.res?.setHeader("Cache-Control", "private, max-age=3600");
61
+ // The canvas reads these pixels back, so the response must be explicitly
62
+ // usable cross-origin even though it is served from our own host.
63
+ event.node?.res?.setHeader("Access-Control-Allow-Origin", "*");
64
+ return result.body;
65
+ });
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Built-in engines (anthropic, ai-sdk) are auto-registered by builtin.ts.
9
9
  */
10
+ import { type BuilderCredentialLookupIdentity } from "../../server/credential-provider.js";
10
11
  import type { AgentEngine, EngineCapabilities } from "./types.js";
11
12
  export interface AgentEngineEntry {
12
13
  /** Unique name, e.g. "anthropic", "ai-sdk:anthropic", "ai-sdk:openai" */
@@ -131,7 +132,7 @@ export declare function detectEngineFromEnvForRequest(): Promise<AgentEngineEntr
131
132
  * `/builder/status` resolves them via the same request org context, and the
132
133
  * chat engine picker must not disagree with that card.
133
134
  */
134
- export declare function detectEngineFromUserSecrets(): Promise<AgentEngineEntry | null>;
135
+ export declare function detectEngineFromUserSecrets(identity?: BuilderCredentialLookupIdentity): Promise<AgentEngineEntry | null>;
135
136
  /**
136
137
  * Legacy inline API keys on the global `agent-engine` settings row are
137
138
  * intentionally ignored. That row is deployment-wide, so treating
@@ -158,7 +159,9 @@ export declare function isStoredEngineUsable(stored: unknown, entry: AgentEngine
158
159
  * deploy env vars; this async helper is what request-time routes should use
159
160
  * when deciding whether a stored engine can actually run for the current user.
160
161
  */
161
- export declare function isStoredEngineUsableForRequest(stored: unknown, entry: AgentEngineEntry): Promise<boolean>;
162
+ export declare function isStoredEngineUsableForRequest(stored: unknown, entry: AgentEngineEntry, options?: {
163
+ credentialIdentity?: BuilderCredentialLookupIdentity;
164
+ }): Promise<boolean>;
162
165
  /**
163
166
  * Request-aware credential preflight for an already-resolved engine instance.
164
167
  * `resolveEngine()` may still return a default or explicitly requested engine
@@ -167,6 +170,7 @@ export declare function isStoredEngineUsableForRequest(stored: unknown, entry: A
167
170
  */
168
171
  export declare function isResolvedEngineUsableForRequest(engine: AgentEngine, options?: {
169
172
  apiKey?: string;
173
+ credentialIdentity?: BuilderCredentialLookupIdentity;
170
174
  }): Promise<boolean>;
171
175
  export interface ResolveEngineConfig {
172
176
  /** Explicit engine name or instance from createAgentChatPlugin options */
@@ -186,6 +190,8 @@ export interface ResolveEngineConfig {
186
190
  model?: string;
187
191
  /** App/template id used for org-scoped per-app model defaults. */
188
192
  appId?: string;
193
+ /** Verified owner identity for request-scoped Builder credential capture. */
194
+ credentialIdentity?: BuilderCredentialLookupIdentity;
189
195
  }
190
196
  /**
191
197
  * Engine name a caller explicitly selected, when {@link resolveEngine} will
@@ -401,20 +401,26 @@ function shouldTraceEngineDetection() {
401
401
  * `/builder/status` resolves them via the same request org context, and the
402
402
  * chat engine picker must not disagree with that card.
403
403
  */
404
- export async function detectEngineFromUserSecrets() {
404
+ export async function detectEngineFromUserSecrets(identity) {
405
405
  const traceLookup = shouldTraceEngineDetection();
406
- let email;
407
- let orgId;
406
+ let email = identity?.userEmail?.trim() || undefined;
407
+ let orgId = identity?.orgId;
408
408
  try {
409
409
  const { getRequestUserEmail, getRequestOrgId } = await import("../../server/request-context.js");
410
- email = getRequestUserEmail();
411
- orgId = getRequestOrgId();
410
+ email ??= getRequestUserEmail();
411
+ if (orgId === undefined)
412
+ orgId = getRequestOrgId();
412
413
  }
413
414
  catch {
415
+ if (!email) {
416
+ if (traceLookup) {
417
+ console.log(`[engine-detect] result=null reason=no-request-context email=(unknown) orgId=(unknown)`);
418
+ }
419
+ return null;
420
+ }
414
421
  if (traceLookup) {
415
- console.log(`[engine-detect] result=null reason=no-request-context email=(unknown) orgId=(unknown)`);
422
+ console.log(`[engine-detect] request context unavailable; using explicit identity email=${email} orgId=${orgId ?? "(none)"}`);
416
423
  }
417
- return null;
418
424
  }
419
425
  if (!email) {
420
426
  if (traceLookup) {
@@ -427,8 +433,9 @@ export async function detectEngineFromUserSecrets() {
427
433
  return false;
428
434
  if (entry.requiredEnvVars.length === 0)
429
435
  return false;
430
- if (entry.name === "builder")
431
- return hasUsableBuilderConnection();
436
+ if (entry.name === "builder") {
437
+ return hasUsableBuilderConnection(identity);
438
+ }
432
439
  for (const key of entry.requiredEnvVars) {
433
440
  // A throw here means the credential store could not be read. Let it
434
441
  // propagate: swallowing it reports "no provider connected" to a user
@@ -520,8 +527,8 @@ async function resolveProviderBaseUrl(envVar) {
520
527
  * keeps that distinction instead of reporting "connect a provider" to a user
521
528
  * whose org-shared keys exist but were unreadable.
522
529
  */
523
- async function hasUsableBuilderConnection() {
524
- const creds = await resolveBuilderCredentialsDetailed();
530
+ async function hasUsableBuilderConnection(identity) {
531
+ const creds = await resolveBuilderCredentialsDetailed(identity);
525
532
  assertCredentialStoreReadable(creds);
526
533
  return Boolean(creds.privateKey && creds.publicKey);
527
534
  }
@@ -567,7 +574,7 @@ async function apiKeyBelongsToDifferentProvider(apiKey, selectedEntry) {
567
574
  }
568
575
  return false;
569
576
  }
570
- async function engineCreateConfigForEntry(entry, apiKey, extra, preferResolvedCredential = false, apiKeyEnvVar) {
577
+ async function engineCreateConfigForEntry(entry, apiKey, extra, preferResolvedCredential = false, apiKeyEnvVar, credentialIdentity) {
571
578
  const safeExtra = { ...(extra ?? {}) };
572
579
  let matchingApiKey = apiKey;
573
580
  // A declared provenance settles the question without inspecting values: a
@@ -632,6 +639,25 @@ async function engineCreateConfigForEntry(entry, apiKey, extra, preferResolvedCr
632
639
  safeExtra.baseUrl = baseUrl;
633
640
  }
634
641
  }
642
+ if (entry.name === "builder" &&
643
+ (credentialIdentity !== undefined || safeExtra.credentials == null)) {
644
+ // Builder authentication is a private/public key pair, not the single
645
+ // provider key carried by ResolveEngineConfig. Capture the scoped pair
646
+ // while the verified request identity is available so a later stream or
647
+ // detached run cannot resolve credentials from the wrong ambient context.
648
+ const creds = await resolveBuilderCredentialsDetailed(credentialIdentity);
649
+ assertCredentialStoreReadable(creds);
650
+ if (credentialIdentity !== undefined ||
651
+ creds.source !== null ||
652
+ creds.lookupFailed) {
653
+ safeExtra.credentials = {
654
+ privateKey: creds.privateKey,
655
+ publicKey: creds.publicKey,
656
+ userId: creds.userId,
657
+ orgName: creds.orgName,
658
+ };
659
+ }
660
+ }
635
661
  return engineCreateConfig(entry, matchingApiKey, safeExtra);
636
662
  }
637
663
  /**
@@ -661,15 +687,16 @@ export function isStoredEngineUsable(stored, entry) {
661
687
  * deploy env vars; this async helper is what request-time routes should use
662
688
  * when deciding whether a stored engine can actually run for the current user.
663
689
  */
664
- export async function isStoredEngineUsableForRequest(stored, entry) {
690
+ export async function isStoredEngineUsableForRequest(stored, entry, options = {}) {
665
691
  if (!isAgentEnginePackageInstalled(entry))
666
692
  return false;
667
693
  if (isAgentEngineSettingConfigured(stored))
668
694
  return true;
669
695
  if (entry.requiredEnvVars.length === 0)
670
696
  return true;
671
- if (entry.name === "builder")
672
- return hasUsableBuilderConnection();
697
+ if (entry.name === "builder") {
698
+ return hasUsableBuilderConnection(options.credentialIdentity);
699
+ }
673
700
  for (const key of entry.requiredEnvVars) {
674
701
  if (!(await resolveUsableProviderSecret(key)))
675
702
  return false;
@@ -692,8 +719,9 @@ export async function isResolvedEngineUsableForRequest(engine, options = {}) {
692
719
  return false;
693
720
  if (entry.requiredEnvVars.length === 0)
694
721
  return true;
695
- if (entry.name === "builder")
696
- return hasUsableBuilderConnection();
722
+ if (entry.name === "builder") {
723
+ return hasUsableBuilderConnection(options.credentialIdentity);
724
+ }
697
725
  if (options.apiKey?.trim()) {
698
726
  const key = entry.requiredEnvVars[0];
699
727
  if (!key)
@@ -776,7 +804,7 @@ export async function getConfiguredEngineNameForRequest(options = {}) {
776
804
  * 7. Default "anthropic" (requires ANTHROPIC_API_KEY)
777
805
  */
778
806
  export async function resolveEngine(config) {
779
- const { engineOption, apiKey, apiKeyEnvVar, model: _model, appId } = config;
807
+ const { engineOption, apiKey, apiKeyEnvVar, model: _model, appId, credentialIdentity, } = config;
780
808
  // 1. Explicit instance passed directly
781
809
  if (engineOption &&
782
810
  typeof engineOption === "object" &&
@@ -792,7 +820,7 @@ export async function resolveEngine(config) {
792
820
  if (!entry)
793
821
  throw new Error(`[agent-engine] Unknown engine: "${name}". Registered: ${[..._registry.keys()].join(", ")}`);
794
822
  assertAgentEnginePackageInstalled(entry);
795
- return entry.create(await engineCreateConfigForEntry(entry, apiKey, engineConfig, false, apiKeyEnvVar));
823
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey, engineConfig, false, apiKeyEnvVar, credentialIdentity));
796
824
  }
797
825
  // 3. Explicit string name from options
798
826
  if (typeof engineOption === "string") {
@@ -800,7 +828,7 @@ export async function resolveEngine(config) {
800
828
  if (!entry)
801
829
  throw new Error(`[agent-engine] Unknown engine: "${engineOption}". Registered: ${[..._registry.keys()].join(", ")}`);
802
830
  assertAgentEnginePackageInstalled(entry);
803
- return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, false, apiKeyEnvVar));
831
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, false, apiKeyEnvVar, credentialIdentity));
804
832
  }
805
833
  // 4. Env var — explicit engine name override
806
834
  const envEngine = process.env.AGENT_ENGINE;
@@ -808,14 +836,17 @@ export async function resolveEngine(config) {
808
836
  const entry = _registry.get(envEngine);
809
837
  if (entry) {
810
838
  assertAgentEnginePackageInstalled(entry);
811
- return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, true, apiKeyEnvVar));
839
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, true, apiKeyEnvVar, credentialIdentity));
812
840
  }
813
841
  }
814
842
  const appDefault = await getAgentAppModelDefaultForCurrentRequest(appId);
815
843
  if (appDefault?.engine) {
816
844
  const entry = _registry.get(appDefault.engine);
817
- if (entry && (await isStoredEngineUsableForRequest(appDefault, entry))) {
818
- return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, true, apiKeyEnvVar));
845
+ if (entry &&
846
+ (await isStoredEngineUsableForRequest(appDefault, entry, {
847
+ credentialIdentity,
848
+ }))) {
849
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey, undefined, true, apiKeyEnvVar, credentialIdentity));
819
850
  }
820
851
  }
821
852
  let stored = null;
@@ -829,7 +860,7 @@ export async function resolveEngine(config) {
829
860
  // (Builder OAuth callback + "paste your own key" settings flow write here,
830
861
  // not env). Stored/app defaults are checked first so an explicit provider
831
862
  // selection can override a connected Builder account.
832
- const detectedFromUser = await detectEngineFromUserSecrets();
863
+ const detectedFromUser = await detectEngineFromUserSecrets(credentialIdentity);
833
864
  // 6. Settings store — only when the stored row's API key is reachable.
834
865
  // This explicit selection beats automatic Builder detection so users can
835
866
  // switch away from Builder credits by saving/applying their own provider key.
@@ -838,26 +869,29 @@ export async function resolveEngine(config) {
838
869
  const storedConfig = storedRaw?.config;
839
870
  if (storedRaw && typeof storedEngine === "string") {
840
871
  const entry = _registry.get(storedEngine);
841
- if (entry && (await isStoredEngineUsableForRequest(storedRaw, entry))) {
842
- return entry.create(await engineCreateConfigForEntry(entry, apiKey, stripInlineApiKeyConfig(storedConfig), true, apiKeyEnvVar));
872
+ if (entry &&
873
+ (await isStoredEngineUsableForRequest(storedRaw, entry, {
874
+ credentialIdentity,
875
+ }))) {
876
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey, stripInlineApiKeyConfig(storedConfig), true, apiKeyEnvVar, credentialIdentity));
843
877
  }
844
878
  }
845
879
  if (detectedFromUser) {
846
- return detectedFromUser.create(await engineCreateConfigForEntry(detectedFromUser, apiKey, undefined, true, apiKeyEnvVar));
880
+ return detectedFromUser.create(await engineCreateConfigForEntry(detectedFromUser, apiKey, undefined, true, apiKeyEnvVar, credentialIdentity));
847
881
  }
848
882
  // 8. Auto-detect from any provider env var — so just dropping a key in
849
883
  // .env works without also setting AGENT_ENGINE. Skip keys with active
850
884
  // auth-failure markers so a rejected deploy key cannot permanently win.
851
885
  const detected = await detectEngineFromEnvForRequest();
852
886
  if (detected) {
853
- return detected.create(await engineCreateConfigForEntry(detected, apiKey, undefined, true, apiKeyEnvVar));
887
+ return detected.create(await engineCreateConfigForEntry(detected, apiKey, undefined, true, apiKeyEnvVar, credentialIdentity));
854
888
  }
855
889
  // 9. Default: anthropic
856
890
  const anthropicEntry = _registry.get("anthropic");
857
891
  if (!anthropicEntry) {
858
892
  throw new Error("[agent-engine] Default Anthropic engine is not registered. Did builtin.ts fail to load?");
859
893
  }
860
- return anthropicEntry.create(await engineCreateConfigForEntry(anthropicEntry, apiKey, undefined, true, apiKeyEnvVar));
894
+ return anthropicEntry.create(await engineCreateConfigForEntry(anthropicEntry, apiKey, undefined, true, apiKeyEnvVar, credentialIdentity));
861
895
  }
862
896
  /**
863
897
  * Read the user-selected model for an engine from the `agent-engine` setting.
@@ -5978,6 +5978,10 @@ export function createProductionAgentHandler(options) {
5978
5978
  // DIAGNOSTIC-ONLY: bracket engine resolution (Builder credential / app-default
5979
5979
  // settings reads inside resolveEngine).
5980
5980
  workerStep("engine_start");
5981
+ const credentialIdentity = {
5982
+ userEmail: ownerEmail,
5983
+ orgId: getRequestOrgId(),
5984
+ };
5981
5985
  let engine;
5982
5986
  try {
5983
5987
  engine = await resolveEngine({
@@ -5986,6 +5990,7 @@ export function createProductionAgentHandler(options) {
5986
5990
  apiKeyEnvVar: effectiveApiKeyEnvVar,
5987
5991
  model: configuredModel,
5988
5992
  appId: options.appId,
5993
+ credentialIdentity,
5989
5994
  });
5990
5995
  }
5991
5996
  catch {
@@ -5993,6 +5998,7 @@ export function createProductionAgentHandler(options) {
5993
5998
  apiKey: effectiveApiKey,
5994
5999
  apiKeyEnvVar: effectiveApiKeyEnvVar,
5995
6000
  appId: options.appId,
6001
+ credentialIdentity,
5996
6002
  });
5997
6003
  }
5998
6004
  // DIAGNOSTIC-ONLY: engine resolution finished.
@@ -6056,6 +6062,7 @@ export function createProductionAgentHandler(options) {
6056
6062
  console.log(`[agent-chat] resolved engine=${engine.name} model=${effectiveModel} requestModel=${requestModel ?? "(none)"} requestEngine=${requestEngine ?? "(none)"} modelSource=${modelSelectionSource} turnId=${requestTurnId ?? "(none)"}`);
6057
6063
  if (!(await isResolvedEngineUsableForRequest(engine, {
6058
6064
  apiKey: effectiveApiKey,
6065
+ credentialIdentity,
6059
6066
  }))) {
6060
6067
  setResponseHeader(event, "Content-Type", "text/event-stream");
6061
6068
  setResponseHeader(event, "Cache-Control", "no-cache");
@@ -164,10 +164,11 @@ export declare const DEFAULT_COMPLETED_RUN_RETENTION_MS: number;
164
164
  */
165
165
  export declare const DEFAULT_ERRORED_RUN_RETENTION_MS: number;
166
166
  /**
167
- * How recently a terminal run must have started for `/runs/active` to surface
168
- * it. Reconnect after this window won't replay the run — typical real-world
169
- * disconnects resolve in seconds, so 10 minutes is generous while keeping us
170
- * from resurrecting ancient turns when the user reopens an old thread.
167
+ * How recently a terminal run must have completed for `/runs/active` to surface
168
+ * it. This must outlast the durable browser watchdog (13 minutes) and the
169
+ * in-flight reader watchdog (15 minutes) so a quiet event stream can detach
170
+ * and still replay a run that completed while the browser was waiting.
171
+ * Reconnect after this window won't replay the run.
171
172
  */
172
173
  export declare const TERMINAL_RUN_RECONNECT_WINDOW_MS: number;
173
174
  /** Fast poll cadence while a SQL-backed SSE subscription is actively receiving rows. */
@@ -180,12 +180,13 @@ export const DEFAULT_COMPLETED_RUN_RETENTION_MS = 24 * 60 * 60 * 1000;
180
180
  */
181
181
  export const DEFAULT_ERRORED_RUN_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
182
182
  /**
183
- * How recently a terminal run must have started for `/runs/active` to surface
184
- * it. Reconnect after this window won't replay the run — typical real-world
185
- * disconnects resolve in seconds, so 10 minutes is generous while keeping us
186
- * from resurrecting ancient turns when the user reopens an old thread.
183
+ * How recently a terminal run must have completed for `/runs/active` to surface
184
+ * it. This must outlast the durable browser watchdog (13 minutes) and the
185
+ * in-flight reader watchdog (15 minutes) so a quiet event stream can detach
186
+ * and still replay a run that completed while the browser was waiting.
187
+ * Reconnect after this window won't replay the run.
187
188
  */
188
- export const TERMINAL_RUN_RECONNECT_WINDOW_MS = 10 * 60 * 1000;
189
+ export const TERMINAL_RUN_RECONNECT_WINDOW_MS = 20 * 60 * 1000;
189
190
  /** Fast poll cadence while a SQL-backed SSE subscription is actively receiving rows. */
190
191
  export const SQL_SUBSCRIPTION_ACTIVE_POLL_MS = 125;
191
192
  /** Baseline SQL-backed SSE poll cadence when the run is idle. */
@@ -42,6 +42,7 @@ export interface RunClaudeCodeParticipantOptions {
42
42
  prompt: string;
43
43
  cwd: string;
44
44
  model?: string;
45
+ effort?: string;
45
46
  session?: ClaudeCodeParticipantSession;
46
47
  /** Either the fixed CLI name or an absolute executable path for packaged apps. */
47
48
  command?: string;
@@ -71,6 +72,6 @@ export interface ClaudeCodeSubscriptionStatusOptions {
71
72
  }
72
73
  export declare function readClaudeCodeSubscriptionStatus(options?: ClaudeCodeSubscriptionStatusOptions): Promise<ClaudeCodeSubscriptionStatus>;
73
74
  export declare function runClaudeCodeParticipant(options: RunClaudeCodeParticipantOptions): Promise<ClaudeCodeParticipantResult>;
74
- export declare function buildClaudeCodeParticipantArgs(options: Pick<RunClaudeCodeParticipantOptions, "role" | "model" | "session">): string[];
75
+ export declare function buildClaudeCodeParticipantArgs(options: Pick<RunClaudeCodeParticipantOptions, "role" | "model" | "effort" | "session">): string[];
75
76
  export declare const CLAUDE_CODE_PARTICIPANT_TESTED_VERSION = "2.1.208";
76
77
  export {};
@@ -102,6 +102,9 @@ export function buildClaudeCodeParticipantArgs(options) {
102
102
  const model = readString(options.model);
103
103
  if (model)
104
104
  args.push("--model", model);
105
+ const effort = normalizeClaudeEffort(options.effort);
106
+ if (effort)
107
+ args.push("--effort", effort);
105
108
  const sessionId = readString(options.session?.sessionId);
106
109
  const resumeSessionId = readString(options.session?.resumeSessionId);
107
110
  if (sessionId)
@@ -112,6 +115,22 @@ export function buildClaudeCodeParticipantArgs(options) {
112
115
  args.push("--no-session-persistence");
113
116
  return args;
114
117
  }
118
+ function normalizeClaudeEffort(effort) {
119
+ switch (effort) {
120
+ case "low":
121
+ case "medium":
122
+ case "high":
123
+ case "max":
124
+ return effort;
125
+ case "minimal":
126
+ case "none":
127
+ return "low";
128
+ case "xhigh":
129
+ return "high";
130
+ default:
131
+ return undefined;
132
+ }
133
+ }
115
134
  function collectClaudeCodeParticipantResult(child, options) {
116
135
  const maxEvents = boundedLimit(options.maxEvents, DEFAULT_MAX_EVENTS);
117
136
  const maxStreamBytes = boundedLimit(options.maxStreamBytes, DEFAULT_MAX_STREAM_BYTES);