@k2b/cloud 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/src/api/capabilities.ts
CHANGED
|
@@ -4,19 +4,37 @@ import { z } from "zod";
|
|
|
4
4
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
5
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
6
|
import { getCapability, listApps } from "../_internal/registry";
|
|
7
|
+
import { resolveCommandLink } from "../capabilities/command-link";
|
|
8
|
+
import {
|
|
9
|
+
type CapabilityClaimScope,
|
|
10
|
+
type CapabilityExecutionStatus,
|
|
11
|
+
capabilityIdempotencyKeyHash,
|
|
12
|
+
capabilityRequestHash,
|
|
13
|
+
capabilityValueMeta,
|
|
14
|
+
claimCapabilityIdempotency,
|
|
15
|
+
completeCapabilityClaim,
|
|
16
|
+
markCapabilityClaimUncertain,
|
|
17
|
+
recordCapabilityExecution,
|
|
18
|
+
releaseCapabilityClaim,
|
|
19
|
+
resolveCapabilityClaim,
|
|
20
|
+
} from "../capabilities/executions";
|
|
7
21
|
import {
|
|
8
22
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
9
23
|
CAPABILITY_MAX_CATALOG_BYTES,
|
|
10
24
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
11
25
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
26
|
+
CAPABILITY_ORIGIN_HEADER,
|
|
12
27
|
CAPABILITY_PROTOCOL_VERSION,
|
|
13
28
|
CapabilityActionReviewSchema,
|
|
14
29
|
type CapabilityCatalog,
|
|
15
30
|
CapabilityCatalogSchema,
|
|
16
31
|
CapabilityErrorSchema,
|
|
17
32
|
CapabilityIdempotencyKeySchema,
|
|
33
|
+
type CapabilityOrigin,
|
|
18
34
|
capabilityResultJsonSchema,
|
|
19
35
|
} from "../contracts/capabilities";
|
|
36
|
+
import { CapabilityStreamSchema } from "../contracts/capability-streams";
|
|
37
|
+
import { CommandLinkSchema, CommandRequestSchema } from "../contracts/commands";
|
|
20
38
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
21
39
|
import {
|
|
22
40
|
type AuthContext,
|
|
@@ -30,7 +48,7 @@ import {
|
|
|
30
48
|
requiresAuth,
|
|
31
49
|
v,
|
|
32
50
|
} from "../server";
|
|
33
|
-
import { logger } from "../services";
|
|
51
|
+
import { logger, settings } from "../services";
|
|
34
52
|
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
35
53
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
36
54
|
import type { InvocationAuthority } from "../services/identity/invocation-token";
|
|
@@ -39,6 +57,7 @@ import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
|
39
57
|
import { withMandateIssueAuthority } from "../services/mandates";
|
|
40
58
|
import { resolveAppPresentation } from "../shared/app-presentation";
|
|
41
59
|
import { capabilityMessages } from "../shared/capability-messages";
|
|
60
|
+
import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
|
|
42
61
|
|
|
43
62
|
const log = logger("capabilities");
|
|
44
63
|
const QUERY_TIMEOUT_MS = 15_000;
|
|
@@ -83,6 +102,7 @@ export type CapabilityRouteDependencies = {
|
|
|
83
102
|
withMandateIssueAuthority?: typeof withMandateIssueAuthority;
|
|
84
103
|
queryTimeoutMs?: number;
|
|
85
104
|
actionTimeoutMs?: number;
|
|
105
|
+
appUrl?: () => Promise<string>;
|
|
86
106
|
};
|
|
87
107
|
|
|
88
108
|
export type CapabilityDispatchDependencies = Pick<
|
|
@@ -203,12 +223,9 @@ const waitWithin = <T>(value: Promise<T>, signal: AbortSignal): Promise<T> =>
|
|
|
203
223
|
);
|
|
204
224
|
});
|
|
205
225
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
* schema pinning, timeout, and response validation path.
|
|
210
|
-
*/
|
|
211
|
-
export const dispatchCapability = async (params: {
|
|
226
|
+
export type CapabilityDispatchParams = {
|
|
227
|
+
/** Trusted Core continuation binding; never read from client headers. */
|
|
228
|
+
continuation?: string;
|
|
212
229
|
request: Request;
|
|
213
230
|
kind: "queries" | "actions";
|
|
214
231
|
review?: boolean;
|
|
@@ -216,11 +233,37 @@ export const dispatchCapability = async (params: {
|
|
|
216
233
|
capabilityId: string;
|
|
217
234
|
input: unknown;
|
|
218
235
|
locale?: string;
|
|
236
|
+
/** Cloud surface this invocation came from; recorded on the execution row. */
|
|
237
|
+
origin: CapabilityOrigin;
|
|
219
238
|
authority?: RequestAuthority;
|
|
220
239
|
mandate?: MandatedCapabilityAuthority;
|
|
221
240
|
callingAppId?: string;
|
|
222
241
|
dependencies?: CapabilityDispatchDependencies;
|
|
223
|
-
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
type CapabilityDispatchOutcome = { body: unknown; status: number; replayed?: boolean };
|
|
245
|
+
|
|
246
|
+
/** Principal resolved while dispatching. A mandate only reveals it during issuance. */
|
|
247
|
+
type CapabilityDispatchObservation = {
|
|
248
|
+
subject: { type: "user" | "service_account"; id: string } | null;
|
|
249
|
+
destructive: boolean;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type CapabilityPrincipal = { type: "user" | "service_account"; id: string } | null;
|
|
253
|
+
|
|
254
|
+
/** Acting principal for the execution row and the idempotency claim scope. A mandate reveals it during issuance. */
|
|
255
|
+
const dispatchPrincipal = (params: CapabilityDispatchParams, observed: CapabilityDispatchObservation): CapabilityPrincipal => {
|
|
256
|
+
const actor = params.authority?.actor;
|
|
257
|
+
if (actor?.kind === "user") return { type: "user", id: actor.user.id };
|
|
258
|
+
if (actor?.kind === "service_account") return { type: "service_account", id: actor.serviceAccount.id };
|
|
259
|
+
return observed.subject;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const runCapabilityDispatch = async (
|
|
263
|
+
params: CapabilityDispatchParams,
|
|
264
|
+
requestId: string,
|
|
265
|
+
observed: CapabilityDispatchObservation,
|
|
266
|
+
): Promise<CapabilityDispatchOutcome> => {
|
|
224
267
|
const messages = capabilityMessages(params.locale ?? preferredLocale(params.request.headers));
|
|
225
268
|
const registryEntry = params.dependencies?.getCapability ?? getCapability;
|
|
226
269
|
const fetchUpstream = params.dependencies?.fetch ?? globalThis.fetch;
|
|
@@ -233,15 +276,16 @@ export const dispatchCapability = async (params: {
|
|
|
233
276
|
error: error instanceof Error ? error.message : String(error),
|
|
234
277
|
});
|
|
235
278
|
const unavailable = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, messages.registryUnavailable, 503);
|
|
236
|
-
return
|
|
279
|
+
return unavailable;
|
|
237
280
|
}
|
|
238
281
|
if (!entry) {
|
|
239
282
|
const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, messages.appUnavailable({ appId: params.appId }), 503);
|
|
240
|
-
return
|
|
283
|
+
return error;
|
|
241
284
|
}
|
|
242
285
|
|
|
243
286
|
const operations = params.kind === "queries" ? entry.manifest.queries : entry.manifest.actions;
|
|
244
287
|
const operation = operations.find((candidate) => candidate.localId === params.capabilityId);
|
|
288
|
+
if (operation && "destructive" in operation) observed.destructive = operation.destructive;
|
|
245
289
|
if (!operation) {
|
|
246
290
|
const label = params.kind === "queries" ? messages.query : messages.action;
|
|
247
291
|
const error = errorResponse(
|
|
@@ -249,7 +293,7 @@ export const dispatchCapability = async (params: {
|
|
|
249
293
|
messages.notFound({ kind: label, reference: `${params.appId}.${params.capabilityId}` }),
|
|
250
294
|
404,
|
|
251
295
|
);
|
|
252
|
-
return
|
|
296
|
+
return error;
|
|
253
297
|
}
|
|
254
298
|
if (params.review && (params.kind !== "actions" || !("review" in operation) || operation.review !== true)) {
|
|
255
299
|
const error = errorResponse(
|
|
@@ -257,7 +301,7 @@ export const dispatchCapability = async (params: {
|
|
|
257
301
|
messages.notFound({ kind: messages.review, reference: `${params.appId}.${params.capabilityId}` }),
|
|
258
302
|
404,
|
|
259
303
|
);
|
|
260
|
-
return
|
|
304
|
+
return error;
|
|
261
305
|
}
|
|
262
306
|
|
|
263
307
|
const rawIdempotencyKey = params.request.headers.get("idempotency-key");
|
|
@@ -265,47 +309,50 @@ export const dispatchCapability = async (params: {
|
|
|
265
309
|
const action = operation as (typeof entry.manifest.actions)[number];
|
|
266
310
|
if (action.idempotency === "required" && rawIdempotencyKey === null) {
|
|
267
311
|
const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyRequired, messages.idempotencyRequired, 400);
|
|
268
|
-
return
|
|
312
|
+
return error;
|
|
269
313
|
}
|
|
270
314
|
if (action.idempotency === "none" && rawIdempotencyKey !== null) {
|
|
271
315
|
const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed, messages.idempotencyNotAllowed, 400);
|
|
272
|
-
return
|
|
316
|
+
return error;
|
|
273
317
|
}
|
|
274
318
|
} else if (rawIdempotencyKey !== null) {
|
|
275
319
|
const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed, messages.idempotencyOnlyActions, 400);
|
|
276
|
-
return
|
|
320
|
+
return error;
|
|
277
321
|
}
|
|
278
322
|
const idempotencyKey = rawIdempotencyKey === null ? null : CapabilityIdempotencyKeySchema.safeParse(rawIdempotencyKey);
|
|
279
323
|
if (idempotencyKey && !idempotencyKey.success) {
|
|
280
324
|
const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.idempotencyInvalid, 400, {
|
|
281
325
|
issues: idempotencyKey.error.issues.map((issue) => ({ message: issue.message })),
|
|
282
326
|
});
|
|
283
|
-
return
|
|
327
|
+
return error;
|
|
284
328
|
}
|
|
285
329
|
|
|
286
330
|
const inputValidator = schemaValidator(`${operation.schemaHash}:input`, operation.inputSchema);
|
|
287
331
|
if (!inputValidator) {
|
|
288
332
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.unsupportedSchema({ appId: params.appId }), 502);
|
|
289
|
-
return
|
|
333
|
+
return invalid;
|
|
290
334
|
}
|
|
291
335
|
const input = inputValidator.safeParse(params.input);
|
|
292
336
|
if (!input.success) {
|
|
293
337
|
const invalid = errorResponse("VALIDATION_FAILED", messages.inputSchemaMismatch, 400, {
|
|
294
338
|
issues: input.error.issues.slice(0, 20).map((issue) => ({ path: issue.path.join("."), message: issue.message })),
|
|
295
339
|
});
|
|
296
|
-
return
|
|
340
|
+
return invalid;
|
|
297
341
|
}
|
|
298
342
|
|
|
343
|
+
if (params.mandate && operation.stream)
|
|
344
|
+
return errorResponse("STREAM_UNSUPPORTED_AUTHORITY", "Streams require an interactive user or service-account request", 403);
|
|
345
|
+
|
|
299
346
|
let requestBody: string;
|
|
300
347
|
try {
|
|
301
348
|
requestBody = JSON.stringify({ input: input.data });
|
|
302
349
|
} catch {
|
|
303
350
|
const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.inputNotSerializable, 400);
|
|
304
|
-
return
|
|
351
|
+
return invalid;
|
|
305
352
|
}
|
|
306
353
|
if (new TextEncoder().encode(requestBody).byteLength > CAPABILITY_MAX_REQUEST_BYTES) {
|
|
307
354
|
const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.requestTooLarge, 400);
|
|
308
|
-
return
|
|
355
|
+
return invalid;
|
|
309
356
|
}
|
|
310
357
|
|
|
311
358
|
const invocationOperation = capabilityInvocationOperation(params.kind, params.capabilityId, params.review);
|
|
@@ -332,17 +379,20 @@ export const dispatchCapability = async (params: {
|
|
|
332
379
|
targetAppId: params.appId,
|
|
333
380
|
operation: invocationOperation,
|
|
334
381
|
actionApproval: mandate.actionApproval ?? "none",
|
|
335
|
-
|
|
382
|
+
input: params.input,
|
|
383
|
+
capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
|
|
384
|
+
requestId,
|
|
336
385
|
},
|
|
337
386
|
async (mandateAuthority) => {
|
|
338
387
|
signal.throwIfAborted();
|
|
388
|
+
observed.subject = mandateAuthority.subject;
|
|
339
389
|
return (params.dependencies?.signInvocation ?? signInvocationToken)({
|
|
340
390
|
targetAppId: params.appId,
|
|
341
391
|
callingAppId: mandate.ownerAppId,
|
|
342
392
|
operation: invocationOperation,
|
|
343
393
|
schemaHash: operation.schemaHash,
|
|
344
394
|
authority: invocationAuthorityFromMandate(mandateAuthority),
|
|
345
|
-
requestId
|
|
395
|
+
requestId,
|
|
346
396
|
signer,
|
|
347
397
|
issuer: signer.issuer,
|
|
348
398
|
});
|
|
@@ -376,7 +426,7 @@ export const dispatchCapability = async (params: {
|
|
|
376
426
|
operation: invocationOperation,
|
|
377
427
|
schemaHash: operation.schemaHash,
|
|
378
428
|
authority: invocationAuthorityFromRequest(authority),
|
|
379
|
-
requestId
|
|
429
|
+
requestId,
|
|
380
430
|
signer,
|
|
381
431
|
issuer: signer.issuer,
|
|
382
432
|
}),
|
|
@@ -397,7 +447,7 @@ export const dispatchCapability = async (params: {
|
|
|
397
447
|
error.status === 409 ? "Mandate revision changed" : "Mandate does not authorize this capability invocation",
|
|
398
448
|
error.status,
|
|
399
449
|
);
|
|
400
|
-
return
|
|
450
|
+
return denied;
|
|
401
451
|
}
|
|
402
452
|
if (params.request.signal.aborted || timeout.aborted) {
|
|
403
453
|
const cancelled = params.request.signal.aborted;
|
|
@@ -407,15 +457,16 @@ export const dispatchCapability = async (params: {
|
|
|
407
457
|
cancelled ? 499 : 504,
|
|
408
458
|
{ retrySafe: true },
|
|
409
459
|
);
|
|
410
|
-
return
|
|
460
|
+
return failure;
|
|
411
461
|
}
|
|
412
462
|
console.error("[capabilities] Invocation issuance failed", { appId: params.appId, operation: invocationOperation });
|
|
413
463
|
const failure = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.issuanceFailed, 500, { retrySafe: true });
|
|
414
|
-
return
|
|
464
|
+
return failure;
|
|
415
465
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
466
|
+
headers.set("x-request-id", requestId);
|
|
467
|
+
headers.set(CAPABILITY_ORIGIN_HEADER, params.origin);
|
|
468
|
+
for (const name of ["traceparent", "tracestate"] as const) {
|
|
469
|
+
const value = params.request.headers.get(name);
|
|
419
470
|
if (value) headers.set(name, value);
|
|
420
471
|
}
|
|
421
472
|
const invocationLocale = preferredLocale(params.request.headers);
|
|
@@ -424,11 +475,74 @@ export const dispatchCapability = async (params: {
|
|
|
424
475
|
headers.set("x-cloud-capability-schema-hash", operation.schemaHash);
|
|
425
476
|
const actionWithoutRetrySafety =
|
|
426
477
|
params.kind === "actions" && !params.review && "idempotency" in operation && operation.idempotency === "none";
|
|
427
|
-
const outcomeUnknown = ():
|
|
478
|
+
const outcomeUnknown = (): CapabilityDispatchOutcome => {
|
|
428
479
|
const unknown = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown, messages.actionOutcomeUnknown, 502, {
|
|
429
480
|
retrySafe: false,
|
|
430
481
|
});
|
|
431
|
-
return
|
|
482
|
+
return unknown;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const claimPrincipal = dispatchPrincipal(params, observed);
|
|
486
|
+
const claimScope: CapabilityClaimScope | null =
|
|
487
|
+
params.kind === "actions" &&
|
|
488
|
+
!params.review &&
|
|
489
|
+
"idempotency" in operation &&
|
|
490
|
+
operation.idempotency === "required" &&
|
|
491
|
+
idempotencyKey?.success
|
|
492
|
+
? {
|
|
493
|
+
appId: params.appId,
|
|
494
|
+
capability: `${params.appId}.${params.capabilityId}`,
|
|
495
|
+
principal: claimPrincipal ? `${claimPrincipal.type}:${claimPrincipal.id}` : "anonymous",
|
|
496
|
+
keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
|
|
497
|
+
}
|
|
498
|
+
: null;
|
|
499
|
+
if (claimScope) {
|
|
500
|
+
let claim: Awaited<ReturnType<typeof claimCapabilityIdempotency>>;
|
|
501
|
+
try {
|
|
502
|
+
claim = await claimCapabilityIdempotency(claimScope, capabilityRequestHash(input.data));
|
|
503
|
+
} catch (error) {
|
|
504
|
+
// Nothing has been forwarded yet, so refusing the call is the safe answer.
|
|
505
|
+
log.error("Capability idempotency claim failed", {
|
|
506
|
+
appId: params.appId,
|
|
507
|
+
capabilityId: params.capabilityId,
|
|
508
|
+
error: error instanceof Error ? error.message : String(error),
|
|
509
|
+
});
|
|
510
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.cloudUnavailable, 503, { retrySafe: true });
|
|
511
|
+
}
|
|
512
|
+
if (claim.state === "replay") return { body: claim.body, status: claim.status, replayed: true };
|
|
513
|
+
if (claim.state === "conflict")
|
|
514
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyConflict, messages.idempotencyConflict, 409);
|
|
515
|
+
if (claim.state === "in_flight") {
|
|
516
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyInProgress, messages.idempotencyInProgress, 409, {
|
|
517
|
+
retrySafe: false,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (claim.state === "uncertain") {
|
|
521
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyUncertain, 409, { retrySafe: false });
|
|
522
|
+
}
|
|
523
|
+
if (claim.state === "not_retained") {
|
|
524
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyResultNotRetained, 409, {
|
|
525
|
+
retrySafe: false,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Resolve the claim once the outcome is known: a definitive success is
|
|
532
|
+
* replayable, a failure that proves nothing happened frees the key for a
|
|
533
|
+
* corrected retry, and anything ambiguous freezes the key.
|
|
534
|
+
*/
|
|
535
|
+
const settle = async (outcome: CapabilityDispatchOutcome, disposition: "succeeded" | "nothing_happened" | "uncertain") => {
|
|
536
|
+
if (!claimScope) return outcome;
|
|
537
|
+
await resolveCapabilityClaim(
|
|
538
|
+
disposition === "succeeded"
|
|
539
|
+
? completeCapabilityClaim(claimScope, outcome.status, outcome.body)
|
|
540
|
+
: disposition === "nothing_happened"
|
|
541
|
+
? releaseCapabilityClaim(claimScope)
|
|
542
|
+
: markCapabilityClaimUncertain(claimScope),
|
|
543
|
+
claimScope,
|
|
544
|
+
);
|
|
545
|
+
return outcome;
|
|
432
546
|
};
|
|
433
547
|
|
|
434
548
|
let response: Response;
|
|
@@ -449,18 +563,18 @@ export const dispatchCapability = async (params: {
|
|
|
449
563
|
kind: params.kind,
|
|
450
564
|
error: error instanceof Error ? error.message : String(error),
|
|
451
565
|
});
|
|
452
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
566
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
453
567
|
if (params.request.signal.aborted) {
|
|
454
568
|
const cancelled = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.requestCancelled, messages.requestCancelled, 499, {
|
|
455
569
|
retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
|
|
456
570
|
});
|
|
457
|
-
return
|
|
571
|
+
return settle(cancelled, "uncertain");
|
|
458
572
|
}
|
|
459
573
|
if (timeout.aborted) {
|
|
460
574
|
const deadline = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded, messages.deadlineExceeded, 504, {
|
|
461
575
|
retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
|
|
462
576
|
});
|
|
463
|
-
return
|
|
577
|
+
return settle(deadline, "uncertain");
|
|
464
578
|
}
|
|
465
579
|
const unavailable = errorResponse(
|
|
466
580
|
CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
@@ -468,28 +582,30 @@ export const dispatchCapability = async (params: {
|
|
|
468
582
|
503,
|
|
469
583
|
{ retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required") },
|
|
470
584
|
);
|
|
471
|
-
return
|
|
585
|
+
return settle(unavailable, "uncertain");
|
|
472
586
|
}
|
|
473
587
|
|
|
474
588
|
const upstreamBody = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
|
|
475
589
|
if (!upstreamBody.ok) {
|
|
476
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
590
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
477
591
|
const code =
|
|
478
592
|
upstreamBody.reason === "too_large"
|
|
479
593
|
? CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge
|
|
480
594
|
: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse;
|
|
481
595
|
const invalid = errorResponse(code, messages.invalidCapabilityJson({ appId: params.appId }), 502);
|
|
482
|
-
return
|
|
596
|
+
return settle(invalid, "uncertain");
|
|
483
597
|
}
|
|
484
598
|
|
|
485
599
|
if (!response.ok) {
|
|
486
600
|
const parsed = CapabilityErrorSchema.safeParse(upstreamBody.data);
|
|
487
601
|
if (parsed.success && response.status >= 400 && response.status <= 599) {
|
|
488
|
-
|
|
602
|
+
// A 4xx from the owning app is a decision about the request itself and
|
|
603
|
+
// proves the effect did not happen; a 5xx proves nothing.
|
|
604
|
+
return settle({ body: parsed.data, status: response.status }, response.status < 500 ? "nothing_happened" : "uncertain");
|
|
489
605
|
}
|
|
490
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
606
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
491
607
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.invalidCapabilityError({ appId: params.appId }), 502);
|
|
492
|
-
return
|
|
608
|
+
return settle(invalid, "uncertain");
|
|
493
609
|
}
|
|
494
610
|
|
|
495
611
|
const resultValidator = params.review
|
|
@@ -503,12 +619,112 @@ export const dispatchCapability = async (params: {
|
|
|
503
619
|
!parsedReview?.success ||
|
|
504
620
|
(operation.approval === "rememberable" ? parsedReview.data.approvalScope !== undefined : parsedReview.data.approvalScope === undefined);
|
|
505
621
|
if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
|
|
506
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
622
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
507
623
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
|
|
508
|
-
return
|
|
624
|
+
return settle(invalid, "uncertain");
|
|
509
625
|
}
|
|
510
626
|
|
|
511
|
-
|
|
627
|
+
const body = upstreamBody.data as Record<string, unknown>;
|
|
628
|
+
if (!params.review && body.stream !== undefined) {
|
|
629
|
+
const stream = CapabilityStreamSchema.safeParse(body.stream);
|
|
630
|
+
if (
|
|
631
|
+
!stream.success ||
|
|
632
|
+
!operation.stream ||
|
|
633
|
+
stream.data.direction !== operation.stream.direction ||
|
|
634
|
+
stream.data.size > operation.stream.maxBytes ||
|
|
635
|
+
Date.parse(stream.data.expiresAt) <= Date.now() ||
|
|
636
|
+
Date.parse(stream.data.expiresAt) > Date.now() + 86_400_000 ||
|
|
637
|
+
!params.authority
|
|
638
|
+
)
|
|
639
|
+
return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
|
|
640
|
+
try {
|
|
641
|
+
body.stream = await sealCapabilityStream(stream.data, {
|
|
642
|
+
appId: params.appId,
|
|
643
|
+
kind: params.kind,
|
|
644
|
+
capabilityId: params.capabilityId,
|
|
645
|
+
schemaHash: operation.schemaHash,
|
|
646
|
+
authority: params.authority,
|
|
647
|
+
requestId,
|
|
648
|
+
origin: params.origin,
|
|
649
|
+
continuation: params.continuation,
|
|
650
|
+
});
|
|
651
|
+
if (Buffer.byteLength(JSON.stringify(body)) > CAPABILITY_MAX_RESULT_BYTES) throw new Error("Stream result too large");
|
|
652
|
+
} catch {
|
|
653
|
+
return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return settle({ body, status: 200 }, "succeeded");
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const executionStatus = (status: number, code: string | null): CapabilityExecutionStatus => {
|
|
660
|
+
if (status < 400) return "succeeded";
|
|
661
|
+
if (status === 400) return "invalid_input";
|
|
662
|
+
if (status === 401 || status === 403) return "denied";
|
|
663
|
+
if (status === 504 || code === CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded) return "timed_out";
|
|
664
|
+
return "failed";
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const bodyErrorCode = (body: unknown): string | null =>
|
|
668
|
+
body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
|
|
669
|
+
|
|
670
|
+
const resultDataMeta = (body: unknown) =>
|
|
671
|
+
capabilityValueMeta(body && typeof body === "object" && "data" in body ? (body as { data: unknown }).data : body);
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Dispatch one already-parsed capability invocation through the live registry.
|
|
675
|
+
* HTTP, MCP, and assistant callers share this exact app lookup, credential
|
|
676
|
+
* forwarding, schema pinning, timeout, and response validation path.
|
|
677
|
+
*
|
|
678
|
+
* One invocation
|
|
679
|
+
* produces exactly one execution row, whatever its outcome. Binary stream
|
|
680
|
+
* continuations record their own row under the same request ID. Action reviews are a
|
|
681
|
+
* read-only preview and are not executions, so they are not recorded.
|
|
682
|
+
*/
|
|
683
|
+
export const dispatchCapability = async (params: CapabilityDispatchParams): Promise<Response> => {
|
|
684
|
+
const requestId = normalizeInvocationRequestId(params.request.headers.get("x-request-id")) ?? crypto.randomUUID();
|
|
685
|
+
const observed: CapabilityDispatchObservation = { subject: null, destructive: false };
|
|
686
|
+
const startedAt = new Date();
|
|
687
|
+
const outcome = await runCapabilityDispatch(params, requestId, observed);
|
|
688
|
+
if (!params.review) {
|
|
689
|
+
const actor = params.authority?.actor;
|
|
690
|
+
const principal = dispatchPrincipal(params, observed);
|
|
691
|
+
const accessSubject = params.authority?.accessSubject;
|
|
692
|
+
const subject =
|
|
693
|
+
accessSubject?.type === "user"
|
|
694
|
+
? ({ type: "user", id: accessSubject.userId } as const)
|
|
695
|
+
: accessSubject?.type === "service_account"
|
|
696
|
+
? ({ type: "service_account", id: accessSubject.serviceAccountId } as const)
|
|
697
|
+
: null;
|
|
698
|
+
const errorCode = outcome.status >= 400 ? bodyErrorCode(outcome.body) : null;
|
|
699
|
+
await recordCapabilityExecution({
|
|
700
|
+
requestId,
|
|
701
|
+
origin: params.origin,
|
|
702
|
+
appId: params.appId,
|
|
703
|
+
capability: `${params.appId}.${params.capabilityId}`,
|
|
704
|
+
kind: params.kind === "queries" ? "query" : "action",
|
|
705
|
+
destructive: observed.destructive,
|
|
706
|
+
actorKind: principal?.type ?? null,
|
|
707
|
+
actorId: principal?.id ?? null,
|
|
708
|
+
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
|
|
709
|
+
accessSubject: subject && subject.id !== principal?.id ? subject : null,
|
|
710
|
+
status: executionStatus(outcome.status, errorCode),
|
|
711
|
+
errorCode,
|
|
712
|
+
inputMeta: capabilityValueMeta(params.input),
|
|
713
|
+
outputMeta: outcome.status < 400 ? resultDataMeta(outcome.body) : null,
|
|
714
|
+
idempotencyKey: CapabilityIdempotencyKeySchema.safeParse(params.request.headers.get("idempotency-key")).data ?? null,
|
|
715
|
+
replayed: outcome.replayed === true,
|
|
716
|
+
startedAt,
|
|
717
|
+
completedAt: new Date(),
|
|
718
|
+
}).catch((error) => {
|
|
719
|
+
// History is operational evidence; losing a row must not fail the call.
|
|
720
|
+
log.error("Capability execution could not be recorded", {
|
|
721
|
+
appId: params.appId,
|
|
722
|
+
capabilityId: params.capabilityId,
|
|
723
|
+
error: error instanceof Error ? error.message : String(error),
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return capabilityJsonResponse(outcome.body, outcome.status);
|
|
512
728
|
};
|
|
513
729
|
|
|
514
730
|
class MandateDispatchError extends Error {
|
|
@@ -526,6 +742,23 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
526
742
|
return new Hono<AuthContext>()
|
|
527
743
|
.use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
528
744
|
.use("/capabilities/v1/*", rejectReservedWorkloadCredential)
|
|
745
|
+
.post(
|
|
746
|
+
"/capabilities/v1/streams/:verb",
|
|
747
|
+
describeRoute({
|
|
748
|
+
tags: ["Capabilities"],
|
|
749
|
+
summary: "Transfer or inspect one authorized binary stream",
|
|
750
|
+
...requiresAuth,
|
|
751
|
+
description:
|
|
752
|
+
"POST read/write/status/abort with x-cloud-stream-id. Write sends raw bytes; read returns raw bytes. Status and abort return the upload state. No automatic retries.",
|
|
753
|
+
responses: {
|
|
754
|
+
200: { description: "Binary content, write receipt, or transfer state" },
|
|
755
|
+
403: { description: "Stream does not belong to this caller" },
|
|
756
|
+
409: { description: "Transfer or contract changed" },
|
|
757
|
+
410: { description: "Stream expired" },
|
|
758
|
+
},
|
|
759
|
+
}),
|
|
760
|
+
async (c) => dispatchCapabilityStream(c.req.raw, auth.getAuthority(c), c.req.param("verb"), dependencies),
|
|
761
|
+
)
|
|
529
762
|
.get(
|
|
530
763
|
"/capabilities/v1/catalog",
|
|
531
764
|
describeRoute({
|
|
@@ -554,6 +787,52 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
554
787
|
}
|
|
555
788
|
},
|
|
556
789
|
)
|
|
790
|
+
.post(
|
|
791
|
+
"/capabilities/v1/commands/:appId/:capabilityId",
|
|
792
|
+
describeRoute({
|
|
793
|
+
tags: ["Capabilities"],
|
|
794
|
+
summary: "Resolve an interactive Command link",
|
|
795
|
+
...requiresAuth,
|
|
796
|
+
responses: {
|
|
797
|
+
200: jsonResponse(CommandLinkSchema, "Command entry URL"),
|
|
798
|
+
400: jsonResponse(CapabilityErrorSchema, "Invalid command input"),
|
|
799
|
+
404: jsonResponse(CapabilityErrorSchema, "Command unavailable"),
|
|
800
|
+
503: jsonResponse(CapabilityErrorSchema, "Route owner unavailable"),
|
|
801
|
+
},
|
|
802
|
+
}),
|
|
803
|
+
requireReadScope,
|
|
804
|
+
async (c) => {
|
|
805
|
+
const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
806
|
+
const request = body.ok ? CommandRequestSchema.safeParse(body.data) : null;
|
|
807
|
+
if (!request?.success) return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).requestBodyJson }, 400);
|
|
808
|
+
const appId = c.req.param("appId");
|
|
809
|
+
const entry = await (dependencies.getCapability ?? getCapability)(appId);
|
|
810
|
+
const command = entry?.manifest.commands.find((command) => command.localId === c.req.param("capabilityId"));
|
|
811
|
+
if (!command)
|
|
812
|
+
return c.json(
|
|
813
|
+
{
|
|
814
|
+
code: "CAPABILITY_NOT_FOUND",
|
|
815
|
+
message: capabilityMessages(getLocale(c)).notFound({ kind: "Command", reference: `${appId}.${c.req.param("capabilityId")}` }),
|
|
816
|
+
},
|
|
817
|
+
404,
|
|
818
|
+
);
|
|
819
|
+
const apps = await (dependencies.listApps ?? listApps)();
|
|
820
|
+
const pathname = new URL(command.path, "https://cloud.invalid").pathname;
|
|
821
|
+
const owner = apps
|
|
822
|
+
.flatMap((app) => app.routes.map((prefix) => ({ app, prefix })))
|
|
823
|
+
.filter(({ prefix }) => pathname === prefix || pathname.startsWith(`${prefix}/`))
|
|
824
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)[0]?.app;
|
|
825
|
+
if (owner?.id !== appId)
|
|
826
|
+
return c.json({ code: "APP_UNAVAILABLE", message: capabilityMessages(getLocale(c)).appUnavailable({ appId }) }, 503);
|
|
827
|
+
try {
|
|
828
|
+
const appUrl = await (dependencies.appUrl ?? (() => settings.get<string>("app.url")))();
|
|
829
|
+
return c.json(resolveCommandLink(appId, command, request.data.input, { returnTo: request.data.returnTo }, appUrl));
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (!(error instanceof z.ZodError) && !(error instanceof Error && error.message === "Command link is too large")) throw error;
|
|
832
|
+
return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).inputSchemaMismatch }, 400);
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
)
|
|
557
836
|
.post(
|
|
558
837
|
"/capabilities/v1/actions/:appId/:capabilityId/review",
|
|
559
838
|
describeRoute({
|
|
@@ -594,6 +873,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
594
873
|
request: c.req.raw,
|
|
595
874
|
kind: "actions",
|
|
596
875
|
review: true,
|
|
876
|
+
origin: "http",
|
|
597
877
|
appId: c.req.param("appId") ?? "",
|
|
598
878
|
capabilityId: c.req.param("capabilityId") ?? "",
|
|
599
879
|
input: request.data.input,
|
|
@@ -644,6 +924,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
644
924
|
return dispatchCapability({
|
|
645
925
|
request: c.req.raw,
|
|
646
926
|
kind: c.req.param("kind") as "queries" | "actions",
|
|
927
|
+
origin: "http",
|
|
647
928
|
appId: c.req.param("appId") ?? "",
|
|
648
929
|
capabilityId: c.req.param("capabilityId") ?? "",
|
|
649
930
|
input: request.data.input,
|