@k2b/cloud 0.6.0-rc.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 +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isServiceError, type Result, type ServiceError } from "@k2b/stdlib";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
CAPABILITY_ERROR_STATUSES,
|
|
6
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
7
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
8
|
+
CAPABILITY_PROTOCOL_VERSION,
|
|
9
|
+
type CapabilityActionDefinition,
|
|
10
|
+
type CapabilityActionManifest,
|
|
11
|
+
type CapabilityActionReviewResult,
|
|
12
|
+
CapabilityActionReviewSchema,
|
|
13
|
+
type CapabilityDefinitions,
|
|
14
|
+
type CapabilityError,
|
|
15
|
+
CapabilityErrorSchema,
|
|
16
|
+
type CapabilityExecutionContext,
|
|
17
|
+
CapabilityIdempotencyKeySchema,
|
|
18
|
+
type CapabilityInvocationResult,
|
|
19
|
+
CapabilityLocalIdSchema,
|
|
20
|
+
type CapabilityManifest,
|
|
21
|
+
CapabilityManifestSchema,
|
|
22
|
+
type CapabilityOperationPresentationTranslation,
|
|
23
|
+
type CapabilityPresentationCatalog,
|
|
24
|
+
type CapabilityPresentationTranslation,
|
|
25
|
+
type CapabilityQueryDefinition,
|
|
26
|
+
type CapabilityQueryManifest,
|
|
27
|
+
capabilityResultSchema,
|
|
28
|
+
UniversalSearchDataSchema,
|
|
29
|
+
UniversalSearchInputSchema,
|
|
30
|
+
} from "../contracts/capabilities";
|
|
31
|
+
import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
|
|
32
|
+
|
|
33
|
+
type JsonSchema = Record<string, unknown>;
|
|
34
|
+
const MAX_CAPABILITY_MANIFEST_BYTES = 256 * 1024;
|
|
35
|
+
|
|
36
|
+
export type CompiledCapabilityQuery = {
|
|
37
|
+
definition: CapabilityQueryDefinition;
|
|
38
|
+
manifest: CapabilityQueryManifest;
|
|
39
|
+
resultSchema: z.ZodType;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type CompiledCapabilityAction = {
|
|
43
|
+
definition: CapabilityActionDefinition;
|
|
44
|
+
manifest: CapabilityActionManifest;
|
|
45
|
+
resultSchema: z.ZodType;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type CompiledCapabilities = {
|
|
49
|
+
manifest: CapabilityManifest;
|
|
50
|
+
presentation?: CapabilityPresentationCatalog;
|
|
51
|
+
typeIds: ReadonlySet<string>;
|
|
52
|
+
queries: ReadonlyMap<string, CompiledCapabilityQuery>;
|
|
53
|
+
actions: ReadonlyMap<string, CompiledCapabilityAction>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type CapabilityProviderResult = CapabilityInvocationResult<unknown> | CapabilityActionReviewResult;
|
|
57
|
+
type CapabilityProviderFailure = { ok: false; error: CapabilityError };
|
|
58
|
+
const capabilityProviderErrorStatuses = new Set<number>(CAPABILITY_ERROR_STATUSES);
|
|
59
|
+
|
|
60
|
+
const isCapabilityProviderErrorStatus = (value: unknown): value is CapabilityError["status"] =>
|
|
61
|
+
typeof value === "number" && capabilityProviderErrorStatuses.has(value);
|
|
62
|
+
|
|
63
|
+
const invalidProviderError = (message: string): CapabilityProviderFailure => ({
|
|
64
|
+
ok: false,
|
|
65
|
+
error: {
|
|
66
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse,
|
|
67
|
+
message,
|
|
68
|
+
status: 500,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const normalizeProviderError = (value: unknown, message: string, onInvalid?: (error: unknown) => void): CapabilityProviderFailure => {
|
|
73
|
+
const error = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
74
|
+
const parsed = CapabilityErrorSchema.safeParse({ code: error.code, message: error.message, details: error.details });
|
|
75
|
+
if (parsed.success && isCapabilityProviderErrorStatus(error.status)) {
|
|
76
|
+
return { ok: false, error: { ...parsed.data, status: error.status } };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
onInvalid?.(new Error(message));
|
|
80
|
+
} catch {
|
|
81
|
+
// Observability must not change the public failure contract.
|
|
82
|
+
}
|
|
83
|
+
return invalidProviderError(message);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const providerPayload = (code: string, message: string, details?: Record<string, unknown>): Record<string, unknown> => ({
|
|
87
|
+
code,
|
|
88
|
+
message,
|
|
89
|
+
...(details ? { details } : {}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** Serializes one framework-owned provider response exactly once and enforces the public byte bound. */
|
|
93
|
+
export const serializeCapabilityProviderResult = (
|
|
94
|
+
result: CapabilityProviderResult,
|
|
95
|
+
options: { nonIdempotentAction?: boolean } = {},
|
|
96
|
+
): { body: string; status: number } => {
|
|
97
|
+
if (options.nonIdempotentAction && !result.ok && result.error.code === CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse) {
|
|
98
|
+
const unknown = providerPayload(
|
|
99
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown,
|
|
100
|
+
"The Action result could not be validated and its outcome is unknown; do not retry automatically",
|
|
101
|
+
{ retrySafe: false },
|
|
102
|
+
);
|
|
103
|
+
return { body: JSON.stringify(unknown), status: 502 };
|
|
104
|
+
}
|
|
105
|
+
const payload = result.ok ? result.data : providerPayload(result.error.code, result.error.message, result.error.details);
|
|
106
|
+
try {
|
|
107
|
+
const body = JSON.stringify(payload);
|
|
108
|
+
if (new TextEncoder().encode(body).byteLength <= CAPABILITY_MAX_RESULT_BYTES) {
|
|
109
|
+
return { body, status: result.ok ? 200 : result.error.status };
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall through to the fixed bounded framework error below.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fallback = options.nonIdempotentAction
|
|
116
|
+
? providerPayload(
|
|
117
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown,
|
|
118
|
+
"The Action result could not be returned and its outcome is unknown; do not retry automatically",
|
|
119
|
+
{ retrySafe: false },
|
|
120
|
+
)
|
|
121
|
+
: providerPayload(
|
|
122
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge,
|
|
123
|
+
"Capability response exceeds the shared size limit; narrow the request before retrying",
|
|
124
|
+
{ retrySafe: false },
|
|
125
|
+
);
|
|
126
|
+
return { body: JSON.stringify(fallback), status: options.nonIdempotentAction ? 502 : 500 };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const stableJsonValue = (value: unknown): unknown => {
|
|
130
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
131
|
+
if (typeof value !== "object" || value === null) return value;
|
|
132
|
+
return Object.fromEntries(
|
|
133
|
+
Object.entries(value as Record<string, unknown>)
|
|
134
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
135
|
+
.map(([key, entry]) => [key, stableJsonValue(entry)]),
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const capabilityHash = (value: unknown): string =>
|
|
140
|
+
createHash("sha256")
|
|
141
|
+
.update(JSON.stringify(stableJsonValue(value)))
|
|
142
|
+
.digest("hex");
|
|
143
|
+
|
|
144
|
+
const assertText = (value: string, label: string, max: number): void => {
|
|
145
|
+
if (!value.trim()) throw new Error(`${label} is required`);
|
|
146
|
+
if (value.length > max) throw new Error(`${label} must be at most ${max} characters`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const schemaFieldPaths = (schema: unknown, prefix = ""): Set<string> => {
|
|
150
|
+
const paths = new Set<string>();
|
|
151
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return paths;
|
|
152
|
+
const value = schema as Record<string, unknown>;
|
|
153
|
+
const properties = value.properties;
|
|
154
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
155
|
+
for (const [field, definition] of Object.entries(properties)) {
|
|
156
|
+
const path = prefix ? `${prefix}.${field}` : field;
|
|
157
|
+
paths.add(path);
|
|
158
|
+
for (const child of schemaFieldPaths(definition, path)) paths.add(child);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (value.items) {
|
|
162
|
+
const arrayPrefix = `${prefix}[]`;
|
|
163
|
+
for (const child of schemaFieldPaths(value.items, arrayPrefix)) paths.add(child);
|
|
164
|
+
}
|
|
165
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
|
|
166
|
+
if (!Array.isArray(value[keyword])) continue;
|
|
167
|
+
for (const entry of value[keyword]) {
|
|
168
|
+
for (const child of schemaFieldPaths(entry, prefix)) paths.add(child);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return paths;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const compileSchemaPresentation = (
|
|
175
|
+
value: unknown,
|
|
176
|
+
schema: Record<string, unknown>,
|
|
177
|
+
label: string,
|
|
178
|
+
): Readonly<Record<string, string>> | undefined => {
|
|
179
|
+
if (value === undefined) return undefined;
|
|
180
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
181
|
+
const paths = schemaFieldPaths(schema);
|
|
182
|
+
const entries = Object.entries(value as Record<string, unknown>).map(([path, description]) => {
|
|
183
|
+
if (!paths.has(path)) throw new Error(`${label} field path "${path}" does not exist in the projected schema`);
|
|
184
|
+
if (typeof description !== "string") throw new Error(`${label}.${path} must be text`);
|
|
185
|
+
assertText(description, `${label}.${path}`, 1000);
|
|
186
|
+
return [path, description.trim()] as const;
|
|
187
|
+
});
|
|
188
|
+
return Object.fromEntries(entries);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const compileOperationPresentation = (
|
|
192
|
+
value: unknown,
|
|
193
|
+
operation: CapabilityQueryManifest | CapabilityActionManifest,
|
|
194
|
+
label: string,
|
|
195
|
+
): CapabilityOperationPresentationTranslation => {
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
197
|
+
const translation = value as Record<string, unknown>;
|
|
198
|
+
const allowed = new Set(["title", "description", "input", "data", "searchTags"]);
|
|
199
|
+
const extra = Object.keys(translation).find((key) => !allowed.has(key));
|
|
200
|
+
if (extra) throw new Error(`${label} contains unsupported field "${extra}"`);
|
|
201
|
+
if (translation.title !== undefined) {
|
|
202
|
+
if (typeof translation.title !== "string") throw new Error(`${label}.title must be text`);
|
|
203
|
+
assertText(translation.title, `${label}.title`, 120);
|
|
204
|
+
}
|
|
205
|
+
if (translation.description !== undefined) {
|
|
206
|
+
if (typeof translation.description !== "string") throw new Error(`${label}.description must be text`);
|
|
207
|
+
assertText(translation.description, `${label}.description`, 1000);
|
|
208
|
+
}
|
|
209
|
+
let searchTags: CapabilityOperationPresentationTranslation["searchTags"];
|
|
210
|
+
if (translation.searchTags !== undefined) {
|
|
211
|
+
if (!("universalSearch" in operation) || !operation.universalSearch) throw new Error(`${label}.searchTags requires Universal Search`);
|
|
212
|
+
if (!translation.searchTags || typeof translation.searchTags !== "object" || Array.isArray(translation.searchTags)) {
|
|
213
|
+
throw new Error(`${label}.searchTags must be an object`);
|
|
214
|
+
}
|
|
215
|
+
const tags = new Map(operation.universalSearch.tags.map((tag) => [tag.tag, tag]));
|
|
216
|
+
searchTags = Object.fromEntries(
|
|
217
|
+
Object.entries(translation.searchTags as Record<string, unknown>).map(([tag, presentation]) => {
|
|
218
|
+
if (!tags.has(tag)) throw new Error(`${label}.searchTags references unknown stable tag "${tag}"`);
|
|
219
|
+
if (!presentation || typeof presentation !== "object" || Array.isArray(presentation)) {
|
|
220
|
+
throw new Error(`${label}.searchTags.${tag} must be an object`);
|
|
221
|
+
}
|
|
222
|
+
const copy = presentation as Record<string, unknown>;
|
|
223
|
+
const extraField = Object.keys(copy).find((key) => key !== "title" && key !== "description");
|
|
224
|
+
if (extraField) throw new Error(`${label}.searchTags.${tag} contains unsupported field "${extraField}"`);
|
|
225
|
+
if (copy.title !== undefined) {
|
|
226
|
+
if (typeof copy.title !== "string") throw new Error(`${label}.searchTags.${tag}.title must be text`);
|
|
227
|
+
assertText(copy.title, `${label}.searchTags.${tag}.title`, 120);
|
|
228
|
+
}
|
|
229
|
+
if (copy.description !== undefined) {
|
|
230
|
+
if (typeof copy.description !== "string") throw new Error(`${label}.searchTags.${tag}.description must be text`);
|
|
231
|
+
assertText(copy.description, `${label}.searchTags.${tag}.description`, 500);
|
|
232
|
+
}
|
|
233
|
+
return [
|
|
234
|
+
tag,
|
|
235
|
+
{ ...(copy.title ? { title: copy.title.trim() } : {}), ...(copy.description ? { description: copy.description.trim() } : {}) },
|
|
236
|
+
];
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const input = compileSchemaPresentation(translation.input, operation.inputSchema, `${label}.input`);
|
|
241
|
+
const data = compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`);
|
|
242
|
+
return {
|
|
243
|
+
...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
|
|
244
|
+
...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
|
|
245
|
+
...(input ? { input } : {}),
|
|
246
|
+
...(data ? { data } : {}),
|
|
247
|
+
...(searchTags ? { searchTags } : {}),
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const compileCapabilityPresentation = (manifest: CapabilityManifest, value: unknown): CapabilityPresentationCatalog | undefined => {
|
|
252
|
+
if (value === undefined) return undefined;
|
|
253
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Capability presentation must be an object");
|
|
254
|
+
const catalog = value as Record<string, unknown>;
|
|
255
|
+
if (Object.keys(catalog).some((key) => key !== "baseLocale" && key !== "translations")) {
|
|
256
|
+
throw new Error("Capability presentation contains unsupported fields");
|
|
257
|
+
}
|
|
258
|
+
const baseLocale = typeof catalog.baseLocale === "string" ? canonicalLocale(catalog.baseLocale) : undefined;
|
|
259
|
+
if (!baseLocale) throw new Error("Capability presentation baseLocale must be a valid BCP 47 locale");
|
|
260
|
+
if (!catalog.translations || typeof catalog.translations !== "object" || Array.isArray(catalog.translations)) {
|
|
261
|
+
throw new Error("Capability presentation translations must be an object");
|
|
262
|
+
}
|
|
263
|
+
const types = new Map(manifest.types.map((type) => [type.localId, type]));
|
|
264
|
+
const queries = new Map(manifest.queries.map((operation) => [operation.localId, operation]));
|
|
265
|
+
const actions = new Map(manifest.actions.map((operation) => [operation.localId, operation]));
|
|
266
|
+
const translations: Record<string, CapabilityPresentationTranslation> = {};
|
|
267
|
+
for (const [locale, rawTranslation] of Object.entries(catalog.translations as Record<string, unknown>)) {
|
|
268
|
+
const canonical = canonicalLocale(locale);
|
|
269
|
+
if (!canonical) throw new Error(`Capability presentation locale "${locale}" is invalid`);
|
|
270
|
+
if (canonical === baseLocale) throw new Error(`Capability presentation translations must not repeat base locale ${baseLocale}`);
|
|
271
|
+
if (translations[canonical]) throw new Error(`Capability presentation locale "${locale}" duplicates ${canonical}`);
|
|
272
|
+
if (!rawTranslation || typeof rawTranslation !== "object" || Array.isArray(rawTranslation)) {
|
|
273
|
+
throw new Error(`Capability presentation translation ${canonical} must be an object`);
|
|
274
|
+
}
|
|
275
|
+
const raw = rawTranslation as Record<string, unknown>;
|
|
276
|
+
const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions");
|
|
277
|
+
if (extra) throw new Error(`Capability presentation translation ${canonical} contains unsupported field "${extra}"`);
|
|
278
|
+
const compileGroup = <T>(
|
|
279
|
+
group: unknown,
|
|
280
|
+
definitions: ReadonlyMap<string, T>,
|
|
281
|
+
kind: string,
|
|
282
|
+
compile: (entry: unknown, definition: T, label: string) => unknown,
|
|
283
|
+
): Readonly<Record<string, never>> | undefined => {
|
|
284
|
+
if (group === undefined) return undefined;
|
|
285
|
+
if (!group || typeof group !== "object" || Array.isArray(group)) throw new Error(`${kind} translations must be an object`);
|
|
286
|
+
return Object.fromEntries(
|
|
287
|
+
Object.entries(group as Record<string, unknown>).map(([localId, entry]) => {
|
|
288
|
+
const definition = definitions.get(localId);
|
|
289
|
+
if (!definition) throw new Error(`${kind} translation references unknown localId "${localId}"`);
|
|
290
|
+
return [localId, compile(entry, definition, `${kind} ${localId}`)];
|
|
291
|
+
}),
|
|
292
|
+
) as Readonly<Record<string, never>>;
|
|
293
|
+
};
|
|
294
|
+
const translatedTypes = compileGroup(raw.types, types, "Resource type", (entry, _definition, label) => {
|
|
295
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`${label} must be an object`);
|
|
296
|
+
const copy = entry as Record<string, unknown>;
|
|
297
|
+
const extraField = Object.keys(copy).find((key) => key !== "title" && key !== "description");
|
|
298
|
+
if (extraField) throw new Error(`${label} contains unsupported field "${extraField}"`);
|
|
299
|
+
if (copy.title !== undefined) {
|
|
300
|
+
if (typeof copy.title !== "string") throw new Error(`${label}.title must be text`);
|
|
301
|
+
assertText(copy.title, `${label}.title`, 120);
|
|
302
|
+
}
|
|
303
|
+
if (copy.description !== undefined) {
|
|
304
|
+
if (typeof copy.description !== "string") throw new Error(`${label}.description must be text`);
|
|
305
|
+
assertText(copy.description, `${label}.description`, 500);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
...(copy.title ? { title: String(copy.title).trim() } : {}),
|
|
309
|
+
...(copy.description ? { description: String(copy.description).trim() } : {}),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
const translatedQueries = compileGroup(raw.queries, queries, "Query", compileOperationPresentation);
|
|
313
|
+
const translatedActions = compileGroup(raw.actions, actions, "Action", compileOperationPresentation);
|
|
314
|
+
translations[canonical] = {
|
|
315
|
+
...(translatedTypes ? { types: translatedTypes } : {}),
|
|
316
|
+
...(translatedQueries ? { queries: translatedQueries } : {}),
|
|
317
|
+
...(translatedActions ? { actions: translatedActions } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return { baseLocale, translations };
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
|
|
324
|
+
try {
|
|
325
|
+
const projected = z.toJSONSchema(schema, { io }) as JsonSchema;
|
|
326
|
+
z.fromJSONSchema(projected);
|
|
327
|
+
return projected;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
330
|
+
throw new Error(`${label} is not JSON-Schema-round-trippable: ${message}`);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const assertPropertyDescriptions = (schema: unknown, label: string, path: readonly string[] = []): void => {
|
|
335
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return;
|
|
336
|
+
const value = schema as Record<string, unknown>;
|
|
337
|
+
const properties = value.properties;
|
|
338
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
339
|
+
for (const [field, definition] of Object.entries(properties)) {
|
|
340
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition)) continue;
|
|
341
|
+
const property = definition as Record<string, unknown>;
|
|
342
|
+
if (typeof property.description !== "string" || !property.description.trim()) {
|
|
343
|
+
throw new Error(`${label}.${[...path, field].join(".")} needs a concise Zod description`);
|
|
344
|
+
}
|
|
345
|
+
assertPropertyDescriptions(property, label, [...path, field]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (value.items) assertPropertyDescriptions(value.items, label, [...path, "[]"]);
|
|
349
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
|
|
350
|
+
if (!Array.isArray(value[keyword])) continue;
|
|
351
|
+
value[keyword].forEach((entry, index) => assertPropertyDescriptions(entry, label, [...path, `${keyword}[${index}]`]));
|
|
352
|
+
}
|
|
353
|
+
const definitions = value.$defs;
|
|
354
|
+
if (definitions && typeof definitions === "object" && !Array.isArray(definitions)) {
|
|
355
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
356
|
+
assertPropertyDescriptions(definition, label, [...path, `$defs.${name}`]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const assertCanonicalReaderInput = (schema: JsonSchema, label: string): void => {
|
|
362
|
+
const properties = schema.properties;
|
|
363
|
+
const id = properties && typeof properties === "object" && !Array.isArray(properties) ? (properties as JsonSchema).id : undefined;
|
|
364
|
+
if (!id || typeof id !== "object" || Array.isArray(id) || (id as JsonSchema).type !== "string") {
|
|
365
|
+
throw new Error(`${label} must require a string id field`);
|
|
366
|
+
}
|
|
367
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((field): field is string => typeof field === "string") : [];
|
|
368
|
+
if (!required.includes("id")) throw new Error(`${label} must require a string id field`);
|
|
369
|
+
const additionalRequired = required.filter((field) => field !== "id");
|
|
370
|
+
if (additionalRequired.length > 0) {
|
|
371
|
+
throw new Error(`${label} cannot require fields other than id: ${additionalRequired.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const assertClosedObjectInput = (schema: JsonSchema, label: string): void => {
|
|
376
|
+
if (schema.type !== "object") throw new Error(`${label} must be a Zod object schema`);
|
|
377
|
+
if (schema.additionalProperties !== false) throw new Error(`${label} must reject unknown properties`);
|
|
378
|
+
assertPropertyDescriptions(schema, label);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const qualifiedId = (appId: string, localId: string): string => `${appId}.${localId}`;
|
|
382
|
+
|
|
383
|
+
const assertLocalId = (localId: string, label: string): void => {
|
|
384
|
+
const parsed = CapabilityLocalIdSchema.safeParse(localId);
|
|
385
|
+
if (!parsed.success) throw new Error(`${label} key "${localId}" is invalid: ${parsed.error.issues[0]?.message ?? "invalid id"}`);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const normalizeSearchTags = (definition: CapabilityQueryDefinition, label: string) => {
|
|
389
|
+
if (!definition.universalSearch) return undefined;
|
|
390
|
+
const seen = new Set<string>();
|
|
391
|
+
const tags = definition.universalSearch.tags.map((tag) => {
|
|
392
|
+
assertText(tag.tag, `${label} search tag`, 64);
|
|
393
|
+
assertText(tag.title, `${label} search tag title`, 120);
|
|
394
|
+
assertText(tag.description, `${label} search tag description`, 500);
|
|
395
|
+
const canonical = tag.tag.trim().toLowerCase();
|
|
396
|
+
const aliases = [...new Set((tag.aliases ?? []).map((alias) => alias.trim().toLowerCase()))].filter(Boolean);
|
|
397
|
+
for (const value of [canonical, ...aliases]) {
|
|
398
|
+
if (!/^[^\s#]+$/.test(value)) throw new Error(`${label} search tag "${value}" is invalid`);
|
|
399
|
+
if (seen.has(value)) throw new Error(`${label} declares duplicate search tag or alias "${value}"`);
|
|
400
|
+
seen.add(value);
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
tag: canonical,
|
|
404
|
+
title: tag.title.trim(),
|
|
405
|
+
description: tag.description.trim(),
|
|
406
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
return { tags };
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const compileOperationSchemas = (definition: CapabilityQueryDefinition | CapabilityActionDefinition, label: string) => {
|
|
413
|
+
assertText(definition.title, `${label} title`, 120);
|
|
414
|
+
assertText(definition.description, `${label} description`, 1000);
|
|
415
|
+
const inputSchema = projectSchema(definition.input, `${label} input`, "input");
|
|
416
|
+
assertClosedObjectInput(inputSchema, `${label} input`);
|
|
417
|
+
if (
|
|
418
|
+
"idempotency" in definition &&
|
|
419
|
+
Object.hasOwn((inputSchema.properties as Record<string, unknown> | undefined) ?? {}, "idempotencyKey")
|
|
420
|
+
) {
|
|
421
|
+
throw new Error(`${label} input field "idempotencyKey" is reserved for capability transports`);
|
|
422
|
+
}
|
|
423
|
+
const resultZodSchema = capabilityResultSchema(definition.data);
|
|
424
|
+
const dataSchema = projectSchema(definition.data, `${label} data`, "output");
|
|
425
|
+
return {
|
|
426
|
+
inputSchema,
|
|
427
|
+
dataSchema,
|
|
428
|
+
resultZodSchema,
|
|
429
|
+
schemaHash: capabilityHash({ inputSchema, dataSchema }),
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export const compileCapabilities = (appId: string, definitions: CapabilityDefinitions): CompiledCapabilities => {
|
|
434
|
+
if (definitions.protocolVersion !== CAPABILITY_PROTOCOL_VERSION) {
|
|
435
|
+
throw new Error(`Unsupported capability protocol version ${String(definitions.protocolVersion)}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const localIds = new Set<string>();
|
|
439
|
+
const registerLocalId = (localId: string, kind: string): void => {
|
|
440
|
+
assertLocalId(localId, kind);
|
|
441
|
+
if (localIds.has(localId)) throw new Error(`localId ${localId} is declared more than once across Types, Queries, and Actions`);
|
|
442
|
+
localIds.add(localId);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const types = Object.entries(definitions.types ?? {})
|
|
446
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
447
|
+
.map(([localId, definition]) => {
|
|
448
|
+
registerLocalId(localId, "Resource type");
|
|
449
|
+
assertText(definition.title, `Resource type ${localId} title`, 120);
|
|
450
|
+
assertText(definition.description, `Resource type ${localId} description`, 500);
|
|
451
|
+
return {
|
|
452
|
+
localId,
|
|
453
|
+
title: definition.title.trim(),
|
|
454
|
+
description: definition.description.trim(),
|
|
455
|
+
...(definition.icon ? { icon: definition.icon } : {}),
|
|
456
|
+
...(definition.reader !== undefined ? { reader: CapabilityLocalIdSchema.parse(definition.reader) } : {}),
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
const typeIds = new Set(types.map((type) => qualifiedId(appId, type.localId)));
|
|
460
|
+
|
|
461
|
+
const queries = new Map<string, CompiledCapabilityQuery>();
|
|
462
|
+
for (const [localId, definition] of Object.entries(definitions.queries ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
463
|
+
registerLocalId(localId, "Query");
|
|
464
|
+
const label = `Query ${localId}`;
|
|
465
|
+
const schemas = compileOperationSchemas(definition, label);
|
|
466
|
+
if (definition.universalSearch) {
|
|
467
|
+
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
468
|
+
const expectedData = projectSchema(UniversalSearchDataSchema, "Universal Search data", "output");
|
|
469
|
+
if (capabilityHash(schemas.inputSchema) !== capabilityHash(expectedInput)) {
|
|
470
|
+
throw new Error(`${label} exposed through Universal Search must use UniversalSearchInputSchema`);
|
|
471
|
+
}
|
|
472
|
+
if (capabilityHash(schemas.dataSchema) !== capabilityHash(expectedData)) {
|
|
473
|
+
throw new Error(`${label} exposed through Universal Search must use UniversalSearchDataSchema`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const manifest = {
|
|
477
|
+
localId,
|
|
478
|
+
title: definition.title.trim(),
|
|
479
|
+
description: definition.description.trim(),
|
|
480
|
+
inputSchema: schemas.inputSchema,
|
|
481
|
+
dataSchema: schemas.dataSchema,
|
|
482
|
+
schemaHash: schemas.schemaHash,
|
|
483
|
+
openWorld: definition.openWorld,
|
|
484
|
+
universalSearch: normalizeSearchTags(definition, label),
|
|
485
|
+
} satisfies CapabilityQueryManifest;
|
|
486
|
+
queries.set(localId, {
|
|
487
|
+
definition,
|
|
488
|
+
manifest,
|
|
489
|
+
resultSchema: schemas.resultZodSchema,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
for (const type of types) {
|
|
493
|
+
if (!type.reader) continue;
|
|
494
|
+
const reader = queries.get(type.reader);
|
|
495
|
+
if (!reader) throw new Error(`Resource type ${type.localId} reader ${type.reader} must name an existing Query`);
|
|
496
|
+
assertCanonicalReaderInput(reader.manifest.inputSchema, `Resource type ${type.localId} reader ${type.reader} input`);
|
|
497
|
+
}
|
|
498
|
+
const actions = new Map<string, CompiledCapabilityAction>();
|
|
499
|
+
for (const [localId, definition] of Object.entries(definitions.actions ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
500
|
+
registerLocalId(localId, "Action");
|
|
501
|
+
const label = `Action ${localId}`;
|
|
502
|
+
if (definition.approval === "rememberable" && definition.openWorld) {
|
|
503
|
+
throw new Error(`${label} cannot remember approval for an open-world effect`);
|
|
504
|
+
}
|
|
505
|
+
if (definition.approval === "rememberable" && !definition.review) {
|
|
506
|
+
throw new Error(`${label} must provide a review before approval can be remembered`);
|
|
507
|
+
}
|
|
508
|
+
const schemas = compileOperationSchemas(definition, label);
|
|
509
|
+
const manifest = {
|
|
510
|
+
localId,
|
|
511
|
+
title: definition.title.trim(),
|
|
512
|
+
description: definition.description.trim(),
|
|
513
|
+
inputSchema: schemas.inputSchema,
|
|
514
|
+
dataSchema: schemas.dataSchema,
|
|
515
|
+
schemaHash: schemas.schemaHash,
|
|
516
|
+
destructive: definition.destructive,
|
|
517
|
+
openWorld: definition.openWorld,
|
|
518
|
+
idempotency: definition.idempotency,
|
|
519
|
+
...(definition.approval ? { approval: definition.approval } : {}),
|
|
520
|
+
...(definition.review ? { review: true as const } : {}),
|
|
521
|
+
} satisfies CapabilityActionManifest;
|
|
522
|
+
actions.set(localId, {
|
|
523
|
+
definition,
|
|
524
|
+
manifest,
|
|
525
|
+
resultSchema: schemas.resultZodSchema,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const manifestBase = {
|
|
530
|
+
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
531
|
+
appId,
|
|
532
|
+
types,
|
|
533
|
+
queries: [...queries.values()].map((entry) => entry.manifest),
|
|
534
|
+
actions: [...actions.values()].map((entry) => entry.manifest),
|
|
535
|
+
};
|
|
536
|
+
const manifest = CapabilityManifestSchema.parse({
|
|
537
|
+
...manifestBase,
|
|
538
|
+
manifestHash: capabilityHash(manifestBase),
|
|
539
|
+
});
|
|
540
|
+
const manifestBytes = new TextEncoder().encode(JSON.stringify(manifest)).byteLength;
|
|
541
|
+
if (manifestBytes > MAX_CAPABILITY_MANIFEST_BYTES) {
|
|
542
|
+
throw new Error(`Capability manifest exceeds the ${MAX_CAPABILITY_MANIFEST_BYTES}-byte registry limit`);
|
|
543
|
+
}
|
|
544
|
+
const presentation = compileCapabilityPresentation(manifest, definitions.presentation);
|
|
545
|
+
return { manifest, presentation, typeIds, queries, actions };
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const applySchemaPresentation = (schema: Record<string, unknown>, descriptions: Readonly<Record<string, string>> | undefined) => {
|
|
549
|
+
if (!descriptions || Object.keys(descriptions).length === 0) return schema;
|
|
550
|
+
const localized = structuredClone(schema);
|
|
551
|
+
const visit = (value: unknown, prefix = ""): void => {
|
|
552
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
553
|
+
const node = value as Record<string, unknown>;
|
|
554
|
+
const properties = node.properties;
|
|
555
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
556
|
+
for (const [field, definition] of Object.entries(properties)) {
|
|
557
|
+
const path = prefix ? `${prefix}.${field}` : field;
|
|
558
|
+
if (definition && typeof definition === "object" && !Array.isArray(definition) && descriptions[path]) {
|
|
559
|
+
(definition as Record<string, unknown>).description = descriptions[path];
|
|
560
|
+
}
|
|
561
|
+
visit(definition, path);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (node.items) visit(node.items, `${prefix}[]`);
|
|
565
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
|
|
566
|
+
if (Array.isArray(node[keyword])) node[keyword].forEach((entry) => visit(entry, prefix));
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
visit(localized);
|
|
570
|
+
return localized;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const presentationOverlays = (catalog: CapabilityPresentationCatalog, requestedLocale: string): CapabilityPresentationTranslation[] => {
|
|
574
|
+
const byLocale = new Map(
|
|
575
|
+
Object.entries(catalog.translations).flatMap(([locale, translation]) => {
|
|
576
|
+
const canonical = canonicalLocale(locale);
|
|
577
|
+
return canonical ? [[canonical, translation] as const] : [];
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
return localeFallbackChain(requestedLocale, normalizeLocale(catalog.baseLocale))
|
|
581
|
+
.filter((locale) => locale !== normalizeLocale(catalog.baseLocale))
|
|
582
|
+
.reverse()
|
|
583
|
+
.flatMap((locale) => {
|
|
584
|
+
const translation = byLocale.get(locale);
|
|
585
|
+
return translation ? [translation] : [];
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
/** Resolve only human presentation; stable IDs, flags, tags, aliases, and data shapes stay untouched. */
|
|
590
|
+
export const resolveCapabilityManifestPresentation = (
|
|
591
|
+
manifest: CapabilityManifest,
|
|
592
|
+
catalog: CapabilityPresentationCatalog | undefined,
|
|
593
|
+
requestedLocale: string,
|
|
594
|
+
): CapabilityManifest => {
|
|
595
|
+
if (!catalog) return manifest;
|
|
596
|
+
let current = manifest;
|
|
597
|
+
for (const translation of presentationOverlays(catalog, requestedLocale)) {
|
|
598
|
+
const manifestBase = {
|
|
599
|
+
protocolVersion: current.protocolVersion,
|
|
600
|
+
appId: current.appId,
|
|
601
|
+
types: current.types.map((type) => {
|
|
602
|
+
const copy = translation.types?.[type.localId];
|
|
603
|
+
return copy ? { ...type, title: copy.title ?? type.title, description: copy.description ?? type.description } : type;
|
|
604
|
+
}),
|
|
605
|
+
queries: current.queries.map((operation) => {
|
|
606
|
+
const copy = translation.queries?.[operation.localId];
|
|
607
|
+
if (!copy) return operation;
|
|
608
|
+
return {
|
|
609
|
+
...operation,
|
|
610
|
+
title: copy.title ?? operation.title,
|
|
611
|
+
description: copy.description ?? operation.description,
|
|
612
|
+
inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
|
|
613
|
+
dataSchema: applySchemaPresentation(operation.dataSchema, copy.data),
|
|
614
|
+
...(operation.universalSearch
|
|
615
|
+
? {
|
|
616
|
+
universalSearch: {
|
|
617
|
+
tags: operation.universalSearch.tags.map((tag) => {
|
|
618
|
+
const tagCopy = copy.searchTags?.[tag.tag];
|
|
619
|
+
return tagCopy
|
|
620
|
+
? { ...tag, title: tagCopy.title ?? tag.title, description: tagCopy.description ?? tag.description }
|
|
621
|
+
: tag;
|
|
622
|
+
}),
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
: {}),
|
|
626
|
+
};
|
|
627
|
+
}),
|
|
628
|
+
actions: current.actions.map((operation) => {
|
|
629
|
+
const copy = translation.actions?.[operation.localId];
|
|
630
|
+
return copy
|
|
631
|
+
? {
|
|
632
|
+
...operation,
|
|
633
|
+
title: copy.title ?? operation.title,
|
|
634
|
+
description: copy.description ?? operation.description,
|
|
635
|
+
inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
|
|
636
|
+
dataSchema: applySchemaPresentation(operation.dataSchema, copy.data),
|
|
637
|
+
}
|
|
638
|
+
: operation;
|
|
639
|
+
}),
|
|
640
|
+
};
|
|
641
|
+
current = CapabilityManifestSchema.parse({ ...manifestBase, manifestHash: capabilityHash(manifestBase) });
|
|
642
|
+
}
|
|
643
|
+
return current;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/** Validates an untrusted live manifest and recomputes every integrity hash. */
|
|
647
|
+
export const parseCapabilityManifest = (value: unknown, expectedAppId: string): CapabilityManifest => {
|
|
648
|
+
const manifest = CapabilityManifestSchema.parse(value);
|
|
649
|
+
if (manifest.appId !== expectedAppId) throw new Error(`manifest appId must be ${expectedAppId}`);
|
|
650
|
+
|
|
651
|
+
const localIds = new Set<string>();
|
|
652
|
+
const registerLocalId = (localId: string, kind: string): void => {
|
|
653
|
+
if (localIds.has(localId)) throw new Error(`localId ${localId} is declared more than once across Types, Queries, and Actions`);
|
|
654
|
+
localIds.add(localId);
|
|
655
|
+
assertLocalId(localId, kind);
|
|
656
|
+
};
|
|
657
|
+
for (const type of manifest.types) registerLocalId(type.localId, "Resource type");
|
|
658
|
+
for (const operation of [...manifest.queries, ...manifest.actions]) {
|
|
659
|
+
registerLocalId(operation.localId, "Operation");
|
|
660
|
+
const inputSchema = structuredClone(operation.inputSchema);
|
|
661
|
+
const dataSchema = structuredClone(operation.dataSchema);
|
|
662
|
+
try {
|
|
663
|
+
z.fromJSONSchema(inputSchema);
|
|
664
|
+
z.fromJSONSchema(dataSchema);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Operation ${operation.localId} contains unsupported JSON Schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
assertClosedObjectInput(inputSchema, `Operation ${operation.localId} input`);
|
|
671
|
+
const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema });
|
|
672
|
+
if (operation.schemaHash !== expectedSchemaHash)
|
|
673
|
+
throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
|
|
674
|
+
if ("universalSearch" in operation && operation.universalSearch) {
|
|
675
|
+
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
676
|
+
const expectedData = projectSchema(UniversalSearchDataSchema, "Universal Search data", "output");
|
|
677
|
+
if (
|
|
678
|
+
capabilityHash(operation.inputSchema) !== capabilityHash(expectedInput) ||
|
|
679
|
+
capabilityHash(operation.dataSchema) !== capabilityHash(expectedData)
|
|
680
|
+
) {
|
|
681
|
+
throw new Error(`Operation ${operation.localId} advertises Universal Search with non-canonical schemas`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const queries = new Map(manifest.queries.map((query) => [query.localId, query]));
|
|
686
|
+
for (const type of manifest.types) {
|
|
687
|
+
if (!type.reader) continue;
|
|
688
|
+
const reader = queries.get(type.reader);
|
|
689
|
+
if (!reader) throw new Error(`Resource type ${type.localId} reader ${type.reader} must name an existing Query`);
|
|
690
|
+
assertCanonicalReaderInput(reader.inputSchema, `Resource type ${type.localId} reader ${type.reader} input`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const { manifestHash: _manifestHash, ...manifestBase } = manifest;
|
|
694
|
+
if (manifest.manifestHash !== capabilityHash(manifestBase)) throw new Error("manifestHash does not match the manifest");
|
|
695
|
+
return manifest;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const schemaSemantics = (value: unknown): unknown => {
|
|
699
|
+
if (Array.isArray(value)) return value.map(schemaSemantics);
|
|
700
|
+
if (typeof value !== "object" || value === null) return value;
|
|
701
|
+
return Object.fromEntries(
|
|
702
|
+
Object.entries(value as Record<string, unknown>)
|
|
703
|
+
.filter(([key]) => key !== "title" && key !== "description")
|
|
704
|
+
.map(([key, entry]) => [key, schemaSemantics(entry)]),
|
|
705
|
+
);
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const sameSchema = (left: unknown, right: unknown): boolean =>
|
|
709
|
+
JSON.stringify(stableJsonValue(schemaSemantics(left))) === JSON.stringify(stableJsonValue(schemaSemantics(right)));
|
|
710
|
+
|
|
711
|
+
const schemaEvolutionIssues = (previous: JsonSchema, next: JsonSchema, path: string, direction: "input" | "data"): string[] => {
|
|
712
|
+
for (const keyword of ["oneOf", "anyOf"] as const) {
|
|
713
|
+
const previousBranches = previous[keyword];
|
|
714
|
+
const nextBranches = next[keyword];
|
|
715
|
+
if (!Array.isArray(previousBranches) || !Array.isArray(nextBranches)) continue;
|
|
716
|
+
const previousRest = { ...previous };
|
|
717
|
+
const nextRest = { ...next };
|
|
718
|
+
delete previousRest[keyword];
|
|
719
|
+
delete nextRest[keyword];
|
|
720
|
+
if (!sameSchema(previousRest, nextRest) || previousBranches.length !== nextBranches.length) {
|
|
721
|
+
return [`${path} changed`];
|
|
722
|
+
}
|
|
723
|
+
return previousBranches.flatMap((branch, index) =>
|
|
724
|
+
schemaEvolutionIssues(branch as JsonSchema, nextBranches[index] as JsonSchema, `${path} ${keyword}[${index}]`, direction),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (previous.type === "array" && next.type === "array") {
|
|
728
|
+
const previousRest = { ...previous };
|
|
729
|
+
const nextRest = { ...next };
|
|
730
|
+
const previousItems = previousRest.items;
|
|
731
|
+
const nextItems = nextRest.items;
|
|
732
|
+
delete previousRest.items;
|
|
733
|
+
delete nextRest.items;
|
|
734
|
+
const issues = sameSchema(previousRest, nextRest) ? [] : [`${path} constraints changed`];
|
|
735
|
+
if (previousItems && nextItems) {
|
|
736
|
+
issues.push(...schemaEvolutionIssues(previousItems as JsonSchema, nextItems as JsonSchema, `${path} items`, direction));
|
|
737
|
+
} else if (!sameSchema(previousItems, nextItems)) {
|
|
738
|
+
issues.push(`${path} items changed`);
|
|
739
|
+
}
|
|
740
|
+
return issues;
|
|
741
|
+
}
|
|
742
|
+
if (previous.type !== "object" || next.type !== "object") {
|
|
743
|
+
return sameSchema(previous, next) ? [] : [`${path} changed`];
|
|
744
|
+
}
|
|
745
|
+
const issues: string[] = [];
|
|
746
|
+
const previousRest = { ...previous };
|
|
747
|
+
const nextRest = { ...next };
|
|
748
|
+
delete previousRest.properties;
|
|
749
|
+
delete previousRest.required;
|
|
750
|
+
delete nextRest.properties;
|
|
751
|
+
delete nextRest.required;
|
|
752
|
+
if (!sameSchema(previousRest, nextRest)) issues.push(`${path} constraints changed`);
|
|
753
|
+
|
|
754
|
+
const previousProperties = (previous.properties ?? {}) as Record<string, JsonSchema>;
|
|
755
|
+
const nextProperties = (next.properties ?? {}) as Record<string, JsonSchema>;
|
|
756
|
+
for (const [name, schema] of Object.entries(previousProperties)) {
|
|
757
|
+
const nextSchema = nextProperties[name];
|
|
758
|
+
if (!nextSchema) issues.push(`${path}.${name} was removed`);
|
|
759
|
+
else issues.push(...schemaEvolutionIssues(schema, nextSchema, `${path}.${name}`, direction));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const previousRequired = new Set(Array.isArray(previous.required) ? (previous.required as string[]) : []);
|
|
763
|
+
const nextRequired = new Set(Array.isArray(next.required) ? (next.required as string[]) : []);
|
|
764
|
+
const invalidRequired =
|
|
765
|
+
direction === "input"
|
|
766
|
+
? [...nextRequired].filter((name) => !previousRequired.has(name))
|
|
767
|
+
: [...previousRequired].filter((name) => !nextRequired.has(name));
|
|
768
|
+
for (const name of invalidRequired) {
|
|
769
|
+
issues.push(direction === "input" ? `${path}.${name} became required` : `${path}.${name} is no longer guaranteed in results`);
|
|
770
|
+
}
|
|
771
|
+
return issues;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const searchTokens = (operation: CapabilityQueryManifest): Set<string> =>
|
|
775
|
+
new Set(operation.universalSearch?.tags.flatMap((tag) => [tag.tag, ...(tag.aliases ?? [])]) ?? []);
|
|
776
|
+
|
|
777
|
+
/** Returns breaking same-id changes; additions and optional object fields are additive. */
|
|
778
|
+
export const capabilityManifestEvolutionIssues = (previous: CapabilityManifest, next: CapabilityManifest): string[] => {
|
|
779
|
+
const issues: string[] = [];
|
|
780
|
+
if (previous.appId !== next.appId) issues.push(`appId changed from ${previous.appId} to ${next.appId}`);
|
|
781
|
+
if (previous.protocolVersion !== next.protocolVersion) issues.push("protocolVersion changed");
|
|
782
|
+
|
|
783
|
+
const nextTypes = new Map(next.types.map((type) => [type.localId, type]));
|
|
784
|
+
for (const type of previous.types) {
|
|
785
|
+
const current = nextTypes.get(type.localId);
|
|
786
|
+
if (!current) issues.push(`Type ${type.localId} was removed`);
|
|
787
|
+
else if (type.reader && current.reader !== type.reader) issues.push(`Type ${type.localId} reader changed`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const nextQueries = new Map(next.queries.map((operation) => [operation.localId, operation]));
|
|
791
|
+
const nextActions = new Map(next.actions.map((operation) => [operation.localId, operation]));
|
|
792
|
+
for (const operation of previous.queries) {
|
|
793
|
+
const current = nextQueries.get(operation.localId);
|
|
794
|
+
if (!current) {
|
|
795
|
+
issues.push(
|
|
796
|
+
nextActions.has(operation.localId) ? `Query ${operation.localId} changed kind` : `Query ${operation.localId} was removed`,
|
|
797
|
+
);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (operation.openWorld !== current.openWorld) issues.push(`Query ${operation.localId} openWorld changed`);
|
|
801
|
+
const currentSearchTokens = searchTokens(current);
|
|
802
|
+
for (const token of searchTokens(operation)) {
|
|
803
|
+
if (!currentSearchTokens.has(token)) issues.push(`Query ${operation.localId} removed Universal Search token ${token}`);
|
|
804
|
+
}
|
|
805
|
+
issues.push(...schemaEvolutionIssues(operation.inputSchema, current.inputSchema, `Query ${operation.localId} input`, "input"));
|
|
806
|
+
issues.push(...schemaEvolutionIssues(operation.dataSchema, current.dataSchema, `Query ${operation.localId} data`, "data"));
|
|
807
|
+
}
|
|
808
|
+
for (const operation of previous.actions) {
|
|
809
|
+
const current = nextActions.get(operation.localId);
|
|
810
|
+
if (!current) {
|
|
811
|
+
issues.push(
|
|
812
|
+
nextQueries.has(operation.localId) ? `Action ${operation.localId} changed kind` : `Action ${operation.localId} was removed`,
|
|
813
|
+
);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (operation.openWorld !== current.openWorld) issues.push(`Action ${operation.localId} openWorld changed`);
|
|
817
|
+
if (operation.destructive !== current.destructive) issues.push(`Action ${operation.localId} destructive changed`);
|
|
818
|
+
if (operation.idempotency !== current.idempotency) issues.push(`Action ${operation.localId} idempotency changed`);
|
|
819
|
+
if (operation.review && !current.review) issues.push(`Action ${operation.localId} review was removed`);
|
|
820
|
+
issues.push(...schemaEvolutionIssues(operation.inputSchema, current.inputSchema, `Action ${operation.localId} input`, "input"));
|
|
821
|
+
issues.push(...schemaEvolutionIssues(operation.dataSchema, current.dataSchema, `Action ${operation.localId} data`, "data"));
|
|
822
|
+
}
|
|
823
|
+
return issues;
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const resultRefs = (result: unknown): Array<{ type: string; id: string }> => {
|
|
827
|
+
if (typeof result !== "object" || result === null) return [];
|
|
828
|
+
const value = result as { refs?: unknown; data?: unknown };
|
|
829
|
+
const refs = Array.isArray(value.refs) ? value.refs : [];
|
|
830
|
+
const resources = UniversalSearchDataSchema.safeParse(value.data);
|
|
831
|
+
if (!resources.success) return refs as Array<{ type: string; id: string }>;
|
|
832
|
+
return [...(refs as Array<{ type: string; id: string }>), ...resources.data.map((entry) => entry.ref)] as Array<{
|
|
833
|
+
type: string;
|
|
834
|
+
id: string;
|
|
835
|
+
}>;
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
export const validateCapabilityResult = (
|
|
839
|
+
compiled: CompiledCapabilities,
|
|
840
|
+
operation: CompiledCapabilityQuery | CompiledCapabilityAction,
|
|
841
|
+
result: unknown,
|
|
842
|
+
onInvalidResult?: (error: unknown) => void,
|
|
843
|
+
): Result<unknown, ServiceError> => {
|
|
844
|
+
const parsed = operation.resultSchema.safeParse(result);
|
|
845
|
+
if (!parsed.success) {
|
|
846
|
+
try {
|
|
847
|
+
onInvalidResult?.(
|
|
848
|
+
new Error(
|
|
849
|
+
`Capability ${operation.manifest.localId} returned data outside its registered schema: ${JSON.stringify(
|
|
850
|
+
parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
|
|
851
|
+
)}`,
|
|
852
|
+
),
|
|
853
|
+
);
|
|
854
|
+
} catch {
|
|
855
|
+
// Observability must not change the public failure contract.
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
error: {
|
|
860
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse,
|
|
861
|
+
message: "Capability returned an invalid result",
|
|
862
|
+
status: 500,
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
for (const ref of resultRefs(parsed.data)) {
|
|
867
|
+
if (ref.type.startsWith(`${compiled.manifest.appId}.`) && !compiled.typeIds.has(ref.type)) {
|
|
868
|
+
return {
|
|
869
|
+
ok: false,
|
|
870
|
+
error: {
|
|
871
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse,
|
|
872
|
+
message: `Capability returned undeclared resource type ${ref.type}`,
|
|
873
|
+
status: 500,
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if ("universalSearch" in operation.manifest && operation.manifest.universalSearch) {
|
|
879
|
+
const resources = UniversalSearchDataSchema.safeParse((parsed.data as { data?: unknown }).data);
|
|
880
|
+
if (!resources.success || resources.data.some((resource) => !resource.links.some((link) => link.rel === "open"))) {
|
|
881
|
+
return {
|
|
882
|
+
ok: false,
|
|
883
|
+
error: {
|
|
884
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse,
|
|
885
|
+
message: "Universal Search results must include an open link",
|
|
886
|
+
status: 500,
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return { ok: true, data: parsed.data };
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
export const invokeCompiledCapability = async (params: {
|
|
895
|
+
compiled: CompiledCapabilities;
|
|
896
|
+
kind: "query" | "action";
|
|
897
|
+
localId: string;
|
|
898
|
+
input: unknown;
|
|
899
|
+
expectedSchemaHash: string | null;
|
|
900
|
+
context: CapabilityExecutionContext;
|
|
901
|
+
onUnexpectedError?: (error: unknown) => void;
|
|
902
|
+
}): Promise<CapabilityInvocationResult<unknown>> => {
|
|
903
|
+
const operation = params.kind === "query" ? params.compiled.queries.get(params.localId) : params.compiled.actions.get(params.localId);
|
|
904
|
+
if (!operation) {
|
|
905
|
+
return {
|
|
906
|
+
ok: false,
|
|
907
|
+
error: {
|
|
908
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.capabilityNotFound,
|
|
909
|
+
message: `Capability ${params.kind} not found`,
|
|
910
|
+
status: 404,
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (params.expectedSchemaHash !== operation.manifest.schemaHash) {
|
|
915
|
+
return {
|
|
916
|
+
ok: false,
|
|
917
|
+
error: {
|
|
918
|
+
code: "SCHEMA_MISMATCH",
|
|
919
|
+
message: "Capability schema changed; refresh the live catalog and retry",
|
|
920
|
+
status: 409,
|
|
921
|
+
details: { expected: operation.manifest.schemaHash },
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
let context = params.context;
|
|
926
|
+
if (params.kind === "action") {
|
|
927
|
+
const action = operation as CompiledCapabilityAction;
|
|
928
|
+
if (action.manifest.idempotency === "required" && !params.context.idempotencyKey) {
|
|
929
|
+
return {
|
|
930
|
+
ok: false,
|
|
931
|
+
error: {
|
|
932
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyRequired,
|
|
933
|
+
message: "This action requires an Idempotency-Key",
|
|
934
|
+
status: 400,
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (action.manifest.idempotency === "none" && params.context.idempotencyKey) {
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
error: {
|
|
942
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed,
|
|
943
|
+
message: "This Action does not support idempotent retries; omit Idempotency-Key",
|
|
944
|
+
status: 400,
|
|
945
|
+
},
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
} else if (params.context.idempotencyKey) {
|
|
949
|
+
return {
|
|
950
|
+
ok: false,
|
|
951
|
+
error: {
|
|
952
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed,
|
|
953
|
+
message: "Idempotency-Key is only valid for Actions that require it",
|
|
954
|
+
status: 400,
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (params.context.idempotencyKey) {
|
|
959
|
+
const idempotencyKey = CapabilityIdempotencyKeySchema.safeParse(params.context.idempotencyKey);
|
|
960
|
+
if (!idempotencyKey.success) {
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
error: {
|
|
964
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed,
|
|
965
|
+
message: "Idempotency-Key is invalid",
|
|
966
|
+
status: 400,
|
|
967
|
+
details: { issues: idempotencyKey.error.issues },
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
context = { ...params.context, idempotencyKey: idempotencyKey.data };
|
|
972
|
+
}
|
|
973
|
+
const input = operation.definition.input.safeParse(params.input);
|
|
974
|
+
if (!input.success) {
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
error: {
|
|
978
|
+
code: "VALIDATION_FAILED",
|
|
979
|
+
message: "Capability input is invalid",
|
|
980
|
+
status: 400,
|
|
981
|
+
details: { issues: input.error.issues },
|
|
982
|
+
},
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const invoked = await operation.definition.run(input.data, context);
|
|
987
|
+
if (!invoked.ok) {
|
|
988
|
+
return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
|
|
989
|
+
}
|
|
990
|
+
const validated = validateCapabilityResult(params.compiled, operation, invoked.data, params.onUnexpectedError);
|
|
991
|
+
return validated.ok
|
|
992
|
+
? ({
|
|
993
|
+
ok: true,
|
|
994
|
+
data: validated.data,
|
|
995
|
+
} as CapabilityInvocationResult<unknown>)
|
|
996
|
+
: { ok: false, error: validated.error };
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (isServiceError(error)) return normalizeProviderError(error, "Capability threw an invalid service error", params.onUnexpectedError);
|
|
999
|
+
try {
|
|
1000
|
+
params.onUnexpectedError?.(error);
|
|
1001
|
+
} catch {
|
|
1002
|
+
// Observability must not change the public failure contract.
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
error: {
|
|
1007
|
+
code: "INTERNAL",
|
|
1008
|
+
message: "Capability execution failed",
|
|
1009
|
+
status: 500,
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
export const reviewCompiledCapability = async (params: {
|
|
1016
|
+
compiled: CompiledCapabilities;
|
|
1017
|
+
localId: string;
|
|
1018
|
+
input: unknown;
|
|
1019
|
+
expectedSchemaHash: string | null;
|
|
1020
|
+
context: CapabilityExecutionContext;
|
|
1021
|
+
onUnexpectedError?: (error: unknown) => void;
|
|
1022
|
+
}): Promise<CapabilityActionReviewResult> => {
|
|
1023
|
+
if (params.context.idempotencyKey) {
|
|
1024
|
+
return {
|
|
1025
|
+
ok: false,
|
|
1026
|
+
error: {
|
|
1027
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed,
|
|
1028
|
+
message: "Idempotency-Key is not valid for an Action review",
|
|
1029
|
+
status: 400,
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const operation = params.compiled.actions.get(params.localId);
|
|
1034
|
+
if (!operation || !operation.definition.review) {
|
|
1035
|
+
return {
|
|
1036
|
+
ok: false,
|
|
1037
|
+
error: {
|
|
1038
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.capabilityNotFound,
|
|
1039
|
+
message: "Capability action review not found",
|
|
1040
|
+
status: 404,
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (params.expectedSchemaHash !== operation.manifest.schemaHash) {
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
error: {
|
|
1048
|
+
code: "SCHEMA_MISMATCH",
|
|
1049
|
+
message: "Capability schema changed; refresh the live catalog and retry",
|
|
1050
|
+
status: 409,
|
|
1051
|
+
details: { expected: operation.manifest.schemaHash },
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
const input = operation.definition.input.safeParse(params.input);
|
|
1056
|
+
if (!input.success) {
|
|
1057
|
+
return {
|
|
1058
|
+
ok: false,
|
|
1059
|
+
error: {
|
|
1060
|
+
code: "VALIDATION_FAILED",
|
|
1061
|
+
message: "Capability input is invalid",
|
|
1062
|
+
status: 400,
|
|
1063
|
+
details: { issues: input.error.issues },
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const reviewed = await operation.definition.review(input.data, params.context);
|
|
1069
|
+
if (!reviewed.ok) {
|
|
1070
|
+
return normalizeProviderError(reviewed.error, "Capability review returned an invalid error", params.onUnexpectedError);
|
|
1071
|
+
}
|
|
1072
|
+
const parsed = CapabilityActionReviewSchema.safeParse(reviewed.data);
|
|
1073
|
+
const approvalScopeIsValid =
|
|
1074
|
+
parsed.success &&
|
|
1075
|
+
(operation.manifest.approval === "rememberable" ? parsed.data.approvalScope !== undefined : parsed.data.approvalScope === undefined);
|
|
1076
|
+
if (!parsed.success || !approvalScopeIsValid) {
|
|
1077
|
+
try {
|
|
1078
|
+
params.onUnexpectedError?.(
|
|
1079
|
+
new Error(
|
|
1080
|
+
parsed.success
|
|
1081
|
+
? `Capability ${operation.manifest.localId} review returned an approval scope inconsistent with its manifest`
|
|
1082
|
+
: `Capability ${operation.manifest.localId} review returned data outside its registered schema: ${JSON.stringify(
|
|
1083
|
+
parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
|
|
1084
|
+
)}`,
|
|
1085
|
+
),
|
|
1086
|
+
);
|
|
1087
|
+
} catch {
|
|
1088
|
+
// Observability must not change the public failure contract.
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return parsed.success && approvalScopeIsValid
|
|
1092
|
+
? { ok: true, data: parsed.data }
|
|
1093
|
+
: {
|
|
1094
|
+
ok: false,
|
|
1095
|
+
error: {
|
|
1096
|
+
code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse,
|
|
1097
|
+
message: "Capability review returned an invalid result",
|
|
1098
|
+
status: 500,
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (isServiceError(error))
|
|
1103
|
+
return normalizeProviderError(error, "Capability review threw an invalid service error", params.onUnexpectedError);
|
|
1104
|
+
try {
|
|
1105
|
+
params.onUnexpectedError?.(error);
|
|
1106
|
+
} catch {
|
|
1107
|
+
// Observability must not change the public failure contract.
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
ok: false,
|
|
1111
|
+
error: {
|
|
1112
|
+
code: "INTERNAL",
|
|
1113
|
+
message: "Capability review failed",
|
|
1114
|
+
status: 500,
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
};
|