@k2b/cloud 0.38.0 → 0.40.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 +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
package/README.md
CHANGED
|
@@ -83,6 +83,14 @@ Application setup, public contracts, and deployment guidance:
|
|
|
83
83
|
- **[Create the first application](https://github.com/k2b-dev/cloud/blob/main/docs-site/docs/en/build/getting-started.md)** — standalone project setup and a verified API endpoint
|
|
84
84
|
- **[github.com/k2b-dev/cloud](https://github.com/k2b-dev/cloud)** — the platform monorepo (gateway, core, all platform apps)
|
|
85
85
|
|
|
86
|
+
## Security
|
|
87
|
+
|
|
88
|
+
Report a vulnerability privately through
|
|
89
|
+
[GitHub Security Advisories](https://github.com/k2b-dev/cloud/security/advisories/new),
|
|
90
|
+
never in a public issue. Security fixes appear in this package's changelog,
|
|
91
|
+
and a published advisory names the first patched version. See the
|
|
92
|
+
[security policy](https://github.com/k2b-dev/cloud/blob/main/SECURITY.md).
|
|
93
|
+
|
|
86
94
|
## License
|
|
87
95
|
|
|
88
96
|
GNU Affero General Public License v3.0 or later.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -34,14 +34,17 @@
|
|
|
34
34
|
"./browser/app-badges": "./src/browser/app-badges.ts",
|
|
35
35
|
"./browser/live": "./src/browser/live.ts",
|
|
36
36
|
"./browser/mermaid": "./src/browser/mermaid.ts",
|
|
37
|
+
"./browser/hints": "./src/browser/hints.ts",
|
|
37
38
|
"./browser/reload": "./src/browser/reload.ts",
|
|
38
39
|
"./browser/notifications": "./src/browser/notifications.ts",
|
|
39
40
|
"./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
|
|
40
41
|
"./browser/commands": "./src/browser/commands.ts",
|
|
42
|
+
"./browser/panel": "./src/browser/panel.ts",
|
|
41
43
|
"./browser/search": "./src/browser/search.ts",
|
|
42
44
|
"./browser/widgets": "./src/browser/widgets.ts",
|
|
43
45
|
"./browser/testing": "./src/browser/testing.ts",
|
|
44
46
|
"./browser/resource-picker": "./src/browser/resource-picker.tsx",
|
|
47
|
+
"./browser/composer-insert": "./src/browser/composer-insert.ts",
|
|
45
48
|
"./browser/files": "./src/browser/files.tsx",
|
|
46
49
|
"./capabilities": "./src/capabilities/client.ts",
|
|
47
50
|
"./capabilities/server": "./src/capabilities/server.ts",
|
|
@@ -54,6 +57,7 @@
|
|
|
54
57
|
"./cli/capabilities": "./src/cli/capabilities.ts",
|
|
55
58
|
"./cli/admin": "./src/cli/admin/index.ts",
|
|
56
59
|
"./shared": "./src/shared/index.ts",
|
|
60
|
+
"./shared-text": "./src/shared-text/index.ts",
|
|
57
61
|
"./services": "./src/services/index.ts",
|
|
58
62
|
"./services/image-metadata": "./src/services/image-metadata.ts",
|
|
59
63
|
"./services/document-extraction": "./src/services/document-extraction.ts",
|
|
@@ -103,13 +107,14 @@
|
|
|
103
107
|
"@firecrawl/anydoc": "0.2.4",
|
|
104
108
|
"@napi-rs/canvas": "1.0.9",
|
|
105
109
|
"pdfjs-dist": "6.3.289",
|
|
110
|
+
"sharp": "0.35.4",
|
|
106
111
|
"@simplewebauthn/server": "14.0.3",
|
|
107
112
|
"@tabler/icons": "3.48.0",
|
|
108
113
|
"@tailwindcss/typography": "0.5.20",
|
|
109
114
|
"@k2b/nessi": "0.18.0",
|
|
110
|
-
"@k2b/ssr": "0.
|
|
111
|
-
"@k2b/ui": "0.
|
|
112
|
-
"@k2b/stdlib": "0.
|
|
115
|
+
"@k2b/ssr": "0.18.0",
|
|
116
|
+
"@k2b/ui": "0.30.0",
|
|
117
|
+
"@k2b/stdlib": "0.30.0",
|
|
113
118
|
"@k2b/sync": "7.0.0",
|
|
114
119
|
"@nats-io/jetstream": "3.4.0",
|
|
115
120
|
"@nats-io/transport-node": "3.4.0",
|
|
@@ -122,6 +127,7 @@
|
|
|
122
127
|
"liquidjs": "10.29.0",
|
|
123
128
|
"marked": "18.0.14",
|
|
124
129
|
"mermaid": "12.0.0",
|
|
130
|
+
"mmdb-lib": "3.0.3",
|
|
125
131
|
"nodemailer": "10.0.10",
|
|
126
132
|
"postcss": "8.5.28",
|
|
127
133
|
"reflect-metadata": "0.2.2",
|
|
@@ -131,6 +137,7 @@
|
|
|
131
137
|
"web-push": "3.6.7",
|
|
132
138
|
"web-vitals": "6.2.2",
|
|
133
139
|
"yaml": "2.9.1",
|
|
140
|
+
"yjs": "13.6.33",
|
|
134
141
|
"hash-wasm": "4.12.0",
|
|
135
142
|
"@types/sanitize-html": "2.16.1",
|
|
136
143
|
"@types/web-push": "3.6.4"
|
|
@@ -141,7 +148,7 @@
|
|
|
141
148
|
"@types/bun": "1.4.2",
|
|
142
149
|
"babel-preset-solid": "1.9.15",
|
|
143
150
|
"hono": "4.13.9",
|
|
144
|
-
"playwright": "1.
|
|
151
|
+
"playwright": "1.64.1",
|
|
145
152
|
"solid-js": "1.9.15",
|
|
146
153
|
"typescript": "7.0.2",
|
|
147
154
|
"zod": "4.6.5"
|
|
@@ -1,58 +1,138 @@
|
|
|
1
|
-
import { cp, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { cp, mkdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { readImageRecords } from "../src/services/pdf/image-protocol";
|
|
5
6
|
|
|
6
|
-
/** Isolated native canvas workers, each packaged only when the server bundle reaches the module that starts it. */
|
|
7
7
|
const CANVAS_WORKERS = [
|
|
8
|
-
{ reachedBy: "/ai/pdf-render.ts", entry: "src/ai/pdf-render-worker.ts", output: "pdf-worker.js", packages: ["pdfjs-dist"] },
|
|
9
8
|
{
|
|
10
|
-
|
|
9
|
+
define: "__CLOUD_PDF_RENDER_WORKER__",
|
|
10
|
+
entry: "src/ai/pdf-render-worker.ts",
|
|
11
|
+
output: "pdf-worker.js",
|
|
12
|
+
packages: ["@napi-rs/canvas", "pdfjs-dist"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
define: "__CLOUD_ICON_RENDER_WORKER__",
|
|
11
16
|
entry: "src/services/branding/icon-render-worker.ts",
|
|
12
17
|
output: "icon-worker.js",
|
|
13
|
-
packages: [],
|
|
18
|
+
packages: ["@napi-rs/canvas"],
|
|
14
19
|
},
|
|
20
|
+
{ define: "__CLOUD_PDF_IMAGE_WORKER__", entry: "src/services/pdf/image-worker.ts", output: "image-worker.js", packages: ["sharp"] },
|
|
15
21
|
] as const;
|
|
16
22
|
|
|
23
|
+
/** Server bundle defines: each caller starts its worker from `canvas/` next to `server.js`. */
|
|
24
|
+
export const CANVAS_WORKER_DEFINES = Object.fromEntries(
|
|
25
|
+
CANVAS_WORKERS.map((worker) => [worker.define, JSON.stringify(`./canvas/${worker.output}`)]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const Manifest = z.object({
|
|
29
|
+
version: z.string(),
|
|
30
|
+
dependencies: z.record(z.string(), z.string()).default({}),
|
|
31
|
+
optionalDependencies: z.record(z.string(), z.string()).default({}),
|
|
32
|
+
os: z.array(z.string()).optional(),
|
|
33
|
+
cpu: z.array(z.string()).optional(),
|
|
34
|
+
libc: z.array(z.string()).optional(),
|
|
35
|
+
});
|
|
36
|
+
const matches = (values: string[] | undefined, target: string) =>
|
|
37
|
+
!values || (!values.includes(`!${target}`) && (values.every((value) => value.startsWith("!")) || values.includes(target)));
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
40
|
+
* Bundle each reached native worker with only its runtime packages external. Copy their real dependency closures flat,
|
|
41
|
+
* dereferenced and filtered by OS, CPU and libc; reject version collisions and missing native binaries. Smoke-test sharp
|
|
42
|
+
* in the packaged subprocess so the build proves that its native closure loads on this architecture.
|
|
20
43
|
*/
|
|
21
|
-
export async function buildCanvasWorkers(dist: string,
|
|
22
|
-
|
|
44
|
+
export async function buildCanvasWorkers(dist: string, serverCode: string) {
|
|
45
|
+
// A worker ships only when the server code still contains its path, which only the code that starts it carries.
|
|
46
|
+
// Metafile inputs also list tree-shaken modules, and a retained module binding contributes bytes without its caller.
|
|
47
|
+
const workers = CANVAS_WORKERS.filter((worker) => serverCode.includes(`./canvas/${worker.output}`));
|
|
23
48
|
if (workers.length === 0) return;
|
|
24
49
|
const framework = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
50
|
const out = resolve(dist, "canvas");
|
|
26
51
|
await mkdir(out, { recursive: true });
|
|
27
|
-
const external = ["@napi-rs/canvas", ...workers.flatMap((worker) => worker.packages)];
|
|
28
52
|
for (const worker of workers) {
|
|
29
53
|
const build = await Bun.build({
|
|
30
54
|
entrypoints: [resolve(framework, worker.entry)],
|
|
31
55
|
target: "bun",
|
|
32
56
|
outdir: out,
|
|
33
57
|
naming: worker.output,
|
|
34
|
-
external,
|
|
58
|
+
external: [...worker.packages],
|
|
35
59
|
});
|
|
36
|
-
if (!build.success) throw new Error(`
|
|
60
|
+
if (!build.success) throw new Error(`Native worker ${worker.output} build failed: ${build.logs.join("\n")}`);
|
|
37
61
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
const libc = z.object({ header: z.object({ glibcVersionRuntime: z.string().optional() }) }).parse(process.report.getReport()).header
|
|
63
|
+
.glibcVersionRuntime
|
|
64
|
+
? "glibc"
|
|
65
|
+
: "musl";
|
|
66
|
+
const copied = new Map<string, string>();
|
|
67
|
+
const copyPackage = async (name: string, dependent: string, optional = false): Promise<boolean> => {
|
|
68
|
+
let source: string;
|
|
69
|
+
try {
|
|
70
|
+
source = await realpath(dirname(Bun.resolveSync(`${name}/package.json`, dependent)));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (optional) return false;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const manifest = Manifest.parse(await Bun.file(resolve(source, "package.json")).json());
|
|
76
|
+
if (!matches(manifest.os, process.platform) || !matches(manifest.cpu, process.arch) || !matches(manifest.libc, libc)) {
|
|
77
|
+
if (optional) return false;
|
|
78
|
+
throw new Error(`Native worker dependency ${name} does not support the build platform.`);
|
|
79
|
+
}
|
|
80
|
+
const existing = copied.get(name);
|
|
81
|
+
if (existing) {
|
|
82
|
+
if (existing !== manifest.version)
|
|
83
|
+
throw new Error(`Native worker dependency collision: ${name} ${existing} and ${manifest.version}.`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
copied.set(name, manifest.version);
|
|
87
|
+
await cp(source, resolve(out, "node_modules", name), {
|
|
88
|
+
recursive: true,
|
|
89
|
+
dereference: true,
|
|
90
|
+
filter: (path) => basename(path) !== "node_modules",
|
|
91
|
+
});
|
|
92
|
+
for (const dependency of Object.keys(manifest.dependencies)) {
|
|
93
|
+
if (dependency in manifest.optionalDependencies) continue;
|
|
94
|
+
await copyPackage(dependency, source);
|
|
95
|
+
}
|
|
45
96
|
let nativePackages = 0;
|
|
46
97
|
for (const dependency of Object.keys(manifest.optionalDependencies)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
if (await copyPackage(dependency, source, true)) {
|
|
99
|
+
if (
|
|
100
|
+
(name === "sharp" && dependency.startsWith("@img/sharp-") && !dependency.startsWith("@img/sharp-libvips-")) ||
|
|
101
|
+
(name === "@napi-rs/canvas" && dependency.startsWith("@napi-rs/canvas-"))
|
|
102
|
+
)
|
|
103
|
+
nativePackages++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ((name === "sharp" || name === "@napi-rs/canvas") && !nativePackages) {
|
|
107
|
+
throw new Error(`Native workers need an installed ${name} binary for the build platform.`);
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
for (const name of new Set(workers.flatMap((worker) => [...worker.packages]))) await copyPackage(name, framework);
|
|
112
|
+
if (workers.some((worker) => worker.output === "image-worker.js")) await smokeImageWorker(resolve(out, "image-worker.js"));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function smokeImageWorker(entry: string) {
|
|
116
|
+
// A fixed 2x2 RGBA PNG; the build process never loads sharp.
|
|
117
|
+
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR4nGP4z8DQAMIMMAYAOOgF/REzMMkAAAAASUVORK5CYII=", "base64");
|
|
118
|
+
const child = Bun.spawn([process.execPath, "--no-env-file", entry], {
|
|
119
|
+
stdin: new Blob([
|
|
120
|
+
`${JSON.stringify({ bytes: png.byteLength, boxes: [{ width: 2, height: 2 }], maxBytes: 4096, timeoutMs: 10_000 })}\n`,
|
|
121
|
+
png,
|
|
122
|
+
]),
|
|
123
|
+
stdout: "pipe",
|
|
124
|
+
stderr: "ignore",
|
|
125
|
+
env: { PATH: process.env.PATH },
|
|
126
|
+
});
|
|
127
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 15_000);
|
|
128
|
+
try {
|
|
129
|
+
const result = await readImageRecords(child.stdout, { boxes: [{ width: 2, height: 2 }], maxBytes: 4096 });
|
|
130
|
+
if ((await child.exited) !== 0 || !result.ok || result.variants.length !== 1 || !result.variants[0]) {
|
|
131
|
+
throw new Error("Packaged image worker could not convert its PNG: sharp's native dependency closure is incomplete or unusable.");
|
|
55
132
|
}
|
|
56
|
-
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
child.kill("SIGKILL");
|
|
136
|
+
await child.exited;
|
|
57
137
|
}
|
|
58
138
|
}
|
package/scripts/build.ts
CHANGED
|
@@ -35,7 +35,7 @@ import tailwind from "bun-plugin-tailwind";
|
|
|
35
35
|
import type { AppCliModules } from "../src/contracts/app";
|
|
36
36
|
import { writeAppFavicon } from "./app-favicon";
|
|
37
37
|
import { buildBrowserPerformance } from "./browser-performance";
|
|
38
|
-
import { buildCanvasWorkers } from "./build-canvas-workers";
|
|
38
|
+
import { buildCanvasWorkers, CANVAS_WORKER_DEFINES } from "./build-canvas-workers";
|
|
39
39
|
|
|
40
40
|
const appId = process.env.APP_ID;
|
|
41
41
|
if (!appId) throw new Error("APP_ID env var required");
|
|
@@ -127,13 +127,11 @@ try {
|
|
|
127
127
|
naming: "server.js",
|
|
128
128
|
target: "bun",
|
|
129
129
|
minify: true,
|
|
130
|
-
metafile: true,
|
|
131
130
|
define: {
|
|
132
131
|
__CLOUD_VERSION__: JSON.stringify(version),
|
|
133
132
|
__CLOUD_RELEASE__: JSON.stringify(release),
|
|
134
133
|
__CLOUD_SYNC_VERSION__: JSON.stringify(syncVersion),
|
|
135
|
-
|
|
136
|
-
__CLOUD_ICON_RENDER_WORKER__: JSON.stringify("./canvas/icon-worker.js"),
|
|
134
|
+
...CANVAS_WORKER_DEFINES,
|
|
137
135
|
__CLOUD_APP_ASSETS__: JSON.stringify("./assets/"),
|
|
138
136
|
__CLOUD_CLI_PLUGINS__: JSON.stringify("./cli/"),
|
|
139
137
|
},
|
|
@@ -146,7 +144,7 @@ if (!server.success) {
|
|
|
146
144
|
for (const m of server.logs) console.error(m);
|
|
147
145
|
throw new Error("Server bundle failed");
|
|
148
146
|
}
|
|
149
|
-
await buildCanvasWorkers(dist,
|
|
147
|
+
await buildCanvasWorkers(dist, await Bun.file(resolve(dist, "server.js")).text());
|
|
150
148
|
|
|
151
149
|
// 2. Per-app Tailwind stylesheet.
|
|
152
150
|
const appCss = resolve(appDir, "src/styles/app.css");
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export const appFaviconHref = (appId: string, version: number): string =>
|
|
2
|
-
appId === "core"
|
|
1
|
+
export const appFaviconHref = (appId: string, version: string | number | undefined): string =>
|
|
2
|
+
appId === "core"
|
|
3
|
+
? "/branding/favicon"
|
|
4
|
+
: `/public/${appId}/favicon.svg${version === undefined ? "" : `?v=${encodeURIComponent(version)}`}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** An absolute path on the current origin: one leading slash, no backslash or whitespace. */
|
|
2
|
+
export const SAME_ORIGIN_PATH = /^\/(?![\/\\])[^\\\s]*$/;
|
|
3
|
+
|
|
4
|
+
/** A route as the gateway reads it: trimmed and without empty segments, so `//pwa/x/` is `/pwa/x`. */
|
|
5
|
+
export const canonicalRoute = (route: string): string => {
|
|
6
|
+
const trimmed = route.trim();
|
|
7
|
+
return trimmed.startsWith("/") ? `/${trimmed.split("/").filter(Boolean).join("/")}` : trimmed;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether an application path stays below one of the app's own `routes`. It is
|
|
12
|
+
* checked on the path the browser actually requests, after dot segments, and
|
|
13
|
+
* never starts with `//`, which a URL resolver reads as another host.
|
|
14
|
+
*/
|
|
15
|
+
export const isOwnAppPath = (href: string, routes: readonly string[]): boolean => {
|
|
16
|
+
if (!SAME_ORIGIN_PATH.test(href)) return false;
|
|
17
|
+
const path = new URL(href, "https://cloud.invalid").pathname;
|
|
18
|
+
return (
|
|
19
|
+
!path.startsWith("//") &&
|
|
20
|
+
routes.some((route) => {
|
|
21
|
+
const prefix = canonicalRoute(route);
|
|
22
|
+
return prefix === "/" || path === prefix || path.startsWith(`${prefix}/`);
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -92,7 +92,11 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
|
|
|
92
92
|
if (!canonical) throw new Error(`App presentation locale ${JSON.stringify(locale)} is not a valid BCP 47 locale`);
|
|
93
93
|
if (canonical === baseLocale) throw new Error(`App presentation translations must not repeat base locale ${baseLocale}`);
|
|
94
94
|
if (translations[canonical]) throw new Error(`App presentation locale ${canonical} is duplicated after canonicalization`);
|
|
95
|
+
if (translation.panelLabel !== undefined && !app.panel) {
|
|
96
|
+
throw new Error(`${canonical}.panelLabel contains unknown key "panel"`);
|
|
97
|
+
}
|
|
95
98
|
translations[canonical] = {
|
|
99
|
+
...(translation.panelLabel !== undefined ? { panelLabel: text(translation.panelLabel, `${canonical}.panelLabel`) } : {}),
|
|
96
100
|
...(translation.name !== undefined ? { name: text(translation.name, `${canonical}.name`) } : {}),
|
|
97
101
|
...(translation.description !== undefined
|
|
98
102
|
? { description: text(translation.description, `${canonical}.description`, MAX_DESCRIPTION) }
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Starts a native
|
|
4
|
+
* Starts a native worker (PDF pages, app icons, print images) in a process of its own. RLIMIT_DATA bounds the decoder heap
|
|
5
5
|
* and native writable mappings on Linux. Positional arguments keep paths out of shell code; no elevated privileges.
|
|
6
6
|
* The caller owns the deadline and kills the process on abort.
|
|
7
7
|
*/
|
|
8
|
-
export const spawnCanvasWorker = (worker: URL, name: string, input: string) =>
|
|
8
|
+
export const spawnCanvasWorker = (worker: URL, name: string, input: string | Blob) =>
|
|
9
9
|
Bun.spawn(["/bin/sh", "-c", 'ulimit -d 524288 || exit 70; exec "$@"', name, process.execPath, "--no-env-file", fileURLToPath(worker)], {
|
|
10
10
|
stdin: new Blob([input]),
|
|
11
11
|
stdout: "pipe",
|
|
12
12
|
stderr: "ignore",
|
|
13
13
|
// No application credentials or configuration reach the decoder, only fixed
|
|
14
|
-
//
|
|
14
|
+
// runtime settings: the Bun work pool, canvas tokio runtime, and JSC GC
|
|
15
15
|
// markers otherwise grow with the host's cores, and RLIMIT_DATA also counts
|
|
16
16
|
// each thread's stack and allocator memory.
|
|
17
|
+
// Bun's allocator otherwise commits whole arenas up front, which RLIMIT_DATA counts as used.
|
|
17
18
|
env: {
|
|
18
19
|
PATH: process.env.PATH,
|
|
19
20
|
LANG: "C.UTF-8",
|
|
20
21
|
UV_THREADPOOL_SIZE: "2",
|
|
21
22
|
TOKIO_WORKER_THREADS: "2",
|
|
22
23
|
BUN_JSC_numberOfGCMarkers: "2",
|
|
24
|
+
MIMALLOC_ARENA_EAGER_COMMIT: "0",
|
|
23
25
|
},
|
|
24
26
|
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { capabilityHash, projectSchema, stableJsonValue } from "./capability-schemas";
|
|
2
|
+
|
|
3
|
+
export { capabilityHash } from "./capability-schemas";
|
|
4
|
+
|
|
2
5
|
import { isServiceError, type Result, type ServiceError } from "@k2b/stdlib";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
import {
|
|
@@ -8,8 +11,8 @@ import {
|
|
|
8
11
|
CAPABILITY_PROTOCOL_VERSION,
|
|
9
12
|
type CapabilityActionDefinition,
|
|
10
13
|
type CapabilityActionManifest,
|
|
14
|
+
CapabilityActionPolicySchema,
|
|
11
15
|
type CapabilityActionReviewResult,
|
|
12
|
-
CapabilityActionReviewSchema,
|
|
13
16
|
type CapabilityActionSentences,
|
|
14
17
|
type CapabilityCommandDefinition,
|
|
15
18
|
type CapabilityCommandManifest,
|
|
@@ -33,12 +36,14 @@ import {
|
|
|
33
36
|
type CapabilitySearchTagDefinition,
|
|
34
37
|
type CapabilityStreamDefinition,
|
|
35
38
|
type CapabilityUniversalSearchDefinition,
|
|
39
|
+
capabilityActionPolicy,
|
|
36
40
|
capabilityResultSchema,
|
|
37
41
|
UniversalSearchDataSchema,
|
|
38
42
|
UniversalSearchInputSchema,
|
|
39
43
|
} from "../contracts/capabilities";
|
|
40
44
|
import { fileProviderIssues } from "../contracts/file-provider";
|
|
41
45
|
import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
|
|
46
|
+
import { capabilityActionReceiptIssues, capabilityActionReviewIssues, capabilityQueryChecksActionOutcome } from "./capability-reviews";
|
|
42
47
|
import {
|
|
43
48
|
CAPABILITY_SENTENCE_KEYS,
|
|
44
49
|
CAPABILITY_SENTENCE_MAX_CHARS,
|
|
@@ -94,6 +99,7 @@ const ACTION_FIELDS = {
|
|
|
94
99
|
stream: true,
|
|
95
100
|
idempotency: true,
|
|
96
101
|
approval: true,
|
|
102
|
+
policy: true,
|
|
97
103
|
review: true,
|
|
98
104
|
run: true,
|
|
99
105
|
} satisfies DefinedFields<CapabilityActionDefinition>;
|
|
@@ -221,21 +227,6 @@ export const serializeCapabilityProviderResult = (
|
|
|
221
227
|
return { body: JSON.stringify(fallback), status: options.nonIdempotentAction ? 502 : 500 };
|
|
222
228
|
};
|
|
223
229
|
|
|
224
|
-
const stableJsonValue = (value: unknown): unknown => {
|
|
225
|
-
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
226
|
-
if (typeof value !== "object" || value === null) return value;
|
|
227
|
-
return Object.fromEntries(
|
|
228
|
-
Object.entries(value as Record<string, unknown>)
|
|
229
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
230
|
-
.map(([key, entry]) => [key, stableJsonValue(entry)]),
|
|
231
|
-
);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
export const capabilityHash = (value: unknown): string =>
|
|
235
|
-
createHash("sha256")
|
|
236
|
-
.update(JSON.stringify(stableJsonValue(value)))
|
|
237
|
-
.digest("hex");
|
|
238
|
-
|
|
239
230
|
const assertText = (value: string, label: string, max: number): void => {
|
|
240
231
|
if (!value.trim()) throw new Error(`${label} is required`);
|
|
241
232
|
if (value.length > max) throw new Error(`${label} must be at most ${max} characters`);
|
|
@@ -338,12 +329,12 @@ const compileOperationPresentation = (
|
|
|
338
329
|
operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
|
|
339
330
|
label: string,
|
|
340
331
|
strict = true,
|
|
341
|
-
): CapabilityOperationPresentationTranslation => {
|
|
332
|
+
): CapabilityOperationPresentationTranslation & { keywords?: readonly string[] } => {
|
|
342
333
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
343
334
|
const translation = value as Record<string, unknown>;
|
|
344
335
|
const allowed = new Set(
|
|
345
336
|
"path" in operation
|
|
346
|
-
? ["title", "description", "input"]
|
|
337
|
+
? ["title", "description", "input", "keywords"]
|
|
347
338
|
: ["title", "description", "input", "data", "searchTags", ...(isAction(operation) ? ["sentences"] : [])],
|
|
348
339
|
);
|
|
349
340
|
const extra = Object.keys(translation).find((key) => !allowed.has(key));
|
|
@@ -356,6 +347,10 @@ const compileOperationPresentation = (
|
|
|
356
347
|
if (typeof translation.description !== "string") throw new Error(`${label}.description must be text`);
|
|
357
348
|
assertText(translation.description, `${label}.description`, 1000);
|
|
358
349
|
}
|
|
350
|
+
const keywords =
|
|
351
|
+
"path" in operation && translation.keywords !== undefined
|
|
352
|
+
? CapabilityCommandManifestSchema.shape.keywords.parse(translation.keywords)
|
|
353
|
+
: undefined;
|
|
359
354
|
let searchTags: CapabilityOperationPresentationTranslation["searchTags"];
|
|
360
355
|
const universalSearch = "universalSearch" in operation ? operation.universalSearch : undefined;
|
|
361
356
|
// A reader skips search-tag copy for an operation without Universal Search instead of losing its other copy.
|
|
@@ -398,6 +393,7 @@ const compileOperationPresentation = (
|
|
|
398
393
|
return {
|
|
399
394
|
...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
|
|
400
395
|
...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
|
|
396
|
+
...(keywords ? { keywords } : {}),
|
|
401
397
|
...(input ? { input } : {}),
|
|
402
398
|
...(data ? { data } : {}),
|
|
403
399
|
...(searchTags ? { searchTags } : {}),
|
|
@@ -517,17 +513,6 @@ export const compileCapabilityPresentation = (
|
|
|
517
513
|
return { baseLocale, ...(sentences && Object.keys(sentences).length > 0 ? { sentences } : {}), translations };
|
|
518
514
|
};
|
|
519
515
|
|
|
520
|
-
const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
|
|
521
|
-
try {
|
|
522
|
-
const projected = z.toJSONSchema(schema, { io }) as JsonSchema;
|
|
523
|
-
z.fromJSONSchema(projected);
|
|
524
|
-
return projected;
|
|
525
|
-
} catch (error) {
|
|
526
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
527
|
-
throw new Error(`${label} is not JSON-Schema-round-trippable: ${message}`);
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
516
|
const assertPropertyDescriptions = (schema: unknown, label: string, path: readonly string[] = []): void => {
|
|
532
517
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return;
|
|
533
518
|
const value = schema as Record<string, unknown>;
|
|
@@ -720,6 +705,20 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
720
705
|
const label = `Action ${localId}`;
|
|
721
706
|
assertDefinedFields(definition, ACTION_FIELDS, label);
|
|
722
707
|
assertDefinedStreamFields(definition.stream, label);
|
|
708
|
+
if (definition.policy !== undefined) {
|
|
709
|
+
const parsedPolicy = CapabilityActionPolicySchema.safeParse(definition.policy);
|
|
710
|
+
if (!parsedPolicy.success) throw new Error(`${label} has invalid policy: ${parsedPolicy.error.message}`);
|
|
711
|
+
if (definition.approval !== undefined) throw new Error(`${label} policy and approval are mutually exclusive`);
|
|
712
|
+
if (!definition.review) throw new Error(`${label} policy requires review()`);
|
|
713
|
+
if ("silent" in parsedPolicy.data && (definition.destructive || definition.openWorld))
|
|
714
|
+
throw new Error(`${label} destructive or open-world policy must ask`);
|
|
715
|
+
if (
|
|
716
|
+
"ask" in parsedPolicy.data &&
|
|
717
|
+
(parsedPolicy.data.ask === "sends" || parsedPolicy.data.ask === "deletes") &&
|
|
718
|
+
definition.idempotency !== "required"
|
|
719
|
+
)
|
|
720
|
+
throw new Error(`${label} sends or deletes policy requires idempotency: "required"`);
|
|
721
|
+
}
|
|
723
722
|
// Declaration rule, documented in docs/platform/capabilities.md:
|
|
724
723
|
// destructive means irreversible or externally visible. An irreversible or
|
|
725
724
|
// open-world effect must always be reviewable and safely repeatable, and
|
|
@@ -767,7 +766,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
767
766
|
destructive: definition.destructive,
|
|
768
767
|
openWorld: definition.openWorld,
|
|
769
768
|
idempotency: definition.idempotency,
|
|
770
|
-
...(definition.
|
|
769
|
+
...(definition.policy
|
|
770
|
+
? "silent" in definition.policy
|
|
771
|
+
? { approval: "rememberable" as const }
|
|
772
|
+
: {}
|
|
773
|
+
: definition.approval
|
|
774
|
+
? { approval: definition.approval }
|
|
775
|
+
: {}),
|
|
771
776
|
...(definition.review ? { review: true as const } : {}),
|
|
772
777
|
} satisfies CapabilityActionManifest;
|
|
773
778
|
actions.set(localId, {
|
|
@@ -795,6 +800,9 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
795
800
|
});
|
|
796
801
|
});
|
|
797
802
|
|
|
803
|
+
const policies = Object.fromEntries(
|
|
804
|
+
[...actions].flatMap(([localId, action]) => (action.definition.policy ? [[localId, action.definition.policy]] : [])),
|
|
805
|
+
);
|
|
798
806
|
const fileProvider = definitions.fileProvider;
|
|
799
807
|
const manifestBase = {
|
|
800
808
|
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
@@ -803,6 +811,7 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
803
811
|
queries: [...queries.values()].map((entry) => entry.manifest),
|
|
804
812
|
actions: [...actions.values()].map((entry) => entry.manifest),
|
|
805
813
|
commands,
|
|
814
|
+
...(Object.keys(policies).length ? { policies } : {}),
|
|
806
815
|
// Omitted when unused so manifests without a provider keep their earlier hash.
|
|
807
816
|
...(fileProvider
|
|
808
817
|
? { fileProvider: { list: fileProvider.list, read: fileProvider.read, ...(fileProvider.save ? { save: fileProvider.save } : {}) } }
|
|
@@ -954,6 +963,7 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
954
963
|
title: copy.title ?? operation.title,
|
|
955
964
|
description: copy.description ?? operation.description,
|
|
956
965
|
inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
|
|
966
|
+
keywords: copy.keywords ? [...copy.keywords] : operation.keywords,
|
|
957
967
|
}
|
|
958
968
|
: operation;
|
|
959
969
|
}),
|
|
@@ -970,6 +980,7 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
970
980
|
: operation;
|
|
971
981
|
}),
|
|
972
982
|
...(current.fileProvider ? { fileProvider: current.fileProvider } : {}),
|
|
983
|
+
...(current.policies ? { policies: current.policies } : {}),
|
|
973
984
|
};
|
|
974
985
|
current = CapabilityManifestSchema.parse({ ...manifestBase, manifestHash: capabilityHash(manifestBase) });
|
|
975
986
|
}
|
|
@@ -1213,6 +1224,15 @@ const resultRefs = (result: unknown): Array<{ type: string; id: string }> => {
|
|
|
1213
1224
|
}>;
|
|
1214
1225
|
};
|
|
1215
1226
|
|
|
1227
|
+
const compiledReviewContext = (compiled: CompiledCapabilities, localId: string) => ({
|
|
1228
|
+
policy: capabilityActionPolicy(compiled.manifest, localId),
|
|
1229
|
+
approval: compiled.actions.get(localId)?.manifest.approval,
|
|
1230
|
+
queries: compiled.manifest.queries,
|
|
1231
|
+
validateQueryInput: (query: CapabilityQueryManifest, input: Record<string, unknown>) =>
|
|
1232
|
+
compiled.queries.get(query.localId)?.definition.input.safeParse(input).success === true,
|
|
1233
|
+
...(compiled.presentation ? { locales: [compiled.presentation.baseLocale, ...Object.keys(compiled.presentation.translations)] } : {}),
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1216
1236
|
export const validateCapabilityResult = (
|
|
1217
1237
|
compiled: CompiledCapabilities,
|
|
1218
1238
|
operation: CompiledCapabilityQuery | CompiledCapabilityAction,
|
|
@@ -1220,12 +1240,25 @@ export const validateCapabilityResult = (
|
|
|
1220
1240
|
onInvalidResult?: (error: unknown) => void,
|
|
1221
1241
|
): Result<unknown, ServiceError> => {
|
|
1222
1242
|
const parsed = operation.resultSchema.safeParse(result);
|
|
1223
|
-
|
|
1243
|
+
const field = (value: unknown, key: string): unknown =>
|
|
1244
|
+
value !== null && typeof value === "object" ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
|
|
1245
|
+
const receipt = field(result, "receipt");
|
|
1246
|
+
const links = field(result, "links");
|
|
1247
|
+
const receiptIssues =
|
|
1248
|
+
receipt === undefined
|
|
1249
|
+
? []
|
|
1250
|
+
: "idempotency" in operation.manifest
|
|
1251
|
+
? capabilityActionReceiptIssues(receipt, links)
|
|
1252
|
+
: ["Queries cannot return a receipt"];
|
|
1253
|
+
const nestedReceipt = field(field(result, "data"), "receipt");
|
|
1254
|
+
if (!("idempotency" in operation.manifest) && nestedReceipt !== undefined && capabilityQueryChecksActionOutcome(operation.manifest))
|
|
1255
|
+
receiptIssues.push(...capabilityActionReceiptIssues(nestedReceipt, links));
|
|
1256
|
+
if (!parsed.success || receiptIssues.length > 0) {
|
|
1224
1257
|
try {
|
|
1225
1258
|
onInvalidResult?.(
|
|
1226
1259
|
new Error(
|
|
1227
1260
|
`Capability ${operation.manifest.localId} returned data outside its registered schema: ${JSON.stringify(
|
|
1228
|
-
parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
|
|
1261
|
+
parsed.success ? receiptIssues : parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
|
|
1229
1262
|
)}`,
|
|
1230
1263
|
),
|
|
1231
1264
|
);
|
|
@@ -1468,27 +1501,18 @@ export const reviewCompiledCapability = async (params: {
|
|
|
1468
1501
|
if (!reviewed.ok) {
|
|
1469
1502
|
return normalizeProviderError(reviewed.error, "Capability review returned an invalid error", params.onUnexpectedError);
|
|
1470
1503
|
}
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
const approvalScopeIsValid =
|
|
1474
|
-
parsed.success && (operation.manifest.approval === "rememberable" || parsed.data.approvalScope === undefined);
|
|
1475
|
-
if (!parsed.success || !approvalScopeIsValid) {
|
|
1504
|
+
const issues = capabilityActionReviewIssues(reviewed.data, compiledReviewContext(params.compiled, params.localId));
|
|
1505
|
+
if (issues.length > 0) {
|
|
1476
1506
|
try {
|
|
1477
1507
|
params.onUnexpectedError?.(
|
|
1478
|
-
new Error(
|
|
1479
|
-
parsed.success
|
|
1480
|
-
? `Capability ${operation.manifest.localId} review returned an approval scope inconsistent with its manifest`
|
|
1481
|
-
: `Capability ${operation.manifest.localId} review returned data outside its registered schema: ${JSON.stringify(
|
|
1482
|
-
parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
|
|
1483
|
-
)}`,
|
|
1484
|
-
),
|
|
1508
|
+
new Error(`Capability ${operation.manifest.localId} review returned an invalid result: ${issues.join("; ")}`),
|
|
1485
1509
|
);
|
|
1486
1510
|
} catch {
|
|
1487
|
-
|
|
1511
|
+
/* Observability must not change the public failure contract. */
|
|
1488
1512
|
}
|
|
1489
1513
|
}
|
|
1490
|
-
return
|
|
1491
|
-
? { ok: true, data:
|
|
1514
|
+
return issues.length === 0
|
|
1515
|
+
? { ok: true, data: reviewed.data }
|
|
1492
1516
|
: {
|
|
1493
1517
|
ok: false,
|
|
1494
1518
|
error: {
|