@agent-native/core 0.149.6 → 0.151.0

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 (177) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  3. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
  4. package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
  5. package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  6. package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  7. package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  8. package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  9. package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  10. package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
  11. package/corpus/templates/clips/desktop/src/app.tsx +32 -176
  12. package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
  13. package/corpus/templates/clips/desktop/src/styles.css +1 -166
  14. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
  15. package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
  16. package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
  17. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  18. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
  19. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
  20. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
  21. package/corpus/templates/clips/server/plugins/auth.ts +3 -0
  22. package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  23. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
  24. package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  25. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
  26. package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
  27. package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  28. package/corpus/templates/dispatch/app/global.css +2 -2
  29. package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
  30. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
  31. package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
  32. package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
  33. package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
  34. package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
  35. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
  36. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
  37. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
  38. package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
  39. package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  40. package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  41. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
  42. package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
  43. package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
  44. package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
  45. package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
  46. package/corpus/templates/forms/shared/page-url.ts +54 -0
  47. package/corpus/templates/forms/shared/types.ts +4 -4
  48. package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  49. package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  50. package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  51. package/corpus/templates/slides/actions/add-slide.ts +0 -27
  52. package/corpus/templates/slides/actions/import-file.ts +28 -9
  53. package/corpus/templates/slides/actions/view-screen.ts +4 -4
  54. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
  55. package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
  56. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
  57. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
  58. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
  59. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
  60. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
  61. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
  62. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  63. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
  64. package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
  65. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
  66. package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
  67. package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
  68. package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
  69. package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
  70. package/corpus/templates/slides/app/global.css +1 -1
  71. package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
  73. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
  74. package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
  75. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
  76. package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
  77. package/corpus/templates/slides/app/pages/Index.tsx +28 -3
  78. package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
  80. package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
  81. package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
  83. package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
  84. package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
  87. package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
  88. package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
  89. package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
  90. package/corpus/templates/slides/shared/deck-title.ts +56 -0
  91. package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  92. package/dist/agent/run-store.js +6 -1
  93. package/dist/agent/thread-data-builder.js +19 -44
  94. package/dist/agent/types.d.ts +13 -0
  95. package/dist/catalog.json +1 -0
  96. package/dist/cli/create.js +1 -1
  97. package/dist/client/AssistantChat.js +58 -10
  98. package/dist/client/FileStorageSetupCard.d.ts +6 -0
  99. package/dist/client/FileStorageSetupCard.js +18 -0
  100. package/dist/client/NewWorkspaceAppFlow.js +10 -0
  101. package/dist/client/agent-chat-adapter.js +87 -11
  102. package/dist/client/agent-chat.d.ts +1 -1
  103. package/dist/client/agent-chat.js +8 -3
  104. package/dist/client/chat/attachment-adapters.js +10 -0
  105. package/dist/client/chat/message-components.d.ts +4 -1
  106. package/dist/client/chat/message-components.js +37 -13
  107. package/dist/client/chat/tool-call-display.js +14 -0
  108. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  109. package/dist/client/chat-first/apps-rail.js +6 -6
  110. package/dist/client/chat-first/copy.js +1 -0
  111. package/dist/client/chat-first/types.d.ts +1 -0
  112. package/dist/client/extensions/EmbeddedExtension.js +1 -1
  113. package/dist/client/extensions/ExtensionViewer.js +1 -1
  114. package/dist/client/extensions/InlineExtensionFrame.js +1 -1
  115. package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
  116. package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
  117. package/dist/client/settings/FileStorageSettingsForm.js +113 -0
  118. package/dist/client/settings/SettingsPanel.js +2 -1
  119. package/dist/client/sse-event-processor.js +21 -0
  120. package/dist/collab/awareness.d.ts +2 -2
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/extensions/actions.js +2 -2
  123. package/dist/extensions/html-shell.js +3 -1
  124. package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
  125. package/dist/file-upload/pre-upload-attachments.js +68 -24
  126. package/dist/file-upload/registry.d.ts +4 -5
  127. package/dist/file-upload/registry.js +7 -14
  128. package/dist/file-upload/s3.d.ts +9 -0
  129. package/dist/file-upload/s3.js +168 -0
  130. package/dist/file-upload/types.d.ts +3 -3
  131. package/dist/file-upload/types.js +2 -2
  132. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  133. package/dist/notifications/routes.d.ts +6 -6
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/onboarding/app-profile.js +23 -1
  136. package/dist/onboarding/default-steps.js +69 -0
  137. package/dist/onboarding/types.d.ts +2 -0
  138. package/dist/progress/routes.d.ts +1 -1
  139. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  140. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  141. package/dist/secrets/routes.d.ts +3 -3
  142. package/dist/server/agent-chat/browser-team-tools.js +20 -0
  143. package/dist/server/agent-chat-plugin.js +12 -3
  144. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  145. package/dist/server/auth.js +45 -21
  146. package/dist/server/better-auth-instance.d.ts +2 -0
  147. package/dist/server/better-auth-instance.js +2 -2
  148. package/dist/server/core-routes-plugin.d.ts +1 -0
  149. package/dist/server/core-routes-plugin.js +26 -16
  150. package/dist/server/identity-sso-store.d.ts +3 -0
  151. package/dist/server/identity-sso-store.js +45 -0
  152. package/dist/server/identity-sso.d.ts +20 -10
  153. package/dist/server/identity-sso.js +79 -31
  154. package/dist/server/index.d.ts +1 -1
  155. package/dist/server/index.js +1 -1
  156. package/dist/server/realtime-token.d.ts +1 -1
  157. package/dist/server/transcribe-voice.d.ts +1 -1
  158. package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  159. package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  160. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  161. package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  162. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  163. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  164. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
  165. package/docs/content/authentication.mdx +27 -0
  166. package/docs/content/cross-app-sso.mdx +38 -8
  167. package/docs/content/extensions.mdx +15 -15
  168. package/docs/content/file-uploads.mdx +10 -12
  169. package/docs/content/generative-ui.mdx +6 -1
  170. package/package.json +2 -2
  171. package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  172. package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  173. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  174. package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  175. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  176. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  177. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
@@ -393,7 +393,9 @@ export function buildExtensionHtml(content, themeVars, isDark, extensionId, bind
393
393
  type: 'agent-native-send-to-chat',
394
394
  message: text,
395
395
  context: options.context,
396
- submit: options.submit !== false,
396
+ // Extension code can run polling and error handlers without a user
397
+ // gesture; submitting is opt-in for an explicit user action.
398
+ submit: options.submit === true,
397
399
  openSidebar: options.openSidebar !== false,
398
400
  }, '*');
399
401
  return { ok: true };
@@ -30,10 +30,14 @@ export interface PreUploadAttachmentsResult {
30
30
  /** Uploaded non-image files (PDF, generic binary). Parallel to `uploaded`
31
31
  * but for the file/document attachment type. */
32
32
  uploadedFiles: PreUploadedFileAttachment[];
33
- /** True if at least one image attachment failed to upload because no
34
- * file-upload provider is configured. Templates use this to render a
35
- * "Connect Builder.io" suggestion. */
33
+ /** True if at least one image or file could not be uploaded because no
34
+ * file-upload provider is configured. The agent uses this to render the
35
+ * storage setup card. */
36
36
  providerMissing: boolean;
37
+ /** True if at least one configured provider failed while uploading. */
38
+ uploadFailed: boolean;
39
+ /** The first provider error, bounded for safe inclusion in the chat hint. */
40
+ uploadError?: string;
37
41
  /** A pre-formatted block to inject into the user message text so the agent
38
42
  * has each hosted URL inline. Null when nothing was uploaded or no provider
39
43
  * is configured. */
@@ -41,18 +45,18 @@ export interface PreUploadAttachmentsResult {
41
45
  }
42
46
  /**
43
47
  * Returns true when a file-upload provider is currently configured.
44
- * Used to decide whether to attempt upload-first or fall back to base64.
48
+ * Used to decide whether an attachment can be uploaded before the agent turn.
45
49
  */
46
50
  export declare function isFileUploadProviderConfigured(): boolean;
47
51
  /**
48
52
  * Pre-upload chat image attachments through the active file-upload provider
49
53
  * (Builder.io by default) so the agent can embed hosted URLs in HTML, slide
50
- * content, and outbound messages. Keeps the original base64 data URL on the
51
- * attachment so multimodal vision still works — only adds a hosted `url`.
54
+ * content, and outbound messages. Keeps the original data URL in memory for
55
+ * the current multimodal turn and adds a hosted `url`.
52
56
  *
53
57
  * Safe to call when no provider is configured: it returns the attachments
54
- * untouched with `providerMissing: true` so callers can surface a connect-
55
- * Builder.io hint to the agent.
58
+ * untouched with `providerMissing: true` so callers can surface the storage
59
+ * setup card to the agent without persisting the binary payload.
56
60
  */
57
61
  export declare function preUploadImageAttachments(opts: {
58
62
  attachments: AgentChatAttachment[] | undefined;
@@ -66,8 +70,9 @@ export declare function preUploadImageAttachments(opts: {
66
70
  * file-reading still work; callers that persist the attachment can drop the
67
71
  * data when a URL exists.
68
72
  *
69
- * Falls back gracefully when no provider is configured: returns untouched
70
- * attachments with `providerMissing: true` for image-type failures.
73
+ * When no provider is configured, returns untouched in-memory attachments with
74
+ * `providerMissing: true`. The caller may still use the bytes for this turn,
75
+ * but must not persist them as SQL/base64 attachment data.
71
76
  */
72
77
  export declare function preUploadAttachments(opts: {
73
78
  attachments: AgentChatAttachment[] | undefined;
@@ -33,7 +33,7 @@ function escapeXmlAttr(value) {
33
33
  }
34
34
  /**
35
35
  * Returns true when a file-upload provider is currently configured.
36
- * Used to decide whether to attempt upload-first or fall back to base64.
36
+ * Used to decide whether an attachment can be uploaded before the agent turn.
37
37
  */
38
38
  export function isFileUploadProviderConfigured() {
39
39
  return getActiveFileUploadProvider() !== null;
@@ -41,12 +41,12 @@ export function isFileUploadProviderConfigured() {
41
41
  /**
42
42
  * Pre-upload chat image attachments through the active file-upload provider
43
43
  * (Builder.io by default) so the agent can embed hosted URLs in HTML, slide
44
- * content, and outbound messages. Keeps the original base64 data URL on the
45
- * attachment so multimodal vision still works — only adds a hosted `url`.
44
+ * content, and outbound messages. Keeps the original data URL in memory for
45
+ * the current multimodal turn and adds a hosted `url`.
46
46
  *
47
47
  * Safe to call when no provider is configured: it returns the attachments
48
- * untouched with `providerMissing: true` so callers can surface a connect-
49
- * Builder.io hint to the agent.
48
+ * untouched with `providerMissing: true` so callers can surface the storage
49
+ * setup card to the agent without persisting the binary payload.
50
50
  */
51
51
  export async function preUploadImageAttachments(opts) {
52
52
  return preUploadAttachments({ ...opts, includeFiles: false });
@@ -59,8 +59,9 @@ export async function preUploadImageAttachments(opts) {
59
59
  * file-reading still work; callers that persist the attachment can drop the
60
60
  * data when a URL exists.
61
61
  *
62
- * Falls back gracefully when no provider is configured: returns untouched
63
- * attachments with `providerMissing: true` for image-type failures.
62
+ * When no provider is configured, returns untouched in-memory attachments with
63
+ * `providerMissing: true`. The caller may still use the bytes for this turn,
64
+ * but must not persist them as SQL/base64 attachment data.
64
65
  */
65
66
  export async function preUploadAttachments(opts) {
66
67
  const list = Array.isArray(opts.attachments) ? opts.attachments : [];
@@ -68,12 +69,15 @@ export async function preUploadAttachments(opts) {
68
69
  const uploaded = [];
69
70
  const uploadedFiles = [];
70
71
  let providerMissing = false;
72
+ let uploadFailed = false;
73
+ let uploadError;
71
74
  if (list.length === 0) {
72
75
  return {
73
76
  attachments: list,
74
77
  uploaded,
75
78
  uploadedFiles,
76
79
  providerMissing: false,
80
+ uploadFailed: false,
77
81
  injectedText: null,
78
82
  };
79
83
  }
@@ -82,11 +86,9 @@ export async function preUploadAttachments(opts) {
82
86
  const isFile = att.type === "file" || att.type === "document";
83
87
  if (!isImage && !(includeFiles && isFile))
84
88
  continue;
85
- if (typeof att.data !== "string")
86
- continue;
87
- if (att.url) {
89
+ if (typeof att.url === "string" && att.url.trim()) {
88
90
  // Already pre-uploaded earlier in the pipeline — reuse it.
89
- const isReferenceOnlySvg = isSvgAttachment(att);
91
+ const isReferenceOnlySvg = att.referenceOnly === true || isSvgAttachment(att);
90
92
  if (isReferenceOnlySvg) {
91
93
  markReferenceOnlySvgAttachment(att, att.contentType);
92
94
  }
@@ -110,7 +112,21 @@ export async function preUploadAttachments(opts) {
110
112
  }
111
113
  continue;
112
114
  }
113
- const match = att.data.match(FILE_DATA_URL_RE);
115
+ let data = att.data;
116
+ if (typeof data !== "string" &&
117
+ includeFiles &&
118
+ isFile &&
119
+ typeof att.text === "string" &&
120
+ att.text.length > 0) {
121
+ // Text attachments are already decoded by the client. Upload the text
122
+ // bytes too so a later turn has the same durable object URL as binary
123
+ // attachments instead of a SQL-only scratch copy.
124
+ const encoded = Buffer.from(att.text, "utf8").toString("base64");
125
+ data = `data:${normalizeContentType(att.contentType) || "text/plain"};base64,${encoded}`;
126
+ }
127
+ if (typeof data !== "string")
128
+ continue;
129
+ const match = data.match(FILE_DATA_URL_RE);
114
130
  if (!match)
115
131
  continue;
116
132
  const dataUrlMimeType = normalizeContentType(match[1]);
@@ -134,8 +150,8 @@ export async function preUploadAttachments(opts) {
134
150
  ownerEmail: opts.ownerEmail || undefined,
135
151
  });
136
152
  if (!result) {
137
- if (uploadAsImage)
138
- providerMissing = true;
153
+ providerMissing = true;
154
+ att.storageRequired = true;
139
155
  continue;
140
156
  }
141
157
  att.url = result.url;
@@ -168,8 +184,12 @@ export async function preUploadAttachments(opts) {
168
184
  }
169
185
  }
170
186
  catch (err) {
171
- // Real upload failure (network, API). Keep the base64 so the model
172
- // can still see the image/file, but don't crash the turn.
187
+ // Real upload failure (network, API). Keep the bytes in memory for the
188
+ // current turn, but never treat the failure as a durable upload.
189
+ att.storageRequired = true;
190
+ att.storageUploadFailed = true;
191
+ uploadFailed = true;
192
+ uploadError ??= (err instanceof Error ? err.message : String(err)).slice(0, 500);
173
193
  console.warn("[agent-native] pre-upload of chat attachment failed:", err instanceof Error ? err.message : String(err));
174
194
  }
175
195
  }
@@ -199,21 +219,43 @@ export async function preUploadAttachments(opts) {
199
219
  lines.push(`<chat-file-attachment ${attrs.join(" ")} />`);
200
220
  }
201
221
  const hasReferenceOnlySvg = uploadedFiles.some((file) => file.referenceOnly && isSvgAttachment(file));
202
- injectedText = [
222
+ const linesWithMetadata = [
203
223
  hasReferenceOnlySvg
204
224
  ? '<chat-attachments note="The user attached these files. Image attachment URLs may be used for embedding. File attachment URLs are references; SVG files are unsanitized vector source and must not be inlined as HTML or embedded in outbound content unless the target app sanitizes or stores them safely.">'
205
225
  : '<chat-attachments note="The user attached these files. Image attachment URLs may be used for embedding in HTML, slide content, or outbound messages. File attachment URLs are references for reading or attaching in target apps.">',
206
226
  ...lines,
207
227
  "</chat-attachments>",
208
- ].join("\n");
228
+ ];
229
+ if (providerMissing || uploadFailed) {
230
+ const failureLines = [
231
+ providerMissing
232
+ ? "One or more attachments could not be stored because no durable object-storage provider is configured."
233
+ : null,
234
+ uploadFailed
235
+ ? `A configured object-storage provider failed to upload an attachment${uploadError ? `: ${escapeXmlAttr(uploadError)}` : "."}`
236
+ : null,
237
+ ].filter((line) => Boolean(line));
238
+ linesWithMetadata.push("<chat-file-attachment-upload-error>", ...failureLines, ...(providerMissing
239
+ ? [
240
+ "Call `connect-file-storage` now so the user can connect Builder or configure custom storage keys, then continue using the hosted references.",
241
+ ]
242
+ : [
243
+ "Retry the upload or inspect the configured storage provider. Do not claim the attachment is durably available until it succeeds.",
244
+ ]), "</chat-file-attachment-upload-error>");
245
+ }
246
+ injectedText = linesWithMetadata.join("\n");
209
247
  }
210
- else if (providerMissing) {
248
+ else if (providerMissing || uploadFailed) {
211
249
  injectedText = [
212
- "<chat-image-attachment-upload-error>",
213
- "The user attached one or more images, but no file-upload provider is configured for this app.",
214
- "If `connect-builder` is available, use it to render the inline Builder.io connection card. Workspaces with a custom storage provider can also use one registered via registerFileUploadProvider().",
215
- "Until that's done, you can still SEE the image, but you do NOT have a URL to embed it in HTML or share with other apps.",
216
- "</chat-image-attachment-upload-error>",
250
+ "<chat-file-attachment-upload-error>",
251
+ providerMissing
252
+ ? "The user attached one or more images or files, but durable object storage is not configured for this app."
253
+ : `The user attached one or more images or files, but the configured storage provider failed to upload them${uploadError ? `: ${escapeXmlAttr(uploadError)}` : "."}`,
254
+ providerMissing
255
+ ? "Call `connect-file-storage` now to render the inline storage setup card. The user can connect Builder for managed storage or open the same card's custom-key setup for S3-compatible object storage."
256
+ : "Retry the upload or inspect the configured storage provider. Do not claim the attachment is durably available until it succeeds.",
257
+ "Do not persist the base64 contents in SQL. Until storage succeeds, use the attachment only for this turn.",
258
+ "</chat-file-attachment-upload-error>",
217
259
  ].join("\n");
218
260
  }
219
261
  return {
@@ -221,6 +263,8 @@ export async function preUploadAttachments(opts) {
221
263
  uploaded,
222
264
  uploadedFiles,
223
265
  providerMissing,
266
+ uploadFailed,
267
+ ...(uploadError ? { uploadError } : {}),
224
268
  injectedText,
225
269
  };
226
270
  }
@@ -9,15 +9,14 @@ export declare function listFileUploadProviders(): FileUploadProvider[];
9
9
  /**
10
10
  * Returns the first configured provider, checking user-registered ones first
11
11
  * and falling back to the built-in Builder.io provider when its env is set.
12
- * Returns `null` when nothing is configured — callers should then use the
13
- * SQL fallback.
12
+ * Returns `null` when nothing is configured. Callers must fail closed rather
13
+ * than persisting the original binary payload in SQL.
14
14
  */
15
15
  export declare function getActiveFileUploadProvider(): FileUploadProvider | null;
16
16
  export declare function getActiveFileUploadProviderForRequest(): Promise<FileUploadProvider | null>;
17
17
  /**
18
18
  * Upload a file via the active provider, or `null` if no provider is
19
- * configured. Callers use `null` as the signal to fall back to SQL
20
- * storage. On the first fallback we log a one-time warning because
21
- * storing files in SQL is not optimal for production.
19
+ * configured. `null` is an explicit storage-setup state: callers must not
20
+ * turn the input into a base64 SQL fallback.
22
21
  */
23
22
  export declare function uploadFile(input: FileUploadInput): Promise<FileUploadResult | null>;
@@ -18,8 +18,8 @@ export function listFileUploadProviders() {
18
18
  /**
19
19
  * Returns the first configured provider, checking user-registered ones first
20
20
  * and falling back to the built-in Builder.io provider when its env is set.
21
- * Returns `null` when nothing is configured — callers should then use the
22
- * SQL fallback.
21
+ * Returns `null` when nothing is configured. Callers must fail closed rather
22
+ * than persisting the original binary payload in SQL.
23
23
  */
24
24
  export function getActiveFileUploadProvider() {
25
25
  for (const provider of providers.values()) {
@@ -36,14 +36,8 @@ export async function getActiveFileUploadProviderForRequest() {
36
36
  if (provider.isConfigured())
37
37
  return provider;
38
38
  if (provider.isConfiguredForRequest) {
39
- try {
40
- if (await provider.isConfiguredForRequest())
41
- return provider;
42
- }
43
- catch {
44
- // Treat failed scoped credential lookups as unavailable. The upload
45
- // call will surface real provider errors after a provider is selected.
46
- }
39
+ if (await provider.isConfiguredForRequest())
40
+ return provider;
47
41
  }
48
42
  }
49
43
  if (builderFileUploadProvider.isConfigured()) {
@@ -62,9 +56,8 @@ export async function getActiveFileUploadProviderForRequest() {
62
56
  }
63
57
  /**
64
58
  * Upload a file via the active provider, or `null` if no provider is
65
- * configured. Callers use `null` as the signal to fall back to SQL
66
- * storage. On the first fallback we log a one-time warning because
67
- * storing files in SQL is not optimal for production.
59
+ * configured. `null` is an explicit storage-setup state: callers must not
60
+ * turn the input into a base64 SQL fallback.
68
61
  */
69
62
  export async function uploadFile(input) {
70
63
  const provider = await getActiveFileUploadProviderForRequest();
@@ -85,7 +78,7 @@ export async function uploadFile(input) {
85
78
  }
86
79
  catch (err) {
87
80
  // DB unavailable or credential store not ready — can't resolve key.
88
- // Log and fall through to the SQL fallback below.
81
+ // Return an unavailable-provider state below; never fall back to SQL.
89
82
  console.warn("[agent-native] Builder credential check failed:", err instanceof Error ? err.message : String(err));
90
83
  }
91
84
  if (builderKey) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Framework-owned S3-compatible object storage provider.
3
+ *
4
+ * The onboarding form writes these keys to scoped secrets. A public base URL
5
+ * is required because chat attachments need stable URLs that remain usable
6
+ * after the request and across later turns in the thread.
7
+ */
8
+ import type { FileUploadProvider } from "./types.js";
9
+ export declare const s3FileUploadProvider: FileUploadProvider;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Framework-owned S3-compatible object storage provider.
3
+ *
4
+ * The onboarding form writes these keys to scoped secrets. A public base URL
5
+ * is required because chat attachments need stable URLs that remain usable
6
+ * after the request and across later turns in the thread.
7
+ */
8
+ import { resolveSecret } from "../server/credential-provider.js";
9
+ function cleanValue(value) {
10
+ const cleaned = value?.trim();
11
+ return cleaned ? cleaned : undefined;
12
+ }
13
+ function buildConfig(values) {
14
+ const bucket = cleanValue(values.bucket);
15
+ const accessKeyId = cleanValue(values.accessKeyId);
16
+ const secretAccessKey = cleanValue(values.secretAccessKey);
17
+ const endpoint = cleanValue(values.endpoint)?.replace(/\/+$/, "");
18
+ const publicBaseUrl = cleanValue(values.publicBaseUrl)?.replace(/\/+$/, "");
19
+ if (!bucket ||
20
+ !accessKeyId ||
21
+ !secretAccessKey ||
22
+ !endpoint ||
23
+ !publicBaseUrl) {
24
+ return null;
25
+ }
26
+ if (!URL.canParse(endpoint) || !URL.canParse(publicBaseUrl)) {
27
+ return null;
28
+ }
29
+ const endpointUrl = new URL(endpoint);
30
+ const publicUrl = new URL(publicBaseUrl);
31
+ if (!["http:", "https:"].includes(endpointUrl.protocol) ||
32
+ !["http:", "https:"].includes(publicUrl.protocol)) {
33
+ return null;
34
+ }
35
+ return {
36
+ region: cleanValue(values.region) ?? "auto",
37
+ bucket,
38
+ accessKeyId,
39
+ secretAccessKey,
40
+ endpoint,
41
+ publicBaseUrl,
42
+ };
43
+ }
44
+ function readEnvConfig() {
45
+ const env = process.env;
46
+ return buildConfig({
47
+ bucket: env.S3_BUCKET || env.R2_BUCKET,
48
+ accessKeyId: env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID,
49
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY,
50
+ endpoint: env.S3_ENDPOINT || env.R2_ENDPOINT,
51
+ region: env.S3_REGION || env.R2_REGION,
52
+ publicBaseUrl: env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL,
53
+ });
54
+ }
55
+ async function resolveStorageSecret(primary, fallback) {
56
+ const primaryValue = cleanValue(await resolveSecret(primary));
57
+ return primaryValue ?? cleanValue(await resolveSecret(fallback));
58
+ }
59
+ async function readRequestConfig() {
60
+ const scopedConfig = buildConfig({
61
+ bucket: await resolveStorageSecret("S3_BUCKET", "R2_BUCKET"),
62
+ accessKeyId: await resolveStorageSecret("S3_ACCESS_KEY_ID", "R2_ACCESS_KEY_ID"),
63
+ secretAccessKey: await resolveStorageSecret("S3_SECRET_ACCESS_KEY", "R2_SECRET_ACCESS_KEY"),
64
+ endpoint: await resolveStorageSecret("S3_ENDPOINT", "R2_ENDPOINT"),
65
+ region: await resolveStorageSecret("S3_REGION", "R2_REGION"),
66
+ publicBaseUrl: await resolveStorageSecret("S3_PUBLIC_BASE_URL", "R2_PUBLIC_BASE_URL"),
67
+ });
68
+ return scopedConfig ?? readEnvConfig();
69
+ }
70
+ async function hmac(key, message) {
71
+ const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
72
+ return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
73
+ }
74
+ async function sha256(data) {
75
+ const buffer = new ArrayBuffer(data.byteLength);
76
+ new Uint8Array(buffer).set(data);
77
+ return toHex(await crypto.subtle.digest("SHA-256", buffer));
78
+ }
79
+ function toHex(buffer) {
80
+ return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
81
+ }
82
+ async function signingKey(secret, dateStamp, region) {
83
+ const dateKey = await hmac(new TextEncoder().encode(`AWS4${secret}`).buffer, dateStamp);
84
+ const regionKey = await hmac(dateKey, region);
85
+ const serviceKey = await hmac(regionKey, "s3");
86
+ return hmac(serviceKey, "aws4_request");
87
+ }
88
+ function encodePathSegment(value) {
89
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
90
+ }
91
+ function objectPath(config, key) {
92
+ return `/${config.bucket}/${key.split("/").map(encodePathSegment).join("/")}`;
93
+ }
94
+ async function putObject(config, key, data, contentType) {
95
+ const now = new Date();
96
+ const amzDate = now
97
+ .toISOString()
98
+ .replace(/[:-]|\.\d{3}/g, "")
99
+ .slice(0, 15) + "Z";
100
+ const dateStamp = amzDate.slice(0, 8);
101
+ const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
102
+ const host = new URL(config.endpoint).host;
103
+ const canonicalUri = objectPath(config, key);
104
+ const payloadHash = await sha256(data);
105
+ const headers = {
106
+ host,
107
+ "content-type": contentType,
108
+ "x-amz-content-sha256": payloadHash,
109
+ "x-amz-date": amzDate,
110
+ };
111
+ const signedHeaderKeys = Object.keys(headers).sort();
112
+ const signedHeaders = signedHeaderKeys.join(";");
113
+ const canonicalHeaders = signedHeaderKeys
114
+ .map((header) => `${header}:${headers[header]}`)
115
+ .join("\n") + "\n";
116
+ const canonicalRequest = [
117
+ "PUT",
118
+ canonicalUri,
119
+ "",
120
+ canonicalHeaders,
121
+ signedHeaders,
122
+ payloadHash,
123
+ ].join("\n");
124
+ const requestHash = await sha256(new TextEncoder().encode(canonicalRequest));
125
+ const stringToSign = [
126
+ "AWS4-HMAC-SHA256",
127
+ amzDate,
128
+ credentialScope,
129
+ requestHash,
130
+ ].join("\n");
131
+ const signature = toHex(await hmac(await signingKey(config.secretAccessKey, dateStamp, config.region), stringToSign));
132
+ const authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, ` +
133
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
134
+ const response = await fetch(`${config.endpoint}${canonicalUri}`, {
135
+ method: "PUT",
136
+ headers: {
137
+ ...headers,
138
+ Authorization: authorization,
139
+ "Content-Length": String(data.byteLength),
140
+ },
141
+ body: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
142
+ });
143
+ if (!response.ok) {
144
+ const detail = await response.text();
145
+ throw new Error(`S3 PutObject failed (${response.status}): ${detail || response.statusText}`);
146
+ }
147
+ return `${config.publicBaseUrl}/${key.split("/").map(encodePathSegment).join("/")}`;
148
+ }
149
+ function safeFilename(filename) {
150
+ const basename = filename?.split(/[\\/]/).pop()?.trim() || "attachment";
151
+ return (basename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "attachment");
152
+ }
153
+ export const s3FileUploadProvider = {
154
+ id: "s3",
155
+ name: "S3-compatible object storage",
156
+ isConfigured: () => readEnvConfig() !== null,
157
+ isConfiguredForRequest: async () => (await readRequestConfig()) !== null,
158
+ upload: async ({ data, filename, mimeType }) => {
159
+ const config = await readRequestConfig();
160
+ if (!config) {
161
+ throw new Error("S3 object storage requires a bucket, endpoint, credentials, and public base URL");
162
+ }
163
+ const key = `uploads/${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${safeFilename(filename)}`;
164
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
165
+ const url = await putObject(config, key, bytes, mimeType || "application/octet-stream");
166
+ return { url, provider: "s3" };
167
+ },
168
+ };
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Templates call `uploadFile()` to upload a file and get back a URL.
5
5
  * The framework dispatches to whichever provider is registered (Builder.io
6
- * built-in, or a user-supplied one). If no provider is active, it falls back
7
- * to the SQL resources store — fine for dev, not recommended for production.
6
+ * built-in, or a user-supplied one). If no provider is active, uploads fail
7
+ * closed so binary payloads never get persisted as base64 in SQL.
8
8
  */
9
9
  export interface FileUploadInput {
10
10
  /** File contents. */
@@ -13,7 +13,7 @@ export interface FileUploadInput {
13
13
  filename?: string;
14
14
  /** MIME type, e.g. "image/png". */
15
15
  mimeType?: string;
16
- /** Optional owner email for per-user scoping in fallback storage. */
16
+ /** Owner email used for request-scoped provider credentials. */
17
17
  ownerEmail?: string;
18
18
  /** Builder.io upload hint: return one stable URL while compression finishes asynchronously. */
19
19
  stableUrl?: boolean;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Templates call `uploadFile()` to upload a file and get back a URL.
5
5
  * The framework dispatches to whichever provider is registered (Builder.io
6
- * built-in, or a user-supplied one). If no provider is active, it falls back
7
- * to the SQL resources store — fine for dev, not recommended for production.
6
+ * built-in, or a user-supplied one). If no provider is active, uploads fail
7
+ * closed so binary payloads never get persisted as base64 in SQL.
8
8
  */
9
9
  export {};
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
- count?: undefined;
141
140
  query?: undefined;
142
141
  minutes?: undefined;
143
142
  limit?: undefined;
144
143
  clientHint?: undefined;
145
144
  timestamp?: undefined;
146
145
  chapterId?: undefined;
146
+ count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
- count?: undefined;
163
162
  query: {
164
163
  type: string;
165
164
  description: string;
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
175
174
  clientHint?: undefined;
176
175
  timestamp?: undefined;
177
176
  chapterId?: undefined;
177
+ count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
- count?: undefined;
194
193
  minutes: {
195
194
  type: string;
196
195
  description: string;
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
200
199
  clientHint?: undefined;
201
200
  timestamp?: undefined;
202
201
  chapterId?: undefined;
202
+ count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
- count?: undefined;
220
219
  query: {
221
220
  type: string;
222
221
  description: string;
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
235
234
  };
236
235
  timestamp?: undefined;
237
236
  chapterId?: undefined;
237
+ count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
- count?: undefined;
254
253
  query?: undefined;
255
254
  minutes?: undefined;
256
255
  limit?: undefined;
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
264
263
  description: string;
265
264
  };
266
265
  chapterId?: undefined;
266
+ count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
- count?: undefined;
318
317
  query?: undefined;
319
318
  minutes?: undefined;
320
319
  limit?: undefined;
321
320
  clientHint?: undefined;
322
321
  timestamp?: undefined;
323
322
  chapterId?: undefined;
323
+ count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
- count?: undefined;
353
352
  query?: undefined;
354
353
  minutes?: undefined;
355
354
  limit?: undefined;
356
355
  clientHint?: undefined;
357
356
  timestamp?: undefined;
358
357
  chapterId?: undefined;
358
+ count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -13,21 +13,21 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- error?: undefined;
17
16
  ok?: undefined;
17
+ error?: undefined;
18
18
  } | {
19
- count?: undefined;
20
19
  updated: number;
21
- error?: undefined;
22
20
  ok?: undefined;
23
- } | {
21
+ error?: undefined;
24
22
  count?: undefined;
23
+ } | {
25
24
  updated?: undefined;
26
25
  error: string;
27
26
  ok?: undefined;
28
- } | {
29
27
  count?: undefined;
28
+ } | {
30
29
  updated?: undefined;
31
- error?: undefined;
32
30
  ok: boolean;
31
+ error?: undefined;
32
+ count?: undefined;
33
33
  }>>;