@k2b/cloud 0.45.0 → 0.47.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/package.json +11 -4
- package/scripts/build.ts +5 -1
- package/scripts/preload.ts +5 -1
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +40 -0
- package/src/_internal/heartbeat.ts +19 -0
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +9 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +6 -0
- package/src/_internal/workload-credential.ts +153 -0
- package/src/_internal/workload-protocol.ts +36 -0
- package/src/ai/application-tasks.ts +5 -2
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chart-block.ts +36 -711
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tool-contracts.ts +21 -259
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/executor.ts +13 -1
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/protocol.ts +3 -1
- package/src/ai/routes.ts +2 -0
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/admin-app-keys.ts +66 -0
- package/src/api/admin-core-settings.ts +2 -0
- package/src/api/admin-identity.ts +27 -1
- package/src/api/admin-settings-env.ts +120 -0
- package/src/api/capabilities.ts +99 -15
- package/src/api/index.ts +3 -0
- package/src/api/mcp.ts +4 -2
- package/src/api/settings-ops.ts +35 -0
- package/src/app-key.ts +125 -0
- package/src/browser/CloudResourceSearch.tsx +48 -41
- package/src/browser/notifications.ts +25 -8
- package/src/capabilities/discussion-providers.ts +100 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +69 -0
- package/src/capabilities/response.ts +4 -1
- package/src/capabilities/server-invoke.ts +64 -36
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/capabilities/types.ts +1 -1
- package/src/cli/admin/app-credentials.ts +32 -7
- package/src/cli/admin/app-keys.ts +226 -0
- package/src/cli/admin/index.ts +7 -1
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +27 -21
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/index.ts +9 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +37 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/client-ip.ts +1 -1
- package/src/services/accounts/app.ts +28 -6
- package/src/services/accounts/entities.ts +11 -4
- package/src/services/accounts/groups.ts +10 -4
- package/src/services/accounts/local-groups.ts +87 -38
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/app-keys.ts +298 -0
- package/src/services/gateway.ts +6 -0
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/index.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/identity/resource-readers.ts +80 -0
- package/src/services/identity/runtime-config.ts +14 -6
- package/src/services/identity/workload-runtime.ts +253 -0
- package/src/services/index.ts +4 -1
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/service-account-credentials.ts +63 -21
- package/src/services/settings/api.ts +3 -3
- package/src/services/settings/binding.ts +45 -0
- package/src/services/settings/core-settings.ts +99 -1
- package/src/services/settings/defaults.ts +16 -2
- package/src/services/settings/env-status.ts +69 -0
- package/src/services/settings/index.ts +69 -26
- package/src/services/settings/store.ts +73 -8
- package/src/services/settings/validation.ts +40 -0
- package/src/shared/index.ts +11 -10
- package/src/shared/markdown/extensions/tables.ts +10 -1
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/start.ts +36 -0
- package/src/styles/global.css +2 -0
- package/src/workflows/ai/index.ts +1 -0
- package/src/workflows/ai/runtime.ts +16 -8
- package/src/workflows/ai/settings.ts +5 -0
- package/src/shared/markdown/formula.ts +0 -1283
package/src/ai/tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { z } from "zod";
|
|
|
4
4
|
import type { CapabilityActionCallReview } from "../contracts/capabilities";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
6
|
import { aiToolNeedsApproval } from "./approvals";
|
|
7
|
+
import { resolveAiToolResultMaxChars } from "./tool-result-budget";
|
|
7
8
|
import type {
|
|
8
9
|
AiApprovalTarget,
|
|
9
10
|
AiDataBoundary,
|
|
@@ -64,6 +65,8 @@ export const defineAiTool = <TInput extends z.ZodType, TOutput extends z.ZodType
|
|
|
64
65
|
timeZone?: string;
|
|
65
66
|
/** Localized short status; never include payloads or secrets. */
|
|
66
67
|
reportProgress?: (message: string) => Promise<void>;
|
|
68
|
+
/** A validated result snapshot while a server tool is still running. */
|
|
69
|
+
reportResult?: (result: z.infer<TOutput>) => Promise<void>;
|
|
67
70
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
68
71
|
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
69
72
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
@@ -117,6 +120,8 @@ export type AiToolPreparationContext = {
|
|
|
117
120
|
locale?: string;
|
|
118
121
|
timeZone?: string;
|
|
119
122
|
reportToolProgress?: (callId: string, message: string) => Promise<void>;
|
|
123
|
+
reportToolResult?: (callId: string, result: unknown) => Promise<void>;
|
|
124
|
+
maxToolResultChars?: number;
|
|
120
125
|
/** Uses the approved snapshot immediately before the tool executes its effect. */
|
|
121
126
|
consumeApproval?: (approvalCallId: string, input: unknown) => Promise<AiApprovalUse>;
|
|
122
127
|
/** Links a relayed decision to the record the effect owner will consume. */
|
|
@@ -241,6 +246,15 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
241
246
|
locale: input.locale,
|
|
242
247
|
timeZone: input.timeZone,
|
|
243
248
|
reportProgress: callId && report ? (message) => report(callId, message) : undefined,
|
|
249
|
+
reportResult:
|
|
250
|
+
callId && input.reportToolResult
|
|
251
|
+
? async (result) => {
|
|
252
|
+
const validated = tool.def.outputSchema.parse(result);
|
|
253
|
+
const maximum = input.maxToolResultChars ?? resolveAiToolResultMaxChars({ configuredMaxChars: Number.MAX_SAFE_INTEGER });
|
|
254
|
+
if (JSON.stringify(validated).length > maximum) throw new Error("AI_TOOL_INTERIM_RESULT_TOO_LARGE");
|
|
255
|
+
await input.reportToolResult!(callId, validated);
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
244
258
|
reportWebsiteReceipts:
|
|
245
259
|
callId && input.reportWebsiteReceipts ? (receipts) => input.reportWebsiteReceipts!(callId, receipts) : undefined,
|
|
246
260
|
});
|
package/src/ai/types.ts
CHANGED
|
@@ -313,6 +313,7 @@ export type AiStoredMessage = {
|
|
|
313
313
|
toolPresentations?: Record<string, AiToolPresentation>;
|
|
314
314
|
toolReviews?: Record<string, AiStoredActionReview>;
|
|
315
315
|
toolChecks?: Record<string, AiActionCheck>;
|
|
316
|
+
toolInterimResults?: Record<string, unknown>;
|
|
316
317
|
/** How the person decided an approval; `expired` when the turn ended while it still waited. */
|
|
317
318
|
toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
|
|
318
319
|
/** Requests a website approval let through, by tool call and then by `AiWebsiteReceipt.index`. */
|
|
@@ -1159,6 +1160,8 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
|
|
|
1159
1160
|
timeZone?: string;
|
|
1160
1161
|
/** Localized short status; never include payloads or secrets. */
|
|
1161
1162
|
reportProgress?: (message: string) => Promise<void>;
|
|
1163
|
+
/** A validated result snapshot while a server tool is still running. */
|
|
1164
|
+
reportResult?: (result: z.infer<TOutput>) => Promise<void>;
|
|
1162
1165
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
1163
1166
|
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
1164
1167
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { validateAppKeyAppId } from "../app-key";
|
|
6
|
+
import { type AuthContext, auth, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
8
|
+
import { appKeys } from "../services/app-keys";
|
|
9
|
+
|
|
10
|
+
const appId = z.string().refine((value) => {
|
|
11
|
+
try {
|
|
12
|
+
validateAppKeyAppId(value);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}, "Invalid or reserved app ID");
|
|
18
|
+
const params = z.object({ appId });
|
|
19
|
+
const pagination = z.object({
|
|
20
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
21
|
+
perPage: z.coerce.number().int().min(1).max(500).default(100),
|
|
22
|
+
});
|
|
23
|
+
const input = z
|
|
24
|
+
.object({ publicKey: z.string().length(43), name: z.string().trim().min(1).max(120).optional(), expiresAt: z.iso.datetime().optional() })
|
|
25
|
+
.strict();
|
|
26
|
+
|
|
27
|
+
export const createAdminAppKeyRoutes = (
|
|
28
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
29
|
+
service: typeof appKeys = appKeys,
|
|
30
|
+
) =>
|
|
31
|
+
new Hono<AuthContext>()
|
|
32
|
+
.use("*", noTransform())
|
|
33
|
+
.use(authenticate)
|
|
34
|
+
.use("*", async (c, next) => {
|
|
35
|
+
c.header("Cache-Control", "no-store");
|
|
36
|
+
if (["POST", "DELETE"].includes(c.req.method)) return auth.requireOAuthScope("write", "admin")(c, next);
|
|
37
|
+
return next();
|
|
38
|
+
})
|
|
39
|
+
.get(
|
|
40
|
+
"/keys",
|
|
41
|
+
describeRoute({ tags: ["App keys"], summary: "List apps with enrolled keys and the installation ID" }),
|
|
42
|
+
v("query", pagination),
|
|
43
|
+
(c) => respond(c, () => service.listApps({ actor: c.get("user"), ...c.req.valid("query") })),
|
|
44
|
+
)
|
|
45
|
+
.get(
|
|
46
|
+
"/:appId/keys",
|
|
47
|
+
describeRoute({ tags: ["App keys"], summary: "List app public keys and lifecycle metadata" }),
|
|
48
|
+
v("param", params),
|
|
49
|
+
v("query", pagination),
|
|
50
|
+
(c) => respond(c, () => service.list({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("query") })),
|
|
51
|
+
)
|
|
52
|
+
.post(
|
|
53
|
+
"/:appId/keys",
|
|
54
|
+
describeRoute({ tags: ["App keys"], summary: "Idempotently enroll an Ed25519 public key; at most two active keys per app" }),
|
|
55
|
+
v("param", params),
|
|
56
|
+
v("json", input),
|
|
57
|
+
(c) => respond(c, () => service.register({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("json") })),
|
|
58
|
+
)
|
|
59
|
+
.delete(
|
|
60
|
+
"/:appId/keys/:keyId",
|
|
61
|
+
describeRoute({ tags: ["App keys"], summary: "Idempotently revoke an app key" }),
|
|
62
|
+
v("param", params.extend({ keyId: z.uuid() })),
|
|
63
|
+
(c) => respond(c, () => service.revoke({ actor: c.get("user"), ...c.req.valid("param") })),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export default createAdminAppKeyRoutes();
|
|
@@ -27,6 +27,7 @@ import { SETTINGS_MAP, validateSettingValue } from "../services/settings/default
|
|
|
27
27
|
import { assertGeoBudgetPair, GeoBudgetSettingError } from "../services/settings/geo-budget";
|
|
28
28
|
import { invalidateSettingsCacheForAdmin } from "../services/settings/store";
|
|
29
29
|
import { assertSettingVersions } from "../services/settings/versions";
|
|
30
|
+
import { adminSettingsEnvRoutes } from "./admin-settings-env";
|
|
30
31
|
|
|
31
32
|
const BulkUpdatesSchema = z
|
|
32
33
|
.object({
|
|
@@ -72,6 +73,7 @@ const invalidateCommittedSettings = async (keys: readonly string[]): Promise<voi
|
|
|
72
73
|
const liveSettingKeys = async () => (await listApps()).flatMap((app) => [...(app.settingKeys ?? [])]);
|
|
73
74
|
|
|
74
75
|
const app = new Hono<AuthContext>()
|
|
76
|
+
.route("/env", adminSettingsEnvRoutes)
|
|
75
77
|
.get("/web-vitals", auth.requireRole("admin"), async (c) =>
|
|
76
78
|
c.json({ enabled: (await settings.get("observability.web_vitals.enabled")) === true }),
|
|
77
79
|
)
|
|
@@ -86,7 +86,10 @@ type IdentityAdminDependencies = {
|
|
|
86
86
|
status: typeof getIdentitySigningKeyStatus;
|
|
87
87
|
rewrap: typeof rewrapIdentitySigningKeys;
|
|
88
88
|
revoke: typeof revokeIdentitySigningKey;
|
|
89
|
-
serviceAccounts: Pick<
|
|
89
|
+
serviceAccounts: Pick<
|
|
90
|
+
typeof serviceAccounts,
|
|
91
|
+
"getByResource" | "getOrCreateResourceBound" | "createStandalone" | "listStandalone" | "get" | "setStatus"
|
|
92
|
+
>;
|
|
90
93
|
credentials: Pick<
|
|
91
94
|
typeof serviceAccountCredentials,
|
|
92
95
|
"createResourceApiToken" | "createStandaloneApiToken" | "getOverview" | "listOverview" | "revoke"
|
|
@@ -151,6 +154,29 @@ export const createAdminIdentityRoutes = (
|
|
|
151
154
|
}),
|
|
152
155
|
),
|
|
153
156
|
)
|
|
157
|
+
.patch(
|
|
158
|
+
"/workloads/:appId",
|
|
159
|
+
describeRoute({ tags: ["App credentials"], summary: "Enable or disable app workload issuance and calls across all instances" }),
|
|
160
|
+
v("param", WorkloadAppParamsSchema),
|
|
161
|
+
v("json", ServiceAccountStatusInputSchema),
|
|
162
|
+
async (c) =>
|
|
163
|
+
respond(c, async () => {
|
|
164
|
+
const { appId } = c.req.valid("param");
|
|
165
|
+
const { status } = c.req.valid("json");
|
|
166
|
+
const account = await service.serviceAccounts.getByResource({ appId, resourceType: "cloud.app", resourceId: appId });
|
|
167
|
+
if (!account) return fail(err.notFound("Workload service account"));
|
|
168
|
+
const updated = await service.serviceAccounts.setStatus({ id: account.id, status });
|
|
169
|
+
if (!updated.ok) return updated;
|
|
170
|
+
await service.audit.record({
|
|
171
|
+
action: status === "disabled" ? "service_account.disable" : "service_account.enable",
|
|
172
|
+
outcome: "allowed",
|
|
173
|
+
actor: auditActor(c.get("user")),
|
|
174
|
+
target: { type: "service_account", id: account.id, label: account.name },
|
|
175
|
+
metadata: { appId, serviceAccountId: account.id, status },
|
|
176
|
+
});
|
|
177
|
+
return ok({ appId, status });
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
154
180
|
.post(
|
|
155
181
|
"/workloads/:appId/credentials",
|
|
156
182
|
describeRoute({ tags: ["App credentials"], summary: "Create an app-bound credential; return its token once" }),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
|
+
import { getApp, listApps } from "../_internal/registry";
|
|
6
|
+
import { EnvSettingStatusSchema, type EnvSettingsProcessSchema, EnvSettingsStatusSchema } from "../contracts/settings-env";
|
|
7
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin, setRequestTimeout, v } from "../server";
|
|
8
|
+
import { listGatewayRouteSnapshots } from "../services/gateway";
|
|
9
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
10
|
+
import { issueInvocationToken } from "../services/identity/issue-invocation";
|
|
11
|
+
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
12
|
+
import { readEnvSettingsStatus } from "../services/settings/env-status";
|
|
13
|
+
import { settingsInvocationOperation } from "./settings-ops";
|
|
14
|
+
|
|
15
|
+
const reply = z.object({ settings: z.array(EnvSettingStatusSchema) });
|
|
16
|
+
export const adminSettingsEnvRoutes = new Hono<AuthContext>()
|
|
17
|
+
.use("*", auth.requireRole("admin"))
|
|
18
|
+
.use("*", auth.requireOAuthScope("admin"))
|
|
19
|
+
.get(
|
|
20
|
+
"/status",
|
|
21
|
+
describeRoute({
|
|
22
|
+
tags: ["Administration"],
|
|
23
|
+
summary: "Check environment-backed settings without returning values",
|
|
24
|
+
...requiresAdmin,
|
|
25
|
+
responses: { 200: jsonResponse(EnvSettingsStatusSchema, "Migration status") },
|
|
26
|
+
}),
|
|
27
|
+
async (c) => {
|
|
28
|
+
setRequestTimeout(c, 240);
|
|
29
|
+
const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(230_000)]);
|
|
30
|
+
try {
|
|
31
|
+
const processes: z.infer<typeof EnvSettingsProcessSchema>[] = [
|
|
32
|
+
{ appId: "core", status: "ok", settings: await readEnvSettingsStatus() },
|
|
33
|
+
];
|
|
34
|
+
for (const app of await listApps()) {
|
|
35
|
+
if (app.id === "core") continue;
|
|
36
|
+
try {
|
|
37
|
+
const response = await requestSettings(app, "GET", "env", auth.getAuthority(c), undefined, signal);
|
|
38
|
+
const data = await readBoundedJson(response, 4 * 1024 * 1024);
|
|
39
|
+
if (!response.ok || !data.ok) throw new Error("Unavailable");
|
|
40
|
+
processes.push({ appId: app.id, status: "ok", settings: reply.parse(data.data).settings });
|
|
41
|
+
} catch {
|
|
42
|
+
processes.push({ appId: app.id, status: "unavailable", settings: [] });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const gateways = await listGatewayRouteSnapshots();
|
|
46
|
+
if (!gateways.length) processes.push({ appId: "gateway", status: "unavailable", settings: [] });
|
|
47
|
+
for (const gateway of gateways) {
|
|
48
|
+
const parsed = z.array(EnvSettingStatusSchema).safeParse(gateway.envSettings);
|
|
49
|
+
processes.push({
|
|
50
|
+
appId: "gateway",
|
|
51
|
+
instanceId: gateway.instanceId,
|
|
52
|
+
sampledAt: gateway.updatedAt,
|
|
53
|
+
status: parsed.success ? "ok" : "unavailable",
|
|
54
|
+
settings: parsed.success ? parsed.data : [],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return c.json(
|
|
58
|
+
{ needsAttention: processes.some((p) => p.status !== "ok" || p.settings.some((s) => s.needsAttention)), processes },
|
|
59
|
+
200,
|
|
60
|
+
{ "cache-control": "no-store" },
|
|
61
|
+
);
|
|
62
|
+
} finally {
|
|
63
|
+
setRequestTimeout(c, 10);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
.put("/:appId/:key", v("json", z.object({ value: z.unknown(), replaceUnreadable: z.boolean().optional() })), async (c) => {
|
|
68
|
+
const app = await getApp(c.req.param("appId") ?? "");
|
|
69
|
+
if (!app) return c.json({ message: "App is not registered; start it before changing its settings" }, 404);
|
|
70
|
+
try {
|
|
71
|
+
const response = await requestSettings(
|
|
72
|
+
app,
|
|
73
|
+
"PUT",
|
|
74
|
+
c.req.param("key") ?? "",
|
|
75
|
+
auth.getAuthority(c),
|
|
76
|
+
c.req.valid("json"),
|
|
77
|
+
c.req.raw.signal,
|
|
78
|
+
);
|
|
79
|
+
const body = await readBoundedJson(response, 4 * 1024 * 1024);
|
|
80
|
+
if (!body.ok) throw new Error("Unavailable");
|
|
81
|
+
return Response.json(body.data, { status: response.status, headers: { "cache-control": "no-store" } });
|
|
82
|
+
} catch {
|
|
83
|
+
return c.json({ message: "Application settings unavailable" }, 502);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const requestSettings = async (
|
|
88
|
+
app: { id: string; baseUrl: string },
|
|
89
|
+
method: "GET" | "PUT",
|
|
90
|
+
key: string,
|
|
91
|
+
authority: Parameters<typeof invocationAuthorityFromRequest>[0],
|
|
92
|
+
body: unknown,
|
|
93
|
+
parentSignal: AbortSignal,
|
|
94
|
+
) => {
|
|
95
|
+
const signal = AbortSignal.any([parentSignal, AbortSignal.timeout(5_000)]);
|
|
96
|
+
const signed = await withActiveIdentitySigner(
|
|
97
|
+
"invocation",
|
|
98
|
+
(signer, db) =>
|
|
99
|
+
issueInvocationToken(
|
|
100
|
+
{
|
|
101
|
+
targetAppId: app.id,
|
|
102
|
+
callingAppId: "core",
|
|
103
|
+
operation: settingsInvocationOperation(method, key),
|
|
104
|
+
schemaHash: null,
|
|
105
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
106
|
+
signer,
|
|
107
|
+
issuer: signer.issuer,
|
|
108
|
+
},
|
|
109
|
+
{ db, groupsAdmin: signer.groupsAdmin, signal },
|
|
110
|
+
),
|
|
111
|
+
{ signal, timeoutMs: 5_000 },
|
|
112
|
+
);
|
|
113
|
+
return fetch(new URL(`/_internal/settings/${encodeURIComponent(key)}`, app.baseUrl), {
|
|
114
|
+
method,
|
|
115
|
+
headers: { authorization: `Bearer ${signed.token}`, "content-type": "application/json" },
|
|
116
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
117
|
+
signal,
|
|
118
|
+
redirect: "manual",
|
|
119
|
+
});
|
|
120
|
+
};
|
package/src/api/capabilities.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type CatalogSubscription, projectAppEventCatalog, readCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
5
5
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
6
|
-
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
7
7
|
import {
|
|
8
8
|
capabilityActionReceiptIssues,
|
|
9
9
|
capabilityActionReviewIssues,
|
|
10
10
|
capabilityQueryChecksActionOutcome,
|
|
11
11
|
} from "../_internal/capability-reviews";
|
|
12
12
|
import { capabilityTimeoutMs, createCapabilityDeadline, withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
13
|
-
import { getCapability, listApps } from "../_internal/registry";
|
|
13
|
+
import { getApp, getCapability, listApps } from "../_internal/registry";
|
|
14
14
|
import { resolveCommandLink } from "../capabilities/command-link";
|
|
15
15
|
import {
|
|
16
16
|
type CapabilityClaimScope,
|
|
@@ -59,12 +59,13 @@ import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/
|
|
|
59
59
|
import { oauthScopeByOperation } from "../server/middleware/auth";
|
|
60
60
|
import { noTransform } from "../server/middleware/no-transform";
|
|
61
61
|
import { hasOAuthScope } from "../server/oauth-scope";
|
|
62
|
+
import { ratelimit } from "../server/ratelimit";
|
|
62
63
|
import { appAccessAuthority, canUseApp } from "../services/app-access";
|
|
63
64
|
import { discoverableApps } from "../services/app-discovery";
|
|
64
65
|
import { authorityFromParentInvocation, invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
65
66
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
66
67
|
import type { CloudInvocationClaims, InvocationAuthority } from "../services/identity/invocation-token";
|
|
67
|
-
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
68
|
+
import { normalizeInvocationRequestId, signAppQueryInvocation } from "../services/identity/invocation-token";
|
|
68
69
|
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
69
70
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
70
71
|
import {
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
nestedInvocationLeases,
|
|
74
75
|
nestedSourceDeadline,
|
|
75
76
|
} from "../services/identity/nested-invocation";
|
|
77
|
+
import type { AuthenticatedWorkload } from "../services/identity/workload-auth";
|
|
76
78
|
import { logger } from "../services/logging";
|
|
77
79
|
import { withMandateIssueAuthority } from "../services/mandates";
|
|
78
80
|
import * as settings from "../services/settings";
|
|
@@ -80,6 +82,9 @@ import { resolveAppPresentation } from "../shared/app-presentation";
|
|
|
80
82
|
import { capabilityMessages } from "../shared/capability-messages";
|
|
81
83
|
import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
|
|
82
84
|
|
|
85
|
+
// Approved reader-check budget, shared across targets for each calling application.
|
|
86
|
+
export const APP_QUERY_REQUESTS_PER_MINUTE = 1_501;
|
|
87
|
+
const appQueryLimit = ratelimit({ id: "app-queries", limit: APP_QUERY_REQUESTS_PER_MINUTE, windowSecs: 60 });
|
|
83
88
|
const log = logger("capabilities");
|
|
84
89
|
const DEFAULT_CATALOG_LIMIT = 10;
|
|
85
90
|
const MAX_CATALOG_LIMIT = 25;
|
|
@@ -114,6 +119,7 @@ const CapabilityCatalogQuerySchema = z
|
|
|
114
119
|
export type CapabilityRouteDependencies = {
|
|
115
120
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
116
121
|
eventSubscriptions?: () => Promise<CatalogSubscription[]>;
|
|
122
|
+
getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
|
|
117
123
|
getCapability?: (appId: string) => Promise<CapabilityRegistryEntry | null>;
|
|
118
124
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
119
125
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -122,12 +128,15 @@ export type CapabilityRouteDependencies = {
|
|
|
122
128
|
withMandateIssueAuthority?: typeof withMandateIssueAuthority;
|
|
123
129
|
queryTimeoutMs?: number;
|
|
124
130
|
actionTimeoutMs?: number;
|
|
131
|
+
checkAppQueryRate?: (appId: string) => Promise<{ limited: boolean }>;
|
|
125
132
|
reviewTimeoutMs?: number;
|
|
126
133
|
appUrl?: () => Promise<string>;
|
|
127
134
|
};
|
|
128
135
|
|
|
129
136
|
export type CapabilityDispatchDependencies = Pick<
|
|
130
137
|
CapabilityRouteDependencies,
|
|
138
|
+
| "checkAppQueryRate"
|
|
139
|
+
| "getApp"
|
|
131
140
|
| "getCapability"
|
|
132
141
|
| "fetch"
|
|
133
142
|
| "queryTimeoutMs"
|
|
@@ -223,7 +232,7 @@ export const loadCapabilityCatalogPage = async (
|
|
|
223
232
|
appName: presentedEntry.name,
|
|
224
233
|
appIcon: capability.appIcon,
|
|
225
234
|
appDescription: presentedEntry.description,
|
|
226
|
-
manifest,
|
|
235
|
+
manifest: { ...manifest, queries: manifest.queries.filter(isPersonVisibleCapabilityQuery) },
|
|
227
236
|
...(manifest.events ? { events: projectAppEventCatalog(manifest, manifest.events.length ? await loadSubscribers() : []) } : {}),
|
|
228
237
|
};
|
|
229
238
|
const projectedBytes = new TextEncoder().encode(JSON.stringify(projected)).byteLength;
|
|
@@ -251,11 +260,17 @@ const errorResponse = (code: string, message: string, status: number, details?:
|
|
|
251
260
|
status,
|
|
252
261
|
});
|
|
253
262
|
|
|
254
|
-
const capabilityJsonResponse = (body: unknown, status: number): Response =>
|
|
255
|
-
|
|
263
|
+
const capabilityJsonResponse = (body: unknown, status: number): Response => {
|
|
264
|
+
const limited =
|
|
265
|
+
status === 429 ? z.object({ details: z.object({ retryAfter: z.number().int().positive().safe() }) }).safeParse(body) : null;
|
|
266
|
+
return Response.json(body, {
|
|
256
267
|
status,
|
|
257
|
-
headers: {
|
|
268
|
+
headers: {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
...(limited?.success ? { "retry-after": String(limited.data.details.retryAfter) } : {}),
|
|
271
|
+
},
|
|
258
272
|
});
|
|
273
|
+
};
|
|
259
274
|
|
|
260
275
|
export type CapabilityDispatchParams = {
|
|
261
276
|
/** Trusted Core continuation binding; never read from client headers. */
|
|
@@ -276,6 +291,8 @@ export type CapabilityDispatchParams = {
|
|
|
276
291
|
assistantTurn?: { conversationId: string; turnId: string };
|
|
277
292
|
/** Core-only absolute child deadline; cannot be extended by the application. */
|
|
278
293
|
deadline?: number;
|
|
294
|
+
/** Core-only authenticated workload. Never populated from request JSON. */
|
|
295
|
+
workload?: AuthenticatedWorkload;
|
|
279
296
|
mandate?: MandatedCapabilityAuthority;
|
|
280
297
|
callingAppId?: string;
|
|
281
298
|
/** Internal observation only: called immediately before attempting the app request. */
|
|
@@ -342,6 +359,43 @@ const runCapabilityDispatch = async (
|
|
|
342
359
|
);
|
|
343
360
|
return error;
|
|
344
361
|
}
|
|
362
|
+
if ("appOnly" in operation && operation.appOnly !== undefined && !params.workload)
|
|
363
|
+
return errorResponse("FORBIDDEN", "App authority required for this query", 403);
|
|
364
|
+
if (params.workload) {
|
|
365
|
+
if (
|
|
366
|
+
params.authority ||
|
|
367
|
+
params.mandate ||
|
|
368
|
+
params.parentInvocation ||
|
|
369
|
+
params.review ||
|
|
370
|
+
params.kind !== "queries" ||
|
|
371
|
+
!("appOnly" in operation) ||
|
|
372
|
+
operation.appOnly !== true ||
|
|
373
|
+
operation.stream
|
|
374
|
+
)
|
|
375
|
+
return errorResponse("FORBIDDEN", "App caller is not allowed for this query", 403);
|
|
376
|
+
try {
|
|
377
|
+
const caller = await deadline.wait(() => (params.dependencies?.getApp ?? getApp)(params.workload!.appId));
|
|
378
|
+
if (!caller) return errorResponse("APP_UNAVAILABLE", "Calling app registry entry unavailable", 503);
|
|
379
|
+
if (!caller.platformPermissions?.includes("resources:readers"))
|
|
380
|
+
return errorResponse("APP_QUERY_NOT_ADMITTED", "App caller does not declare resources:readers", 403);
|
|
381
|
+
} catch {
|
|
382
|
+
deadline.check();
|
|
383
|
+
return errorResponse("APP_UNAVAILABLE", "App registry unavailable", 503);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
if (
|
|
387
|
+
(
|
|
388
|
+
await deadline.wait(() =>
|
|
389
|
+
(params.dependencies?.checkAppQueryRate ?? ((appId) => appQueryLimit.check(appId)))(params.workload!.appId),
|
|
390
|
+
)
|
|
391
|
+
).limited
|
|
392
|
+
)
|
|
393
|
+
return errorResponse("RATE_LIMITED", "App query rate limit exceeded; retry after one minute", 429);
|
|
394
|
+
} catch {
|
|
395
|
+
deadline.check();
|
|
396
|
+
return errorResponse("APP_UNAVAILABLE", "App query rate limiter unavailable", 503);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
345
399
|
if (params.review && (params.kind !== "actions" || !("review" in operation) || operation.review !== true)) {
|
|
346
400
|
const error = errorResponse(
|
|
347
401
|
"CAPABILITY_NOT_FOUND",
|
|
@@ -488,6 +542,33 @@ const runCapabilityDispatch = async (
|
|
|
488
542
|
});
|
|
489
543
|
})()
|
|
490
544
|
: await (async () => {
|
|
545
|
+
if (params.workload) {
|
|
546
|
+
const workload = params.workload;
|
|
547
|
+
return deadline.wait(() =>
|
|
548
|
+
(params.dependencies?.withActiveSigner ?? withActiveIdentitySigner)(
|
|
549
|
+
"invocation",
|
|
550
|
+
async (signer) => {
|
|
551
|
+
deadline.check();
|
|
552
|
+
const signed = await signAppQueryInvocation({
|
|
553
|
+
workload,
|
|
554
|
+
targetAppId: params.appId,
|
|
555
|
+
capabilityId: params.capabilityId,
|
|
556
|
+
schemaHash: operation.schemaHash,
|
|
557
|
+
input: input.data,
|
|
558
|
+
requestId,
|
|
559
|
+
signer,
|
|
560
|
+
});
|
|
561
|
+
observed.invocationId = signed.claims.jti;
|
|
562
|
+
return new Headers({
|
|
563
|
+
"content-type": "application/json",
|
|
564
|
+
accept: "application/json",
|
|
565
|
+
authorization: `Bearer ${signed.token}`,
|
|
566
|
+
});
|
|
567
|
+
},
|
|
568
|
+
{ signal, timeoutMs: deadline.remainingMs() },
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
491
572
|
if (!params.authority) throw new Error("Resolved request authority is required for Cloud invocation issuance");
|
|
492
573
|
const authority = params.authority;
|
|
493
574
|
const signed = await deadline.wait(() =>
|
|
@@ -665,12 +746,15 @@ const runCapabilityDispatch = async (
|
|
|
665
746
|
try {
|
|
666
747
|
params.onUpstreamAttempt?.();
|
|
667
748
|
const fetchResponse = () =>
|
|
668
|
-
fetchUpstream(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
749
|
+
fetchUpstream(
|
|
750
|
+
`${entry.endpoint}/${params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
|
|
751
|
+
{
|
|
752
|
+
method: "POST",
|
|
753
|
+
headers,
|
|
754
|
+
body: requestBody,
|
|
755
|
+
signal,
|
|
756
|
+
},
|
|
757
|
+
);
|
|
674
758
|
// A read may be released at its deadline; Action effects retain their existing uncertainty handling.
|
|
675
759
|
response = params.kind === "queries" || params.review ? await deadline.wait(fetchResponse) : await fetchResponse();
|
|
676
760
|
} catch (error) {
|
|
@@ -895,12 +979,12 @@ export const dispatchCapability = async (params: CapabilityDispatchParams): Prom
|
|
|
895
979
|
origin: params.origin,
|
|
896
980
|
invocationId: observed.invocationId,
|
|
897
981
|
parentInvocationId: params.parentInvocation?.jti,
|
|
898
|
-
viaAppId: params.parentInvocation ? params.callingAppId : undefined,
|
|
982
|
+
viaAppId: params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
|
|
899
983
|
appId: params.appId,
|
|
900
984
|
capability: `${params.appId}.${params.capabilityId}`,
|
|
901
985
|
kind: params.kind === "queries" ? "query" : "action",
|
|
902
986
|
destructive: observed.destructive,
|
|
903
|
-
actorKind: principal?.type ?? null,
|
|
987
|
+
actorKind: params.workload ? "app" : (principal?.type ?? null),
|
|
904
988
|
actorId: principal?.id ?? null,
|
|
905
989
|
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
|
|
906
990
|
accessSubject: subject && subject.id !== principal?.id ? subject : null,
|
package/src/api/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import adminAiProjectsRoutes from "./admin-ai-projects";
|
|
|
26
26
|
import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
27
27
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
28
28
|
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
29
|
+
import adminAppKeyRoutes from "./admin-app-keys";
|
|
29
30
|
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
30
31
|
import adminDirectorySharesRoutes from "./admin-directory-shares";
|
|
31
32
|
import adminGeoRoutes from "./admin-geo";
|
|
@@ -106,6 +107,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
106
107
|
.route("/admin/core/geo", adminGeoRoutes)
|
|
107
108
|
.route("/admin/identity/directory-shares", adminDirectorySharesRoutes)
|
|
108
109
|
.route("/admin/identity", adminIdentityRoutes)
|
|
110
|
+
.route("/admin/apps", adminAppKeyRoutes)
|
|
109
111
|
.route("/admin/sync", createSyncOpsProxyRoutes())
|
|
110
112
|
.route("/admin/lifecycle", adminLifecycleRoutes)
|
|
111
113
|
.route("/ai/approval-preferences", approvalPreferenceRoutes)
|
|
@@ -133,6 +135,7 @@ export const createCoreApiRouter = (options: CoreApiOptions) => {
|
|
|
133
135
|
return { api };
|
|
134
136
|
};
|
|
135
137
|
|
|
138
|
+
export { createAdminAppKeyRoutes } from "./admin-app-keys";
|
|
136
139
|
export { createAppAccessRoutes } from "./app-access";
|
|
137
140
|
export {
|
|
138
141
|
CLOUD_MCP_PATH,
|
package/src/api/mcp.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Hono, type MiddlewareHandler } from "hono";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { type CatalogSubscription, projectAppEventCatalog, readEventCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
18
18
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
19
|
-
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
19
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
20
20
|
import { capabilitySentenceText } from "../_internal/capability-sentences";
|
|
21
21
|
import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
22
22
|
import {
|
|
@@ -311,7 +311,9 @@ const orderedCapabilityEntries = function* (app: CapabilityRegistryEntry): Gener
|
|
|
311
311
|
for (const operation of [...app.manifest.actions].sort((left, right) => left.localId.localeCompare(right.localId))) {
|
|
312
312
|
yield { app, kind: "actions", operation };
|
|
313
313
|
}
|
|
314
|
-
for (const operation of
|
|
314
|
+
for (const operation of app.manifest.queries
|
|
315
|
+
.filter(isPersonVisibleCapabilityQuery)
|
|
316
|
+
.sort((left, right) => left.localId.localeCompare(right.localId))) {
|
|
315
317
|
yield { app, kind: "queries", operation };
|
|
316
318
|
}
|
|
317
319
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Concrete modules, not the server barrel: defineApp imports this file, and the barrel imports defineApp.
|
|
4
|
+
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
5
|
+
import { v } from "../server/middleware/validator";
|
|
6
|
+
import { settingsService } from "../services/settings/app";
|
|
7
|
+
import { SETTINGS_MAP } from "../services/settings/defaults";
|
|
8
|
+
import { readEnvSettingsStatus } from "../services/settings/env-status";
|
|
9
|
+
|
|
10
|
+
export const settingsInvocationOperation = (method: string, key: string) => {
|
|
11
|
+
try {
|
|
12
|
+
return `settings.${method === "GET" ? "env" : "set"}:${decodeURIComponent(key)}`;
|
|
13
|
+
} catch {
|
|
14
|
+
return "settings.invalid";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const createSettingsOpsRoutes = (keys: readonly string[]) =>
|
|
19
|
+
new Hono<AuthContext>()
|
|
20
|
+
.use("*", async (c, next) => {
|
|
21
|
+
if (!c.get("user")?.roles.includes("admin")) return c.json({ message: "Insufficient permissions" }, 403);
|
|
22
|
+
return next();
|
|
23
|
+
})
|
|
24
|
+
.use("*", auth.requireOAuthScope("admin"))
|
|
25
|
+
.get("/env", async (c) => c.json({ settings: await readEnvSettingsStatus() }, 200, { "cache-control": "no-store" }))
|
|
26
|
+
.put("/:key", v("json", z.object({ value: z.unknown(), replaceUnreadable: z.boolean().optional() })), async (c) => {
|
|
27
|
+
const key = c.req.param("key") ?? "";
|
|
28
|
+
const def = SETTINGS_MAP.get(key);
|
|
29
|
+
if (!keys.includes(key) || !def || !(def.envBootstrap || def.envFallback))
|
|
30
|
+
return c.json({ message: "Unknown application setting" }, 400);
|
|
31
|
+
const body = c.req.valid("json");
|
|
32
|
+
const result = await settingsService.entry.update({ key, value: body.value, replaceUnreadable: body.replaceUnreadable });
|
|
33
|
+
if (!result.ok) return c.json({ message: result.error.message }, 400);
|
|
34
|
+
return c.json({ saved: true });
|
|
35
|
+
});
|