@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
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
1
2
|
/**
|
|
2
3
|
* Core platform API surface — owned by cloud-lib, mounted by the core-app.
|
|
3
4
|
*
|
|
@@ -17,12 +18,14 @@ import { Hono } from "hono";
|
|
|
17
18
|
import { prettyJSON } from "hono/pretty-json";
|
|
18
19
|
import { createAiApprovalPreferenceRoutes } from "../ai/approval-routes";
|
|
19
20
|
import { aiProjectsRoutes } from "../ai/projects-routes";
|
|
21
|
+
import { createCodeCapabilityRoutes } from "../ai/code-capability-routes";
|
|
20
22
|
import { aiRoutes } from "../ai/routes";
|
|
21
23
|
import { aiSkillsRoutes } from "../ai/skills-routes";
|
|
22
24
|
import accountsEntitiesRoutes from "./accounts-entities";
|
|
23
25
|
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
24
26
|
import adminAiProjectsRoutes from "./admin-ai-projects";
|
|
25
27
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
28
|
+
import adminRailRoutes from "./admin-rail";
|
|
26
29
|
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
27
30
|
import adminIdentityRoutes from "./admin-identity";
|
|
28
31
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHelpReader, type HelpReader, type HelpReaderFactory, type HelpMetadata, type HelpArticle } from "../services/help";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -16,16 +17,13 @@ import { Hono, type MiddlewareHandler } from "hono";
|
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
18
19
|
import {
|
|
19
|
-
findHelpDocument,
|
|
20
20
|
HELP_SEARCH_MAX_LIMIT,
|
|
21
|
-
type HelpCatalogDocument,
|
|
22
21
|
helpResourceUri,
|
|
23
|
-
loadHelpCatalog,
|
|
24
22
|
parseHelpResourceUri,
|
|
25
|
-
|
|
26
|
-
searchHelpCatalog,
|
|
23
|
+
readHelpArticle,
|
|
27
24
|
} from "../_internal/help-catalog";
|
|
28
|
-
import { getCapability, listApps
|
|
25
|
+
import { getCapability, listApps } from "../_internal/registry";
|
|
26
|
+
import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
|
|
29
27
|
import {
|
|
30
28
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
31
29
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
@@ -39,8 +37,9 @@ import {
|
|
|
39
37
|
cloudResourceRefAppId,
|
|
40
38
|
resolveCapabilityResourceReader,
|
|
41
39
|
} from "../contracts/capabilities";
|
|
42
|
-
import type { AppRegistryEntry, CapabilityRegistryEntry
|
|
40
|
+
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
43
41
|
import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
|
|
42
|
+
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
44
43
|
import { logger } from "../services/logging";
|
|
45
44
|
import { get } from "../services/settings";
|
|
46
45
|
import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
|
|
@@ -61,7 +60,7 @@ const log = logger("mcp");
|
|
|
61
60
|
|
|
62
61
|
type McpRouteDependencies = CapabilityDispatchDependencies & {
|
|
63
62
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
64
|
-
|
|
63
|
+
help?: HelpReaderFactory;
|
|
65
64
|
getOperatorLocale?: () => Promise<string | undefined>;
|
|
66
65
|
getAppUrl?: () => Promise<string>;
|
|
67
66
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -375,13 +374,12 @@ const semanticResourceLinks = (body: Record<string, unknown>, origin: string): C
|
|
|
375
374
|
}));
|
|
376
375
|
};
|
|
377
376
|
|
|
378
|
-
const helpResource = (document:
|
|
377
|
+
const helpResource = (document: HelpMetadata): Resource => ({
|
|
379
378
|
uri: helpResourceUri(document.appId, document.documentId),
|
|
380
379
|
name: `${document.appName}: ${document.title}`,
|
|
381
380
|
title: document.title,
|
|
382
381
|
description: document.description ?? `Product Help from ${document.appName}.`,
|
|
383
382
|
mimeType: "text/markdown",
|
|
384
|
-
size: new TextEncoder().encode(document.markdown).byteLength,
|
|
385
383
|
annotations: { audience: ["assistant"], priority: 0.8 },
|
|
386
384
|
_meta: {
|
|
387
385
|
"cloud/appId": document.appId,
|
|
@@ -391,10 +389,10 @@ const helpResource = (document: HelpCatalogDocument): Resource => ({
|
|
|
391
389
|
},
|
|
392
390
|
});
|
|
393
391
|
|
|
394
|
-
const helpResourcePage = (catalog: readonly
|
|
392
|
+
const helpResourcePage = (catalog: readonly HelpMetadata[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
|
|
395
393
|
const resources = catalog
|
|
396
394
|
.map(helpResource)
|
|
397
|
-
.sort((left, right) => left.uri.
|
|
395
|
+
.sort((left, right) => (left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0))
|
|
398
396
|
.filter((resource) => !cursor || resource.uri > cursor)
|
|
399
397
|
.slice(0, MCP_RESOURCE_PAGE_SIZE);
|
|
400
398
|
const hasMore =
|
|
@@ -412,7 +410,7 @@ const hasOnlyKeys = (value: Record<string, unknown>, keys: readonly string[]): b
|
|
|
412
410
|
const validationToolError = (message: string): CallToolResult =>
|
|
413
411
|
boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, true);
|
|
414
412
|
|
|
415
|
-
const callHelpTool = async (name: string, argsValue: unknown,
|
|
413
|
+
const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader): Promise<CallToolResult | null> => {
|
|
416
414
|
const args = helpArguments(argsValue);
|
|
417
415
|
if (name === HELP_SEARCH_TOOL) {
|
|
418
416
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
@@ -428,7 +426,7 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
|
|
|
428
426
|
) {
|
|
429
427
|
return validationToolError("cloud__help__search requires query (1-200 characters) and an optional limit from 1 to 25");
|
|
430
428
|
}
|
|
431
|
-
const documents =
|
|
429
|
+
const documents = await reader.search({ query, appId, limit });
|
|
432
430
|
const result = boundedToolResult({ documents }, false);
|
|
433
431
|
result.content.push(
|
|
434
432
|
...documents.map((document) => ({
|
|
@@ -457,7 +455,8 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
|
|
|
457
455
|
"cloud__help__read requires exact appId and documentId values and accepts an optional query up to 200 characters",
|
|
458
456
|
);
|
|
459
457
|
}
|
|
460
|
-
const
|
|
458
|
+
const article = await reader.read({ appId, documentId });
|
|
459
|
+
const document = article ? readHelpArticle(article, query) : null;
|
|
461
460
|
if (!document) return boundedToolResult({ code: "HELP_NOT_FOUND", message: "Help document is not in the current live catalog" }, true);
|
|
462
461
|
const result = boundedToolResult({ document }, false);
|
|
463
462
|
result.content.push({
|
|
@@ -486,8 +485,7 @@ const createMcpServer = (
|
|
|
486
485
|
(helpLocale ??= (dependencies.getOperatorLocale ?? (() => get<string>("app.locale")))().then((operatorDefault) =>
|
|
487
486
|
resolveLocale(request.headers, operatorDefault),
|
|
488
487
|
));
|
|
489
|
-
const
|
|
490
|
-
loadHelpCatalog({ listApps: registry, listHelp: dependencies.listHelp ?? listHelp }, await resolveHelpLocale());
|
|
488
|
+
const helpReader = async () => (dependencies.help ?? ((locale) => createHelpReader(locale, { listApps: registry })))(await resolveHelpLocale());
|
|
491
489
|
const hasScope = (scope: "read" | "write"): boolean =>
|
|
492
490
|
oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
|
|
493
491
|
const requireScope = (scope: "read" | "write"): void => {
|
|
@@ -507,10 +505,10 @@ const createMcpServer = (
|
|
|
507
505
|
throw new McpError(ErrorCode.InvalidParams, "Invalid Cloud Help resource cursor");
|
|
508
506
|
}
|
|
509
507
|
try {
|
|
510
|
-
return helpResourcePage(await
|
|
508
|
+
return helpResourcePage(await (await helpReader()).list(message.params?.cursor), message.params?.cursor);
|
|
511
509
|
} catch (error) {
|
|
512
510
|
log.error("Failed to list MCP Help resources", { error: error instanceof Error ? error.message : String(error) });
|
|
513
|
-
throw new McpError(ErrorCode.InternalError, "Help
|
|
511
|
+
throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
|
|
514
512
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
515
513
|
});
|
|
516
514
|
}
|
|
@@ -520,16 +518,15 @@ const createMcpServer = (
|
|
|
520
518
|
requireScope("read");
|
|
521
519
|
const identity = parseHelpResourceUri(message.params.uri);
|
|
522
520
|
if (!identity) throw new McpError(ErrorCode.InvalidParams, "Unknown Cloud Help resource URI");
|
|
523
|
-
let
|
|
521
|
+
let document: HelpArticle | null;
|
|
524
522
|
try {
|
|
525
|
-
|
|
523
|
+
document = await (await helpReader()).read(identity);
|
|
526
524
|
} catch (error) {
|
|
527
|
-
log.error("Failed to read
|
|
528
|
-
throw new McpError(ErrorCode.InternalError, "Help
|
|
525
|
+
log.error("Failed to read Help", { error: error instanceof Error ? error.message : String(error) });
|
|
526
|
+
throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
|
|
529
527
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
|
|
530
528
|
});
|
|
531
529
|
}
|
|
532
|
-
const document = findHelpDocument(catalog, identity.appId, identity.documentId);
|
|
533
530
|
if (!document) throw new McpError(ErrorCode.InvalidParams, "Help resource is not in the current live catalog");
|
|
534
531
|
return {
|
|
535
532
|
contents: [
|
|
@@ -567,11 +564,11 @@ const createMcpServer = (
|
|
|
567
564
|
return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
|
|
568
565
|
}
|
|
569
566
|
try {
|
|
570
|
-
return (await callHelpTool(message.params.name, message.params.arguments, await
|
|
567
|
+
return (await callHelpTool(message.params.name, message.params.arguments, await helpReader()))!;
|
|
571
568
|
} catch (error) {
|
|
572
569
|
log.error("Failed to call MCP Help tool", { error: error instanceof Error ? error.message : String(error) });
|
|
573
570
|
return boundedToolResult(
|
|
574
|
-
{ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help
|
|
571
|
+
{ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help is currently unavailable" },
|
|
575
572
|
true,
|
|
576
573
|
);
|
|
577
574
|
}
|
|
@@ -615,6 +612,26 @@ const createMcpServer = (
|
|
|
615
612
|
}
|
|
616
613
|
const requiredScope = selected.kind === "actions" ? "write" : "read";
|
|
617
614
|
if (!hasScope(requiredScope)) {
|
|
615
|
+
// Denied before the dispatcher, so this surface records the same row the
|
|
616
|
+
// dispatcher would have written.
|
|
617
|
+
const at = new Date();
|
|
618
|
+
const actor = authority?.actor;
|
|
619
|
+
await recordCapabilityExecution({
|
|
620
|
+
requestId: normalizeInvocationRequestId(request.headers.get("x-request-id")) ?? crypto.randomUUID(),
|
|
621
|
+
origin: "mcp",
|
|
622
|
+
appId: selected.app.appId,
|
|
623
|
+
capability: `${selected.app.appId}.${selected.operation.localId}`,
|
|
624
|
+
kind: selected.kind === "actions" ? "action" : "query",
|
|
625
|
+
destructive: "destructive" in selected.operation ? selected.operation.destructive : false,
|
|
626
|
+
actorKind: actor?.kind ?? null,
|
|
627
|
+
actorId: actor?.kind === "user" ? actor.user.id : (actor?.serviceAccount.id ?? null),
|
|
628
|
+
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? null),
|
|
629
|
+
status: "denied",
|
|
630
|
+
errorCode: "FORBIDDEN",
|
|
631
|
+
inputMeta: capabilityValueMeta(message.params.arguments ?? {}),
|
|
632
|
+
startedAt: at,
|
|
633
|
+
completedAt: at,
|
|
634
|
+
}).catch((error) => log.error("MCP denial could not be recorded", { error: error instanceof Error ? error.message : String(error) }));
|
|
618
635
|
return boundedToolResult({ code: "FORBIDDEN", message: `OAuth scope ${requiredScope} is required` }, true);
|
|
619
636
|
}
|
|
620
637
|
|
|
@@ -626,6 +643,7 @@ const createMcpServer = (
|
|
|
626
643
|
const response = await dispatchCapability({
|
|
627
644
|
request: new Request(request.url, { method: "POST", headers, signal: request.signal }),
|
|
628
645
|
kind: selected.kind,
|
|
646
|
+
origin: "mcp",
|
|
629
647
|
appId: selected.app.appId,
|
|
630
648
|
capabilityId: selected.operation.localId,
|
|
631
649
|
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",
|