@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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readBoundedJson } from "../_internal/bounded-json";
|
|
3
|
+
import { getCapability } from "../_internal/registry";
|
|
4
|
+
import { recordCapabilityExecution } from "../capabilities/executions";
|
|
5
|
+
import { exactStream } from "../capabilities/stream-body";
|
|
6
|
+
import type { CapabilityStream } from "../contracts/capabilities";
|
|
7
|
+
import {
|
|
8
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
9
|
+
CAPABILITY_ORIGIN_HEADER,
|
|
10
|
+
CapabilityErrorSchema,
|
|
11
|
+
CapabilityOriginSchema,
|
|
12
|
+
CapabilityStreamSchema,
|
|
13
|
+
CapabilityStreamStatusSchema,
|
|
14
|
+
capabilityResultSchema,
|
|
15
|
+
} from "../contracts/capabilities";
|
|
16
|
+
import type { RequestAuthority } from "../server";
|
|
17
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
18
|
+
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
19
|
+
import { signInvocationToken } from "../services/identity/invocation-token";
|
|
20
|
+
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
21
|
+
import { logger } from "../services/logging";
|
|
22
|
+
import { decryptSecret, encryptSecret } from "../services/secrets";
|
|
23
|
+
import { LOCALE_HEADER } from "../shared/locale";
|
|
24
|
+
import type { CapabilityDispatchDependencies } from "./capabilities";
|
|
25
|
+
|
|
26
|
+
const log = logger("capability-streams");
|
|
27
|
+
|
|
28
|
+
const Grant = z.object({
|
|
29
|
+
purpose: z.literal("capability-stream-v2"),
|
|
30
|
+
continuation: z.string().nullable(),
|
|
31
|
+
origin: CapabilityOriginSchema.default("http"),
|
|
32
|
+
appId: z.string(),
|
|
33
|
+
kind: z.enum(["queries", "actions"]),
|
|
34
|
+
capabilityId: z.string(),
|
|
35
|
+
schemaHash: z.string(),
|
|
36
|
+
authority: z.string(),
|
|
37
|
+
stream: CapabilityStreamSchema,
|
|
38
|
+
requestId: z.string(),
|
|
39
|
+
});
|
|
40
|
+
const binding = (authority: RequestAuthority) => JSON.stringify(invocationAuthorityFromRequest(authority));
|
|
41
|
+
export async function sealCapabilityStream(
|
|
42
|
+
stream: CapabilityStream,
|
|
43
|
+
params: {
|
|
44
|
+
appId: string;
|
|
45
|
+
kind: "queries" | "actions";
|
|
46
|
+
capabilityId: string;
|
|
47
|
+
schemaHash: string;
|
|
48
|
+
authority: RequestAuthority;
|
|
49
|
+
requestId: string;
|
|
50
|
+
origin?: z.infer<typeof CapabilityOriginSchema>;
|
|
51
|
+
continuation?: string;
|
|
52
|
+
},
|
|
53
|
+
): Promise<CapabilityStream> {
|
|
54
|
+
if (Buffer.byteLength(stream.id) > 2048) throw new Error("Provider stream ID exceeds 2 KiB");
|
|
55
|
+
const grant = Grant.parse({
|
|
56
|
+
...params,
|
|
57
|
+
purpose: "capability-stream-v2",
|
|
58
|
+
continuation: params.continuation ?? null,
|
|
59
|
+
authority: binding(params.authority),
|
|
60
|
+
stream,
|
|
61
|
+
});
|
|
62
|
+
return CapabilityStreamSchema.parse({ ...stream, id: await encryptSecret(grant) });
|
|
63
|
+
}
|
|
64
|
+
const denied = (status = 403, code = "STREAM_DENIED") =>
|
|
65
|
+
Response.json({ code, message: "Stream unavailable or not authorized" }, { status });
|
|
66
|
+
|
|
67
|
+
/** A transfer continues an already-authorized operation; it cannot manufacture a new Action. */
|
|
68
|
+
export async function dispatchCapabilityStream(
|
|
69
|
+
request: Request,
|
|
70
|
+
authority: RequestAuthority,
|
|
71
|
+
verb: string,
|
|
72
|
+
dependencies: CapabilityDispatchDependencies & {
|
|
73
|
+
recordStreamExecution?: typeof recordCapabilityExecution;
|
|
74
|
+
continuation?: string;
|
|
75
|
+
allow?: (operation: { appId: string; capabilityId: string }) => Promise<boolean>;
|
|
76
|
+
} = {},
|
|
77
|
+
): Promise<Response> {
|
|
78
|
+
const id = request.headers.get("x-cloud-stream-id");
|
|
79
|
+
if (!id || id.length > 8192 || !["read", "write", "status", "abort"].includes(verb)) return denied(400);
|
|
80
|
+
let grant: z.infer<typeof Grant>;
|
|
81
|
+
try {
|
|
82
|
+
grant = Grant.parse(await decryptSecret(id));
|
|
83
|
+
} catch {
|
|
84
|
+
return denied();
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
if (grant.authority !== binding(authority)) return denied();
|
|
88
|
+
} catch {
|
|
89
|
+
return denied();
|
|
90
|
+
}
|
|
91
|
+
if (grant.continuation !== (dependencies.continuation ?? null)) return denied();
|
|
92
|
+
if (dependencies.allow && !(await dependencies.allow(grant))) return denied();
|
|
93
|
+
if (Date.parse(grant.stream.expiresAt) <= Date.now()) return denied(410, "STREAM_EXPIRED");
|
|
94
|
+
if ((verb === "read") !== (grant.stream.direction === "read")) return denied();
|
|
95
|
+
if (
|
|
96
|
+
authority.credentialKind === "oauth" &&
|
|
97
|
+
!authority.scopes.some((scope) => scope === "admin" || scope === (grant.kind === "actions" ? "write" : "read"))
|
|
98
|
+
)
|
|
99
|
+
return denied();
|
|
100
|
+
const startedAt = new Date();
|
|
101
|
+
let recorded = false;
|
|
102
|
+
let destructive = false;
|
|
103
|
+
const record = async (status: number, errorCode: string | null = null) => {
|
|
104
|
+
if (recorded || (verb !== "read" && verb !== "write")) return;
|
|
105
|
+
recorded = true;
|
|
106
|
+
const actor = authority.actor;
|
|
107
|
+
const actorId = actor.kind === "user" ? actor.user.id : actor.serviceAccount.id;
|
|
108
|
+
const subject = authority.accessSubject;
|
|
109
|
+
const subjectId = subject.type === "user" ? subject.userId : subject.serviceAccountId;
|
|
110
|
+
await (dependencies.recordStreamExecution ?? recordCapabilityExecution)({
|
|
111
|
+
requestId: grant.requestId,
|
|
112
|
+
origin: grant.origin,
|
|
113
|
+
appId: grant.appId,
|
|
114
|
+
capability: `${grant.appId}.${grant.capabilityId}`,
|
|
115
|
+
kind: grant.kind === "queries" ? "query" : "action",
|
|
116
|
+
destructive,
|
|
117
|
+
actorKind: actor.kind,
|
|
118
|
+
actorId,
|
|
119
|
+
userId: actor.kind === "user" ? actor.user.id : (actor.delegatedUser?.id ?? null),
|
|
120
|
+
accessSubject: subjectId !== actorId ? { type: subject.type, id: subjectId } : null,
|
|
121
|
+
status: status < 400 ? "succeeded" : status === 403 ? "denied" : status === 504 ? "timed_out" : "failed",
|
|
122
|
+
errorCode,
|
|
123
|
+
inputMeta: { type: "object", keys: [`stream.${verb}`], omittedKeys: 0 },
|
|
124
|
+
outputMeta: status < 400 ? { type: "number", value: grant.stream.size } : null,
|
|
125
|
+
startedAt,
|
|
126
|
+
completedAt: new Date(),
|
|
127
|
+
}).catch(() => log.error("Stream execution could not be recorded", { requestId: grant.requestId }));
|
|
128
|
+
};
|
|
129
|
+
const signal = AbortSignal.any([request.signal, AbortSignal.timeout(300_000)]);
|
|
130
|
+
try {
|
|
131
|
+
const entry = await (dependencies.getCapability ?? getCapability)(grant.appId);
|
|
132
|
+
const operation = entry?.manifest[grant.kind].find((item) => item.localId === grant.capabilityId);
|
|
133
|
+
if (
|
|
134
|
+
!entry ||
|
|
135
|
+
!operation ||
|
|
136
|
+
operation.schemaHash !== grant.schemaHash ||
|
|
137
|
+
operation.stream?.direction !== grant.stream.direction ||
|
|
138
|
+
grant.stream.size > operation.stream.maxBytes
|
|
139
|
+
)
|
|
140
|
+
return denied(409, "SCHEMA_MISMATCH");
|
|
141
|
+
destructive = "destructive" in operation && operation.destructive;
|
|
142
|
+
const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
|
|
143
|
+
"invocation",
|
|
144
|
+
(signer) =>
|
|
145
|
+
(dependencies.signInvocation ?? signInvocationToken)({
|
|
146
|
+
targetAppId: grant.appId,
|
|
147
|
+
callingAppId: "core",
|
|
148
|
+
operation: capabilityInvocationOperation(grant.kind, grant.capabilityId),
|
|
149
|
+
schemaHash: operation.schemaHash,
|
|
150
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
151
|
+
requestId: grant.requestId,
|
|
152
|
+
signer,
|
|
153
|
+
issuer: signer.issuer,
|
|
154
|
+
}),
|
|
155
|
+
{ signal, timeoutMs: 30_000 },
|
|
156
|
+
);
|
|
157
|
+
const headers = new Headers({
|
|
158
|
+
authorization: `Bearer ${signed.token}`,
|
|
159
|
+
"x-request-id": grant.requestId,
|
|
160
|
+
"x-cloud-capability-schema-hash": operation.schemaHash,
|
|
161
|
+
"x-cloud-stream-offer": Buffer.from(JSON.stringify(grant.stream)).toString("base64url"),
|
|
162
|
+
"content-type": "application/octet-stream",
|
|
163
|
+
});
|
|
164
|
+
headers.set(CAPABILITY_ORIGIN_HEADER, grant.origin);
|
|
165
|
+
if (request.headers.has(LOCALE_HEADER)) headers.set(LOCALE_HEADER, request.headers.get(LOCALE_HEADER)!);
|
|
166
|
+
if (request.headers.has("accept-language")) headers.set("accept-language", request.headers.get("accept-language")!);
|
|
167
|
+
const body =
|
|
168
|
+
verb === "write"
|
|
169
|
+
? exactStream(
|
|
170
|
+
request.body ??
|
|
171
|
+
new ReadableStream({
|
|
172
|
+
start(c) {
|
|
173
|
+
c.close();
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
grant.stream.size,
|
|
177
|
+
)
|
|
178
|
+
: undefined;
|
|
179
|
+
const response = await (dependencies.fetch ?? fetch)(
|
|
180
|
+
`${entry.endpoint}/streams/${grant.kind}/${encodeURIComponent(grant.capabilityId)}/${verb}`,
|
|
181
|
+
{
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers,
|
|
184
|
+
body,
|
|
185
|
+
signal,
|
|
186
|
+
redirect: "error",
|
|
187
|
+
// @ts-expect-error Bun and Node stream request bodies use duplex.
|
|
188
|
+
duplex: body ? "half" : undefined,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
log.info("Capability stream response", {
|
|
192
|
+
requestId: grant.requestId,
|
|
193
|
+
appId: grant.appId,
|
|
194
|
+
capabilityId: grant.capabilityId,
|
|
195
|
+
verb,
|
|
196
|
+
status: response.status,
|
|
197
|
+
bytes: grant.stream.size,
|
|
198
|
+
});
|
|
199
|
+
if (verb === "read" && response.ok) {
|
|
200
|
+
if (!response.body) return denied(502, "STREAM_FAILED");
|
|
201
|
+
const reader = exactStream(response.body, grant.stream.size).getReader();
|
|
202
|
+
return new Response(
|
|
203
|
+
new ReadableStream({
|
|
204
|
+
async pull(controller) {
|
|
205
|
+
try {
|
|
206
|
+
const chunk = await reader.read();
|
|
207
|
+
if (chunk.done) {
|
|
208
|
+
await record(200);
|
|
209
|
+
controller.close();
|
|
210
|
+
} else controller.enqueue(chunk.value);
|
|
211
|
+
} catch {
|
|
212
|
+
await record(502, "STREAM_FAILED");
|
|
213
|
+
controller.error(new Error("Incomplete capability stream"));
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async cancel(reason) {
|
|
217
|
+
await reader.cancel(reason).catch(() => undefined);
|
|
218
|
+
await record(499, "REQUEST_CANCELLED");
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
{
|
|
222
|
+
headers: {
|
|
223
|
+
"content-type": grant.stream.mediaType,
|
|
224
|
+
"content-length": String(grant.stream.size),
|
|
225
|
+
"cache-control": "no-store",
|
|
226
|
+
"x-content-type-options": "nosniff",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const receipt = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
|
|
232
|
+
if (!receipt.ok) {
|
|
233
|
+
await record(502, "STREAM_FAILED");
|
|
234
|
+
return denied(502, "STREAM_FAILED");
|
|
235
|
+
}
|
|
236
|
+
if (response.ok) {
|
|
237
|
+
const parsed = (verb === "write" ? capabilityResultSchema(z.unknown()) : CapabilityStreamStatusSchema).safeParse(receipt.data);
|
|
238
|
+
if (!parsed.success) {
|
|
239
|
+
await record(502, "STREAM_FAILED");
|
|
240
|
+
return denied(502, "STREAM_FAILED");
|
|
241
|
+
}
|
|
242
|
+
const nested =
|
|
243
|
+
verb === "write" ? parsed.data : "state" in parsed.data && parsed.data.state === "completed" ? parsed.data.result : null;
|
|
244
|
+
if (nested && typeof nested === "object" && "stream" in nested) {
|
|
245
|
+
await record(502, "STREAM_FAILED");
|
|
246
|
+
return denied(502, "STREAM_FAILED");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
const error = CapabilityErrorSchema.safeParse(receipt.data);
|
|
251
|
+
const status = response.status === 401 ? 502 : response.status;
|
|
252
|
+
await record(status, error.success ? error.data.code : "STREAM_FAILED");
|
|
253
|
+
if (!error.success || response.status === 401) return denied(status, "STREAM_FAILED");
|
|
254
|
+
} else await record(response.status);
|
|
255
|
+
return Response.json(receipt.data, { status: response.status, headers: { "cache-control": "no-store" } });
|
|
256
|
+
} catch {
|
|
257
|
+
const status = request.signal.aborted ? 499 : signal.aborted ? 504 : 502;
|
|
258
|
+
const code = status === 499 ? "REQUEST_CANCELLED" : status === 504 ? "DEADLINE_EXCEEDED" : "STREAM_FAILED";
|
|
259
|
+
await record(status, code);
|
|
260
|
+
return Response.json({ code, message: "Transfer interrupted; inspect write status before retrying" }, { status });
|
|
261
|
+
}
|
|
262
|
+
}
|
package/src/api/help.ts
CHANGED
|
@@ -1,78 +1,42 @@
|
|
|
1
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
-
import { createHelpCatalog, findHelpDocument, resolveAppHelp, searchHelpCatalog } from "../_internal/help-catalog";
|
|
3
|
-
import { getApp, getHelp } from "../_internal/registry";
|
|
4
|
-
import type { AppRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
|
|
5
2
|
import { type AuthContext, auth, getLocale } from "../server";
|
|
3
|
+
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
6
4
|
import type { HelpDocumentPayload, HelpSearchPayload } from "../shared/help";
|
|
7
5
|
import { renderHelpMarkdown } from "../shared/markdown";
|
|
8
|
-
import {
|
|
6
|
+
import { helpApiMessages } from "./help-messages";
|
|
9
7
|
|
|
10
8
|
export type HelpRouteDependencies = {
|
|
11
|
-
|
|
12
|
-
getHelp?: (appId: string) => Promise<HelpRegistryEntry | null>;
|
|
9
|
+
help?: HelpReaderFactory;
|
|
13
10
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
14
11
|
renderMarkdown?: (markdown: string) => string;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
const resolveHelp = async (appId: string, dependencies: HelpRouteDependencies, t: HelpApiMessages) => {
|
|
18
|
-
const resolved = await resolveAppHelp(appId, { getApp: dependencies.getApp ?? getApp, getHelp: dependencies.getHelp ?? getHelp });
|
|
19
|
-
if (resolved.status === "missing") {
|
|
20
|
-
return Response.json({ code: "APP_UNAVAILABLE", message: t.unavailable({ appId }) }, { status: 404 });
|
|
21
|
-
}
|
|
22
|
-
if (resolved.status === "stale") {
|
|
23
|
-
return Response.json({ code: "HELP_STALE", message: t.stale({ appId }) }, { status: 503 });
|
|
24
|
-
}
|
|
25
|
-
return resolved;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
14
|
export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
29
|
-
const
|
|
30
|
-
const renderMarkdown = dependencies.renderMarkdown ?? renderHelpMarkdown;
|
|
31
|
-
const renderDocument = (help: HelpRegistryEntry, locale: string, documentId: string, markdown: string): string => {
|
|
32
|
-
let cached = renderedByApp.get(help.appId);
|
|
33
|
-
if (!cached || cached.manifestHash !== help.manifestHash) {
|
|
34
|
-
cached = { manifestHash: help.manifestHash, documents: new Map() };
|
|
35
|
-
renderedByApp.set(help.appId, cached);
|
|
36
|
-
}
|
|
37
|
-
const cacheKey = `${locale}:${documentId}`;
|
|
38
|
-
const html = cached.documents.get(cacheKey);
|
|
39
|
-
if (html !== undefined) return html;
|
|
40
|
-
const rendered = renderMarkdown(markdown);
|
|
41
|
-
cached.documents.set(cacheKey, rendered);
|
|
42
|
-
return rendered;
|
|
43
|
-
};
|
|
44
|
-
|
|
15
|
+
const reader = dependencies.help ?? createHelpReader;
|
|
45
16
|
return new Hono<AuthContext>()
|
|
46
17
|
.use(dependencies.authenticate ?? auth.requireRole("*"))
|
|
47
18
|
.get("/help/v1/:appId/search", async (c) => {
|
|
48
19
|
const locale = getLocale(c);
|
|
49
|
-
const { t } = helpApiMessages.resolve([locale]);
|
|
50
|
-
const resolved = await resolveHelp(c.req.param("appId"), dependencies, t);
|
|
51
|
-
if (resolved instanceof Response) return resolved;
|
|
52
20
|
const query = c.req.query("q")?.trim().slice(0, 200) ?? "";
|
|
53
|
-
const catalog = createHelpCatalog([resolved.help], locale);
|
|
54
21
|
const payload: HelpSearchPayload = {
|
|
55
|
-
locale
|
|
56
|
-
ids:
|
|
22
|
+
locale,
|
|
23
|
+
ids: (await reader(locale).search({ query, appId: c.req.param("appId"), limit: 25 })).map((document) => document.documentId),
|
|
57
24
|
};
|
|
58
25
|
return c.json(payload);
|
|
59
26
|
})
|
|
60
27
|
.get("/help/v1/:appId/documents/:documentId", async (c) => {
|
|
61
28
|
const locale = getLocale(c);
|
|
62
29
|
const { t } = helpApiMessages.resolve([locale]);
|
|
63
|
-
const
|
|
64
|
-
if (resolved instanceof Response) return resolved;
|
|
65
|
-
const document = findHelpDocument(createHelpCatalog([resolved.help], locale), resolved.app.id, c.req.param("documentId"));
|
|
30
|
+
const document = await reader(locale).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
|
|
66
31
|
if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
|
|
67
32
|
const payload: HelpDocumentPayload = {
|
|
68
33
|
locale: document.locale,
|
|
69
34
|
id: document.documentId,
|
|
70
35
|
title: document.title,
|
|
71
36
|
markdown: document.markdown,
|
|
72
|
-
html:
|
|
37
|
+
html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown),
|
|
73
38
|
};
|
|
74
39
|
return c.json(payload);
|
|
75
40
|
});
|
|
76
41
|
};
|
|
77
|
-
|
|
78
42
|
export type HelpApiType = ReturnType<typeof createHelpRoutes>;
|
package/src/api/index.ts
CHANGED
|
@@ -16,21 +16,24 @@
|
|
|
16
16
|
import { Hono } from "hono";
|
|
17
17
|
import { prettyJSON } from "hono/pretty-json";
|
|
18
18
|
import { createAiApprovalPreferenceRoutes } from "../ai/approval-routes";
|
|
19
|
+
import { createCodeCapabilityRoutes } from "../ai/code-capability-routes";
|
|
19
20
|
import { aiProjectsRoutes } from "../ai/projects-routes";
|
|
20
21
|
import { aiRoutes } from "../ai/routes";
|
|
21
22
|
import { aiSkillsRoutes } from "../ai/skills-routes";
|
|
22
23
|
import accountsEntitiesRoutes from "./accounts-entities";
|
|
23
|
-
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
24
24
|
import adminAiProjectsRoutes from "./admin-ai-projects";
|
|
25
|
+
import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
25
26
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
27
|
+
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
26
28
|
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
27
29
|
import adminIdentityRoutes from "./admin-identity";
|
|
28
30
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
29
31
|
import adminLinuxIdentityRoutes from "./admin-linux-identities";
|
|
32
|
+
import adminRailRoutes from "./admin-rail";
|
|
30
33
|
import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
|
|
34
|
+
import { createAppApprovalRoutes } from "./app-approval";
|
|
31
35
|
import { appDiscoveryRoutes } from "./apps";
|
|
32
36
|
import { createAuthRoutes } from "./auth";
|
|
33
|
-
import { createAppApprovalRoutes } from "./app-approval";
|
|
34
37
|
import { createCapabilityRoutes } from "./capabilities";
|
|
35
38
|
import { createHelpRoutes } from "./help";
|
|
36
39
|
import { createMcpRoutes } from "./mcp";
|
|
@@ -66,10 +69,12 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
66
69
|
.route("/apps", appDiscoveryRoutes)
|
|
67
70
|
.route("/announcements", announcementRoutes)
|
|
68
71
|
.route("/admin/core/ai-usage", adminAiUsageRoutes)
|
|
72
|
+
.route("/admin/core/ai-quotas", adminAiQuotaRoutes)
|
|
69
73
|
.route("/admin/core/ai-projects", adminAiProjectsRoutes)
|
|
70
74
|
.route("/admin/core/ai-skills", adminAiSkillsRoutes)
|
|
71
75
|
.route("/admin/core/announcements", adminAnnouncementRoutes)
|
|
72
76
|
.route("/admin/core/settings", adminCoreSettingsRoutes)
|
|
77
|
+
.route("/admin/core/rail", adminRailRoutes)
|
|
73
78
|
.route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
|
|
74
79
|
.route("/admin/identity", adminIdentityRoutes)
|
|
75
80
|
.route("/admin/sync", createSyncOpsProxyRoutes())
|
|
@@ -77,6 +82,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
77
82
|
.route("/ai/approval-preferences", approvalPreferenceRoutes)
|
|
78
83
|
.route("/ai/projects", aiProjectsRoutes)
|
|
79
84
|
.route("/ai/skills", aiSkillsRoutes)
|
|
85
|
+
.route("/_internal/ai/code", createCodeCapabilityRoutes())
|
|
80
86
|
.route("/ai", aiRoutes)
|
|
81
87
|
.route("/", helpRoutes)
|
|
82
88
|
.route("/", capabilityRoutes)
|
package/src/api/mcp.ts
CHANGED
|
@@ -15,17 +15,9 @@ import {
|
|
|
15
15
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type HelpCatalogDocument,
|
|
22
|
-
helpResourceUri,
|
|
23
|
-
loadHelpCatalog,
|
|
24
|
-
parseHelpResourceUri,
|
|
25
|
-
readHelpCatalog,
|
|
26
|
-
searchHelpCatalog,
|
|
27
|
-
} from "../_internal/help-catalog";
|
|
28
|
-
import { getCapability, listApps, listHelp } from "../_internal/registry";
|
|
18
|
+
import { HELP_SEARCH_MAX_LIMIT, helpResourceUri, parseHelpResourceUri, readHelpArticle } from "../_internal/help-catalog";
|
|
19
|
+
import { getCapability, listApps } from "../_internal/registry";
|
|
20
|
+
import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
|
|
29
21
|
import {
|
|
30
22
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
31
23
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
@@ -39,8 +31,10 @@ import {
|
|
|
39
31
|
cloudResourceRefAppId,
|
|
40
32
|
resolveCapabilityResourceReader,
|
|
41
33
|
} from "../contracts/capabilities";
|
|
42
|
-
import type { AppRegistryEntry, CapabilityRegistryEntry
|
|
34
|
+
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
43
35
|
import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
|
|
36
|
+
import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
|
|
37
|
+
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
44
38
|
import { logger } from "../services/logging";
|
|
45
39
|
import { get } from "../services/settings";
|
|
46
40
|
import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
|
|
@@ -61,7 +55,7 @@ const log = logger("mcp");
|
|
|
61
55
|
|
|
62
56
|
type McpRouteDependencies = CapabilityDispatchDependencies & {
|
|
63
57
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
64
|
-
|
|
58
|
+
help?: HelpReaderFactory;
|
|
65
59
|
getOperatorLocale?: () => Promise<string | undefined>;
|
|
66
60
|
getAppUrl?: () => Promise<string>;
|
|
67
61
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -375,13 +369,12 @@ const semanticResourceLinks = (body: Record<string, unknown>, origin: string): C
|
|
|
375
369
|
}));
|
|
376
370
|
};
|
|
377
371
|
|
|
378
|
-
const helpResource = (document:
|
|
372
|
+
const helpResource = (document: HelpMetadata): Resource => ({
|
|
379
373
|
uri: helpResourceUri(document.appId, document.documentId),
|
|
380
374
|
name: `${document.appName}: ${document.title}`,
|
|
381
375
|
title: document.title,
|
|
382
376
|
description: document.description ?? `Product Help from ${document.appName}.`,
|
|
383
377
|
mimeType: "text/markdown",
|
|
384
|
-
size: new TextEncoder().encode(document.markdown).byteLength,
|
|
385
378
|
annotations: { audience: ["assistant"], priority: 0.8 },
|
|
386
379
|
_meta: {
|
|
387
380
|
"cloud/appId": document.appId,
|
|
@@ -391,10 +384,10 @@ const helpResource = (document: HelpCatalogDocument): Resource => ({
|
|
|
391
384
|
},
|
|
392
385
|
});
|
|
393
386
|
|
|
394
|
-
const helpResourcePage = (catalog: readonly
|
|
387
|
+
const helpResourcePage = (catalog: readonly HelpMetadata[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
|
|
395
388
|
const resources = catalog
|
|
396
389
|
.map(helpResource)
|
|
397
|
-
.sort((left, right) => left.uri.
|
|
390
|
+
.sort((left, right) => (left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0))
|
|
398
391
|
.filter((resource) => !cursor || resource.uri > cursor)
|
|
399
392
|
.slice(0, MCP_RESOURCE_PAGE_SIZE);
|
|
400
393
|
const hasMore =
|
|
@@ -412,7 +405,7 @@ const hasOnlyKeys = (value: Record<string, unknown>, keys: readonly string[]): b
|
|
|
412
405
|
const validationToolError = (message: string): CallToolResult =>
|
|
413
406
|
boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, true);
|
|
414
407
|
|
|
415
|
-
const callHelpTool = async (name: string, argsValue: unknown,
|
|
408
|
+
const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader): Promise<CallToolResult | null> => {
|
|
416
409
|
const args = helpArguments(argsValue);
|
|
417
410
|
if (name === HELP_SEARCH_TOOL) {
|
|
418
411
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
@@ -428,7 +421,7 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
|
|
|
428
421
|
) {
|
|
429
422
|
return validationToolError("cloud__help__search requires query (1-200 characters) and an optional limit from 1 to 25");
|
|
430
423
|
}
|
|
431
|
-
const documents =
|
|
424
|
+
const documents = await reader.search({ query, appId, limit });
|
|
432
425
|
const result = boundedToolResult({ documents }, false);
|
|
433
426
|
result.content.push(
|
|
434
427
|
...documents.map((document) => ({
|
|
@@ -457,7 +450,8 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
|
|
|
457
450
|
"cloud__help__read requires exact appId and documentId values and accepts an optional query up to 200 characters",
|
|
458
451
|
);
|
|
459
452
|
}
|
|
460
|
-
const
|
|
453
|
+
const article = await reader.read({ appId, documentId });
|
|
454
|
+
const document = article ? readHelpArticle(article, query) : null;
|
|
461
455
|
if (!document) return boundedToolResult({ code: "HELP_NOT_FOUND", message: "Help document is not in the current live catalog" }, true);
|
|
462
456
|
const result = boundedToolResult({ document }, false);
|
|
463
457
|
result.content.push({
|
|
@@ -486,8 +480,8 @@ const createMcpServer = (
|
|
|
486
480
|
(helpLocale ??= (dependencies.getOperatorLocale ?? (() => get<string>("app.locale")))().then((operatorDefault) =>
|
|
487
481
|
resolveLocale(request.headers, operatorDefault),
|
|
488
482
|
));
|
|
489
|
-
const
|
|
490
|
-
|
|
483
|
+
const helpReader = async () =>
|
|
484
|
+
(dependencies.help ?? ((locale) => createHelpReader(locale, { listApps: registry })))(await resolveHelpLocale());
|
|
491
485
|
const hasScope = (scope: "read" | "write"): boolean =>
|
|
492
486
|
oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
|
|
493
487
|
const requireScope = (scope: "read" | "write"): void => {
|
|
@@ -507,10 +501,10 @@ const createMcpServer = (
|
|
|
507
501
|
throw new McpError(ErrorCode.InvalidParams, "Invalid Cloud Help resource cursor");
|
|
508
502
|
}
|
|
509
503
|
try {
|
|
510
|
-
return helpResourcePage(await
|
|
504
|
+
return helpResourcePage(await (await helpReader()).list(message.params?.cursor), message.params?.cursor);
|
|
511
505
|
} catch (error) {
|
|
512
506
|
log.error("Failed to list MCP Help resources", { error: error instanceof Error ? error.message : String(error) });
|
|
513
|
-
throw new McpError(ErrorCode.InternalError, "Help
|
|
507
|
+
throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
|
|
514
508
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
515
509
|
});
|
|
516
510
|
}
|
|
@@ -520,16 +514,15 @@ const createMcpServer = (
|
|
|
520
514
|
requireScope("read");
|
|
521
515
|
const identity = parseHelpResourceUri(message.params.uri);
|
|
522
516
|
if (!identity) throw new McpError(ErrorCode.InvalidParams, "Unknown Cloud Help resource URI");
|
|
523
|
-
let
|
|
517
|
+
let document: HelpArticle | null;
|
|
524
518
|
try {
|
|
525
|
-
|
|
519
|
+
document = await (await helpReader()).read(identity);
|
|
526
520
|
} catch (error) {
|
|
527
|
-
log.error("Failed to read
|
|
528
|
-
throw new McpError(ErrorCode.InternalError, "Help
|
|
521
|
+
log.error("Failed to read Help", { error: error instanceof Error ? error.message : String(error) });
|
|
522
|
+
throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
|
|
529
523
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
530
524
|
});
|
|
531
525
|
}
|
|
532
|
-
const document = findHelpDocument(catalog, identity.appId, identity.documentId);
|
|
533
526
|
if (!document) throw new McpError(ErrorCode.InvalidParams, "Help resource is not in the current live catalog");
|
|
534
527
|
return {
|
|
535
528
|
contents: [
|
|
@@ -567,13 +560,10 @@ const createMcpServer = (
|
|
|
567
560
|
return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
|
|
568
561
|
}
|
|
569
562
|
try {
|
|
570
|
-
return (await callHelpTool(message.params.name, message.params.arguments, await
|
|
563
|
+
return (await callHelpTool(message.params.name, message.params.arguments, await helpReader()))!;
|
|
571
564
|
} catch (error) {
|
|
572
565
|
log.error("Failed to call MCP Help tool", { error: error instanceof Error ? error.message : String(error) });
|
|
573
|
-
return boundedToolResult(
|
|
574
|
-
{ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help registry is currently unavailable" },
|
|
575
|
-
true,
|
|
576
|
-
);
|
|
566
|
+
return boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help is currently unavailable" }, true);
|
|
577
567
|
}
|
|
578
568
|
}
|
|
579
569
|
let selected: CapabilityTool | null;
|
|
@@ -615,6 +605,26 @@ const createMcpServer = (
|
|
|
615
605
|
}
|
|
616
606
|
const requiredScope = selected.kind === "actions" ? "write" : "read";
|
|
617
607
|
if (!hasScope(requiredScope)) {
|
|
608
|
+
// Denied before the dispatcher, so this surface records the same row the
|
|
609
|
+
// dispatcher would have written.
|
|
610
|
+
const at = new Date();
|
|
611
|
+
const actor = authority?.actor;
|
|
612
|
+
await recordCapabilityExecution({
|
|
613
|
+
requestId: normalizeInvocationRequestId(request.headers.get("x-request-id")) ?? crypto.randomUUID(),
|
|
614
|
+
origin: "mcp",
|
|
615
|
+
appId: selected.app.appId,
|
|
616
|
+
capability: `${selected.app.appId}.${selected.operation.localId}`,
|
|
617
|
+
kind: selected.kind === "actions" ? "action" : "query",
|
|
618
|
+
destructive: "destructive" in selected.operation ? selected.operation.destructive : false,
|
|
619
|
+
actorKind: actor?.kind ?? null,
|
|
620
|
+
actorId: actor?.kind === "user" ? actor.user.id : (actor?.serviceAccount.id ?? null),
|
|
621
|
+
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? null),
|
|
622
|
+
status: "denied",
|
|
623
|
+
errorCode: "FORBIDDEN",
|
|
624
|
+
inputMeta: capabilityValueMeta(message.params.arguments ?? {}),
|
|
625
|
+
startedAt: at,
|
|
626
|
+
completedAt: at,
|
|
627
|
+
}).catch((error) => log.error("MCP denial could not be recorded", { error: error instanceof Error ? error.message : String(error) }));
|
|
618
628
|
return boundedToolResult({ code: "FORBIDDEN", message: `OAuth scope ${requiredScope} is required` }, true);
|
|
619
629
|
}
|
|
620
630
|
|
|
@@ -626,6 +636,7 @@ const createMcpServer = (
|
|
|
626
636
|
const response = await dispatchCapability({
|
|
627
637
|
request: new Request(request.url, { method: "POST", headers, signal: request.signal }),
|
|
628
638
|
kind: selected.kind,
|
|
639
|
+
origin: "mcp",
|
|
629
640
|
appId: selected.app.appId,
|
|
630
641
|
capabilityId: selected.operation.localId,
|
|
631
642
|
input: args,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { WEB_VITALS_MAX_BYTES, type WebVitalsReport, WebVitalsReportSchema } from "../contracts/web-vitals";
|
|
5
|
+
import { type AuthContext, requiresAuth, v } from "../server";
|
|
6
|
+
import { logger } from "../services/logging";
|
|
7
|
+
import { get } from "../services/settings";
|
|
8
|
+
|
|
9
|
+
const log = logger("web-vitals");
|
|
10
|
+
|
|
11
|
+
/** Mounted below /me's authentication, user guard and rate limit. Client reports are diagnostics, not audit facts. */
|
|
12
|
+
export const createMeWebVitalsRoutes = (
|
|
13
|
+
record: (report: WebVitalsReport) => void = (report) => log.info(report.name, report),
|
|
14
|
+
isEnabled: () => Promise<boolean> = async () => (await get("observability.web_vitals.enabled")) === true,
|
|
15
|
+
) =>
|
|
16
|
+
new Hono<AuthContext>().post(
|
|
17
|
+
"/web-vitals",
|
|
18
|
+
describeRoute({
|
|
19
|
+
tags: ["Me"],
|
|
20
|
+
summary: "Report browser page performance",
|
|
21
|
+
...requiresAuth,
|
|
22
|
+
responses: {
|
|
23
|
+
204: { description: "Report processed; discarded when collection is disabled" },
|
|
24
|
+
400: { description: "Invalid report" },
|
|
25
|
+
413: { description: "Report too large" },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
bodyLimit({ maxSize: WEB_VITALS_MAX_BYTES }),
|
|
29
|
+
v("json", WebVitalsReportSchema),
|
|
30
|
+
async (c) => {
|
|
31
|
+
if (!c.get("user")) return c.json({ message: "Authentication required" }, 401);
|
|
32
|
+
if (await isEnabled()) record(c.req.valid("json"));
|
|
33
|
+
return c.body(null, 204);
|
|
34
|
+
},
|
|
35
|
+
);
|
package/src/api/me.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import { MandatePolicyV1Schema } from "../services/mandates";
|
|
51
51
|
|
|
52
52
|
import { createMeRailRoutes } from "./me-rail";
|
|
53
|
+
import { createMeWebVitalsRoutes } from "./me-web-vitals";
|
|
53
54
|
|
|
54
55
|
const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
|
|
55
56
|
userId: user.id,
|
|
@@ -187,6 +188,7 @@ const app = new Hono<AuthContext>()
|
|
|
187
188
|
.use(auth.requireUser())
|
|
188
189
|
.route("/", mandateRoutes)
|
|
189
190
|
.route("/", createMeRailRoutes())
|
|
191
|
+
.route("/", createMeWebVitalsRoutes())
|
|
190
192
|
|
|
191
193
|
.get(
|
|
192
194
|
"/activity",
|