@k2b/cloud 0.6.0 → 0.7.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 +2 -2
- package/package.json +19 -9
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/README.md
CHANGED
|
@@ -78,9 +78,9 @@ A standard app declares four prefixes (`/api/<id>`, `/app/<id>`, `/admin/<id>`,
|
|
|
78
78
|
|
|
79
79
|
## Documentation
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Application setup, public contracts, and deployment guidance:
|
|
82
82
|
|
|
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
86
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Modular Hono+SolidJS framework for building per-app docker services behind a dynamic gateway. Powers cloud.stuve-ulm.de.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"!src/**/*.test.ts",
|
|
19
19
|
"!src/**/*.test.tsx",
|
|
20
20
|
"!src/**/*.integration.test.ts",
|
|
21
|
-
"!scripts/**/*.test.ts"
|
|
21
|
+
"!scripts/**/*.test.ts",
|
|
22
|
+
"!scripts/runtime-recovery-*.ts"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
".": "./src/index.ts",
|
|
@@ -31,9 +32,13 @@
|
|
|
31
32
|
"./browser/live": "./src/browser/live-websocket.ts",
|
|
32
33
|
"./browser/notifications": "./src/browser/notifications.ts",
|
|
33
34
|
"./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
|
|
35
|
+
"./browser/commands": "./src/browser/commands.ts",
|
|
36
|
+
"./browser/search": "./src/browser/search.ts",
|
|
37
|
+
"./browser/testing": "./src/browser/testing.ts",
|
|
34
38
|
"./browser/resource-picker": "./src/browser/resource-picker.tsx",
|
|
35
39
|
"./capabilities": "./src/capabilities/client.ts",
|
|
36
40
|
"./capabilities/server": "./src/capabilities/server.ts",
|
|
41
|
+
"./capabilities/store": "./src/capabilities/executions.ts",
|
|
37
42
|
"./capabilities/testing": "./src/capabilities/testing.ts",
|
|
38
43
|
"./cli": "./src/cli/index.ts",
|
|
39
44
|
"./cli/access": "./src/cli/access.ts",
|
|
@@ -59,6 +64,7 @@
|
|
|
59
64
|
"./access/ui": "./src/access/ui.ts",
|
|
60
65
|
"./ssr": "./src/ssr/index.ts",
|
|
61
66
|
"./ssr/islands": "./src/ssr/islands/index.ts",
|
|
67
|
+
"./ssr/WorkspaceNavigation.island": "./src/ssr/WorkspaceNavigation.island.tsx",
|
|
62
68
|
"./ssr/*": "./src/ssr/*",
|
|
63
69
|
"./config": "./src/config/index.ts",
|
|
64
70
|
"./config/*": "./src/config/*",
|
|
@@ -77,19 +83,22 @@
|
|
|
77
83
|
"./styles/global.css": "./src/styles/global.css"
|
|
78
84
|
},
|
|
79
85
|
"scripts": {
|
|
80
|
-
"test": "bun test",
|
|
86
|
+
"test": "bun test --isolate --path-ignore-patterns '**/*.behavior.test.tsx' --path-ignore-patterns '**/*.dom.test.tsx' && bun run test:dom",
|
|
87
|
+
"test:dom": "bun test --isolate --conditions=browser --preload ../ui/test/solid-dom-preload.ts behavior.test.tsx dom.test.tsx",
|
|
81
88
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false 2>&1 | grep -v node_modules | (! grep -q 'error TS')"
|
|
82
89
|
},
|
|
83
90
|
"dependencies": {
|
|
84
91
|
"@firecrawl/anydoc": "0.1.9",
|
|
92
|
+
"@napi-rs/canvas": "1.0.8",
|
|
93
|
+
"pdfjs-dist": "6.3.289",
|
|
85
94
|
"@hono/standard-validator": "0.2.2",
|
|
86
95
|
"@simplewebauthn/server": "13.3.2",
|
|
87
96
|
"@tabler/icons": "3.46.0",
|
|
88
97
|
"@tailwindcss/typography": "0.5.20",
|
|
89
|
-
"@k2b/nessi": "0.
|
|
90
|
-
"@k2b/ssr": "0.
|
|
91
|
-
"@k2b/ui": "0.
|
|
92
|
-
"@k2b/stdlib": "0.
|
|
98
|
+
"@k2b/nessi": "0.12.0",
|
|
99
|
+
"@k2b/ssr": "0.14.0",
|
|
100
|
+
"@k2b/ui": "0.3.0",
|
|
101
|
+
"@k2b/stdlib": "0.25.0",
|
|
93
102
|
"@k2b/sync": "6.4.0",
|
|
94
103
|
"@nats-io/transport-node": "3.4.0",
|
|
95
104
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
@@ -107,6 +116,7 @@
|
|
|
107
116
|
"seroval": "1.6.0",
|
|
108
117
|
"tailwindcss": "4.3.3",
|
|
109
118
|
"web-push": "3.6.7",
|
|
119
|
+
"web-vitals": "6.2.1",
|
|
110
120
|
"yaml": "2.9.0",
|
|
111
121
|
"hash-wasm": "4.12.0",
|
|
112
122
|
"@types/nodemailer": "7.0.12",
|
|
@@ -116,12 +126,12 @@
|
|
|
116
126
|
"devDependencies": {
|
|
117
127
|
"@babel/core": "7.29.7",
|
|
118
128
|
"@babel/preset-typescript": "7.29.7",
|
|
119
|
-
"@types/bun": "1.
|
|
129
|
+
"@types/bun": "1.4.2",
|
|
120
130
|
"babel-preset-solid": "1.9.12",
|
|
121
131
|
"hono": "4.12.32",
|
|
122
132
|
"solid-js": "1.9.14",
|
|
123
133
|
"typescript": "5.9.3",
|
|
124
|
-
"zod": "4.
|
|
134
|
+
"zod": "4.6.2"
|
|
125
135
|
},
|
|
126
136
|
"peerDependencies": {
|
|
127
137
|
"hono": "^4.12.25",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { WEB_VITALS_ASSET_NAME } from "../src/_internal/web-vitals-asset";
|
|
3
|
+
|
|
4
|
+
export async function buildBrowserPerformance(publicDir: string, appId: string): Promise<void> {
|
|
5
|
+
if (appId !== "core") return;
|
|
6
|
+
const result = await Bun.build({
|
|
7
|
+
entrypoints: [resolve(import.meta.dir, "../src/browser/web-vitals.ts")],
|
|
8
|
+
outdir: publicDir,
|
|
9
|
+
naming: WEB_VITALS_ASSET_NAME,
|
|
10
|
+
target: "browser",
|
|
11
|
+
minify: true,
|
|
12
|
+
});
|
|
13
|
+
if (!result.success) throw new AggregateError(result.logs, "Browser performance build failed");
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/** Package the decoder and native assets only for bundles importing PDF vision. */
|
|
7
|
+
export async function buildPdfRenderer(dist: string) {
|
|
8
|
+
const framework = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const out = resolve(dist, "pdf-render");
|
|
10
|
+
await mkdir(out, { recursive: true });
|
|
11
|
+
const worker = await Bun.build({
|
|
12
|
+
entrypoints: [resolve(framework, "src/ai/pdf-render-worker.ts")],
|
|
13
|
+
target: "bun",
|
|
14
|
+
outdir: out,
|
|
15
|
+
naming: "worker.js",
|
|
16
|
+
external: ["pdfjs-dist", "@napi-rs/canvas"],
|
|
17
|
+
});
|
|
18
|
+
if (!worker.success) throw new Error(`PDF decoder build failed: ${worker.logs.join("\n")}`);
|
|
19
|
+
for (const name of ["pdfjs-dist", "@napi-rs/canvas"]) {
|
|
20
|
+
const source = dirname(Bun.resolveSync(`${name}/package.json`, framework));
|
|
21
|
+
await cp(source, resolve(out, "node_modules", name), { recursive: true, dereference: true });
|
|
22
|
+
if (name !== "@napi-rs/canvas") continue;
|
|
23
|
+
const manifest = z
|
|
24
|
+
.object({ optionalDependencies: z.record(z.string(), z.string()) })
|
|
25
|
+
.parse(await Bun.file(resolve(source, "package.json")).json());
|
|
26
|
+
let nativePackages = 0;
|
|
27
|
+
for (const dependency of Object.keys(manifest.optionalDependencies)) {
|
|
28
|
+
let location: string;
|
|
29
|
+
try {
|
|
30
|
+
location = dirname(Bun.resolveSync(`${dependency}/package.json`, source));
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
} // Only the installed target-platform binary is needed.
|
|
34
|
+
await cp(location, resolve(out, "node_modules", dependency), { recursive: true, dereference: true });
|
|
35
|
+
nativePackages++;
|
|
36
|
+
}
|
|
37
|
+
if (!nativePackages) throw new Error("PDF decoder needs an installed native canvas binary for the build platform.");
|
|
38
|
+
}
|
|
39
|
+
}
|
package/scripts/build.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Two consumer shapes:
|
|
5
5
|
*
|
|
6
6
|
* Monorepo (this repo):
|
|
7
|
-
* APP_ID=<id> bun run packages/cloud/scripts/build.ts
|
|
7
|
+
* NODE_ENV=production APP_ID=<id> bun run packages/cloud/scripts/build.ts
|
|
8
8
|
* # appDir defaults to packages/<APP_ID>, run from workspace root.
|
|
9
9
|
*
|
|
10
10
|
* Standalone (npm consumer, see cloud-template):
|
|
11
|
-
* APP_ID=<id> APP_DIR=. bun run node_modules/@k2b/cloud/scripts/build.ts
|
|
11
|
+
* NODE_ENV=production APP_ID=<id> APP_DIR=. bun run node_modules/@k2b/cloud/scripts/build.ts
|
|
12
12
|
* # appDir = APP_DIR (resolved against cwd) and must be the directory
|
|
13
13
|
* # that CONTAINS src/, since entrypoints resolve to <appDir>/src/*.
|
|
14
14
|
*
|
|
@@ -22,20 +22,21 @@
|
|
|
22
22
|
* it ships a `scripts/build-extras.ts` that this script runs at the end.
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync } from "node:fs";
|
|
25
|
-
import { cp, mkdir,
|
|
25
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
26
26
|
import { dirname, extname, resolve } from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { promisify } from "node:util";
|
|
29
29
|
import { brotliCompress, gzip, constants as zlibConstants } from "node:zlib";
|
|
30
|
-
import {
|
|
30
|
+
import { Glob } from "bun";
|
|
31
31
|
import tailwind from "bun-plugin-tailwind";
|
|
32
32
|
import { writeAppFavicon } from "./app-favicon";
|
|
33
|
+
import { buildBrowserPerformance } from "./browser-performance";
|
|
34
|
+
import { buildPdfRenderer } from "./build-pdf-renderer";
|
|
33
35
|
|
|
34
36
|
const appId = process.env.APP_ID;
|
|
35
37
|
if (!appId) throw new Error("APP_ID env var required");
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
// process.cwd() (set in defineApp), so we use the same here for hash parity.
|
|
39
|
+
// Build output is relative to the consuming project, including npm consumers.
|
|
39
40
|
const root = process.cwd();
|
|
40
41
|
|
|
41
42
|
// Framework dir — works whether this script is in packages/cloud/scripts/
|
|
@@ -59,6 +60,8 @@ const syncVersion = syncPackage.version;
|
|
|
59
60
|
// convention. Resolved against cwd if relative.
|
|
60
61
|
const appDir = process.env.APP_DIR ? resolve(root, process.env.APP_DIR) : resolve(root, "packages", appId);
|
|
61
62
|
if (!existsSync(appDir)) throw new Error(`Unknown app dir: ${appDir} (set APP_DIR or check APP_ID)`);
|
|
63
|
+
// Share the resolved source directory with defineApp before importing config.
|
|
64
|
+
process.env.APP_DIR = appDir;
|
|
62
65
|
|
|
63
66
|
const dist = resolve(root, "dist");
|
|
64
67
|
const distPublic = resolve(dist, "public");
|
|
@@ -68,14 +71,6 @@ const compressGzip = promisify(gzip);
|
|
|
68
71
|
await rm(dist, { recursive: true, force: true });
|
|
69
72
|
await mkdir(distPublic, { recursive: true });
|
|
70
73
|
|
|
71
|
-
// Mirrors @k2b/ssr's island-id (md5 of POSIX path relative to the
|
|
72
|
-
// SSR plugin's rootDir, truncated to 12 chars). Both this script and the
|
|
73
|
-
// plugin use process.cwd() as the rootDir, so hashes match.
|
|
74
|
-
const islandId = (file: string): string => {
|
|
75
|
-
const rel = file.slice(root.length + 1).replace(/\\/g, "/");
|
|
76
|
-
return new CryptoHasher("md5").update(rel).digest("hex").slice(0, 12);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
74
|
const compressibleExtensions = new Set([".css", ".html", ".js", ".json", ".map", ".svg", ".txt", ".xml"]);
|
|
80
75
|
|
|
81
76
|
async function precompressDistAssets(dir: string): Promise<void> {
|
|
@@ -124,11 +119,13 @@ try {
|
|
|
124
119
|
naming: "server.js",
|
|
125
120
|
target: "bun",
|
|
126
121
|
minify: true,
|
|
122
|
+
metafile: true,
|
|
127
123
|
define: {
|
|
128
124
|
__CLOUD_RELEASE__: JSON.stringify(release),
|
|
129
125
|
__CLOUD_SYNC_VERSION__: JSON.stringify(syncVersion),
|
|
126
|
+
__CLOUD_PDF_RENDER_WORKER__: JSON.stringify("./pdf-render/worker.js"),
|
|
130
127
|
},
|
|
131
|
-
plugins: [plugin()],
|
|
128
|
+
plugins: [(app?.plugin ?? plugin)()],
|
|
132
129
|
});
|
|
133
130
|
} finally {
|
|
134
131
|
await rm(serverEntry, { force: true });
|
|
@@ -137,26 +134,8 @@ if (!server.success) {
|
|
|
137
134
|
for (const m of server.logs) console.error(m);
|
|
138
135
|
throw new Error("Server bundle failed");
|
|
139
136
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// across every reachable package (including any island-shaped files in
|
|
143
|
-
// other workspace packages or in node_modules). Keep only this app's own
|
|
144
|
-
// islands plus the framework's; drop the rest. chunk-* files are shared
|
|
145
|
-
// splits and always kept.
|
|
146
|
-
const ssrDir = resolve(dist, "_ssr");
|
|
147
|
-
if (existsSync(ssrDir)) {
|
|
148
|
-
const allowedDirs = [frameworkDir, appDir];
|
|
149
|
-
const allowedIds = new Set<string>();
|
|
150
|
-
for (const dir of allowedDirs) {
|
|
151
|
-
for await (const file of new Glob("**/*.{island,client}.tsx").scan({ cwd: dir, absolute: true })) {
|
|
152
|
-
allowedIds.add(islandId(file));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
for (const entry of await readdir(ssrDir)) {
|
|
156
|
-
if (entry.startsWith("chunk-") || !entry.endsWith(".js")) continue;
|
|
157
|
-
const id = entry.slice(0, -3);
|
|
158
|
-
if (!allowedIds.has(id)) await rm(resolve(ssrDir, entry));
|
|
159
|
-
}
|
|
137
|
+
if (Object.keys(server.metafile?.inputs ?? {}).some(path => path.endsWith("/ai/pdf-render.ts"))) {
|
|
138
|
+
await buildPdfRenderer(dist);
|
|
160
139
|
}
|
|
161
140
|
|
|
162
141
|
// 2. Per-app Tailwind stylesheet.
|
|
@@ -199,6 +178,7 @@ if (existsSync(extras)) {
|
|
|
199
178
|
await import(extras);
|
|
200
179
|
}
|
|
201
180
|
|
|
181
|
+
await buildBrowserPerformance(distPublic, appId);
|
|
202
182
|
await precompressDistAssets(resolve(dist, "public"));
|
|
203
183
|
await precompressDistAssets(resolve(dist, "_ssr"));
|
|
204
184
|
|
package/scripts/preload.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { dirname, resolve } from "node:path";
|
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import tailwind from "bun-plugin-tailwind";
|
|
25
25
|
import { writeAppFavicon } from "./app-favicon";
|
|
26
|
+
import { buildBrowserPerformance } from "./browser-performance";
|
|
26
27
|
|
|
27
28
|
const appId = process.env.APP_ID ?? "core";
|
|
28
29
|
|
|
@@ -38,9 +39,11 @@ const frameworkDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
38
39
|
// convention. Resolved against cwd if relative.
|
|
39
40
|
const appDir = process.env.APP_DIR ? resolve(root, process.env.APP_DIR) : resolve(root, "packages", appId);
|
|
40
41
|
if (!existsSync(appDir)) throw new Error(`Unknown app dir: ${appDir} (set APP_DIR or check APP_ID)`);
|
|
42
|
+
// Share the resolved source directory with defineApp before importing config.
|
|
43
|
+
process.env.APP_DIR = appDir;
|
|
41
44
|
|
|
42
45
|
const { app, plugin } = await import(resolve(appDir, "src/config"));
|
|
43
|
-
Bun.plugin(plugin());
|
|
46
|
+
Bun.plugin((app?.plugin ?? plugin)());
|
|
44
47
|
|
|
45
48
|
// ── Build CSS ───────────────────────────────────────────────────────────────
|
|
46
49
|
const publicDir = resolve(root, "public");
|
|
@@ -125,15 +128,6 @@ if (appId === "core" && existsSync(globalCssEntry)) {
|
|
|
125
128
|
await cp(resolve(frameworkDir, "public/logo.svg"), resolve(publicDir, "logo.svg"));
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
// katex.css is only needed by notebooks (served by core via Traefik /public/katex.css)
|
|
129
|
-
if (appId === "notebooks") {
|
|
130
|
-
try {
|
|
131
|
-
await cp(resolve(root, "node_modules/katex/dist/katex.min.css"), resolve(publicDir, "katex.css"));
|
|
132
|
-
} catch {
|
|
133
|
-
console.warn("[preload] katex.css not found, skipping");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
131
|
// Each app builds its own app.css
|
|
138
132
|
const appPublicDir = resolve(publicDir, appId);
|
|
139
133
|
await mkdir(appPublicDir, { recursive: true });
|
|
@@ -146,9 +140,12 @@ if (appId !== "core" && app) {
|
|
|
146
140
|
});
|
|
147
141
|
}
|
|
148
142
|
|
|
149
|
-
|
|
143
|
+
if (existsSync(appCssPath)) {
|
|
144
|
+
await buildAppCss();
|
|
145
|
+
watchDevCss("app.css", [resolve(appCssPath, "..")], buildAppCss);
|
|
146
|
+
}
|
|
147
|
+
await buildBrowserPerformance(publicDir, appId);
|
|
150
148
|
|
|
151
|
-
watchDevCss("app.css", [resolve(appCssPath, "..")], buildAppCss);
|
|
152
149
|
if (appId === "core" && existsSync(globalCssEntry)) {
|
|
153
150
|
watchDevCss("global.css", [globalCssEntry, resolve(frameworkDir, "src/styles")], buildGlobalCss);
|
|
154
151
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* No provider or application-domain effects; successful recovery deletes only
|
|
8
8
|
* the exact test streams after checking their broker identity metadata.
|
|
9
9
|
*/
|
|
10
|
+
import { env } from "../src/config/env";
|
|
10
11
|
import { createSync, type SyncResourceSummary, type Worker } from "@k2b/sync";
|
|
11
12
|
import { connect, type NatsConnection } from "@nats-io/transport-node";
|
|
12
13
|
|
|
@@ -57,7 +58,7 @@ export const main = async (): Promise<void> => {
|
|
|
57
58
|
.map((value) => value.trim())
|
|
58
59
|
.filter(Boolean);
|
|
59
60
|
const connection = await connect({ servers, name: `${APPLICATION}-${phase}`, ignoreClusterUpdates: true });
|
|
60
|
-
const sync = createSync({ connection, namespace, application: APPLICATION, defaults: { replicas:
|
|
61
|
+
const sync = createSync({ connection, namespace, application: APPLICATION, defaults: { replicas: env.SYNC_REPLICAS } });
|
|
61
62
|
const job = sync.job<{ preparedAt: string }>({
|
|
62
63
|
id: JOB_ID,
|
|
63
64
|
delivery: { ackWaitMs: 30_000, maxAttempts: 3, backoffMs: [1_000, 2_000] },
|
|
@@ -11,12 +11,12 @@ const text = (value: string, label: string, max = MAX_SHORT_TEXT): string => {
|
|
|
11
11
|
return normalized;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const labels = (values: Readonly<Record<string, string>> | undefined, allowed: Set<string>, label: string) => {
|
|
14
|
+
const labels = (values: Readonly<Record<string, string>> | undefined, allowed: Set<string>, label: string, max = MAX_SHORT_TEXT) => {
|
|
15
15
|
if (!values) return undefined;
|
|
16
16
|
const normalized: Record<string, string> = {};
|
|
17
17
|
for (const [key, value] of Object.entries(values)) {
|
|
18
18
|
if (!allowed.has(key)) throw new Error(`${label} contains unknown key ${JSON.stringify(key)}`);
|
|
19
|
-
normalized[key] = text(value, `${label}.${key}
|
|
19
|
+
normalized[key] = text(value, `${label}.${key}`, max);
|
|
20
20
|
}
|
|
21
21
|
return normalized;
|
|
22
22
|
};
|
|
@@ -34,6 +34,7 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
|
|
|
34
34
|
groupIds.add(group.id);
|
|
35
35
|
}
|
|
36
36
|
const adminHrefs = new Set((app.adminNav ?? []).flatMap((group) => group.links.map((link) => link.href)));
|
|
37
|
+
const searchHrefs = new Set((app.searchLinks ?? []).map((link) => link.href));
|
|
37
38
|
const legalHrefs = new Set((app.legalLinks ?? []).map((link) => link.href));
|
|
38
39
|
const translations: Record<string, AppPresentationTranslation> = {};
|
|
39
40
|
|
|
@@ -49,6 +50,17 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
|
|
|
49
50
|
: {}),
|
|
50
51
|
...(translation.adminGroups ? { adminGroups: labels(translation.adminGroups, groupIds, `${canonical}.adminGroups`) } : {}),
|
|
51
52
|
...(translation.adminLinks ? { adminLinks: labels(translation.adminLinks, adminHrefs, `${canonical}.adminLinks`) } : {}),
|
|
53
|
+
...(translation.searchLinks ? { searchLinks: labels(translation.searchLinks, searchHrefs, `${canonical}.searchLinks`) } : {}),
|
|
54
|
+
...(translation.searchLinkDescriptions
|
|
55
|
+
? {
|
|
56
|
+
searchLinkDescriptions: labels(
|
|
57
|
+
translation.searchLinkDescriptions,
|
|
58
|
+
searchHrefs,
|
|
59
|
+
`${canonical}.searchLinkDescriptions`,
|
|
60
|
+
MAX_DESCRIPTION,
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
52
64
|
...(translation.legalLinks ? { legalLinks: labels(translation.legalLinks, legalHrefs, `${canonical}.legalLinks`) } : {}),
|
|
53
65
|
};
|
|
54
66
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type CapabilityActionReviewResult,
|
|
12
12
|
CapabilityActionReviewSchema,
|
|
13
13
|
type CapabilityDefinitions,
|
|
14
|
+
type CapabilityCommandManifest,
|
|
15
|
+
CapabilityCommandManifestSchema,
|
|
14
16
|
type CapabilityError,
|
|
15
17
|
CapabilityErrorSchema,
|
|
16
18
|
type CapabilityExecutionContext,
|
|
@@ -190,12 +192,14 @@ const compileSchemaPresentation = (
|
|
|
190
192
|
|
|
191
193
|
const compileOperationPresentation = (
|
|
192
194
|
value: unknown,
|
|
193
|
-
operation: CapabilityQueryManifest | CapabilityActionManifest,
|
|
195
|
+
operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
|
|
194
196
|
label: string,
|
|
195
197
|
): CapabilityOperationPresentationTranslation => {
|
|
196
198
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
197
199
|
const translation = value as Record<string, unknown>;
|
|
198
|
-
const allowed = new Set(
|
|
200
|
+
const allowed = new Set(
|
|
201
|
+
"path" in operation ? ["title", "description", "input"] : ["title", "description", "input", "data", "searchTags"],
|
|
202
|
+
);
|
|
199
203
|
const extra = Object.keys(translation).find((key) => !allowed.has(key));
|
|
200
204
|
if (extra) throw new Error(`${label} contains unsupported field "${extra}"`);
|
|
201
205
|
if (translation.title !== undefined) {
|
|
@@ -238,7 +242,7 @@ const compileOperationPresentation = (
|
|
|
238
242
|
);
|
|
239
243
|
}
|
|
240
244
|
const input = compileSchemaPresentation(translation.input, operation.inputSchema, `${label}.input`);
|
|
241
|
-
const data = compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`);
|
|
245
|
+
const data = "dataSchema" in operation ? compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`) : undefined;
|
|
242
246
|
return {
|
|
243
247
|
...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
|
|
244
248
|
...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
|
|
@@ -263,6 +267,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
263
267
|
const types = new Map(manifest.types.map((type) => [type.localId, type]));
|
|
264
268
|
const queries = new Map(manifest.queries.map((operation) => [operation.localId, operation]));
|
|
265
269
|
const actions = new Map(manifest.actions.map((operation) => [operation.localId, operation]));
|
|
270
|
+
const commands = new Map(manifest.commands.map((operation) => [operation.localId, operation]));
|
|
266
271
|
const translations: Record<string, CapabilityPresentationTranslation> = {};
|
|
267
272
|
for (const [locale, rawTranslation] of Object.entries(catalog.translations as Record<string, unknown>)) {
|
|
268
273
|
const canonical = canonicalLocale(locale);
|
|
@@ -273,7 +278,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
273
278
|
throw new Error(`Capability presentation translation ${canonical} must be an object`);
|
|
274
279
|
}
|
|
275
280
|
const raw = rawTranslation as Record<string, unknown>;
|
|
276
|
-
const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions");
|
|
281
|
+
const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions" && key !== "commands");
|
|
277
282
|
if (extra) throw new Error(`Capability presentation translation ${canonical} contains unsupported field "${extra}"`);
|
|
278
283
|
const compileGroup = <T>(
|
|
279
284
|
group: unknown,
|
|
@@ -311,10 +316,12 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
311
316
|
});
|
|
312
317
|
const translatedQueries = compileGroup(raw.queries, queries, "Query", compileOperationPresentation);
|
|
313
318
|
const translatedActions = compileGroup(raw.actions, actions, "Action", compileOperationPresentation);
|
|
319
|
+
const translatedCommands = compileGroup(raw.commands, commands, "Command", compileOperationPresentation);
|
|
314
320
|
translations[canonical] = {
|
|
315
321
|
...(translatedTypes ? { types: translatedTypes } : {}),
|
|
316
322
|
...(translatedQueries ? { queries: translatedQueries } : {}),
|
|
317
323
|
...(translatedActions ? { actions: translatedActions } : {}),
|
|
324
|
+
...(translatedCommands ? { commands: translatedCommands } : {}),
|
|
318
325
|
};
|
|
319
326
|
}
|
|
320
327
|
return { baseLocale, translations };
|
|
@@ -406,7 +413,7 @@ const normalizeSearchTags = (definition: CapabilityQueryDefinition, label: strin
|
|
|
406
413
|
...(aliases.length > 0 ? { aliases } : {}),
|
|
407
414
|
};
|
|
408
415
|
});
|
|
409
|
-
return { tags };
|
|
416
|
+
return { tags, ...(definition.universalSearch.scopeTypes ? { scopeTypes: [...new Set(definition.universalSearch.scopeTypes)] } : {}) };
|
|
410
417
|
};
|
|
411
418
|
|
|
412
419
|
const compileOperationSchemas = (definition: CapabilityQueryDefinition | CapabilityActionDefinition, label: string) => {
|
|
@@ -426,7 +433,7 @@ const compileOperationSchemas = (definition: CapabilityQueryDefinition | Capabil
|
|
|
426
433
|
inputSchema,
|
|
427
434
|
dataSchema,
|
|
428
435
|
resultZodSchema,
|
|
429
|
-
schemaHash: capabilityHash({ inputSchema, dataSchema }),
|
|
436
|
+
schemaHash: capabilityHash({ inputSchema, dataSchema, ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}) }),
|
|
430
437
|
};
|
|
431
438
|
};
|
|
432
439
|
|
|
@@ -462,6 +469,8 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
462
469
|
for (const [localId, definition] of Object.entries(definitions.queries ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
463
470
|
registerLocalId(localId, "Query");
|
|
464
471
|
const label = `Query ${localId}`;
|
|
472
|
+
if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
|
|
473
|
+
if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
|
|
465
474
|
const schemas = compileOperationSchemas(definition, label);
|
|
466
475
|
if (definition.universalSearch) {
|
|
467
476
|
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
@@ -480,9 +489,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
480
489
|
inputSchema: schemas.inputSchema,
|
|
481
490
|
dataSchema: schemas.dataSchema,
|
|
482
491
|
schemaHash: schemas.schemaHash,
|
|
492
|
+
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
483
493
|
openWorld: definition.openWorld,
|
|
484
494
|
universalSearch: normalizeSearchTags(definition, label),
|
|
485
495
|
} satisfies CapabilityQueryManifest;
|
|
496
|
+
for (const type of manifest.universalSearch?.scopeTypes ?? []) {
|
|
497
|
+
if (!typeIds.has(qualifiedId(appId, type))) throw new Error(`${label} declares an unknown search scope type ${type}`);
|
|
498
|
+
}
|
|
486
499
|
queries.set(localId, {
|
|
487
500
|
definition,
|
|
488
501
|
manifest,
|
|
@@ -499,12 +512,36 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
499
512
|
for (const [localId, definition] of Object.entries(definitions.actions ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
500
513
|
registerLocalId(localId, "Action");
|
|
501
514
|
const label = `Action ${localId}`;
|
|
515
|
+
// Declaration rule, documented in docs/platform/capabilities.md:
|
|
516
|
+
// destructive means irreversible or externally visible. An irreversible or
|
|
517
|
+
// open-world effect must always be reviewable and safely repeatable, and
|
|
518
|
+
// must never be approved once and then replayed silently.
|
|
519
|
+
if (definition.destructive && !definition.review) {
|
|
520
|
+
throw new Error(`${label} is destructive and must declare review()`);
|
|
521
|
+
}
|
|
522
|
+
if (definition.destructive && definition.idempotency !== "required") {
|
|
523
|
+
throw new Error(`${label} is destructive and must declare idempotency: "required"`);
|
|
524
|
+
}
|
|
525
|
+
if (definition.openWorld && !definition.review) {
|
|
526
|
+
throw new Error(`${label} is open-world and must declare review()`);
|
|
527
|
+
}
|
|
528
|
+
if (definition.openWorld && definition.idempotency !== "required") {
|
|
529
|
+
throw new Error(`${label} is open-world and must declare idempotency: "required"`);
|
|
530
|
+
}
|
|
502
531
|
if (definition.approval === "rememberable" && definition.openWorld) {
|
|
503
532
|
throw new Error(`${label} cannot remember approval for an open-world effect`);
|
|
504
533
|
}
|
|
534
|
+
if (definition.approval === "rememberable" && definition.destructive) {
|
|
535
|
+
throw new Error(`${label} cannot remember approval for a destructive effect`);
|
|
536
|
+
}
|
|
537
|
+
if (definition.approval === "none" && (definition.destructive || definition.openWorld)) {
|
|
538
|
+
throw new Error(`${label} cannot skip approval for destructive or open-world effects`);
|
|
539
|
+
}
|
|
505
540
|
if (definition.approval === "rememberable" && !definition.review) {
|
|
506
541
|
throw new Error(`${label} must provide a review before approval can be remembered`);
|
|
507
542
|
}
|
|
543
|
+
if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
|
|
544
|
+
if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
|
|
508
545
|
const schemas = compileOperationSchemas(definition, label);
|
|
509
546
|
const manifest = {
|
|
510
547
|
localId,
|
|
@@ -513,6 +550,7 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
513
550
|
inputSchema: schemas.inputSchema,
|
|
514
551
|
dataSchema: schemas.dataSchema,
|
|
515
552
|
schemaHash: schemas.schemaHash,
|
|
553
|
+
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
516
554
|
destructive: definition.destructive,
|
|
517
555
|
openWorld: definition.openWorld,
|
|
518
556
|
idempotency: definition.idempotency,
|
|
@@ -526,12 +564,30 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
526
564
|
});
|
|
527
565
|
}
|
|
528
566
|
|
|
567
|
+
const commands = Object.entries(definitions.commands ?? {})
|
|
568
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
569
|
+
.map(([localId, definition]) => {
|
|
570
|
+
registerLocalId(localId, "Command");
|
|
571
|
+
const inputSchema = projectSchema(definition.input, `Command ${localId} input`, "input");
|
|
572
|
+
assertClosedObjectInput(inputSchema, `Command ${localId} input`);
|
|
573
|
+
return CapabilityCommandManifestSchema.parse({
|
|
574
|
+
localId,
|
|
575
|
+
title: definition.title.trim(),
|
|
576
|
+
description: definition.description.trim(),
|
|
577
|
+
icon: definition.icon,
|
|
578
|
+
keywords: [...(definition.keywords ?? [])],
|
|
579
|
+
inputSchema,
|
|
580
|
+
path: definition.path,
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
529
584
|
const manifestBase = {
|
|
530
585
|
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
531
586
|
appId,
|
|
532
587
|
types,
|
|
533
588
|
queries: [...queries.values()].map((entry) => entry.manifest),
|
|
534
589
|
actions: [...actions.values()].map((entry) => entry.manifest),
|
|
590
|
+
commands,
|
|
535
591
|
};
|
|
536
592
|
const manifest = CapabilityManifestSchema.parse({
|
|
537
593
|
...manifestBase,
|
|
@@ -614,6 +670,7 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
614
670
|
...(operation.universalSearch
|
|
615
671
|
? {
|
|
616
672
|
universalSearch: {
|
|
673
|
+
...operation.universalSearch,
|
|
617
674
|
tags: operation.universalSearch.tags.map((tag) => {
|
|
618
675
|
const tagCopy = copy.searchTags?.[tag.tag];
|
|
619
676
|
return tagCopy
|
|
@@ -625,6 +682,17 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
625
682
|
: {}),
|
|
626
683
|
};
|
|
627
684
|
}),
|
|
685
|
+
commands: current.commands.map((operation) => {
|
|
686
|
+
const copy = translation.commands?.[operation.localId];
|
|
687
|
+
return copy
|
|
688
|
+
? {
|
|
689
|
+
...operation,
|
|
690
|
+
title: copy.title ?? operation.title,
|
|
691
|
+
description: copy.description ?? operation.description,
|
|
692
|
+
inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
|
|
693
|
+
}
|
|
694
|
+
: operation;
|
|
695
|
+
}),
|
|
628
696
|
actions: current.actions.map((operation) => {
|
|
629
697
|
const copy = translation.actions?.[operation.localId];
|
|
630
698
|
return copy
|
|
@@ -668,10 +736,19 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
|
|
|
668
736
|
);
|
|
669
737
|
}
|
|
670
738
|
assertClosedObjectInput(inputSchema, `Operation ${operation.localId} input`);
|
|
671
|
-
|
|
739
|
+
if (
|
|
740
|
+
operation.stream?.direction === "write" &&
|
|
741
|
+
(!("idempotency" in operation) || operation.idempotency !== "required")
|
|
742
|
+
) {
|
|
743
|
+
throw new Error(`Operation ${operation.localId} write streams require an idempotent Action`);
|
|
744
|
+
}
|
|
745
|
+
const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema, ...(operation.stream ? { stream: operation.stream } : {}) });
|
|
672
746
|
if (operation.schemaHash !== expectedSchemaHash)
|
|
673
747
|
throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
|
|
674
748
|
if ("universalSearch" in operation && operation.universalSearch) {
|
|
749
|
+
for (const type of operation.universalSearch.scopeTypes ?? []) {
|
|
750
|
+
if (!manifest.types.some((entry) => entry.localId === type)) throw new Error(`Unknown search scope type ${type}`);
|
|
751
|
+
}
|
|
675
752
|
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
676
753
|
const expectedData = projectSchema(UniversalSearchDataSchema, "Universal Search data", "output");
|
|
677
754
|
if (
|
|
@@ -682,6 +759,11 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
|
|
|
682
759
|
}
|
|
683
760
|
}
|
|
684
761
|
}
|
|
762
|
+
for (const command of manifest.commands) {
|
|
763
|
+
registerLocalId(command.localId, "Command");
|
|
764
|
+
z.fromJSONSchema(structuredClone(command.inputSchema));
|
|
765
|
+
assertClosedObjectInput(command.inputSchema, `Command ${command.localId} input`);
|
|
766
|
+
}
|
|
685
767
|
const queries = new Map(manifest.queries.map((query) => [query.localId, query]));
|
|
686
768
|
for (const type of manifest.types) {
|
|
687
769
|
if (!type.reader) continue;
|
|
@@ -787,6 +869,12 @@ export const capabilityManifestEvolutionIssues = (previous: CapabilityManifest,
|
|
|
787
869
|
else if (type.reader && current.reader !== type.reader) issues.push(`Type ${type.localId} reader changed`);
|
|
788
870
|
}
|
|
789
871
|
|
|
872
|
+
const nextCommands = new Map(next.commands.map((command) => [command.localId, command]));
|
|
873
|
+
for (const command of previous.commands) {
|
|
874
|
+
const current = nextCommands.get(command.localId);
|
|
875
|
+
if (!current) issues.push(`Command ${command.localId} was removed`);
|
|
876
|
+
else issues.push(...schemaEvolutionIssues(command.inputSchema, current.inputSchema, `Command ${command.localId} input`, "input"));
|
|
877
|
+
}
|
|
790
878
|
const nextQueries = new Map(next.queries.map((operation) => [operation.localId, operation]));
|
|
791
879
|
const nextActions = new Map(next.actions.map((operation) => [operation.localId, operation]));
|
|
792
880
|
for (const operation of previous.queries) {
|
|
@@ -982,11 +1070,24 @@ export const invokeCompiledCapability = async (params: {
|
|
|
982
1070
|
},
|
|
983
1071
|
};
|
|
984
1072
|
}
|
|
1073
|
+
if ("universalSearch" in operation.manifest && operation.manifest.universalSearch) {
|
|
1074
|
+
const search = UniversalSearchInputSchema.parse(input.data);
|
|
1075
|
+
if (
|
|
1076
|
+
search.scope &&
|
|
1077
|
+
!(operation.manifest.universalSearch.scopeTypes ?? []).some(
|
|
1078
|
+
(type) => qualifiedId(params.compiled.manifest.appId, type) === search.scope!.type,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
return { ok: false, error: { code: "VALIDATION_FAILED", message: "Unsupported search scope", status: 400 } };
|
|
1082
|
+
}
|
|
985
1083
|
try {
|
|
986
1084
|
const invoked = await operation.definition.run(input.data, context);
|
|
987
1085
|
if (!invoked.ok) {
|
|
988
1086
|
return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
|
|
989
1087
|
}
|
|
1088
|
+
const stream = invoked.data.stream;
|
|
1089
|
+
if (stream && (Buffer.byteLength(stream.id) > 2048 || !operation.definition.stream || stream.direction !== operation.definition.stream.direction || stream.size > operation.definition.stream.maxBytes || Date.parse(stream.expiresAt) <= Date.now() || Date.parse(stream.expiresAt) > Date.now() + 86_400_000))
|
|
1090
|
+
return invalidProviderError("Stream is outside its declared policy or 24 hour lifetime");
|
|
990
1091
|
const validated = validateCapabilityResult(params.compiled, operation, invoked.data, params.onUnexpectedError);
|
|
991
1092
|
return validated.ok
|
|
992
1093
|
? ({
|