@agent-native/core-corpus 0.201.0-nightly-20261005153421 → 0.201.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
- package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/analytics/README.md +2 -2
- package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
- package/corpus/templates/analytics/package.json +1 -0
- package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
- package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
- package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
- package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
- package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
- package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
- package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
- package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/assets/CHANGELOG.md +7 -0
- package/corpus/templates/assets/README.md +2 -2
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/server/plugins/auth.ts +2 -1
- package/corpus/templates/assets/server/plugins/otel.ts +6 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
- package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/brain/README.md +12 -8
- package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/lib/audiences.ts +1 -98
- package/corpus/templates/brain/server/lib/connectors.ts +2 -166
- package/corpus/templates/brain/server/plugins/otel.ts +6 -0
- package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/calendar/README.md +2 -2
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
- package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
- package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/chat/README.md +2 -2
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
- package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/clips/CHANGELOG.md +24 -0
- package/corpus/templates/clips/README.md +2 -2
- package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
- package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
- package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
- package/corpus/templates/clips/actions/request-transcript.ts +81 -135
- package/corpus/templates/clips/actions/search-recordings.ts +19 -5
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
- package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
- package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
- package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
- package/corpus/templates/clips/app/routes/record.tsx +16 -6
- package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
- package/corpus/templates/clips/server/plugins/auth.ts +2 -1
- package/corpus/templates/clips/server/plugins/otel.ts +6 -0
- package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/content/CHANGELOG.md +41 -0
- package/corpus/templates/content/README.md +2 -2
- package/corpus/templates/content/actions/_database-navigation.ts +523 -344
- package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
- package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
- package/corpus/templates/content/actions/_database-utils.ts +299 -271
- package/corpus/templates/content/actions/_property-utils.ts +293 -190
- package/corpus/templates/content/actions/_relation-values.ts +3 -1
- package/corpus/templates/content/actions/get-content-database.ts +1 -0
- package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
- package/corpus/templates/content/actions/set-document-property.ts +17 -11
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
- package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
- package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
- package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
- package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
- package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
- package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
- package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
- package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
- package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
- package/corpus/templates/content/app/global.css +16 -8
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
- package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
- package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
- package/corpus/templates/content/app/i18n-data.ts +3 -2
- package/corpus/templates/content/app/lib/content-landing.ts +3 -31
- package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
- package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
- package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
- package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
- package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
- package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
- package/corpus/templates/content/app/root.tsx +3 -1
- package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
- package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
- package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/content/package.json +2 -1
- package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
- package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
- package/corpus/templates/content/server/db/index.ts +1 -1
- package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
- package/corpus/templates/content/server/plugins/auth.ts +2 -1
- package/corpus/templates/content/server/plugins/otel.ts +6 -0
- package/corpus/templates/content/shared/api.ts +17 -0
- package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
- package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
- package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/crm/package.json +1 -0
- package/corpus/templates/crm/server/plugins/otel.ts +6 -0
- package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/design/CHANGELOG.md +41 -0
- package/corpus/templates/design/README.md +2 -2
- package/corpus/templates/design/actions/list-designs.ts +11 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
- package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
- package/corpus/templates/design/app/global.css +1 -34
- package/corpus/templates/design/app/i18n/en-US.ts +4 -0
- package/corpus/templates/design/app/lib/design-filter.ts +4 -2
- package/corpus/templates/design/app/pages/Index.tsx +91 -59
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
- package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/plugins/auth.ts +2 -1
- package/corpus/templates/design/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/dispatch/README.md +2 -2
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
- package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/server/plugins/otel.ts +6 -0
- package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/forms/README.md +2 -2
- package/corpus/templates/forms/package.json +1 -0
- package/corpus/templates/forms/server/plugins/auth.ts +2 -1
- package/corpus/templates/forms/server/plugins/otel.ts +6 -0
- package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/mail/README.md +2 -2
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/server/plugins/auth.ts +2 -1
- package/corpus/templates/mail/server/plugins/otel.ts +6 -0
- package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/plan/CHANGELOG.md +1 -0
- package/corpus/templates/plan/README.md +2 -2
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/server/plugins/auth.ts +2 -1
- package/corpus/templates/plan/server/plugins/otel.ts +6 -0
- package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/slides/CHANGELOG.md +23 -0
- package/corpus/templates/slides/README.md +2 -2
- package/corpus/templates/slides/actions/list-decks.ts +11 -4
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
- package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
- package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
- package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
- package/corpus/templates/slides/app/global.css +3 -54
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
- package/corpus/templates/slides/app/pages/Index.tsx +102 -88
- package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
- package/corpus/templates/slides/server/plugins/auth.ts +2 -1
- package/corpus/templates/slides/server/plugins/otel.ts +6 -0
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
- package/package.json +1 -1
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
import { hasSuggestionBodyTarget } from "../../actions/_suggestion-eligibility.js";
|
|
29
29
|
import { commentIdForIdempotency } from "../../actions/add-comment.js";
|
|
30
30
|
import {
|
|
31
|
-
SUPPORTED_SUGGESTION_BLOCKS,
|
|
32
31
|
SUPPORTED_SUGGESTION_MARKS,
|
|
32
|
+
supportsSuggestionNode,
|
|
33
33
|
} from "../../app/components/editor/suggestions/model.js";
|
|
34
34
|
import { createContentEditorStructuralSchema } from "../../shared/content-editor-structural-schema.js";
|
|
35
35
|
import { mergeDocumentBodyIntents } from "../../shared/document-intent-merge.js";
|
|
@@ -215,8 +215,6 @@ type SuggestionDocumentJson = {
|
|
|
215
215
|
content?: SuggestionDocumentJson[];
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
const SUPPORTED_SUGGESTION_INLINE_NODES = new Set(["hardBreak"]);
|
|
219
|
-
|
|
220
218
|
function unsupportedNotionSpanAttrs(
|
|
221
219
|
attrs: Record<string, unknown> | undefined,
|
|
222
220
|
) {
|
|
@@ -280,11 +278,7 @@ function unsupportedSuggestionStructure(
|
|
|
280
278
|
}
|
|
281
279
|
return result;
|
|
282
280
|
}
|
|
283
|
-
if (
|
|
284
|
-
node.type !== "doc" &&
|
|
285
|
-
!SUPPORTED_SUGGESTION_INLINE_NODES.has(node.type ?? "") &&
|
|
286
|
-
!SUPPORTED_SUGGESTION_BLOCKS.has(node.type ?? "")
|
|
287
|
-
) {
|
|
281
|
+
if (!supportsSuggestionNode(node.type ?? "")) {
|
|
288
282
|
result.push({ path, node });
|
|
289
283
|
return result;
|
|
290
284
|
}
|
|
@@ -325,6 +319,7 @@ function unsupportedStructureKey(markdown: string): string {
|
|
|
325
319
|
function validateSuggestionStructure(
|
|
326
320
|
beforeMarkdown: string,
|
|
327
321
|
afterMarkdown: string,
|
|
322
|
+
refusal: string,
|
|
328
323
|
) {
|
|
329
324
|
const after = parseSuggestionMarkdown(afterMarkdown);
|
|
330
325
|
const surround = unsupportedStructureKey(
|
|
@@ -332,19 +327,14 @@ function validateSuggestionStructure(
|
|
|
332
327
|
);
|
|
333
328
|
if (
|
|
334
329
|
unsupportedStructureKey(beforeMarkdown) !== surround ||
|
|
335
|
-
unsupportedStructureKey(afterMarkdown) !== surround
|
|
336
|
-
) {
|
|
337
|
-
throw new Error(
|
|
338
|
-
"Content v1 suggestions cannot add or change unsupported structures",
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
if (
|
|
330
|
+
unsupportedStructureKey(afterMarkdown) !== surround ||
|
|
342
331
|
JSON.stringify(unsupportedRawNotionSpanAttrs(beforeMarkdown)) !==
|
|
343
|
-
|
|
332
|
+
JSON.stringify(unsupportedRawNotionSpanAttrs(afterMarkdown))
|
|
344
333
|
) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
334
|
+
fail(refusal, {
|
|
335
|
+
statusCode: 422,
|
|
336
|
+
errorCode: "suggestion_structure_unsupported",
|
|
337
|
+
});
|
|
348
338
|
}
|
|
349
339
|
return after;
|
|
350
340
|
}
|
|
@@ -579,11 +569,16 @@ export const contentDocumentSuggestionAdapter: SuggestionAdapter = {
|
|
|
579
569
|
);
|
|
580
570
|
}
|
|
581
571
|
if (before.markdown.includes("<InlineDatabase")) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
572
|
+
fail("Pages with inline databases cannot receive suggestions yet.", {
|
|
573
|
+
statusCode: 409,
|
|
574
|
+
errorCode: "suggestion_body_unavailable",
|
|
575
|
+
});
|
|
585
576
|
}
|
|
586
|
-
validateSuggestionStructure(
|
|
577
|
+
validateSuggestionStructure(
|
|
578
|
+
before.markdown,
|
|
579
|
+
after.markdown,
|
|
580
|
+
"Suggestions cannot change tables, images, or other content or formatting they do not support yet. Suggest changes to the surrounding text instead.",
|
|
581
|
+
);
|
|
587
582
|
return operations;
|
|
588
583
|
},
|
|
589
584
|
async coordinateDecision(context, run) {
|
|
@@ -675,11 +670,13 @@ export const contentDocumentSuggestionAdapter: SuggestionAdapter = {
|
|
|
675
670
|
const nextDocument = validateSuggestionStructure(
|
|
676
671
|
currentContent,
|
|
677
672
|
nextContent,
|
|
673
|
+
"This suggestion changes a table, image, or other content or formatting that suggestions do not support yet, so it cannot be accepted.",
|
|
678
674
|
);
|
|
679
675
|
if (currentContent.includes("<InlineDatabase")) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
676
|
+
fail("Pages containing inline databases cannot accept suggestions yet.", {
|
|
677
|
+
statusCode: 409,
|
|
678
|
+
errorCode: "suggestion_body_unavailable",
|
|
679
|
+
});
|
|
683
680
|
}
|
|
684
681
|
const eligiblePrimaryIds = await assertSuggestionBodyTarget(
|
|
685
682
|
tx,
|
|
@@ -6,7 +6,8 @@ export default createAuthPlugin({
|
|
|
6
6
|
workspaceAppPublicPaths: ["/"],
|
|
7
7
|
marketing: {
|
|
8
8
|
appName: "Content",
|
|
9
|
-
learnMoreUrl:
|
|
9
|
+
learnMoreUrl:
|
|
10
|
+
"https://agent-native.com/apps/content?utm_source=app&utm_medium=product&utm_content=onboarding-learn-more",
|
|
10
11
|
tagline:
|
|
11
12
|
"Open-source Obsidian for MDX: your AI agent edits local docs, creates custom blocks, and organizes everything alongside you.",
|
|
12
13
|
features: [
|
|
@@ -1077,8 +1077,25 @@ export interface ContentDatabaseNavigationPageResponse {
|
|
|
1077
1077
|
hasMore: boolean;
|
|
1078
1078
|
nextCursor: string | null;
|
|
1079
1079
|
};
|
|
1080
|
+
/**
|
|
1081
|
+
* First pages of the requested expanded folders, by parent document ID. A
|
|
1082
|
+
* folder with no children the caller can see is left out.
|
|
1083
|
+
*/
|
|
1084
|
+
branches?: Record<string, ContentDatabaseNavigationPage>;
|
|
1085
|
+
/**
|
|
1086
|
+
* Present when the read asked for expanded folders: true when the cap on
|
|
1087
|
+
* branches per read left out folders it could have opened, which then read
|
|
1088
|
+
* their own pages.
|
|
1089
|
+
*/
|
|
1090
|
+
branchesTruncated?: boolean;
|
|
1080
1091
|
}
|
|
1081
1092
|
|
|
1093
|
+
/** One Files navigation page on its own, without expanded folders. */
|
|
1094
|
+
export type ContentDatabaseNavigationPage = Omit<
|
|
1095
|
+
ContentDatabaseNavigationPageResponse,
|
|
1096
|
+
"branches" | "branchesTruncated"
|
|
1097
|
+
>;
|
|
1098
|
+
|
|
1082
1099
|
export interface BuilderActionTiming {
|
|
1083
1100
|
name: string;
|
|
1084
1101
|
durationMs: number;
|
|
@@ -2,7 +2,9 @@ import { canonicalizeNfm } from "./nfm.js";
|
|
|
2
2
|
import {
|
|
3
3
|
suggestionFormattingChanges,
|
|
4
4
|
suggestionMarkedSourceRanges,
|
|
5
|
+
suggestionSourceAlignment,
|
|
5
6
|
SuggestionFormattingMappingError,
|
|
7
|
+
type SuggestionSourceAlignment,
|
|
6
8
|
} from "./suggestion-formatting.js";
|
|
7
9
|
import { resolveMarkdownSuggestionRange } from "./suggestion-rebase.js";
|
|
8
10
|
|
|
@@ -838,8 +840,13 @@ export function markdownSuggestionOperationsForEditorRevision(input: {
|
|
|
838
840
|
},
|
|
839
841
|
];
|
|
840
842
|
}
|
|
841
|
-
const
|
|
843
|
+
const alignment = suggestionSourceAlignment(input.before);
|
|
844
|
+
const editorBefore = alignment
|
|
845
|
+
? alignment.canonical
|
|
846
|
+
: canonicalizeNfm(input.before);
|
|
842
847
|
const replacements = input.replacements.map(({ from, to }) => {
|
|
848
|
+
const aligned = alignment && alignedRange(alignment, from, to, "stored");
|
|
849
|
+
if (aligned) return aligned;
|
|
843
850
|
const changedText = input.before.slice(from, to);
|
|
844
851
|
const range = resolveMarkdownSuggestionRange(editorBefore, {
|
|
845
852
|
before: { markdown: input.before, changedText },
|
|
@@ -854,47 +861,142 @@ export function markdownSuggestionOperationsForEditorRevision(input: {
|
|
|
854
861
|
if (!range) throw new SuggestionFormattingMappingError();
|
|
855
862
|
return range;
|
|
856
863
|
});
|
|
864
|
+
const operations = markdownSuggestionOperationsForReplacements({
|
|
865
|
+
before: editorBefore,
|
|
866
|
+
after: input.after,
|
|
867
|
+
replacements,
|
|
868
|
+
});
|
|
857
869
|
return isolateSiblingAnchorContexts(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
after: input.after,
|
|
861
|
-
replacements,
|
|
862
|
-
}).map((operation, ordinal) => {
|
|
863
|
-
const range = resolveMarkdownSuggestionRange(input.before, operation);
|
|
864
|
-
if (!range) throw new SuggestionFormattingMappingError();
|
|
865
|
-
return operationForChange(
|
|
870
|
+
(alignment &&
|
|
871
|
+
alignedStoredOperations(
|
|
866
872
|
input.before,
|
|
873
|
+
input.after,
|
|
874
|
+
operations,
|
|
875
|
+
alignment,
|
|
876
|
+
)) ??
|
|
877
|
+
operations.map((operation, ordinal) => {
|
|
878
|
+
const range = resolveMarkdownSuggestionRange(input.before, operation);
|
|
879
|
+
if (!range) throw new SuggestionFormattingMappingError();
|
|
880
|
+
return operationForChange(
|
|
881
|
+
input.before,
|
|
882
|
+
range.from,
|
|
883
|
+
range.to,
|
|
884
|
+
operation.after.changedText,
|
|
885
|
+
ordinal,
|
|
886
|
+
);
|
|
887
|
+
}),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function alignedRange(
|
|
892
|
+
alignment: SuggestionSourceAlignment,
|
|
893
|
+
from: number,
|
|
894
|
+
to: number,
|
|
895
|
+
side: "canonical" | "stored",
|
|
896
|
+
) {
|
|
897
|
+
const alignedFrom = alignment.map(from, side);
|
|
898
|
+
const alignedTo = alignment.map(to, side);
|
|
899
|
+
return alignedFrom === null || alignedTo === null
|
|
900
|
+
? null
|
|
901
|
+
: { from: alignedFrom, to: alignedTo };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function alignedStoredOperations(
|
|
905
|
+
before: string,
|
|
906
|
+
after: string,
|
|
907
|
+
operations: MarkdownSuggestionOperation[],
|
|
908
|
+
alignment: SuggestionSourceAlignment,
|
|
909
|
+
): MarkdownSuggestionOperation[] | null {
|
|
910
|
+
const stored: MarkdownSuggestionOperation[] = [];
|
|
911
|
+
for (const operation of operations) {
|
|
912
|
+
const range = alignedRange(
|
|
913
|
+
alignment,
|
|
914
|
+
operation.anchor.from,
|
|
915
|
+
operation.anchor.to,
|
|
916
|
+
"canonical",
|
|
917
|
+
);
|
|
918
|
+
if (!range) return null;
|
|
919
|
+
stored.push(
|
|
920
|
+
operationForChange(
|
|
921
|
+
before,
|
|
867
922
|
range.from,
|
|
868
923
|
range.to,
|
|
869
924
|
operation.after.changedText,
|
|
870
|
-
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
|
|
925
|
+
stored.length,
|
|
926
|
+
),
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
let reconstructed = before;
|
|
930
|
+
for (const operation of [...stored].reverse())
|
|
931
|
+
reconstructed =
|
|
932
|
+
reconstructed.slice(0, operation.anchor.from) +
|
|
933
|
+
operation.after.changedText +
|
|
934
|
+
reconstructed.slice(operation.anchor.to);
|
|
935
|
+
// An edit spanning a gap writes the editor's syntax for it, which a stored
|
|
936
|
+
// pipe table cannot hold; only a reparse shows the page still matches.
|
|
937
|
+
return reconstructed === after ||
|
|
938
|
+
canonicalizeNfm(reconstructed) === canonicalizeNfm(after)
|
|
939
|
+
? stored
|
|
940
|
+
: null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Replays the operations onto the base's canonical form; the result must be
|
|
944
|
+
// the draft byte for byte, so every anchor lands where the editor shows it.
|
|
945
|
+
function alignedDraftAnchors(
|
|
946
|
+
operations: readonly MarkdownSuggestionOperation[],
|
|
947
|
+
draft: string,
|
|
948
|
+
): MarkdownSuggestionOperation["anchor"][] | null {
|
|
949
|
+
const alignment = suggestionSourceAlignment(operations[0]!.before.markdown);
|
|
950
|
+
if (!alignment) return null;
|
|
951
|
+
const ranges: Array<{ from: number; to: number }> = [];
|
|
952
|
+
let reconstructed = "";
|
|
953
|
+
let cursor = 0;
|
|
954
|
+
for (const operation of operations) {
|
|
955
|
+
const range = alignedRange(
|
|
956
|
+
alignment,
|
|
957
|
+
operation.anchor.from,
|
|
958
|
+
operation.anchor.to,
|
|
959
|
+
"stored",
|
|
960
|
+
);
|
|
961
|
+
if (!range || range.from < cursor) return null;
|
|
962
|
+
reconstructed += alignment.canonical.slice(cursor, range.from);
|
|
963
|
+
ranges.push({
|
|
964
|
+
from: reconstructed.length,
|
|
965
|
+
to: reconstructed.length + operation.after.changedText.length,
|
|
966
|
+
});
|
|
967
|
+
reconstructed += operation.after.changedText;
|
|
968
|
+
cursor = range.to;
|
|
969
|
+
}
|
|
970
|
+
if (reconstructed + alignment.canonical.slice(cursor) !== draft) return null;
|
|
971
|
+
return ranges.map(({ from, to }) => ({
|
|
972
|
+
from,
|
|
973
|
+
to,
|
|
974
|
+
prefix: draft.slice(Math.max(0, from - 32), from),
|
|
975
|
+
suffix: draft.slice(to, to + 32),
|
|
976
|
+
}));
|
|
874
977
|
}
|
|
875
978
|
|
|
876
979
|
export function draftSuggestionAnchors(
|
|
877
980
|
operations: readonly MarkdownSuggestionOperation[],
|
|
878
981
|
draft: string,
|
|
879
982
|
): MarkdownSuggestionOperation["anchor"][] {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
let proposedRaw = operations[0]?.before.markdown ?? draft;
|
|
983
|
+
if (operations.length === 0) return [];
|
|
984
|
+
const aligned = alignedDraftAnchors(operations, draft);
|
|
985
|
+
if (aligned) return aligned;
|
|
986
|
+
let proposedRaw = operations[0]!.before.markdown;
|
|
885
987
|
for (const operation of [...operations].reverse()) {
|
|
886
988
|
proposedRaw =
|
|
887
989
|
proposedRaw.slice(0, operation.anchor.from) +
|
|
888
990
|
operation.after.changedText +
|
|
889
991
|
proposedRaw.slice(operation.anchor.to);
|
|
890
992
|
}
|
|
891
|
-
const parts =
|
|
892
|
-
if (!
|
|
993
|
+
const parts = diffParts(proposedRaw, draft);
|
|
994
|
+
if (!parts) throw new SuggestionFormattingMappingError();
|
|
893
995
|
const boundaryMap = new Array<number>(proposedRaw.length + 1);
|
|
894
996
|
let rawOffset = 0;
|
|
895
997
|
let draftOffset = 0;
|
|
896
998
|
boundaryMap[0] = 0;
|
|
897
|
-
for (const part of parts
|
|
999
|
+
for (const part of parts) {
|
|
898
1000
|
if (part.type === "insert") {
|
|
899
1001
|
draftOffset += part.text.length;
|
|
900
1002
|
boundaryMap[rawOffset] = draftOffset;
|
|
@@ -906,27 +1008,11 @@ export function draftSuggestionAnchors(
|
|
|
906
1008
|
boundaryMap[rawOffset] = draftOffset;
|
|
907
1009
|
}
|
|
908
1010
|
}
|
|
909
|
-
if (
|
|
910
|
-
!canonical &&
|
|
911
|
-
(rawOffset !== proposedRaw.length || draftOffset !== draft.length)
|
|
912
|
-
)
|
|
1011
|
+
if (rawOffset !== proposedRaw.length || draftOffset !== draft.length)
|
|
913
1012
|
throw new SuggestionFormattingMappingError();
|
|
914
1013
|
|
|
915
1014
|
let delta = 0;
|
|
916
1015
|
return operations.map((operation) => {
|
|
917
|
-
if (canonical) {
|
|
918
|
-
const from = operation.anchor.from + delta;
|
|
919
|
-
const to = from + operation.after.changedText.length;
|
|
920
|
-
delta +=
|
|
921
|
-
operation.after.changedText.length -
|
|
922
|
-
operation.before.changedText.length;
|
|
923
|
-
return {
|
|
924
|
-
from,
|
|
925
|
-
to,
|
|
926
|
-
prefix: draft.slice(Math.max(0, from - 32), from),
|
|
927
|
-
suffix: draft.slice(to, to + 32),
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
1016
|
const rawFrom = operation.anchor.from + delta;
|
|
931
1017
|
const rawTo = rawFrom + operation.after.changedText.length;
|
|
932
1018
|
const from = boundaryMap[rawFrom];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
canonicalizeNfm,
|
|
2
3
|
docToNfm,
|
|
3
4
|
nfmToDoc,
|
|
4
5
|
serializeInlineTextNodeWithOffsets,
|
|
@@ -55,7 +56,11 @@ function longestBacktickRun(text: string): number {
|
|
|
55
56
|
return Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function formattingRuns(source: string): {
|
|
59
|
+
function formattingRuns(source: string): {
|
|
60
|
+
runs: TextRun[];
|
|
61
|
+
restored: string;
|
|
62
|
+
canonicalSpans: SourceSpan[];
|
|
63
|
+
} | null {
|
|
59
64
|
const doc = nfmToDoc(source);
|
|
60
65
|
let markerPrefix = "suggestiontextboundary";
|
|
61
66
|
while (source.includes(markerPrefix)) markerPrefix += "z";
|
|
@@ -133,12 +138,103 @@ function formattingRuns(source: string): { runs: TextRun[] } | null {
|
|
|
133
138
|
},
|
|
134
139
|
);
|
|
135
140
|
if (unmappable || runs.some((run) => run.sourceFrom < 0)) return null;
|
|
141
|
+
const canonicalSpans = runs.map(({ sourceFrom, sourceTo, textOffsets }) => ({
|
|
142
|
+
from: sourceFrom,
|
|
143
|
+
to: sourceTo,
|
|
144
|
+
textOffsets,
|
|
145
|
+
}));
|
|
136
146
|
if (
|
|
137
147
|
restored !== source &&
|
|
138
148
|
!mapStoredSourceRuns(source, restored, runs, markers, withPlaceholders)
|
|
139
149
|
)
|
|
140
150
|
return null;
|
|
141
|
-
return { runs };
|
|
151
|
+
return { runs, restored, canonicalSpans };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type SourceSpan = TextRange & { textOffsets: number[] };
|
|
155
|
+
type SourceSide = "canonical" | "stored";
|
|
156
|
+
|
|
157
|
+
export type SuggestionSourceAlignment = {
|
|
158
|
+
canonical: string;
|
|
159
|
+
map(offset: number, from: SourceSide): number | null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let lastAlignment: {
|
|
163
|
+
source: string;
|
|
164
|
+
alignment: SuggestionSourceAlignment | null;
|
|
165
|
+
} | null = null;
|
|
166
|
+
|
|
167
|
+
// A draft session maps every keystroke against the same stored base.
|
|
168
|
+
export function suggestionSourceAlignment(
|
|
169
|
+
source: string,
|
|
170
|
+
): SuggestionSourceAlignment | null {
|
|
171
|
+
if (lastAlignment?.source !== source)
|
|
172
|
+
lastAlignment = { source, alignment: sourceAlignment(source) };
|
|
173
|
+
return lastAlignment.alignment;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pairs each text run's canonical and stored bytes. An offset maps inside a
|
|
177
|
+
// run, at a run or gap edge, or inside a gap whose bytes match on both sides;
|
|
178
|
+
// anywhere else the two serializations disagree and the offset has no image.
|
|
179
|
+
function sourceAlignment(source: string): SuggestionSourceAlignment | null {
|
|
180
|
+
const canonical = canonicalizeNfm(source);
|
|
181
|
+
const inside = (offset: number, text: string) =>
|
|
182
|
+
Number.isInteger(offset) && offset >= 0 && offset <= text.length;
|
|
183
|
+
if (canonical === source)
|
|
184
|
+
return {
|
|
185
|
+
canonical,
|
|
186
|
+
map: (offset) => (inside(offset, source) ? offset : null),
|
|
187
|
+
};
|
|
188
|
+
const mapped = formattingRuns(source);
|
|
189
|
+
if (!mapped || mapped.restored !== canonical) return null;
|
|
190
|
+
const texts = { canonical, stored: source };
|
|
191
|
+
const spans: Array<Record<SourceSide, SourceSpan>> = mapped.runs.map(
|
|
192
|
+
(run, index) => ({
|
|
193
|
+
canonical: mapped.canonicalSpans[index]!,
|
|
194
|
+
stored: {
|
|
195
|
+
from: run.sourceFrom,
|
|
196
|
+
to: run.sourceTo,
|
|
197
|
+
textOffsets: run.textOffsets,
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
canonical,
|
|
203
|
+
map(offset, from) {
|
|
204
|
+
if (!inside(offset, texts[from])) return null;
|
|
205
|
+
const to: SourceSide = from === "canonical" ? "stored" : "canonical";
|
|
206
|
+
type Edge = Record<SourceSide, number>;
|
|
207
|
+
const gap = (start: Edge, end: Edge) => {
|
|
208
|
+
if (offset === start[from]) return start[to];
|
|
209
|
+
if (offset === end[from]) return end[to];
|
|
210
|
+
return texts[from].slice(start[from], end[from]) ===
|
|
211
|
+
texts[to].slice(start[to], end[to])
|
|
212
|
+
? start[to] + offset - start[from]
|
|
213
|
+
: null;
|
|
214
|
+
};
|
|
215
|
+
let previous: Edge = { canonical: 0, stored: 0 };
|
|
216
|
+
for (const span of spans) {
|
|
217
|
+
const own = span[from];
|
|
218
|
+
const other = span[to];
|
|
219
|
+
if (offset < own.from)
|
|
220
|
+
return gap(previous, {
|
|
221
|
+
canonical: span.canonical.from,
|
|
222
|
+
stored: span.stored.from,
|
|
223
|
+
});
|
|
224
|
+
if (offset <= own.to) {
|
|
225
|
+
if (offset === own.from) return other.from;
|
|
226
|
+
if (offset === own.to) return other.to;
|
|
227
|
+
const index = own.textOffsets.indexOf(offset - own.from);
|
|
228
|
+
return index < 0 ? null : other.from + other.textOffsets[index]!;
|
|
229
|
+
}
|
|
230
|
+
previous = { canonical: span.canonical.to, stored: span.stored.to };
|
|
231
|
+
}
|
|
232
|
+
return gap(previous, {
|
|
233
|
+
canonical: canonical.length,
|
|
234
|
+
stored: source.length,
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
};
|
|
142
238
|
}
|
|
143
239
|
|
|
144
240
|
function normalizedSourceGap(source: string, from: number, to: number): string {
|
|
@@ -155,6 +251,28 @@ function normalizedSourceGap(source: string, from: number, to: number): string {
|
|
|
155
251
|
.slice(prefix.length);
|
|
156
252
|
}
|
|
157
253
|
|
|
254
|
+
const TABLE_TAG = /<\/?(?:table|tr|td|th)\b[^>]*>/;
|
|
255
|
+
|
|
256
|
+
// Canonical NFM writes a stored pipe table as an HTML table. Both reduce to
|
|
257
|
+
// "|" between cells and a newline between rows, with or without the stored
|
|
258
|
+
// rows' optional outer pipes; the final reparse proves the cells.
|
|
259
|
+
function tableBoundaries(
|
|
260
|
+
gap: string,
|
|
261
|
+
startsLine: boolean,
|
|
262
|
+
endsLine: boolean,
|
|
263
|
+
): string {
|
|
264
|
+
return `${startsLine ? "\n" : ""}${gap}${endsLine ? "\n" : ""}`
|
|
265
|
+
.replace(/^[ \t|:-]*$/gm, (line) =>
|
|
266
|
+
line.includes("|") && line.includes("-") ? "" : line,
|
|
267
|
+
)
|
|
268
|
+
.replace(/[ \t]*\|?[ \t]*\n[ \t]*\|?/g, "\n")
|
|
269
|
+
.replace(/<tr\b[^>]*>\s*<t[dh]\b[^>]*>/g, "\n")
|
|
270
|
+
.replace(/<t[dh]\b[^>]*>/g, "|")
|
|
271
|
+
.replace(/<\/?(?:table|tr|td|th)\b[^>]*>/g, "\n")
|
|
272
|
+
.replace(/\s*\|\s*/g, "|")
|
|
273
|
+
.replace(/\s*\n\s*/g, "\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
158
276
|
function mapStoredSourceRuns(
|
|
159
277
|
source: string,
|
|
160
278
|
canonical: string,
|
|
@@ -188,7 +306,20 @@ function mapStoredSourceRuns(
|
|
|
188
306
|
stored = stored.replace(/\n+$/, "");
|
|
189
307
|
expected = expected.replace(/\n+$/, "");
|
|
190
308
|
}
|
|
191
|
-
|
|
309
|
+
if (stored === expected) return true;
|
|
310
|
+
const lineEdges = (text: string, start: number, end: number) =>
|
|
311
|
+
[
|
|
312
|
+
start === 0 || /[\r\n]/.test(text[start - 1]!),
|
|
313
|
+
end === text.length || /[\r\n]/.test(text[end]!),
|
|
314
|
+
] as const;
|
|
315
|
+
return (
|
|
316
|
+
TABLE_TAG.test(canonical.slice(canonicalFrom, canonicalTo)) &&
|
|
317
|
+
tableBoundaries(stored, ...lineEdges(source, from, to)) ===
|
|
318
|
+
tableBoundaries(
|
|
319
|
+
expected,
|
|
320
|
+
...lineEdges(canonical, canonicalFrom, canonicalTo),
|
|
321
|
+
)
|
|
322
|
+
);
|
|
192
323
|
};
|
|
193
324
|
let cursor = 0;
|
|
194
325
|
let canonicalCursor = 0;
|
|
@@ -290,9 +290,16 @@ export default defineEventHandler(async (event) => {
|
|
|
290
290
|
|
|
291
291
|
## Data Scoping
|
|
292
292
|
|
|
293
|
-
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views
|
|
293
|
+
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views, and checks every statement before it runs so it can only reach those views.
|
|
294
294
|
|
|
295
|
-
The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path)
|
|
295
|
+
The `db-query` / `db-exec` / `db-patch` tools (and the extension SQL bridge, which shares the same path) read each statement the way Postgres does and refuse anything that could reach data around the temp views:
|
|
296
|
+
|
|
297
|
+
- schema- or database-qualified names like `public.<table>`, which resolve to the base table;
|
|
298
|
+
- names that resolve to anything other than the current user's temp views, such as materialized views or tables the scoping does not cover;
|
|
299
|
+
- app-defined SQL or PL/pgSQL functions and operators, whose bodies read tables without the views;
|
|
300
|
+
- built-ins that run SQL text, change session settings, or reach outside the database (`query_to_xml`, `set_config`, `dblink`, most `pg_*` functions).
|
|
301
|
+
|
|
302
|
+
`db-query` runs in a read-only transaction, and an `INSERT` must list its columns when the table has access-control columns. Use bare table names and plain expressions; scoping is applied automatically. When the agent needs data the views do not expose, add an app action with its own access checks.
|
|
296
303
|
|
|
297
304
|
### Per-User Scoping (`owner_email`)
|
|
298
305
|
|
|
@@ -75,10 +75,11 @@ state their PostgreSQL types directly.
|
|
|
75
75
|
|
|
76
76
|
Member offboarding and email changes refuse to run while any column named
|
|
77
77
|
`email`, `*_email`, `*scope_id`, `created_by`, `updated_by`, `invited_by`,
|
|
78
|
-
`owner`, `principal_id`, `session_id`, or `user_id` has no policy.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
`owner`, `principal_id`, `session_id`, or `user_id` has no policy.
|
|
79
|
+
`createSharesTable()` tables are handled for you, and undeclared `owner_email`
|
|
80
|
+
columns transfer to the successor, which is right only for owned content.
|
|
81
|
+
Declare every other one, including columns that are not member identities, from
|
|
82
|
+
the app's database plugin graph (Clips does it in `server/db/index.ts`):
|
|
82
83
|
|
|
83
84
|
```ts
|
|
84
85
|
import { registerIdentityColumns } from "@agent-native/core/org";
|
|
@@ -88,13 +89,17 @@ registerIdentityColumns([
|
|
|
88
89
|
{ table: "space_members", column: "email", emailChange: "rekey", offboard: "delete", orgScope: { column: "space_id", references: { table: "spaces", column: "id", orgColumn: "org_id" } }, reason: "Space membership grants access." },
|
|
89
90
|
// Someone else's address: never rewritten.
|
|
90
91
|
{ table: "meeting_participants", column: "email", emailChange: "retain", offboard: "retain", reason: "Attendee address from the calendar provider." },
|
|
92
|
+
// A credential in an owner_email table: never handed to the successor.
|
|
93
|
+
{ table: "api_keys", column: "owner_email", emailChange: "rekey", offboard: "revoke", reason: "Bearer keys act as their owner." },
|
|
91
94
|
]);
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Choose `delete` for grants, credentials, and pending tokens
|
|
97
|
+
Choose `delete` for grants, credentials, and pending tokens, or `revoke` (sets
|
|
98
|
+
`revoked_at`) when a reader treats a missing row as "not revoked"; `retain` for
|
|
95
99
|
attribution, history, and third-party addresses; `transfer` only for owned
|
|
96
|
-
data.
|
|
97
|
-
|
|
100
|
+
data. Declare `owner_email` tables that hold credentials or grants, or
|
|
101
|
+
offboarding hands them to the successor. A table without `org_id` needs
|
|
102
|
+
`orgScope` or an organization-scoped removal leaves its rows alone.
|
|
98
103
|
|
|
99
104
|
| Template | Tables |
|
|
100
105
|
| ------------ | --------------------------------------------- |
|
|
@@ -290,9 +290,16 @@ export default defineEventHandler(async (event) => {
|
|
|
290
290
|
|
|
291
291
|
## Data Scoping
|
|
292
292
|
|
|
293
|
-
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views
|
|
293
|
+
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views, and checks every statement before it runs so it can only reach those views.
|
|
294
294
|
|
|
295
|
-
The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path)
|
|
295
|
+
The `db-query` / `db-exec` / `db-patch` tools (and the extension SQL bridge, which shares the same path) read each statement the way Postgres does and refuse anything that could reach data around the temp views:
|
|
296
|
+
|
|
297
|
+
- schema- or database-qualified names like `public.<table>`, which resolve to the base table;
|
|
298
|
+
- names that resolve to anything other than the current user's temp views, such as materialized views or tables the scoping does not cover;
|
|
299
|
+
- app-defined SQL or PL/pgSQL functions and operators, whose bodies read tables without the views;
|
|
300
|
+
- built-ins that run SQL text, change session settings, or reach outside the database (`query_to_xml`, `set_config`, `dblink`, most `pg_*` functions).
|
|
301
|
+
|
|
302
|
+
`db-query` runs in a read-only transaction, and an `INSERT` must list its columns when the table has access-control columns. Use bare table names and plain expressions; scoping is applied automatically. When the agent needs data the views do not expose, add an app action with its own access checks.
|
|
296
303
|
|
|
297
304
|
### Per-User Scoping (`owner_email`)
|
|
298
305
|
|