@anchrd/intel 0.59.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/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GateClient } from "@anchrd/gate-sdk";
|
|
2
|
+
import type { AuditService } from "../audit/audit.types.js";
|
|
3
|
+
import type { BrowserAuth } from "../auth/auth.types.js";
|
|
4
|
+
import type { BoardService } from "../boards/boards.types.js";
|
|
5
|
+
import type { BundleService } from "../bundle/bundle.types.js";
|
|
6
|
+
import type { FeedService } from "../feed/feed.types.js";
|
|
7
|
+
import type { FlowService } from "../flows/flows.types.js";
|
|
8
|
+
import type { NodeService } from "../nodes/nodes.types.js";
|
|
9
|
+
import type { ToolService } from "../tools/tools.types.js";
|
|
10
|
+
export interface HttpDeps {
|
|
11
|
+
gate: Pick<GateClient, "authorize">;
|
|
12
|
+
nodes: NodeService;
|
|
13
|
+
flows: FlowService;
|
|
14
|
+
tools: ToolService;
|
|
15
|
+
bundle: BundleService;
|
|
16
|
+
audit: AuditService;
|
|
17
|
+
feed: FeedService;
|
|
18
|
+
boards: BoardService;
|
|
19
|
+
resource: string;
|
|
20
|
+
resourceMetadataUrl: string;
|
|
21
|
+
auth?: Pick<BrowserAuth, "resolve">;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { MaxIndexedAttachmentBytes } from "../contract/node.js";
|
|
2
|
+
import { documentText } from "../shared/document-text/document-text.js";
|
|
3
|
+
export class PermanentIndexingError extends Error {
|
|
4
|
+
}
|
|
5
|
+
// The key under which a node with exactly one vector is filed — since #390 that is every kind. It
|
|
6
|
+
// is the bare node id in the index itself, which is why nothing written before anchrd/intel#301 has
|
|
7
|
+
// to be renamed or embedded again.
|
|
8
|
+
const wholeNodeChunkKey = "";
|
|
9
|
+
// What the pass hands to `purgeVectors` at its end: the names it has just written, in case the node
|
|
10
|
+
// was archived while it worked and the record of them was refused (anchrd/intel#348).
|
|
11
|
+
const writtenKeys = (chunks) => chunks.map((chunk) => chunk.key);
|
|
12
|
+
function indexText(mediaType, content) {
|
|
13
|
+
const text = documentText(mediaType, content);
|
|
14
|
+
// Immutable content never becomes parsable on a retry, so treating this as transient would
|
|
15
|
+
// requeue the version until the queue gives up and would bury the real cause. `documentText`
|
|
16
|
+
// answers `null` rather than throwing, because the other caller — `prompts/get` — owes its reader
|
|
17
|
+
// a different sentence about the same broken payload (#775).
|
|
18
|
+
if (text === null) {
|
|
19
|
+
throw new PermanentIndexingError("Node version content is not a valid BlockNote document");
|
|
20
|
+
}
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What one chunk is embedded as (anchrd/intel#301).
|
|
25
|
+
*
|
|
26
|
+
* The node's title in front of a whole-node chunk, which is the exact string the semantic adapter
|
|
27
|
+
* used to compose on its own side. It moved here because the fingerprint has to cover what is
|
|
28
|
+
* embedded and nothing else: a prefix added downstream would sit outside the comparison and change
|
|
29
|
+
* nothing when it changed. The vector it produces therefore keeps the same name AND the same
|
|
30
|
+
* content it had before this — an upgrading installation loses no answer, though its first pass
|
|
31
|
+
* over any node does embed once more, because `node_vectors` starts empty and has no fingerprint to
|
|
32
|
+
* compare against. That cost is one embedding per node, paid when that node is next saved or when
|
|
33
|
+
* an administrator runs `reindex`, and never per card.
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ Only the WHOLE-node chunk is prefixed, and the distinction is what kept a rename cheap while
|
|
36
|
+
* a node could hold many chunks: a per-chunk prefix would have put the node's title into every
|
|
37
|
+
* fingerprint, so renaming it once cost one embedding per chunk.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ The description travels with the title, for the same reason and at the same place (#816). It
|
|
40
|
+
* is a sentence about the whole node, so a per-chunk prefix would have cost one embedding per chunk
|
|
41
|
+
* for every edit of that one sentence. It is NOT put into `chunk.text`: that field is the passage a
|
|
42
|
+
* searcher is shown, and leading every citation with the description would change what the reader
|
|
43
|
+
* reads rather than what the index can match.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ An absent description leaves this string byte-identical to what it produced before #816, so an
|
|
46
|
+
* upgrading installation re-embeds only the nodes that actually carry one. A node that has a
|
|
47
|
+
* description pays exactly one embedding, on its next save or on the next `reindex`.
|
|
48
|
+
*/
|
|
49
|
+
function embeddedText(target, chunk) {
|
|
50
|
+
if (chunk.key !== wholeNodeChunkKey)
|
|
51
|
+
return chunk.text;
|
|
52
|
+
const description = target.description?.trim() ?? "";
|
|
53
|
+
const prefix = description === "" ? target.title : `${target.title}\n\n${description}`;
|
|
54
|
+
return `${prefix}\n\n${chunk.text}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The vectors this node needs, minus the ones it already has (anchrd/intel#301).
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ This is the answer to "an unchanged chunk must not be embedded again". A save writes the
|
|
60
|
+
* whole document, so without this every save would re-embed everything it holds. The fingerprint of
|
|
61
|
+
* the exact text that was embedded is what tells an untouched chunk from a changed one.
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ The comparison is against D1 and deliberately not against Vectorize. Vectorize writes are
|
|
64
|
+
* asynchronous — a vector upserted a moment ago is not readable yet — so a check made there would
|
|
65
|
+
* answer about the save before last and re-embed a node that had just been saved twice.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ The record is written only after the upsert returned, in the caller. A pass that dies in
|
|
68
|
+
* between leaves the record short, so the next one embeds again; the opposite order would leave a
|
|
69
|
+
* record claiming a vector nobody ever wrote, and nothing would ever notice.
|
|
70
|
+
*/
|
|
71
|
+
async function replaceVectors(deps, target, chunks) {
|
|
72
|
+
const semantic = deps.semantic;
|
|
73
|
+
if (!semantic)
|
|
74
|
+
return;
|
|
75
|
+
const stored = await deps.repository.listVectors(target.nodeId);
|
|
76
|
+
const known = new Map(stored.map((record) => [record.chunkKey, record.fingerprint]));
|
|
77
|
+
const records = [];
|
|
78
|
+
const changed = [];
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
const text = embeddedText(target, chunk);
|
|
81
|
+
const fingerprint = await deps.hash(text);
|
|
82
|
+
if (known.get(chunk.key) !== fingerprint)
|
|
83
|
+
changed.push({ key: chunk.key, text });
|
|
84
|
+
records.push({ chunkKey: chunk.key, fingerprint, passage: chunk.text });
|
|
85
|
+
}
|
|
86
|
+
const wanted = new Set(chunks.map((chunk) => chunk.key));
|
|
87
|
+
const stale = stored.map((record) => record.chunkKey).filter((key) => !wanted.has(key));
|
|
88
|
+
// ⚠️ The one-time sweep of the vector this node had BEFORE it was chunked. An empty record with a
|
|
89
|
+
// chunked node means this is the first pass since anchrd/intel#301 (or since `reindex` emptied
|
|
90
|
+
// the record), and the whole-node vector written under the bare node id is still sitting there —
|
|
91
|
+
// matching questions and answering with the wrong card's passage. Once a record exists this
|
|
92
|
+
// branch is never taken again, so it costs one call per node rather than one per save.
|
|
93
|
+
if (stored.length === 0 && !wanted.has(wholeNodeChunkKey))
|
|
94
|
+
stale.push(wholeNodeChunkKey);
|
|
95
|
+
await semantic.upsert(target, changed);
|
|
96
|
+
if (stale.length > 0)
|
|
97
|
+
await semantic.remove(target.nodeId, stale);
|
|
98
|
+
await deps.repository.replaceVectors(target, records);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Everything an archived node had in the vector index, taken out of it (anchrd/intel#348).
|
|
102
|
+
*
|
|
103
|
+
* ⚠️ The ORDER is the whole safety argument, and it is the exact mirror of the one above. There the
|
|
104
|
+
* record is written last, because a row claiming a vector nobody wrote would make the next pass skip
|
|
105
|
+
* that card forever. Here the record is DELETED last, because the row is the only thing that knows
|
|
106
|
+
* what a vector is called: drop it first and the vectors stay in the index under names nothing left
|
|
107
|
+
* anywhere can produce. A pass that dies in between leaves vectors already gone and a record that
|
|
108
|
+
* still names them — which costs the next pass one embedding per card and nothing else, because the
|
|
109
|
+
* queue redelivers this message and the second run asks Vectorize to forget names it has already
|
|
110
|
+
* forgotten. Both failures are repaired by repeating; neither ends in a wrong answer.
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ The bare node id goes with them whether or not it is in the record. That name is the ONE vector
|
|
113
|
+
* a node can hold without any record of it — everything written before anchrd/intel#301 is filed
|
|
114
|
+
* under it — and an archived node has no later pass in which the one-time sweep in `replaceVectors`
|
|
115
|
+
* could find it. The standing cost of that is one `deleteByIds` of a single name on a redelivered
|
|
116
|
+
* message, which Vectorize answers without complaint for an id it no longer holds.
|
|
117
|
+
*
|
|
118
|
+
* ⚠️ `alsoNamed` is what makes this run after a NORMAL pass too, and it closes the one hole a purge
|
|
119
|
+
* driven from `archive` alone cannot. A pass that has already upserted its vectors when somebody
|
|
120
|
+
* archives the node writes no record at all — `replaceVectors` refuses on `archived_at IS NULL` —
|
|
121
|
+
* so those vectors would sit in the index with nothing anywhere able to name them, and no message
|
|
122
|
+
* left to repair it. The pass knows the names it just wrote, so it hands them over.
|
|
123
|
+
*
|
|
124
|
+
* ⚠️ Without a semantic index configured nothing happens at all, `node_vectors` included. The rows
|
|
125
|
+
* can only have been written while one WAS configured, so a deployment that has temporarily lost the
|
|
126
|
+
* binding must not take the record away — that would leave the vectors behind it unnameable, which
|
|
127
|
+
* is the failure this whole function exists to avoid.
|
|
128
|
+
*
|
|
129
|
+
* ⚠️ One race is left and it is the harmless direction: a restore that lands between the read below
|
|
130
|
+
* and the deletion loses the vectors that restore's own pass had just written. The node keeps
|
|
131
|
+
* answering out of the lexical half — `hydrateVisibleCitations` falls back on the full-text passage
|
|
132
|
+
* when there is no `node_vectors` row — and the next save or `reindex` puts the vectors back. The
|
|
133
|
+
* record is gone with them, so nothing claims otherwise in the meantime.
|
|
134
|
+
*/
|
|
135
|
+
async function purgeVectors(deps, versionId, alsoNamed = []) {
|
|
136
|
+
const semantic = deps.semantic;
|
|
137
|
+
if (!semantic)
|
|
138
|
+
return;
|
|
139
|
+
const nodeId = await deps.repository.archivedNodeId(versionId);
|
|
140
|
+
if (nodeId === null)
|
|
141
|
+
return;
|
|
142
|
+
const recorded = (await deps.repository.listVectors(nodeId)).map((record) => record.chunkKey);
|
|
143
|
+
const keys = [...new Set([wholeNodeChunkKey, ...recorded, ...alsoNamed])];
|
|
144
|
+
await semantic.remove(nodeId, keys);
|
|
145
|
+
await deps.repository.deleteVectors(nodeId);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether this version's bytes are read at all (#821).
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ The question is asked from the SIZE, before anything is fetched, and that is the point: a
|
|
151
|
+
* 50 MB attachment read into memory and handed to the converter as a blob beside it is the whole
|
|
152
|
+
* budget of the isolate, and the pass would end at a limit rather than in an answer. The file is
|
|
153
|
+
* stored either way; what it loses is a search over its text, and it keeps one over its name
|
|
154
|
+
* because the title is its own column in `node_fts` and weighted above the content.
|
|
155
|
+
*
|
|
156
|
+
* ⚠️ Attachments only. Every other kind is text this Worker wrote itself, and the one whose size
|
|
157
|
+
* this field cannot describe — a table, whose content is spread over its versions — is not an
|
|
158
|
+
* attachment.
|
|
159
|
+
*/
|
|
160
|
+
function tooLargeToRead(target) {
|
|
161
|
+
return target.kind === "attachment" && target.size > MaxIndexedAttachmentBytes;
|
|
162
|
+
}
|
|
163
|
+
async function readCanonical(deps, target) {
|
|
164
|
+
if (target.kind === "attachment") {
|
|
165
|
+
const key = target.contentKeys[0];
|
|
166
|
+
return key === undefined ? null : await deps.content.getBytes(key);
|
|
167
|
+
}
|
|
168
|
+
const segments = await Promise.all(target.contentKeys.map(async (key) => await deps.content.get(key)));
|
|
169
|
+
if (segments.length === 0 || segments.some((segment) => segment === null))
|
|
170
|
+
return null;
|
|
171
|
+
return segments.join("");
|
|
172
|
+
}
|
|
173
|
+
export function createIndexing(deps) {
|
|
174
|
+
return {
|
|
175
|
+
async index(versionId) {
|
|
176
|
+
const target = await deps.repository.getTarget(versionId);
|
|
177
|
+
// ⚠️ No target has meant "nothing to do" since the pass existed, and for a superseded version
|
|
178
|
+
// it still does. For an ARCHIVED node it never did: its vectors stayed in the index, invisible
|
|
179
|
+
// because every citation is hydrated through a join on `nodes` — and costing places in a
|
|
180
|
+
// candidate list that Vectorize caps at 100 for everybody (anchrd/intel#348). `purgeVectors`
|
|
181
|
+
// is the one that tells the two apart; this call cannot.
|
|
182
|
+
if (!target) {
|
|
183
|
+
await purgeVectors(deps, versionId);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// A file too big to hold: indexed under its name, with no text beside it, and NOT read
|
|
187
|
+
// (#821). It ends as a finished pass rather than an error, because nothing here is broken and
|
|
188
|
+
// a retry would fail in exactly the same way, forever.
|
|
189
|
+
if (tooLargeToRead(target)) {
|
|
190
|
+
const chunks = [{ key: wholeNodeChunkKey, title: target.title, text: "" }];
|
|
191
|
+
await deps.repository.replace(target, chunks);
|
|
192
|
+
await replaceVectors(deps, target, chunks);
|
|
193
|
+
await purgeVectors(deps, versionId, writtenKeys(chunks));
|
|
194
|
+
await deps.repository.markIndexed(versionId, deps.now().toISOString());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// An attachment is one object of bytes; text kinds may be several, because a table's content
|
|
198
|
+
// is the join of its versions (#40). A missing segment fails the whole pass rather than
|
|
199
|
+
// indexing a table with a hole in it.
|
|
200
|
+
const canonical = await readCanonical(deps, target);
|
|
201
|
+
if (canonical === null) {
|
|
202
|
+
await deps.repository.markError(versionId, "content_missing", deps.now().toISOString());
|
|
203
|
+
throw new Error("Node version content is missing");
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const text = typeof canonical === "string"
|
|
207
|
+
? indexText(target.mediaType, canonical)
|
|
208
|
+
: target.mediaType.startsWith("text/")
|
|
209
|
+
? new TextDecoder().decode(canonical)
|
|
210
|
+
: await deps.converter?.convert(target.title, target.mediaType, canonical);
|
|
211
|
+
if (text === undefined) {
|
|
212
|
+
throw new PermanentIndexingError(`No document converter is configured for ${target.mediaType}`);
|
|
213
|
+
}
|
|
214
|
+
// ⚠️ One chunk per node since #390, and the LIST is what stays. Both indexes read it, so
|
|
215
|
+
// they cannot come to disagree about what a node is made of — which is the property that
|
|
216
|
+
// let a board be one passage and one vector per card (#285, anchrd/intel#301) without
|
|
217
|
+
// either half being told about it separately.
|
|
218
|
+
const chunks = [{ key: wholeNodeChunkKey, title: target.title, text }];
|
|
219
|
+
await deps.repository.replace(target, chunks);
|
|
220
|
+
await replaceVectors(deps, target, chunks);
|
|
221
|
+
// ⚠️ Asked again, at the END of a pass that started on a live node (anchrd/intel#348). The
|
|
222
|
+
// node may have been archived while this pass was embedding, and then `replaceVectors` above
|
|
223
|
+
// wrote no record at all — its statements refuse on `archived_at IS NULL`. The vectors are in
|
|
224
|
+
// the index either way, so without this they would stay there with nothing anywhere able to
|
|
225
|
+
// name them and no message left to try again: the archive's own pass has an empty record to
|
|
226
|
+
// read from. One extra `SELECT` per pass buys that, and on a live node it answers null.
|
|
227
|
+
await purgeVectors(deps, versionId, writtenKeys(chunks));
|
|
228
|
+
await deps.repository.markIndexed(versionId, deps.now().toISOString());
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
// Only Intel's own permanent messages are stored. A converter or fetch failure can carry
|
|
232
|
+
// content excerpts or URLs with query parameters, and this column is read by operators and
|
|
233
|
+
// shown in the UI, so an arbitrary upstream message must not be persisted here.
|
|
234
|
+
await deps.repository.markError(versionId, error instanceof PermanentIndexingError ? error.message.slice(0, 500) : "indexing_failed", deps.now().toISOString());
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DocumentConverter } from "../adapters/document-converter/document-converter.types.js";
|
|
2
|
+
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
|
+
import type { ContentStore, NodeIndexRepository } from "../nodes/nodes.types.js";
|
|
4
|
+
export interface IndexingDeps {
|
|
5
|
+
repository: NodeIndexRepository;
|
|
6
|
+
content: ContentStore;
|
|
7
|
+
semantic?: SemanticIndex | undefined;
|
|
8
|
+
converter?: DocumentConverter | undefined;
|
|
9
|
+
hash(content: string): Promise<string>;
|
|
10
|
+
now(): Date;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { createHttp } from "../http/http.js";
|
|
3
|
+
import { handleMcp } from "../mcp/mcp.js";
|
|
4
|
+
import { authorize, authorizeBearer, bearer, } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
|
+
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
|
+
import { intelProblem, problemDetails } from "../shared/problem-details/problem-details.js";
|
|
7
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
8
|
+
export function createIntel(deps) {
|
|
9
|
+
const baseUrl = deps.baseUrl.replace(/\/+$/, "");
|
|
10
|
+
const resource = `${baseUrl}/mcp`;
|
|
11
|
+
// RFC 9728 inserts the well-known segment between host and resource path: the document for a
|
|
12
|
+
// resource ending in /mcp belongs at /.well-known/oauth-protected-resource/mcp. A client that
|
|
13
|
+
// constructs that URL itself — Intel's own portal client in adapters/openid is one — finds
|
|
14
|
+
// nothing under the bare root path, and only a client that follows WWW-Authenticate gets through.
|
|
15
|
+
const resourceMetadataUrl = `${baseUrl}/.well-known/oauth-protected-resource/mcp`;
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
app.onError((error, context) => {
|
|
18
|
+
if (error instanceof IntelError) {
|
|
19
|
+
return context.json(intelProblem(error), error.status);
|
|
20
|
+
}
|
|
21
|
+
// An IntelError is an expected refusal and explains itself; the unknown exception must leave a
|
|
22
|
+
// trace, or the 500 is undiagnosable — the worker answered, so the platform records no
|
|
23
|
+
// exception of its own (#93). The body stays generic: the log is the operator's channel.
|
|
24
|
+
reportUnexpectedError(error);
|
|
25
|
+
return context.json(problemDetails(500, "internal_error", "Internal server error"), 500);
|
|
26
|
+
});
|
|
27
|
+
app.get("/health", (context) => context.json({ status: "ok" }));
|
|
28
|
+
const resourceMetadata = {
|
|
29
|
+
resource,
|
|
30
|
+
authorization_servers: [deps.gateUrl.replace(/\/+$/, "")],
|
|
31
|
+
bearer_methods_supported: ["header"],
|
|
32
|
+
};
|
|
33
|
+
app.get("/.well-known/oauth-protected-resource/mcp", (context) => context.json(resourceMetadata));
|
|
34
|
+
// The root path stays as a compatibility alias: 0.2.0 published the document only here, and a
|
|
35
|
+
// client that stored that URL from an earlier WWW-Authenticate header keeps working.
|
|
36
|
+
app.get("/.well-known/oauth-protected-resource", (context) => context.json(resourceMetadata));
|
|
37
|
+
const browserAuth = deps.auth;
|
|
38
|
+
if (browserAuth) {
|
|
39
|
+
app.get("/auth/login", async (context) => await browserAuth.login(new URL(context.req.url)));
|
|
40
|
+
// One connect route for the one portal: there are no per-source connections any more. With
|
|
41
|
+
// `?silent=1` it runs `prompt=none`, which is how the Tools screen reaches it without anybody
|
|
42
|
+
// clicking (#60); the route itself is unchanged otherwise, including who may use it.
|
|
43
|
+
app.get("/auth/connect", async (context) => {
|
|
44
|
+
const connectReturnTo = encodeURIComponent("/auth/connect?returnTo=/tools");
|
|
45
|
+
const session = await browserAuth.resolve(context.req.raw.headers);
|
|
46
|
+
if (!session) {
|
|
47
|
+
return context.redirect(`/auth/login?returnTo=${connectReturnTo}`);
|
|
48
|
+
}
|
|
49
|
+
const authorization = await authorizeBearer(deps.gate, session.bearer, resource);
|
|
50
|
+
if (!authorization) {
|
|
51
|
+
return context.redirect(`/auth/login?returnTo=${connectReturnTo}`);
|
|
52
|
+
}
|
|
53
|
+
// ⚠️ Two questions, and both are answered BEFORE the first redirect leaves Intel (#434).
|
|
54
|
+
// `tools:read` is Intel's own door. `mcpPortalRefused()` is Gate's answer to a question Intel
|
|
55
|
+
// cannot ask itself: does this person carry `mcp.connect` at ANY service? Measured against the
|
|
56
|
+
// running installation, somebody without it was sent to `mcp.anchrd.sh/servers-callback` and
|
|
57
|
+
// landed on a white page saying "No allowed servers available, check your Zero trust policies"
|
|
58
|
+
// — a sentence written for an administrator, on a foreign domain, with not one clickable
|
|
59
|
+
// element on it.
|
|
60
|
+
//
|
|
61
|
+
// ⚠️ The helper, never the raw `mcpConnectAnywhere` beside it. The field has THREE states, and
|
|
62
|
+
// `undefined` means "this Gate is older than the question" (anchrd/gate#277), not "nowhere".
|
|
63
|
+
// `if (!authorization.mcpConnectAnywhere)` passes `tsc --noEmit` and `biome check` alike and
|
|
64
|
+
// would turn every reader of such an installation away without a word — the difference is a
|
|
65
|
+
// value, not a type, so nothing but this line prevents it (anchrd/gate#288).
|
|
66
|
+
//
|
|
67
|
+
// ⚠️ And it is deliberately NOT `can("mcp", "connect")`, which is what this guard was first
|
|
68
|
+
// built as: Gate trims `rules` to the ASKING service (GATE-64, least privilege), so that asks
|
|
69
|
+
// `intel.mcp.connect`. The portal is a door onto every server — somebody carrying
|
|
70
|
+
// `dwh.mcp.connect` reaches it and sees the dwh tools, and that spelling would have locked
|
|
71
|
+
// them out. The bit knows no service names, on purpose; it is one bit and carries no more.
|
|
72
|
+
//
|
|
73
|
+
// ⚠️ It spares the certainly wasted trip and promises no successful one: the portal decides
|
|
74
|
+
// on the `mcp` claim frozen into the ID token, and a running Access session carries the old
|
|
75
|
+
// claim for up to 24 hours. Somebody granted a minute ago is NOT refused here, walks on, and
|
|
76
|
+
// meets Access deciding on the very claim state this ticket is about — inside that window the
|
|
77
|
+
// old dead end is still open, and this guard does not close it. It closes the certain one.
|
|
78
|
+
if (!authorization.can("tools", "read") || authorization.mcpPortalRefused()) {
|
|
79
|
+
return context.redirect("/tools?connectError=permission_required");
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return await browserAuth.connect(new URL(context.req.url), context.req.raw.headers, authorization.identity.id);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof IntelError) {
|
|
86
|
+
return context.redirect(`/tools?connectError=${encodeURIComponent(error.code)}`);
|
|
87
|
+
}
|
|
88
|
+
// ⚠️ Everything else used to be rethrown, and the outer handler turned it into a bare 500
|
|
89
|
+
// with a generic body (#425). For an API that is the right answer; for a route a BROWSER is
|
|
90
|
+
// sent to it is a dead end — the reader gets JSON instead of a screen, and the Tools page
|
|
91
|
+
// never learns that the attempt happened at all, so it says "no access" about a sign-in
|
|
92
|
+
// that crashed.
|
|
93
|
+
//
|
|
94
|
+
// The trace still goes to the log, which is the operator's channel; what changes is that
|
|
95
|
+
// the reader is returned to a screen that can say "this broke" instead of "you may not".
|
|
96
|
+
reportUnexpectedError(error);
|
|
97
|
+
return context.redirect("/tools?connectError=portal_sign_in_failed");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// The other end of the same walk: `/auth/connect` starts it, `/auth/callback` finishes it, and
|
|
101
|
+
// a reader who lands in a white JSON page does not distinguish which of the two put them there
|
|
102
|
+
// (#444). Everything that reaches this catch has already lost its handoff — `callback` answers
|
|
103
|
+
// the paths that still know where the person wanted to go — so the destination here is a
|
|
104
|
+
// DECIDED one rather than a remembered one.
|
|
105
|
+
//
|
|
106
|
+
// ⚠️ `/tools` and not `/`: it is the only screen that reads `connectError` and turns it into a
|
|
107
|
+
// sentence (`portalAnswerOf`), and every code but the two about access lands on "the sign-in is
|
|
108
|
+
// broken" there — which is what happened. The root would take the reader somewhere that says
|
|
109
|
+
// nothing at all about the attempt they just made. Somebody who has no Intel session either is
|
|
110
|
+
// sent on to the login by the interface's own 401 handling, once, bounded by the sign-in loop
|
|
111
|
+
// guard (#118).
|
|
112
|
+
app.get("/auth/callback", async (context) => {
|
|
113
|
+
try {
|
|
114
|
+
return await browserAuth.callback(new URL(context.req.url), context.req.raw.headers);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// ⚠️ The IntelError is logged here although `app.onError` deliberately does not log one:
|
|
118
|
+
// an expected refusal explains itself through its body, and this route no longer has a
|
|
119
|
+
// body. `oauth_session_invalid` is raised for two different situations — no readable
|
|
120
|
+
// handoff at all, and a Gate login handoff that ran out — and a deployment where EVERY
|
|
121
|
+
// callback fails (a rotated session key, a clock that drifted, a cookie the browser stopped
|
|
122
|
+
// sending) looks from the outside exactly like one person with a stale tab. Without this
|
|
123
|
+
// line nothing anywhere tells the two apart.
|
|
124
|
+
reportUnexpectedError(error);
|
|
125
|
+
const code = error instanceof IntelError ? error.code : "portal_sign_in_failed";
|
|
126
|
+
return context.redirect(`/tools?connectError=${encodeURIComponent(code)}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
app.post("/auth/logout", () => browserAuth.logout());
|
|
130
|
+
}
|
|
131
|
+
app.route("/api/v1", createHttp({
|
|
132
|
+
gate: deps.gate,
|
|
133
|
+
nodes: deps.nodes,
|
|
134
|
+
flows: deps.flows,
|
|
135
|
+
tools: deps.tools,
|
|
136
|
+
bundle: deps.bundle,
|
|
137
|
+
audit: deps.audit,
|
|
138
|
+
feed: deps.feed,
|
|
139
|
+
boards: deps.boards,
|
|
140
|
+
resource,
|
|
141
|
+
resourceMetadataUrl,
|
|
142
|
+
...(deps.auth ? { auth: deps.auth } : {}),
|
|
143
|
+
}));
|
|
144
|
+
app.all("/mcp", async (context) => {
|
|
145
|
+
const authorization = await authorize(deps.gate, context.req.raw.headers, resource);
|
|
146
|
+
if (!authorization) {
|
|
147
|
+
context.header("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
148
|
+
return context.json({ jsonrpc: "2.0", id: null, error: { code: -32003, message: "authentication required" } }, 401);
|
|
149
|
+
}
|
|
150
|
+
// ⚠️ Intel's OWN portal door, asked per request — and it is deliberately the service-trimmed
|
|
151
|
+
// `can`, the exact spelling the guard on `/auth/connect` above must never use (anchrd/gate#293).
|
|
152
|
+
// The two questions are different and both are needed:
|
|
153
|
+
//
|
|
154
|
+
// /auth/connect "is anything in the SHARED portal for this person" mcpPortalRefused()
|
|
155
|
+
// /mcp "may they reach THIS server" can("mcp", "connect")
|
|
156
|
+
//
|
|
157
|
+
// Until now only Cloudflare Access asked the second one, comparing the `mcp` claim of the Access
|
|
158
|
+
// application against the service name. That claim is collected when the person signs in and a
|
|
159
|
+
// running Access session carries it for up to 24 hours, so a revoked grant kept working for a
|
|
160
|
+
// day while `rules` — the same rows, read live on every call — already said no. This line moves
|
|
161
|
+
// the decision to where the answer is: a revocation lands on the NEXT call, with no new sign-in,
|
|
162
|
+
// no session revocation and nobody pressing anything. It also lets the Access policy go coarse
|
|
163
|
+
// ("is a Gate user") without opening the server to everyone.
|
|
164
|
+
//
|
|
165
|
+
// ⚠️ NOT `permits(...)`: `intel.admin` is the bootstrap escape for Intel's own resource ACLs
|
|
166
|
+
// (ADR-0004 §6), and there is no bootstrap here — this grant is set in Gate, not in Intel, so no
|
|
167
|
+
// admin has to be able to hand it to themselves through this door. Widening it would let people
|
|
168
|
+
// in whom the Access policy turns away today, which is the opposite of moving a check.
|
|
169
|
+
//
|
|
170
|
+
// ⚠️ Fails CLOSED, and that is the whole reason a `403` and not a `401`: the caller IS
|
|
171
|
+
// authenticated and merely may not, so a `WWW-Authenticate` challenge would send an MCP client
|
|
172
|
+
// round the OAuth loop it just completed — the certain dead end of #434 in another costume.
|
|
173
|
+
if (!authorization.can("mcp", "connect")) {
|
|
174
|
+
return context.json({ jsonrpc: "2.0", id: null, error: { code: -32003, message: "permission required" } }, 403);
|
|
175
|
+
}
|
|
176
|
+
return await handleMcp(context.req.raw, {
|
|
177
|
+
authorization,
|
|
178
|
+
// ⚠️ The very token this call was authorized with, so a tool that has to speak to Gate does
|
|
179
|
+
// it as the caller and not as Intel. There is no cookie path on `/mcp`, so `bearer` is the
|
|
180
|
+
// same value `authorize` just used — read again rather than passed out of it, because a
|
|
181
|
+
// function that returned a credential beside its answer would be one somebody logs.
|
|
182
|
+
bearer: bearer(context.req.raw.headers) ?? "",
|
|
183
|
+
flows: deps.flows,
|
|
184
|
+
nodes: deps.nodes,
|
|
185
|
+
tools: deps.tools,
|
|
186
|
+
bundle: deps.bundle,
|
|
187
|
+
audit: deps.audit,
|
|
188
|
+
feed: deps.feed,
|
|
189
|
+
boards: deps.boards,
|
|
190
|
+
prompts: deps.prompts,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return app;
|
|
194
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GateClient } from "@anchrd/gate-sdk";
|
|
2
|
+
import type { AuditService } from "../audit/audit.types.js";
|
|
3
|
+
import type { BrowserAuth } from "../auth/auth.types.js";
|
|
4
|
+
import type { BoardService } from "../boards/boards.types.js";
|
|
5
|
+
import type { BundleService } from "../bundle/bundle.types.js";
|
|
6
|
+
import type { FeedService } from "../feed/feed.types.js";
|
|
7
|
+
import type { FlowService } from "../flows/flows.types.js";
|
|
8
|
+
import type { NodeService } from "../nodes/nodes.types.js";
|
|
9
|
+
import type { Prompts } from "../prompts/prompts.types.js";
|
|
10
|
+
import type { ToolService } from "../tools/tools.types.js";
|
|
11
|
+
export interface IntelDeps {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
gateUrl: string;
|
|
14
|
+
gate: Pick<GateClient, "authorize">;
|
|
15
|
+
nodes: NodeService;
|
|
16
|
+
flows: FlowService;
|
|
17
|
+
tools: ToolService;
|
|
18
|
+
bundle: BundleService;
|
|
19
|
+
audit: AuditService;
|
|
20
|
+
feed: FeedService;
|
|
21
|
+
boards: BoardService;
|
|
22
|
+
prompts: Prompts;
|
|
23
|
+
auth?: BrowserAuth;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|