@k2b/cloud 0.6.0-rc.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +30 -17
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/solid.tsx +3 -3
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/scripts/app-favicon.test.ts +0 -49
- package/src/ai/chat/blocks.render.test.tsx +0 -1108
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
- package/src/ssr/Layout.render.test.tsx +0 -148
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/src/api/capabilities.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
+
import { sealCapabilityStream, dispatchCapabilityStream } from "./capability-streams";
|
|
2
|
+
import { CapabilityStreamSchema } from "../contracts/capability-streams";
|
|
1
3
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
4
|
import { describeRoute } from "hono-openapi";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
7
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
8
|
import { getCapability, listApps } from "../_internal/registry";
|
|
9
|
+
import {
|
|
10
|
+
type CapabilityClaimScope,
|
|
11
|
+
type CapabilityExecutionStatus,
|
|
12
|
+
capabilityIdempotencyKeyHash,
|
|
13
|
+
capabilityRequestHash,
|
|
14
|
+
capabilityValueMeta,
|
|
15
|
+
claimCapabilityIdempotency,
|
|
16
|
+
completeCapabilityClaim,
|
|
17
|
+
markCapabilityClaimUncertain,
|
|
18
|
+
recordCapabilityExecution,
|
|
19
|
+
releaseCapabilityClaim,
|
|
20
|
+
resolveCapabilityClaim,
|
|
21
|
+
} from "../capabilities/executions";
|
|
7
22
|
import {
|
|
8
23
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
9
24
|
CAPABILITY_MAX_CATALOG_BYTES,
|
|
10
25
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
11
26
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
27
|
+
CAPABILITY_ORIGIN_HEADER,
|
|
12
28
|
CAPABILITY_PROTOCOL_VERSION,
|
|
13
29
|
CapabilityActionReviewSchema,
|
|
14
30
|
type CapabilityCatalog,
|
|
15
31
|
CapabilityCatalogSchema,
|
|
16
32
|
CapabilityErrorSchema,
|
|
17
33
|
CapabilityIdempotencyKeySchema,
|
|
34
|
+
type CapabilityOrigin,
|
|
18
35
|
capabilityResultJsonSchema,
|
|
19
36
|
} from "../contracts/capabilities";
|
|
20
37
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
@@ -30,7 +47,9 @@ import {
|
|
|
30
47
|
requiresAuth,
|
|
31
48
|
v,
|
|
32
49
|
} from "../server";
|
|
33
|
-
import { logger } from "../services";
|
|
50
|
+
import { logger, settings } from "../services";
|
|
51
|
+
import { resolveCommandLink } from "../capabilities/command-link";
|
|
52
|
+
import { CommandRequestSchema, CommandLinkSchema } from "../contracts/commands";
|
|
34
53
|
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
35
54
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
36
55
|
import type { InvocationAuthority } from "../services/identity/invocation-token";
|
|
@@ -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,49 @@ 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) return errorResponse("STREAM_UNSUPPORTED_AUTHORITY", "Streams require an interactive user or service-account request", 403);
|
|
344
|
+
|
|
299
345
|
let requestBody: string;
|
|
300
346
|
try {
|
|
301
347
|
requestBody = JSON.stringify({ input: input.data });
|
|
302
348
|
} catch {
|
|
303
349
|
const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.inputNotSerializable, 400);
|
|
304
|
-
return
|
|
350
|
+
return invalid;
|
|
305
351
|
}
|
|
306
352
|
if (new TextEncoder().encode(requestBody).byteLength > CAPABILITY_MAX_REQUEST_BYTES) {
|
|
307
353
|
const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.requestTooLarge, 400);
|
|
308
|
-
return
|
|
354
|
+
return invalid;
|
|
309
355
|
}
|
|
310
356
|
|
|
311
357
|
const invocationOperation = capabilityInvocationOperation(params.kind, params.capabilityId, params.review);
|
|
@@ -332,17 +378,20 @@ export const dispatchCapability = async (params: {
|
|
|
332
378
|
targetAppId: params.appId,
|
|
333
379
|
operation: invocationOperation,
|
|
334
380
|
actionApproval: mandate.actionApproval ?? "none",
|
|
335
|
-
|
|
381
|
+
input: params.input,
|
|
382
|
+
capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
|
|
383
|
+
requestId,
|
|
336
384
|
},
|
|
337
385
|
async (mandateAuthority) => {
|
|
338
386
|
signal.throwIfAborted();
|
|
387
|
+
observed.subject = mandateAuthority.subject;
|
|
339
388
|
return (params.dependencies?.signInvocation ?? signInvocationToken)({
|
|
340
389
|
targetAppId: params.appId,
|
|
341
390
|
callingAppId: mandate.ownerAppId,
|
|
342
391
|
operation: invocationOperation,
|
|
343
392
|
schemaHash: operation.schemaHash,
|
|
344
393
|
authority: invocationAuthorityFromMandate(mandateAuthority),
|
|
345
|
-
requestId
|
|
394
|
+
requestId,
|
|
346
395
|
signer,
|
|
347
396
|
issuer: signer.issuer,
|
|
348
397
|
});
|
|
@@ -376,7 +425,7 @@ export const dispatchCapability = async (params: {
|
|
|
376
425
|
operation: invocationOperation,
|
|
377
426
|
schemaHash: operation.schemaHash,
|
|
378
427
|
authority: invocationAuthorityFromRequest(authority),
|
|
379
|
-
requestId
|
|
428
|
+
requestId,
|
|
380
429
|
signer,
|
|
381
430
|
issuer: signer.issuer,
|
|
382
431
|
}),
|
|
@@ -397,7 +446,7 @@ export const dispatchCapability = async (params: {
|
|
|
397
446
|
error.status === 409 ? "Mandate revision changed" : "Mandate does not authorize this capability invocation",
|
|
398
447
|
error.status,
|
|
399
448
|
);
|
|
400
|
-
return
|
|
449
|
+
return denied;
|
|
401
450
|
}
|
|
402
451
|
if (params.request.signal.aborted || timeout.aborted) {
|
|
403
452
|
const cancelled = params.request.signal.aborted;
|
|
@@ -407,15 +456,16 @@ export const dispatchCapability = async (params: {
|
|
|
407
456
|
cancelled ? 499 : 504,
|
|
408
457
|
{ retrySafe: true },
|
|
409
458
|
);
|
|
410
|
-
return
|
|
459
|
+
return failure;
|
|
411
460
|
}
|
|
412
461
|
console.error("[capabilities] Invocation issuance failed", { appId: params.appId, operation: invocationOperation });
|
|
413
462
|
const failure = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.issuanceFailed, 500, { retrySafe: true });
|
|
414
|
-
return
|
|
463
|
+
return failure;
|
|
415
464
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
465
|
+
headers.set("x-request-id", requestId);
|
|
466
|
+
headers.set(CAPABILITY_ORIGIN_HEADER, params.origin);
|
|
467
|
+
for (const name of ["traceparent", "tracestate"] as const) {
|
|
468
|
+
const value = params.request.headers.get(name);
|
|
419
469
|
if (value) headers.set(name, value);
|
|
420
470
|
}
|
|
421
471
|
const invocationLocale = preferredLocale(params.request.headers);
|
|
@@ -424,11 +474,74 @@ export const dispatchCapability = async (params: {
|
|
|
424
474
|
headers.set("x-cloud-capability-schema-hash", operation.schemaHash);
|
|
425
475
|
const actionWithoutRetrySafety =
|
|
426
476
|
params.kind === "actions" && !params.review && "idempotency" in operation && operation.idempotency === "none";
|
|
427
|
-
const outcomeUnknown = ():
|
|
477
|
+
const outcomeUnknown = (): CapabilityDispatchOutcome => {
|
|
428
478
|
const unknown = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown, messages.actionOutcomeUnknown, 502, {
|
|
429
479
|
retrySafe: false,
|
|
430
480
|
});
|
|
431
|
-
return
|
|
481
|
+
return unknown;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const claimPrincipal = dispatchPrincipal(params, observed);
|
|
485
|
+
const claimScope: CapabilityClaimScope | null =
|
|
486
|
+
params.kind === "actions" &&
|
|
487
|
+
!params.review &&
|
|
488
|
+
"idempotency" in operation &&
|
|
489
|
+
operation.idempotency === "required" &&
|
|
490
|
+
idempotencyKey?.success
|
|
491
|
+
? {
|
|
492
|
+
appId: params.appId,
|
|
493
|
+
capability: `${params.appId}.${params.capabilityId}`,
|
|
494
|
+
principal: claimPrincipal ? `${claimPrincipal.type}:${claimPrincipal.id}` : "anonymous",
|
|
495
|
+
keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
|
|
496
|
+
}
|
|
497
|
+
: null;
|
|
498
|
+
if (claimScope) {
|
|
499
|
+
let claim: Awaited<ReturnType<typeof claimCapabilityIdempotency>>;
|
|
500
|
+
try {
|
|
501
|
+
claim = await claimCapabilityIdempotency(claimScope, capabilityRequestHash(input.data));
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// Nothing has been forwarded yet, so refusing the call is the safe answer.
|
|
504
|
+
log.error("Capability idempotency claim failed", {
|
|
505
|
+
appId: params.appId,
|
|
506
|
+
capabilityId: params.capabilityId,
|
|
507
|
+
error: error instanceof Error ? error.message : String(error),
|
|
508
|
+
});
|
|
509
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.cloudUnavailable, 503, { retrySafe: true });
|
|
510
|
+
}
|
|
511
|
+
if (claim.state === "replay") return { body: claim.body, status: claim.status, replayed: true };
|
|
512
|
+
if (claim.state === "conflict")
|
|
513
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyConflict, messages.idempotencyConflict, 409);
|
|
514
|
+
if (claim.state === "in_flight") {
|
|
515
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyInProgress, messages.idempotencyInProgress, 409, {
|
|
516
|
+
retrySafe: false,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (claim.state === "uncertain") {
|
|
520
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyUncertain, 409, { retrySafe: false });
|
|
521
|
+
}
|
|
522
|
+
if (claim.state === "not_retained") {
|
|
523
|
+
return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyResultNotRetained, 409, {
|
|
524
|
+
retrySafe: false,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Resolve the claim once the outcome is known: a definitive success is
|
|
531
|
+
* replayable, a failure that proves nothing happened frees the key for a
|
|
532
|
+
* corrected retry, and anything ambiguous freezes the key.
|
|
533
|
+
*/
|
|
534
|
+
const settle = async (outcome: CapabilityDispatchOutcome, disposition: "succeeded" | "nothing_happened" | "uncertain") => {
|
|
535
|
+
if (!claimScope) return outcome;
|
|
536
|
+
await resolveCapabilityClaim(
|
|
537
|
+
disposition === "succeeded"
|
|
538
|
+
? completeCapabilityClaim(claimScope, outcome.status, outcome.body)
|
|
539
|
+
: disposition === "nothing_happened"
|
|
540
|
+
? releaseCapabilityClaim(claimScope)
|
|
541
|
+
: markCapabilityClaimUncertain(claimScope),
|
|
542
|
+
claimScope,
|
|
543
|
+
);
|
|
544
|
+
return outcome;
|
|
432
545
|
};
|
|
433
546
|
|
|
434
547
|
let response: Response;
|
|
@@ -449,18 +562,18 @@ export const dispatchCapability = async (params: {
|
|
|
449
562
|
kind: params.kind,
|
|
450
563
|
error: error instanceof Error ? error.message : String(error),
|
|
451
564
|
});
|
|
452
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
565
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
453
566
|
if (params.request.signal.aborted) {
|
|
454
567
|
const cancelled = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.requestCancelled, messages.requestCancelled, 499, {
|
|
455
568
|
retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
|
|
456
569
|
});
|
|
457
|
-
return
|
|
570
|
+
return settle(cancelled, "uncertain");
|
|
458
571
|
}
|
|
459
572
|
if (timeout.aborted) {
|
|
460
573
|
const deadline = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded, messages.deadlineExceeded, 504, {
|
|
461
574
|
retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
|
|
462
575
|
});
|
|
463
|
-
return
|
|
576
|
+
return settle(deadline, "uncertain");
|
|
464
577
|
}
|
|
465
578
|
const unavailable = errorResponse(
|
|
466
579
|
CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
@@ -468,28 +581,30 @@ export const dispatchCapability = async (params: {
|
|
|
468
581
|
503,
|
|
469
582
|
{ retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required") },
|
|
470
583
|
);
|
|
471
|
-
return
|
|
584
|
+
return settle(unavailable, "uncertain");
|
|
472
585
|
}
|
|
473
586
|
|
|
474
587
|
const upstreamBody = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
|
|
475
588
|
if (!upstreamBody.ok) {
|
|
476
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
589
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
477
590
|
const code =
|
|
478
591
|
upstreamBody.reason === "too_large"
|
|
479
592
|
? CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge
|
|
480
593
|
: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse;
|
|
481
594
|
const invalid = errorResponse(code, messages.invalidCapabilityJson({ appId: params.appId }), 502);
|
|
482
|
-
return
|
|
595
|
+
return settle(invalid, "uncertain");
|
|
483
596
|
}
|
|
484
597
|
|
|
485
598
|
if (!response.ok) {
|
|
486
599
|
const parsed = CapabilityErrorSchema.safeParse(upstreamBody.data);
|
|
487
600
|
if (parsed.success && response.status >= 400 && response.status <= 599) {
|
|
488
|
-
|
|
601
|
+
// A 4xx from the owning app is a decision about the request itself and
|
|
602
|
+
// proves the effect did not happen; a 5xx proves nothing.
|
|
603
|
+
return settle({ body: parsed.data, status: response.status }, response.status < 500 ? "nothing_happened" : "uncertain");
|
|
489
604
|
}
|
|
490
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
605
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
491
606
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.invalidCapabilityError({ appId: params.appId }), 502);
|
|
492
|
-
return
|
|
607
|
+
return settle(invalid, "uncertain");
|
|
493
608
|
}
|
|
494
609
|
|
|
495
610
|
const resultValidator = params.review
|
|
@@ -503,12 +618,93 @@ export const dispatchCapability = async (params: {
|
|
|
503
618
|
!parsedReview?.success ||
|
|
504
619
|
(operation.approval === "rememberable" ? parsedReview.data.approvalScope !== undefined : parsedReview.data.approvalScope === undefined);
|
|
505
620
|
if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
|
|
506
|
-
if (actionWithoutRetrySafety) return outcomeUnknown();
|
|
621
|
+
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
507
622
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
|
|
508
|
-
return
|
|
623
|
+
return settle(invalid, "uncertain");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const body = upstreamBody.data as Record<string, unknown>;
|
|
627
|
+
if (!params.review && body.stream !== undefined) {
|
|
628
|
+
const stream = CapabilityStreamSchema.safeParse(body.stream);
|
|
629
|
+
if (!stream.success || !operation.stream || stream.data.direction !== operation.stream.direction || stream.data.size > operation.stream.maxBytes || Date.parse(stream.data.expiresAt) <= Date.now() || Date.parse(stream.data.expiresAt) > Date.now() + 86_400_000 || !params.authority)
|
|
630
|
+
return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
|
|
631
|
+
try {
|
|
632
|
+
body.stream = await sealCapabilityStream(stream.data, { appId: params.appId, kind: params.kind, capabilityId: params.capabilityId, schemaHash: operation.schemaHash, authority: params.authority, requestId, origin: params.origin, continuation: params.continuation });
|
|
633
|
+
if (Buffer.byteLength(JSON.stringify(body)) > CAPABILITY_MAX_RESULT_BYTES) throw new Error("Stream result too large");
|
|
634
|
+
} catch { return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain"); }
|
|
509
635
|
}
|
|
636
|
+
return settle({ body, status: 200 }, "succeeded");
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const executionStatus = (status: number, code: string | null): CapabilityExecutionStatus => {
|
|
640
|
+
if (status < 400) return "succeeded";
|
|
641
|
+
if (status === 400) return "invalid_input";
|
|
642
|
+
if (status === 401 || status === 403) return "denied";
|
|
643
|
+
if (status === 504 || code === CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded) return "timed_out";
|
|
644
|
+
return "failed";
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const bodyErrorCode = (body: unknown): string | null =>
|
|
648
|
+
body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
|
|
510
649
|
|
|
511
|
-
|
|
650
|
+
const resultDataMeta = (body: unknown) =>
|
|
651
|
+
capabilityValueMeta(body && typeof body === "object" && "data" in body ? (body as { data: unknown }).data : body);
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Dispatch one already-parsed capability invocation through the live registry.
|
|
655
|
+
* HTTP, MCP, and assistant callers share this exact app lookup, credential
|
|
656
|
+
* forwarding, schema pinning, timeout, and response validation path.
|
|
657
|
+
*
|
|
658
|
+
* One invocation
|
|
659
|
+
* produces exactly one execution row, whatever its outcome. Binary stream
|
|
660
|
+
* continuations record their own row under the same request ID. Action reviews are a
|
|
661
|
+
* read-only preview and are not executions, so they are not recorded.
|
|
662
|
+
*/
|
|
663
|
+
export const dispatchCapability = async (params: CapabilityDispatchParams): Promise<Response> => {
|
|
664
|
+
const requestId = normalizeInvocationRequestId(params.request.headers.get("x-request-id")) ?? crypto.randomUUID();
|
|
665
|
+
const observed: CapabilityDispatchObservation = { subject: null, destructive: false };
|
|
666
|
+
const startedAt = new Date();
|
|
667
|
+
const outcome = await runCapabilityDispatch(params, requestId, observed);
|
|
668
|
+
if (!params.review) {
|
|
669
|
+
const actor = params.authority?.actor;
|
|
670
|
+
const principal = dispatchPrincipal(params, observed);
|
|
671
|
+
const accessSubject = params.authority?.accessSubject;
|
|
672
|
+
const subject =
|
|
673
|
+
accessSubject?.type === "user"
|
|
674
|
+
? ({ type: "user", id: accessSubject.userId } as const)
|
|
675
|
+
: accessSubject?.type === "service_account"
|
|
676
|
+
? ({ type: "service_account", id: accessSubject.serviceAccountId } as const)
|
|
677
|
+
: null;
|
|
678
|
+
const errorCode = outcome.status >= 400 ? bodyErrorCode(outcome.body) : null;
|
|
679
|
+
await recordCapabilityExecution({
|
|
680
|
+
requestId,
|
|
681
|
+
origin: params.origin,
|
|
682
|
+
appId: params.appId,
|
|
683
|
+
capability: `${params.appId}.${params.capabilityId}`,
|
|
684
|
+
kind: params.kind === "queries" ? "query" : "action",
|
|
685
|
+
destructive: observed.destructive,
|
|
686
|
+
actorKind: principal?.type ?? null,
|
|
687
|
+
actorId: principal?.id ?? null,
|
|
688
|
+
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
|
|
689
|
+
accessSubject: subject && subject.id !== principal?.id ? subject : null,
|
|
690
|
+
status: executionStatus(outcome.status, errorCode),
|
|
691
|
+
errorCode,
|
|
692
|
+
inputMeta: capabilityValueMeta(params.input),
|
|
693
|
+
outputMeta: outcome.status < 400 ? resultDataMeta(outcome.body) : null,
|
|
694
|
+
idempotencyKey: CapabilityIdempotencyKeySchema.safeParse(params.request.headers.get("idempotency-key")).data ?? null,
|
|
695
|
+
replayed: outcome.replayed === true,
|
|
696
|
+
startedAt,
|
|
697
|
+
completedAt: new Date(),
|
|
698
|
+
}).catch((error) => {
|
|
699
|
+
// History is operational evidence; losing a row must not fail the call.
|
|
700
|
+
log.error("Capability execution could not be recorded", {
|
|
701
|
+
appId: params.appId,
|
|
702
|
+
capabilityId: params.capabilityId,
|
|
703
|
+
error: error instanceof Error ? error.message : String(error),
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return capabilityJsonResponse(outcome.body, outcome.status);
|
|
512
708
|
};
|
|
513
709
|
|
|
514
710
|
class MandateDispatchError extends Error {
|
|
@@ -526,6 +722,11 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
526
722
|
return new Hono<AuthContext>()
|
|
527
723
|
.use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
528
724
|
.use("/capabilities/v1/*", rejectReservedWorkloadCredential)
|
|
725
|
+
.post("/capabilities/v1/streams/:verb", describeRoute({
|
|
726
|
+
tags:["Capabilities"],summary:"Transfer or inspect one authorized binary stream",...requiresAuth,
|
|
727
|
+
description:"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.",
|
|
728
|
+
responses:{200:{description:"Binary content, write receipt, or transfer state"},403:{description:"Stream does not belong to this caller"},409:{description:"Transfer or contract changed"},410:{description:"Stream expired"}},
|
|
729
|
+
}), async c => dispatchCapabilityStream(c.req.raw, auth.getAuthority(c), c.req.param("verb"), dependencies))
|
|
529
730
|
.get(
|
|
530
731
|
"/capabilities/v1/catalog",
|
|
531
732
|
describeRoute({
|
|
@@ -554,6 +755,52 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
554
755
|
}
|
|
555
756
|
},
|
|
556
757
|
)
|
|
758
|
+
.post(
|
|
759
|
+
"/capabilities/v1/commands/:appId/:capabilityId",
|
|
760
|
+
describeRoute({
|
|
761
|
+
tags: ["Capabilities"],
|
|
762
|
+
summary: "Resolve an interactive Command link",
|
|
763
|
+
...requiresAuth,
|
|
764
|
+
responses: {
|
|
765
|
+
200: jsonResponse(CommandLinkSchema, "Command entry URL"),
|
|
766
|
+
400: jsonResponse(CapabilityErrorSchema, "Invalid command input"),
|
|
767
|
+
404: jsonResponse(CapabilityErrorSchema, "Command unavailable"),
|
|
768
|
+
503: jsonResponse(CapabilityErrorSchema, "Route owner unavailable"),
|
|
769
|
+
},
|
|
770
|
+
}),
|
|
771
|
+
requireReadScope,
|
|
772
|
+
async (c) => {
|
|
773
|
+
const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
774
|
+
const request = body.ok ? CommandRequestSchema.safeParse(body.data) : null;
|
|
775
|
+
if (!request?.success) return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).requestBodyJson }, 400);
|
|
776
|
+
const appId = c.req.param("appId");
|
|
777
|
+
const entry = await (dependencies.getCapability ?? getCapability)(appId);
|
|
778
|
+
const command = entry?.manifest.commands.find((command) => command.localId === c.req.param("capabilityId"));
|
|
779
|
+
if (!command)
|
|
780
|
+
return c.json(
|
|
781
|
+
{
|
|
782
|
+
code: "CAPABILITY_NOT_FOUND",
|
|
783
|
+
message: capabilityMessages(getLocale(c)).notFound({ kind: "Command", reference: `${appId}.${c.req.param("capabilityId")}` }),
|
|
784
|
+
},
|
|
785
|
+
404,
|
|
786
|
+
);
|
|
787
|
+
const apps = await (dependencies.listApps ?? listApps)();
|
|
788
|
+
const pathname = new URL(command.path, "https://cloud.invalid").pathname;
|
|
789
|
+
const owner = apps
|
|
790
|
+
.flatMap((app) => app.routes.map((prefix) => ({ app, prefix })))
|
|
791
|
+
.filter(({ prefix }) => pathname === prefix || pathname.startsWith(`${prefix}/`))
|
|
792
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)[0]?.app;
|
|
793
|
+
if (owner?.id !== appId)
|
|
794
|
+
return c.json({ code: "APP_UNAVAILABLE", message: capabilityMessages(getLocale(c)).appUnavailable({ appId }) }, 503);
|
|
795
|
+
try {
|
|
796
|
+
const appUrl = await (dependencies.appUrl ?? (() => settings.get<string>("app.url")))();
|
|
797
|
+
return c.json(resolveCommandLink(appId, command, request.data.input, { returnTo: request.data.returnTo }, appUrl));
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (!(error instanceof z.ZodError) && !(error instanceof Error && error.message === "Command link is too large")) throw error;
|
|
800
|
+
return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).inputSchemaMismatch }, 400);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
)
|
|
557
804
|
.post(
|
|
558
805
|
"/capabilities/v1/actions/:appId/:capabilityId/review",
|
|
559
806
|
describeRoute({
|
|
@@ -594,6 +841,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
594
841
|
request: c.req.raw,
|
|
595
842
|
kind: "actions",
|
|
596
843
|
review: true,
|
|
844
|
+
origin: "http",
|
|
597
845
|
appId: c.req.param("appId") ?? "",
|
|
598
846
|
capabilityId: c.req.param("capabilityId") ?? "",
|
|
599
847
|
input: request.data.input,
|
|
@@ -644,6 +892,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
644
892
|
return dispatchCapability({
|
|
645
893
|
request: c.req.raw,
|
|
646
894
|
kind: c.req.param("kind") as "queries" | "actions",
|
|
895
|
+
origin: "http",
|
|
647
896
|
appId: c.req.param("appId") ?? "",
|
|
648
897
|
capabilityId: c.req.param("capabilityId") ?? "",
|
|
649
898
|
input: request.data.input,
|