@k2b/cloud 0.39.0 → 0.41.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 +11 -4
- package/src/_internal/app-favicon.ts +4 -2
- 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 +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -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 +464 -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 +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -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-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 +2 -0
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- 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 +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- 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 +166 -262
- package/src/approvals/approval-card.ts +68 -0
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -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 +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- 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 +123 -16
- 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/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -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/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- 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 +13 -0
- 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/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/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -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 +10 -2
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +213 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- 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 +17 -22
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.41.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,6 +34,7 @@
|
|
|
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",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"./cli/capabilities": "./src/cli/capabilities.ts",
|
|
57
58
|
"./cli/admin": "./src/cli/admin/index.ts",
|
|
58
59
|
"./shared": "./src/shared/index.ts",
|
|
60
|
+
"./shared-text": "./src/shared-text/index.ts",
|
|
59
61
|
"./services": "./src/services/index.ts",
|
|
60
62
|
"./services/image-metadata": "./src/services/image-metadata.ts",
|
|
61
63
|
"./services/document-extraction": "./src/services/document-extraction.ts",
|
|
@@ -65,10 +67,12 @@
|
|
|
65
67
|
"./ai": "./src/ai/index.ts",
|
|
66
68
|
"./ai/admin": "./src/ai/admin.ts",
|
|
67
69
|
"./ai/browser": "./src/ai/browser.ts",
|
|
70
|
+
"./ai/question-text": "./src/ai/question-text.ts",
|
|
68
71
|
"./ai/live": "./src/ai/live.ts",
|
|
69
72
|
"./ai/live-events": "./src/ai/live-events.ts",
|
|
70
73
|
"./ai/runtime": "./src/ai/runtime.ts",
|
|
71
74
|
"./ai/solid": "./src/ai/solid.ts",
|
|
75
|
+
"./ai/testing": "./src/ai/testing.ts",
|
|
72
76
|
"./ai/tools": "./src/ai/runtime-tools.ts",
|
|
73
77
|
"./ai/ui": "./src/ai/ui.tsx",
|
|
74
78
|
"./account/ui": "./src/account/ui.ts",
|
|
@@ -110,21 +114,23 @@
|
|
|
110
114
|
"@tabler/icons": "3.48.0",
|
|
111
115
|
"@tailwindcss/typography": "0.5.20",
|
|
112
116
|
"@k2b/nessi": "0.18.0",
|
|
113
|
-
"@k2b/ssr": "0.
|
|
114
|
-
"@k2b/ui": "0.
|
|
115
|
-
"@k2b/stdlib": "0.
|
|
117
|
+
"@k2b/ssr": "0.19.0",
|
|
118
|
+
"@k2b/ui": "0.31.0",
|
|
119
|
+
"@k2b/stdlib": "0.30.0",
|
|
116
120
|
"@k2b/sync": "7.0.0",
|
|
117
121
|
"@nats-io/jetstream": "3.4.0",
|
|
118
122
|
"@nats-io/transport-node": "3.4.0",
|
|
119
123
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
120
124
|
"bun-plugin-tailwind": "0.1.2",
|
|
121
125
|
"hono-openapi": "1.3.3",
|
|
126
|
+
"entities": "8.0.0",
|
|
122
127
|
"htmlparser2": "12.0.0",
|
|
123
128
|
"jose": "6.2.12",
|
|
124
129
|
"katex": "0.18.9",
|
|
125
130
|
"liquidjs": "10.29.0",
|
|
126
131
|
"marked": "18.0.14",
|
|
127
132
|
"mermaid": "12.0.0",
|
|
133
|
+
"mmdb-lib": "3.0.3",
|
|
128
134
|
"nodemailer": "10.0.10",
|
|
129
135
|
"postcss": "8.5.28",
|
|
130
136
|
"reflect-metadata": "0.2.2",
|
|
@@ -134,6 +140,7 @@
|
|
|
134
140
|
"web-push": "3.6.7",
|
|
135
141
|
"web-vitals": "6.2.2",
|
|
136
142
|
"yaml": "2.9.1",
|
|
143
|
+
"yjs": "13.6.33",
|
|
137
144
|
"hash-wasm": "4.12.0",
|
|
138
145
|
"@types/sanitize-html": "2.16.1",
|
|
139
146
|
"@types/web-push": "3.6.4"
|
|
@@ -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)}`}`;
|
|
@@ -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: {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { capabilityPreviewIssues, capabilityReviewFormIssues } from "../capabilities/reviews";
|
|
3
|
+
import {
|
|
4
|
+
CAPABILITY_REACHES,
|
|
5
|
+
CapabilityActionCheckSchema,
|
|
6
|
+
type CapabilityActionPolicy,
|
|
7
|
+
CapabilityActionReceiptSchema,
|
|
8
|
+
CapabilityActionReviewDataSchema,
|
|
9
|
+
type CapabilityQueryManifest,
|
|
10
|
+
CapabilitySemanticLinkSchema,
|
|
11
|
+
} from "../contracts/capabilities";
|
|
12
|
+
import { localeFallbackChain } from "../shared/locale";
|
|
13
|
+
import { capabilityHash, projectSchema } from "./capability-schemas";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
actionReview,
|
|
17
|
+
type CapabilityReviewForms,
|
|
18
|
+
capabilityReviewFormIssues,
|
|
19
|
+
capabilityReviewRememberScope,
|
|
20
|
+
capabilityReviewView,
|
|
21
|
+
type SlotNames,
|
|
22
|
+
} from "../capabilities/reviews";
|
|
23
|
+
|
|
24
|
+
export { CAPABILITY_MAX_REVIEW_BYTES } from "../contracts/capabilities";
|
|
25
|
+
export type CapabilityReviewContext = {
|
|
26
|
+
policy?: CapabilityActionPolicy;
|
|
27
|
+
approval?: "rememberable" | "none";
|
|
28
|
+
queries: readonly CapabilityQueryManifest[];
|
|
29
|
+
validateQueryInput: (query: CapabilityQueryManifest, input: Record<string, unknown>) => boolean;
|
|
30
|
+
locales?: readonly string[];
|
|
31
|
+
};
|
|
32
|
+
const schemaIssues = (error: z.ZodError): string[] => error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
33
|
+
let actionCheckDataHash: string | undefined;
|
|
34
|
+
/** Whether a Query answers an Action's `check`: its data schema is exactly the projected `CapabilityActionCheckSchema`. */
|
|
35
|
+
export const capabilityQueryChecksActionOutcome = (query: CapabilityQueryManifest): boolean => {
|
|
36
|
+
actionCheckDataHash ??= capabilityHash(projectSchema(CapabilityActionCheckSchema, "Action check", "output"));
|
|
37
|
+
return capabilityHash(query.dataSchema) === actionCheckDataHash;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const capabilityActionReviewIssues = (review: unknown, context: CapabilityReviewContext): string[] => {
|
|
41
|
+
const parsed = CapabilityActionReviewDataSchema.safeParse(review);
|
|
42
|
+
if (!parsed.success) return schemaIssues(parsed.error);
|
|
43
|
+
const data = parsed.data;
|
|
44
|
+
if (!("sentence" in data))
|
|
45
|
+
return context.policy
|
|
46
|
+
? ["A policy Action must return a new review"]
|
|
47
|
+
: data.approvalScope !== undefined && context.approval !== "rememberable"
|
|
48
|
+
? ["approvalScope requires rememberable approval"]
|
|
49
|
+
: [];
|
|
50
|
+
if (!context.policy) return ["An Action without policy must return a legacy review"];
|
|
51
|
+
const issues = capabilityReviewFormIssues(data);
|
|
52
|
+
if (
|
|
53
|
+
data.reach !== undefined &&
|
|
54
|
+
(!("silent" in context.policy) || CAPABILITY_REACHES.indexOf(data.reach) > CAPABILITY_REACHES.indexOf(context.policy.silent))
|
|
55
|
+
)
|
|
56
|
+
issues.push("reach exceeds the declared silent reach");
|
|
57
|
+
if (context.locales && !localeFallbackChain(data.locale, data.locale).some((locale) => context.locales?.includes(locale)))
|
|
58
|
+
issues.push("locale is not supported by the app");
|
|
59
|
+
if (data.check) {
|
|
60
|
+
const query = context.queries.find((query) => query.localId === data.check?.query);
|
|
61
|
+
if (!query || !capabilityQueryChecksActionOutcome(query)) issues.push("check.query must return CapabilityActionCheckSchema");
|
|
62
|
+
else if (!context.validateQueryInput(query, data.check.input)) issues.push("check.input does not match the Query");
|
|
63
|
+
}
|
|
64
|
+
issues.push(
|
|
65
|
+
...capabilityPreviewIssues(
|
|
66
|
+
data.preview,
|
|
67
|
+
data.links,
|
|
68
|
+
// A per-call reason never lifts the declared `sends` completeness rule.
|
|
69
|
+
data.ask === "sends" || ("ask" in context.policy && context.policy.ask === "sends"),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
return [...new Set(issues)];
|
|
73
|
+
};
|
|
74
|
+
/** A receipt's own rules; completeness links are the result envelope's top-level `links`. */
|
|
75
|
+
export const capabilityActionReceiptIssues = (receipt: unknown, links: unknown): string[] => {
|
|
76
|
+
const parsed = CapabilityActionReceiptSchema.safeParse(receipt);
|
|
77
|
+
const resultLinks = z.array(CapabilitySemanticLinkSchema).safeParse(links);
|
|
78
|
+
return parsed.success
|
|
79
|
+
? capabilityPreviewIssues(parsed.data.preview, resultLinks.success ? resultLinks.data : undefined)
|
|
80
|
+
: schemaIssues(parsed.error);
|
|
81
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
type JsonSchema = Record<string, unknown>;
|
|
5
|
+
export const stableJsonValue = (value: unknown): unknown => {
|
|
6
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
7
|
+
if (typeof value !== "object" || value === null) return value;
|
|
8
|
+
return Object.fromEntries(
|
|
9
|
+
Object.entries(value)
|
|
10
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
11
|
+
.map(([key, entry]) => [key, stableJsonValue(entry)]),
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const capabilityHash = (value: unknown): string =>
|
|
16
|
+
createHash("sha256")
|
|
17
|
+
.update(JSON.stringify(stableJsonValue(value)))
|
|
18
|
+
.digest("hex");
|
|
19
|
+
|
|
20
|
+
export const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
|
|
21
|
+
try {
|
|
22
|
+
const projected = z.toJSONSchema(schema, { io });
|
|
23
|
+
z.fromJSONSchema(projected);
|
|
24
|
+
return projected;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new Error(`${label} is not JSON-Schema-round-trippable: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
};
|