@agent-native/core 0.101.6 → 0.101.10

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 (237) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/artifact-response.ts +255 -12
  16. package/corpus/core/src/agent/thread-data-builder.ts +84 -11
  17. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  18. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  20. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  21. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  22. package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
  23. package/corpus/core/src/integrations/adapters/discord.ts +3 -1
  24. package/corpus/core/src/integrations/adapters/email.ts +4 -1
  25. package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
  26. package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
  27. package/corpus/core/src/integrations/adapters/slack.ts +38 -9
  28. package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
  29. package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
  30. package/corpus/core/src/integrations/index.ts +1 -0
  31. package/corpus/core/src/integrations/types.ts +9 -2
  32. package/corpus/core/src/integrations/webhook-handler.ts +69 -45
  33. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
  34. package/corpus/core/src/server/auth.ts +25 -1
  35. package/corpus/core/src/server/builder-browser.ts +353 -1
  36. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  37. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  38. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  39. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  40. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  41. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  42. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  43. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  44. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  45. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  46. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
  47. package/corpus/templates/content/AGENTS.md +32 -9
  48. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  49. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  50. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  51. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  52. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  53. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  54. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  55. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  56. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  57. package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
  58. package/corpus/templates/content/actions/_database-utils.ts +104 -13
  59. package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
  60. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  61. package/corpus/templates/content/actions/_property-utils.ts +7 -1
  62. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  63. package/corpus/templates/content/actions/configure-document-property.ts +11 -0
  64. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  65. package/corpus/templates/content/actions/create-document.ts +9 -0
  66. package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
  67. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  68. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  69. package/corpus/templates/content/actions/export-content-source.ts +1 -0
  70. package/corpus/templates/content/actions/get-content-database.ts +13 -1
  71. package/corpus/templates/content/actions/get-document.ts +12 -2
  72. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  73. package/corpus/templates/content/actions/import-content-source.ts +6 -0
  74. package/corpus/templates/content/actions/list-documents.ts +3 -0
  75. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  76. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  77. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  78. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  79. package/corpus/templates/content/actions/pull-document.ts +2 -0
  80. package/corpus/templates/content/actions/search-documents.ts +5 -1
  81. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  82. package/corpus/templates/content/actions/update-document.ts +16 -1
  83. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  84. package/corpus/templates/content/actions/view-screen.ts +15 -2
  85. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
  86. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
  87. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  89. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  90. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
  91. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  92. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  93. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  94. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  95. package/corpus/templates/content/app/global.css +18 -1
  96. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  97. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  98. package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
  99. package/corpus/templates/content/app/i18n-data.ts +339 -0
  100. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  101. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  102. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  103. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  104. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  105. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  106. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  107. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  110. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  111. package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
  112. package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
  113. package/corpus/templates/content/parity/matrix.md +27 -25
  114. package/corpus/templates/content/parity/matrix.ts +50 -2
  115. package/corpus/templates/content/server/db/schema.ts +51 -0
  116. package/corpus/templates/content/server/lib/document-context.ts +76 -0
  117. package/corpus/templates/content/server/lib/public-documents.ts +3 -0
  118. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  119. package/corpus/templates/content/server/plugins/db.ts +59 -0
  120. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
  121. package/corpus/templates/content/shared/api.ts +87 -1
  122. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  123. package/corpus/templates/content/shared/content-source.ts +7 -0
  124. package/corpus/templates/content/shared/nfm.ts +3 -3
  125. package/corpus/templates/content/shared/properties.ts +2 -0
  126. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  127. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  128. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  129. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  130. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  131. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  132. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  133. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  134. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  135. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  136. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  137. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  138. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  140. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  141. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  142. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  144. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  145. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  146. package/dist/a2a/artifact-response.d.ts +15 -0
  147. package/dist/a2a/artifact-response.d.ts.map +1 -1
  148. package/dist/a2a/artifact-response.js +204 -5
  149. package/dist/a2a/artifact-response.js.map +1 -1
  150. package/dist/agent/thread-data-builder.d.ts +6 -0
  151. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  152. package/dist/agent/thread-data-builder.js +71 -8
  153. package/dist/agent/thread-data-builder.js.map +1 -1
  154. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  155. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  156. package/dist/client/review/ReviewCommentComposer.js +3 -3
  157. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  158. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  159. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  160. package/dist/client/review/ReviewThreadPanel.js +6 -4
  161. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  162. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  163. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  164. package/dist/client/settings/SettingsPanel.js +11 -4
  165. package/dist/client/settings/SettingsPanel.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +19 -6
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  171. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  172. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  173. package/dist/collab/awareness.d.ts +2 -2
  174. package/dist/collab/awareness.d.ts.map +1 -1
  175. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  176. package/dist/integrations/a2a-continuation-processor.js +72 -6
  177. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  178. package/dist/integrations/adapters/discord.d.ts.map +1 -1
  179. package/dist/integrations/adapters/discord.js +1 -0
  180. package/dist/integrations/adapters/discord.js.map +1 -1
  181. package/dist/integrations/adapters/email.d.ts.map +1 -1
  182. package/dist/integrations/adapters/email.js +2 -0
  183. package/dist/integrations/adapters/email.js.map +1 -1
  184. package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
  185. package/dist/integrations/adapters/google-docs.js +2 -0
  186. package/dist/integrations/adapters/google-docs.js.map +1 -1
  187. package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
  188. package/dist/integrations/adapters/microsoft-teams.js +1 -0
  189. package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
  190. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  191. package/dist/integrations/adapters/slack.js +21 -6
  192. package/dist/integrations/adapters/slack.js.map +1 -1
  193. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  194. package/dist/integrations/adapters/telegram.js +11 -2
  195. package/dist/integrations/adapters/telegram.js.map +1 -1
  196. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  197. package/dist/integrations/adapters/whatsapp.js +3 -0
  198. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  199. package/dist/integrations/index.d.ts +1 -1
  200. package/dist/integrations/index.d.ts.map +1 -1
  201. package/dist/integrations/index.js.map +1 -1
  202. package/dist/integrations/types.d.ts +8 -2
  203. package/dist/integrations/types.d.ts.map +1 -1
  204. package/dist/integrations/types.js.map +1 -1
  205. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  206. package/dist/integrations/webhook-handler.js +40 -43
  207. package/dist/integrations/webhook-handler.js.map +1 -1
  208. package/dist/observability/routes.d.ts +5 -5
  209. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  210. package/dist/secrets/routes.d.ts +9 -9
  211. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  212. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  213. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  214. package/dist/server/auth.d.ts +1 -0
  215. package/dist/server/auth.d.ts.map +1 -1
  216. package/dist/server/auth.js +19 -2
  217. package/dist/server/auth.js.map +1 -1
  218. package/dist/server/builder-browser.d.ts +78 -0
  219. package/dist/server/builder-browser.d.ts.map +1 -1
  220. package/dist/server/builder-browser.js +246 -1
  221. package/dist/server/builder-browser.js.map +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +41 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +260 -14
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/docs/content/deployment.mdx +9 -7
  227. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  228. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  229. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  230. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  231. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  232. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  233. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  234. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  235. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  236. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  237. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BuilderCmsModelFieldSummary,
2
3
  BuilderCmsPublicationTransitionIntent,
3
4
  BuilderCmsWriteEffect,
4
5
  ContentDatabaseSource,
@@ -8,7 +9,10 @@ import type {
8
9
  ContentDatabaseSourceWriteMode,
9
10
  } from "../shared/api.js";
10
11
  import { BUILDER_CMS_SAFE_WRITE_MODEL as SAFE_WRITE_MODEL } from "../shared/api.js";
11
- import { builderCmsSourceRowIdentityState } from "./_builder-cms-source-adapter.js";
12
+ import {
13
+ BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
14
+ builderCmsSourceRowIdentityState,
15
+ } from "./_builder-cms-source-adapter.js";
12
16
  import { builderCmsPushModeForTier } from "./_builder-cms-write-settings.js";
13
17
 
14
18
  export type { BuilderCmsWriteEffect };
@@ -72,6 +76,243 @@ export function builderCmsExecutionIdempotencyKey(args: {
72
76
  return `builder-cms:${args.sourceId}:${args.changeSetId}:${args.pushMode}`;
73
77
  }
74
78
 
79
+ export const BUILDER_CMS_EXECUTION_MARKER_FIELD = "agentNativeTestNote";
80
+
81
+ function builderFieldName(sourceFieldKey: string) {
82
+ return sourceFieldKey.replace(/^data\./, "").trim();
83
+ }
84
+
85
+ function builderModelFieldForOperation(args: {
86
+ source: ContentDatabaseSource;
87
+ sourceFieldKey: string;
88
+ }) {
89
+ const name = builderFieldName(args.sourceFieldKey);
90
+ return (
91
+ args.source.metadata.builderModelFields?.find(
92
+ (field) => field.name.trim() === name,
93
+ ) ?? null
94
+ );
95
+ }
96
+
97
+ function normalizedBuilderFieldKind(field: BuilderCmsModelFieldSummary | null) {
98
+ return [field?.type, field?.inputType]
99
+ .filter((value): value is string => typeof value === "string")
100
+ .join(" ")
101
+ .trim()
102
+ .toLowerCase();
103
+ }
104
+
105
+ function optionIdFromLabel(label: string) {
106
+ return (
107
+ label
108
+ .trim()
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9]+/g, "-")
111
+ .replace(/^-+|-+$/g, "") || "option"
112
+ );
113
+ }
114
+
115
+ function builderOptionLabels(field: BuilderCmsModelFieldSummary | null) {
116
+ const labels = [...(field?.options ?? []), ...(field?.enum ?? [])];
117
+ const seenLabels = new Set<string>();
118
+ const usedIds = new Set<string>();
119
+ const result: Array<{ id: string; label: string }> = [];
120
+ for (const rawLabel of labels) {
121
+ const label = rawLabel.trim();
122
+ const labelKey = label.toLowerCase();
123
+ if (!label || seenLabels.has(labelKey)) continue;
124
+ seenLabels.add(labelKey);
125
+ const baseId = optionIdFromLabel(label);
126
+ let id = baseId;
127
+ let suffix = 2;
128
+ while (usedIds.has(id)) id = `${baseId}-${suffix++}`;
129
+ usedIds.add(id);
130
+ result.push({ id, label });
131
+ }
132
+ return result;
133
+ }
134
+
135
+ function builderNativeFieldValue(args: {
136
+ source: ContentDatabaseSource;
137
+ sourceFieldKey: string;
138
+ value: unknown;
139
+ }): { value?: unknown; blocker?: string } {
140
+ if (args.source.sourceTable !== SAFE_WRITE_MODEL) {
141
+ return { value: args.value };
142
+ }
143
+ const field = builderModelFieldForOperation(args);
144
+ const fieldLabel = field?.label?.trim() || field?.name || args.sourceFieldKey;
145
+ const kind = normalizedBuilderFieldKind(field);
146
+
147
+ if (/\b(date|datetime)\b/.test(kind)) {
148
+ const raw =
149
+ args.value && typeof args.value === "object" && !Array.isArray(args.value)
150
+ ? (args.value as { start?: unknown }).start
151
+ : args.value;
152
+ if (typeof raw !== "string" && typeof raw !== "number") {
153
+ return { blocker: `${fieldLabel} must be a valid Builder date.` };
154
+ }
155
+ const timestamp =
156
+ typeof raw === "number" ? raw : new Date(raw.trim()).getTime();
157
+ if (!Number.isFinite(timestamp)) {
158
+ return { blocker: `${fieldLabel} must be a valid Builder date.` };
159
+ }
160
+ return { value: timestamp };
161
+ }
162
+
163
+ if (/\b(file|image)\b/.test(kind)) {
164
+ const candidates = Array.isArray(args.value) ? args.value : [args.value];
165
+ if (candidates.length !== 1 || typeof candidates[0] !== "string") {
166
+ return { blocker: `${fieldLabel} must contain exactly one file URL.` };
167
+ }
168
+ try {
169
+ const url = new URL(candidates[0]);
170
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw 0;
171
+ return { value: url.toString() };
172
+ } catch {
173
+ return { blocker: `${fieldLabel} must contain exactly one file URL.` };
174
+ }
175
+ }
176
+
177
+ if (/\b(reference|relation)\b/.test(kind)) {
178
+ if (
179
+ args.value &&
180
+ typeof args.value === "object" &&
181
+ !Array.isArray(args.value)
182
+ ) {
183
+ const reference = args.value as Record<string, unknown>;
184
+ if (
185
+ reference["@type"] === "@builder.io/core:Reference" &&
186
+ typeof reference.id === "string" &&
187
+ reference.id.trim() &&
188
+ typeof reference.model === "string" &&
189
+ reference.model.trim()
190
+ ) {
191
+ return {
192
+ value: {
193
+ "@type": "@builder.io/core:Reference",
194
+ id: reference.id.trim(),
195
+ model: reference.model.trim(),
196
+ },
197
+ };
198
+ }
199
+ }
200
+ const ids = Array.isArray(args.value) ? args.value : [args.value];
201
+ const id = ids.length === 1 ? ids[0] : null;
202
+ if (typeof id !== "string" || !id.trim() || !field?.model?.trim()) {
203
+ return {
204
+ blocker: `${fieldLabel} must contain one Builder reference with a model.`,
205
+ };
206
+ }
207
+ return {
208
+ value: {
209
+ "@type": "@builder.io/core:Reference",
210
+ id: id.trim(),
211
+ model: field.model.trim(),
212
+ },
213
+ };
214
+ }
215
+
216
+ const choices = builderOptionLabels(field);
217
+ const isList = /\b(list|array|tags?|multi[-_\s]?select)\b/.test(kind);
218
+ if (isList || choices.length > 0) {
219
+ const values = Array.isArray(args.value) ? args.value : [args.value];
220
+ const labels = values.map((value) => {
221
+ if (typeof value !== "string") return null;
222
+ const trimmed = value.trim();
223
+ // Builder Tags/list fields can be free-form: unlike enums and optioned
224
+ // selects, their model metadata has no finite label inventory. Preserve
225
+ // strict mapping whenever choices exist, but pass non-empty labels
226
+ // through when Builder explicitly exposes no choices to map against.
227
+ if (isList && choices.length === 0) return trimmed || null;
228
+ return (
229
+ choices.find(
230
+ (choice) =>
231
+ choice.label.toLowerCase() === trimmed.toLowerCase() ||
232
+ choice.id === trimmed,
233
+ )?.label ?? null
234
+ );
235
+ });
236
+ if (labels.some((label) => label === null)) {
237
+ return {
238
+ blocker: `${fieldLabel} contains an option that cannot be mapped to a Builder label.`,
239
+ };
240
+ }
241
+ return { value: isList ? labels : labels[0] };
242
+ }
243
+
244
+ return { value: args.value };
245
+ }
246
+
247
+ function hasBuilderRequiredValue(
248
+ value: unknown,
249
+ field: BuilderCmsModelFieldSummary,
250
+ ) {
251
+ if (value === null || value === undefined || value === "") return false;
252
+ if (Array.isArray(value) && value.length === 0) return false;
253
+ const kind = normalizedBuilderFieldKind(field);
254
+ if (/\b(date|datetime)\b/.test(kind)) {
255
+ return (
256
+ (typeof value === "number" && Number.isFinite(value)) ||
257
+ (typeof value === "string" && Number.isFinite(new Date(value).getTime()))
258
+ );
259
+ }
260
+ if (/\b(file|image)\b/.test(kind)) {
261
+ if (typeof value !== "string") return false;
262
+ try {
263
+ const url = new URL(value);
264
+ return url.protocol === "https:" || url.protocol === "http:";
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+ if (/\b(reference|relation)\b/.test(kind)) {
270
+ if (!value || typeof value !== "object" || Array.isArray(value))
271
+ return false;
272
+ const reference = value as Record<string, unknown>;
273
+ return (
274
+ reference["@type"] === "@builder.io/core:Reference" &&
275
+ typeof reference.id === "string" &&
276
+ reference.id.trim().length > 0 &&
277
+ typeof reference.model === "string" &&
278
+ reference.model.trim().length > 0
279
+ );
280
+ }
281
+ if (/\b(list|array|tags?|multi[-_\s]?select)\b/.test(kind)) {
282
+ return Array.isArray(value) && value.length > 0;
283
+ }
284
+ if (/\b(number)\b/.test(kind)) {
285
+ return typeof value === "number" && Number.isFinite(value);
286
+ }
287
+ if (/\b(boolean|checkbox)\b/.test(kind)) return typeof value === "boolean";
288
+ if (/\b(string|text|url|select)\b/.test(kind)) {
289
+ return typeof value === "string" && value.trim().length > 0;
290
+ }
291
+ return true;
292
+ }
293
+
294
+ function portableIntentHash(input: string) {
295
+ const hashPart = (seed: number) => {
296
+ let hash = (0x811c9dc5 ^ seed) >>> 0;
297
+ for (let index = 0; index < input.length; index += 1) {
298
+ hash = Math.imul(hash ^ input.charCodeAt(index), 0x01000193) >>> 0;
299
+ }
300
+ hash ^= hash >>> 16;
301
+ hash = Math.imul(hash, 0x85ebca6b) >>> 0;
302
+ hash ^= hash >>> 13;
303
+ hash = Math.imul(hash, 0xc2b2ae35) >>> 0;
304
+ hash ^= hash >>> 16;
305
+ return (hash >>> 0).toString(16).padStart(8, "0");
306
+ };
307
+
308
+ return [0, 0x9e3779b9, 0x7f4a7c15].map(hashPart).join("");
309
+ }
310
+
311
+ /** A compact, non-secret marker for recovering an ambiguous safe-model POST. */
312
+ export function builderCmsExecutionIntentMarker(idempotencyKey: string) {
313
+ return `agent-native-execution:${portableIntentHash(idempotencyKey)}`;
314
+ }
315
+
75
316
  function builderEffectForWrite(args: {
76
317
  pushMode: ContentDatabaseSourcePushMode;
77
318
  writeMode?: ContentDatabaseSourceWriteMode | null;
@@ -219,6 +460,42 @@ function mergeBuilderPatch(
219
460
  return merged;
220
461
  }
221
462
 
463
+ function requiredBuilderReferencePatch(args: {
464
+ source: ContentDatabaseSource;
465
+ targetRow: ContentDatabaseSource["rows"][number] | null;
466
+ effect: BuilderCmsWriteEffect;
467
+ }) {
468
+ if (
469
+ args.source.sourceTable !== SAFE_WRITE_MODEL ||
470
+ args.effect !== "publish" ||
471
+ !args.targetRow
472
+ ) {
473
+ return {};
474
+ }
475
+ const data: Record<string, unknown> = {};
476
+ for (const field of args.source.metadata.builderModelFields ?? []) {
477
+ if (
478
+ !field.required ||
479
+ !/\b(reference|relation)\b/.test(normalizedBuilderFieldKind(field)) ||
480
+ !field.model?.trim()
481
+ ) {
482
+ continue;
483
+ }
484
+ const sourceFieldKey = `data.${field.name.trim()}`;
485
+ const id =
486
+ args.targetRow.sourceValues?.[
487
+ `__agent_native_builder_reference_id:${sourceFieldKey}`
488
+ ];
489
+ if (typeof id !== "string" || !id.trim()) continue;
490
+ data[field.name] = {
491
+ "@type": "@builder.io/core:Reference",
492
+ id: id.trim(),
493
+ model: field.model.trim(),
494
+ };
495
+ }
496
+ return Object.keys(data).length > 0 ? { data } : {};
497
+ }
498
+
222
499
  function builderBodyPatch(changeSet: ContentDatabaseSourceChangeSet) {
223
500
  const bodyChange = changeSet.bodyChange;
224
501
  const checks: string[] = [];
@@ -263,9 +540,22 @@ function builderRequestForEffect(args: {
263
540
  model: string;
264
541
  entryId: string | null;
265
542
  bodyPatch: Record<string, unknown>;
543
+ currentTitle?: string | null;
544
+ intentMarker?: string;
266
545
  }): BuilderCmsExecutionPayload["request"] {
267
546
  const entryPath = args.entryId ? `/${encodeURIComponent(args.entryId)}` : "";
268
547
  const basePath = `/api/v1/write/${encodeURIComponent(args.model)}${entryPath}`;
548
+ const patchedTitle =
549
+ args.bodyPatch.data &&
550
+ typeof args.bodyPatch.data === "object" &&
551
+ !Array.isArray(args.bodyPatch.data) &&
552
+ typeof (args.bodyPatch.data as Record<string, unknown>).title === "string"
553
+ ? (args.bodyPatch.data as Record<string, unknown>).title
554
+ : null;
555
+ const safeEntryName =
556
+ args.model === SAFE_WRITE_MODEL
557
+ ? patchedTitle || args.currentTitle?.trim() || null
558
+ : null;
269
559
  if (args.effect === "autosave") {
270
560
  return {
271
561
  method: "PATCH",
@@ -274,7 +564,10 @@ function builderRequestForEffect(args: {
274
564
  autoSaveOnly: "true",
275
565
  triggerWebhooks: "false",
276
566
  },
277
- body: args.bodyPatch,
567
+ body: {
568
+ ...args.bodyPatch,
569
+ ...(safeEntryName ? { name: safeEntryName } : {}),
570
+ },
278
571
  };
279
572
  }
280
573
  if (args.effect === "update_in_place") {
@@ -284,7 +577,10 @@ function builderRequestForEffect(args: {
284
577
  query: {
285
578
  triggerWebhooks: "true",
286
579
  },
287
- body: args.bodyPatch,
580
+ body: {
581
+ ...args.bodyPatch,
582
+ ...(safeEntryName ? { name: safeEntryName } : {}),
583
+ },
288
584
  };
289
585
  }
290
586
  if (args.effect === "publish") {
@@ -321,11 +617,70 @@ function builderRequestForEffect(args: {
321
617
  },
322
618
  body: {
323
619
  ...args.bodyPatch,
620
+ ...(safeEntryName ? { name: safeEntryName } : {}),
621
+ data: {
622
+ ...((args.bodyPatch.data && typeof args.bodyPatch.data === "object"
623
+ ? args.bodyPatch.data
624
+ : {}) as Record<string, unknown>),
625
+ ...(args.intentMarker
626
+ ? { [BUILDER_CMS_EXECUTION_MARKER_FIELD]: args.intentMarker }
627
+ : {}),
628
+ },
324
629
  published: "draft",
325
630
  },
326
631
  };
327
632
  }
328
633
 
634
+ function builderRequiredFieldBlockers(args: {
635
+ source: ContentDatabaseSource;
636
+ effect: BuilderCmsWriteEffect;
637
+ targetRow: ContentDatabaseSource["rows"][number] | null;
638
+ request: BuilderCmsExecutionPayload["request"];
639
+ }) {
640
+ if (
641
+ args.source.sourceTable !== SAFE_WRITE_MODEL ||
642
+ (args.effect !== "create_draft" && args.effect !== "publish")
643
+ ) {
644
+ return [];
645
+ }
646
+ const modelFields = args.source.metadata.builderModelFields;
647
+ // Legacy persisted execution gates predate model-schema snapshots. Preserve
648
+ // their exact behavior; newly prepared safe-model gates carry the schema and
649
+ // are validated below before they can become ready.
650
+ if (!modelFields?.length) return [];
651
+ const existingData = Object.fromEntries(
652
+ Object.entries(args.targetRow?.sourceValues ?? {}).flatMap(([key, value]) =>
653
+ key.startsWith("data.") ? [[key.slice("data.".length), value]] : [],
654
+ ),
655
+ );
656
+ const requestData =
657
+ args.request.body.data &&
658
+ typeof args.request.body.data === "object" &&
659
+ !Array.isArray(args.request.body.data)
660
+ ? (args.request.body.data as Record<string, unknown>)
661
+ : {};
662
+ const effectiveData = { ...existingData, ...requestData };
663
+ if (
664
+ effectiveData.blocks === undefined &&
665
+ typeof args.targetRow?.sourceValues?.[BUILDER_CMS_BODY_BLOCKS_HASH_KEY] ===
666
+ "string" &&
667
+ args.targetRow.sourceValues[BUILDER_CMS_BODY_BLOCKS_HASH_KEY].trim()
668
+ ) {
669
+ // Body blocks are intentionally stored outside sourceValues. A reconciled
670
+ // body hash proves that the hydrated Builder body exists without copying a
671
+ // large blocks payload into SQL merely to satisfy this gate.
672
+ effectiveData.blocks = ["reconciled-builder-body"];
673
+ }
674
+ return modelFields.flatMap((field) => {
675
+ if (!field.required) return [];
676
+ const value = effectiveData[field.name];
677
+ if (hasBuilderRequiredValue(value, field)) return [];
678
+ return [
679
+ `Required Builder field ${field.label?.trim() || field.name} is missing or invalid.`,
680
+ ];
681
+ });
682
+ }
683
+
329
684
  function builderSafetyChecks(args: {
330
685
  source: ContentDatabaseSource;
331
686
  changeSet: ContentDatabaseSourceChangeSet;
@@ -339,13 +694,25 @@ function builderSafetyChecks(args: {
339
694
  hasBodyOperation: boolean;
340
695
  bodyChecks: string[];
341
696
  bodyBlockers: string[];
697
+ fieldBlockers: string[];
698
+ requiredFieldBlockers: string[];
342
699
  }) {
343
700
  const checks = [
344
701
  "Requires explicit approval before execution.",
345
702
  "Uses the stored execution idempotency key.",
346
703
  ...args.bodyChecks,
347
704
  ];
348
- const blockers: string[] = [...args.bodyBlockers];
705
+ const blockers: string[] = [
706
+ ...args.bodyBlockers,
707
+ ...args.fieldBlockers,
708
+ ...args.requiredFieldBlockers,
709
+ ];
710
+
711
+ if (args.source.sourceTable !== SAFE_WRITE_MODEL) {
712
+ blockers.push(
713
+ `Live Builder writes are only allowed for ${SAFE_WRITE_MODEL}.`,
714
+ );
715
+ }
349
716
 
350
717
  if (args.operations.length === 0 && !args.hasBodyOperation) {
351
718
  blockers.push("No Builder operations are available for this change.");
@@ -396,14 +763,6 @@ function builderSafetyChecks(args: {
396
763
  if (allowedModes?.length && !allowedModes.includes(args.pushMode)) {
397
764
  blockers.push(`Push mode ${args.pushMode} is not allowed for this source.`);
398
765
  }
399
- if (
400
- args.source.capabilities.liveWritesEnabled === true &&
401
- args.source.sourceTable !== SAFE_WRITE_MODEL
402
- ) {
403
- blockers.push(
404
- `Live Builder writes are only allowed for ${SAFE_WRITE_MODEL}.`,
405
- );
406
- }
407
766
  if (args.source.capabilities.liveWritesEnabled !== true) {
408
767
  checks.push("Does not run while live Builder writes are disabled.");
409
768
  if (
@@ -465,6 +824,7 @@ export function buildBuilderCmsExecutionPlan(args: {
465
824
  }
466
825
 
467
826
  const {
827
+ targetRow,
468
828
  target,
469
829
  entryId: targetEntryId,
470
830
  sourceQualifiedId: targetSourceQualifiedId,
@@ -478,14 +838,44 @@ export function buildBuilderCmsExecutionPlan(args: {
478
838
  entryId: targetEntryId,
479
839
  publicationTransition: args.publicationTransition,
480
840
  });
481
- const operations = args.changeSet.fieldChanges.map((field) => ({
482
- sourceFieldKey: field.sourceFieldKey,
483
- localFieldKey: field.localFieldKey,
484
- value: field.proposedValue,
485
- }));
841
+ const operations: BuilderCmsExecutionOperation[] = [];
842
+ const fieldBlockers: string[] = [];
843
+ for (const field of args.changeSet.fieldChanges) {
844
+ let candidateValue: unknown = field.proposedValue;
845
+ if (
846
+ args.source.sourceTable === SAFE_WRITE_MODEL &&
847
+ field.builderValueJson !== undefined
848
+ ) {
849
+ try {
850
+ candidateValue = JSON.parse(field.builderValueJson) as unknown;
851
+ } catch {
852
+ fieldBlockers.push(
853
+ `${field.propertyName ?? field.sourceFieldKey} has invalid Builder provider JSON.`,
854
+ );
855
+ continue;
856
+ }
857
+ }
858
+ const converted = builderNativeFieldValue({
859
+ source: args.source,
860
+ sourceFieldKey: field.sourceFieldKey,
861
+ value: candidateValue,
862
+ });
863
+ if (converted.blocker) {
864
+ fieldBlockers.push(converted.blocker);
865
+ continue;
866
+ }
867
+ operations.push({
868
+ sourceFieldKey: field.sourceFieldKey,
869
+ localFieldKey: field.localFieldKey,
870
+ value: converted.value,
871
+ });
872
+ }
486
873
  const bodyDiffPatch = builderBodyPatch(args.changeSet);
487
874
  const bodyPatch = mergeBuilderPatch(
488
- nestedBuilderPatch(operations),
875
+ mergeBuilderPatch(
876
+ requiredBuilderReferencePatch({ source: args.source, targetRow, effect }),
877
+ nestedBuilderPatch(operations),
878
+ ),
489
879
  bodyDiffPatch.patch,
490
880
  );
491
881
  // State-preserving effects must not include `published` in the body. Builder
@@ -496,6 +886,23 @@ export function buildBuilderCmsExecutionPlan(args: {
496
886
  model: args.source.sourceTable,
497
887
  entryId: targetEntryId,
498
888
  bodyPatch,
889
+ currentTitle: targetRow?.sourceDisplayKey ?? null,
890
+ intentMarker:
891
+ effect === "create_draft" && args.source.sourceTable === SAFE_WRITE_MODEL
892
+ ? builderCmsExecutionIntentMarker(
893
+ builderCmsExecutionIdempotencyKey({
894
+ sourceId: args.source.id,
895
+ changeSetId: args.changeSet.id,
896
+ pushMode,
897
+ }),
898
+ )
899
+ : undefined,
900
+ });
901
+ const requiredFieldBlockers = builderRequiredFieldBlockers({
902
+ source: args.source,
903
+ effect,
904
+ targetRow,
905
+ request,
499
906
  });
500
907
  const safety = builderSafetyChecks({
501
908
  source: args.source,
@@ -513,6 +920,8 @@ export function buildBuilderCmsExecutionPlan(args: {
513
920
  hasBodyOperation: bodyDiffPatch.hasBodyOperation,
514
921
  bodyChecks: bodyDiffPatch.checks,
515
922
  bodyBlockers: bodyDiffPatch.blockers,
923
+ fieldBlockers,
924
+ requiredFieldBlockers,
516
925
  });
517
926
  const state: ContentDatabaseSourceExecutionState =
518
927
  safety.blockers.length > 0
@@ -1,7 +1,3 @@
1
- import { request as httpRequest } from "node:http";
2
- import type { ClientRequest, IncomingMessage, RequestOptions } from "node:http";
3
- import { request as httpsRequest } from "node:https";
4
-
5
1
  import { resolveBuilderCredential } from "@agent-native/core/server";
6
2
 
7
3
  export interface BuilderCmsWriteRequest {
@@ -17,14 +13,10 @@ export interface BuilderCmsWriteResult {
17
13
  entryId?: string;
18
14
  responseBody: unknown;
19
15
  error?: string;
16
+ ambiguity?: "timeout" | "transport";
20
17
  }
21
18
 
22
19
  type FetchLike = typeof fetch;
23
- type NodeRequestLike = (
24
- url: URL,
25
- options: RequestOptions,
26
- callback: (response: IncomingMessage) => void,
27
- ) => ClientRequest;
28
20
 
29
21
  function builderWriteApiHost() {
30
22
  return (
@@ -97,52 +89,12 @@ function buildWriteResult(args: {
97
89
  };
98
90
  }
99
91
 
100
- async function executeNodeRequest(args: {
101
- url: URL;
102
- method: "POST" | "PATCH";
103
- headers: Record<string, string>;
104
- body: string;
105
- requestImpl?: NodeRequestLike;
106
- }): Promise<BuilderCmsWriteResult> {
107
- const requestImpl =
108
- args.requestImpl ??
109
- (args.url.protocol === "http:" ? httpRequest : httpsRequest);
110
-
111
- return await new Promise((resolve, reject) => {
112
- const request = requestImpl(
113
- args.url,
114
- {
115
- method: args.method,
116
- headers: args.headers,
117
- },
118
- (response) => {
119
- const chunks: Buffer[] = [];
120
- response.on("data", (chunk: Buffer | string) => {
121
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
122
- });
123
- response.on("end", () => {
124
- const status = response.statusCode ?? 0;
125
- resolve(
126
- buildWriteResult({
127
- ok: status >= 200 && status < 300,
128
- status,
129
- responseText: Buffer.concat(chunks).toString("utf8"),
130
- }),
131
- );
132
- });
133
- response.on("error", reject);
134
- },
135
- );
136
- request.on("error", reject);
137
- request.write(args.body);
138
- request.end();
139
- });
140
- }
141
-
142
92
  export async function executeBuilderCmsWrite(args: {
143
93
  request: BuilderCmsWriteRequest;
144
94
  fetchImpl?: FetchLike;
145
- nodeRequestImpl?: NodeRequestLike;
95
+ /** @deprecated Never used: retrying another transport after dispatch is unsafe. */
96
+ nodeRequestImpl?: unknown;
97
+ timeoutMs?: number;
146
98
  }): Promise<BuilderCmsWriteResult> {
147
99
  const privateKey = await readBuilderPrivateKey();
148
100
  if (!privateKey) {
@@ -166,11 +118,15 @@ export async function executeBuilderCmsWrite(args: {
166
118
  "content-type": "application/json",
167
119
  };
168
120
 
121
+ const controller = new AbortController();
122
+ const timeoutMs = Math.max(1, args.timeoutMs ?? 15_000);
123
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
169
124
  try {
170
125
  const response = await (args.fetchImpl ?? fetch)(url, {
171
126
  method: args.request.method,
172
127
  headers,
173
128
  body,
129
+ signal: controller.signal,
174
130
  });
175
131
  return buildWriteResult({
176
132
  ok: response.ok,
@@ -178,31 +134,19 @@ export async function executeBuilderCmsWrite(args: {
178
134
  responseText: await response.text(),
179
135
  });
180
136
  } catch (error) {
181
- if (
182
- args.request.method === "PATCH" &&
183
- (args.nodeRequestImpl || !args.fetchImpl)
184
- ) {
185
- try {
186
- return await executeNodeRequest({
187
- url,
188
- method: args.request.method,
189
- headers,
190
- body,
191
- requestImpl: args.nodeRequestImpl,
192
- });
193
- } catch {
194
- // Return the original fetch failure below; it is usually more specific.
195
- }
196
- }
197
-
137
+ const timedOut = controller.signal.aborted;
198
138
  return {
199
139
  ok: false,
200
140
  status: 0,
201
141
  responseBody: null,
202
- error:
203
- error instanceof Error
204
- ? `Builder write request failed: ${error.message}`
205
- : "Builder write request failed.",
142
+ ambiguity: timedOut ? "timeout" : "transport",
143
+ error: timedOut
144
+ ? `Builder write timed out after ${timeoutMs}ms; remote outcome is unknown.`
145
+ : error instanceof Error
146
+ ? `Builder write transport failed after dispatch; remote outcome is unknown: ${error.message}`
147
+ : "Builder write transport failed after dispatch; remote outcome is unknown.",
206
148
  };
149
+ } finally {
150
+ clearTimeout(timeout);
207
151
  }
208
152
  }