@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
|
@@ -19,6 +19,13 @@ import { withSyncBudgets } from "./sync-budget";
|
|
|
19
19
|
|
|
20
20
|
let current: Sync | undefined;
|
|
21
21
|
let starting = false;
|
|
22
|
+
let processNats: NatsConnection | undefined;
|
|
23
|
+
|
|
24
|
+
/** Existing non-retained NATS transport, owned and drained by the process lifecycle. */
|
|
25
|
+
export const getProcessNats = (): NatsConnection => {
|
|
26
|
+
if (!processNats) throw new Error("NATS is not available before app.start()");
|
|
27
|
+
return processNats;
|
|
28
|
+
};
|
|
22
29
|
|
|
23
30
|
/** Bind the process instance. `startProcessSync()` does this; tests may bind their own. */
|
|
24
31
|
export const bindProcessSync = (sync: Sync): void => {
|
|
@@ -173,6 +180,7 @@ export const startProcessSync = async ({ application }: { application: string })
|
|
|
173
180
|
starting = true;
|
|
174
181
|
try {
|
|
175
182
|
const { sync: active, connection } = await waitForNats(() => connectReadySync(application), { application });
|
|
183
|
+
processNats = connection;
|
|
176
184
|
let stopPromise: Promise<void> | undefined;
|
|
177
185
|
return {
|
|
178
186
|
sync: active,
|
|
@@ -185,6 +193,7 @@ export const startProcessSync = async ({ application }: { application: string })
|
|
|
185
193
|
await flushSyncTraceEvents();
|
|
186
194
|
} finally {
|
|
187
195
|
if (current === active) unbindProcessSync();
|
|
196
|
+
if (processNats === connection) processNats = undefined;
|
|
188
197
|
await connection.drain();
|
|
189
198
|
}
|
|
190
199
|
}
|
|
@@ -3,4 +3,9 @@ import { buildMetadata } from "./build-metadata";
|
|
|
3
3
|
export const APP_READINESS_PATH = "/_cloud/ready";
|
|
4
4
|
|
|
5
5
|
export const appReadinessResponse = (appId: string): Response =>
|
|
6
|
-
Response.json({
|
|
6
|
+
Response.json({
|
|
7
|
+
status: "ready",
|
|
8
|
+
appId,
|
|
9
|
+
version: buildMetadata.version,
|
|
10
|
+
release: buildMetadata.release,
|
|
11
|
+
});
|
|
@@ -160,8 +160,8 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
160
160
|
return invalid("widgets", "an array of valid widgets");
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
if (value.platformPermissions !== undefined && !
|
|
164
|
-
return invalid("platformPermissions", "an array of
|
|
163
|
+
if (value.platformPermissions !== undefined && !isStringArray(value.platformPermissions))
|
|
164
|
+
return invalid("platformPermissions", "an array of permission IDs");
|
|
165
165
|
if (value.settingKeys !== undefined && !isStringArray(value.settingKeys)) return invalid("settingKeys", "an array of strings");
|
|
166
166
|
if (
|
|
167
167
|
value.cliModules !== undefined &&
|
|
@@ -198,5 +198,18 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
198
198
|
return null;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
export const asAppRegistryEntry = (value: unknown): AppRegistryEntry | null =>
|
|
202
|
-
validateAppRegistryEntry(value)
|
|
201
|
+
export const asAppRegistryEntry = (value: unknown): AppRegistryEntry | null => {
|
|
202
|
+
if (validateAppRegistryEntry(value) !== null) return null;
|
|
203
|
+
const entry = value as AppRegistryEntry;
|
|
204
|
+
return {
|
|
205
|
+
...entry,
|
|
206
|
+
...(entry.platformPermissions !== undefined
|
|
207
|
+
? {
|
|
208
|
+
platformPermissions: entry.platformPermissions.flatMap((permission) => {
|
|
209
|
+
const parsed = PlatformPermissionSchema.safeParse(permission);
|
|
210
|
+
return parsed.success ? [parsed.data] : [];
|
|
211
|
+
}),
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { EphemeralConfig, MutexConfig } from "@k2b/sync";
|
|
|
3
3
|
import type { AppAppearanceColor } from "../contracts/app";
|
|
4
4
|
import { calendarSourceIssues } from "../contracts/calendar-source";
|
|
5
5
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
|
|
6
|
+
import { discussionsIssues } from "../contracts/discussions";
|
|
6
7
|
import { fileProviderIssues } from "../contracts/file-provider";
|
|
7
8
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
8
9
|
import { resourcePageIssue } from "../contracts/resource-pages";
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
import { resolveAppPresentations } from "../shared/app-presentation";
|
|
19
20
|
import { compileCapabilityPresentation, parseCapabilityManifest } from "./capabilities";
|
|
20
21
|
import { lazySync } from "./process-sync";
|
|
21
|
-
import { validateAppRegistryEntry } from "./registry-validation";
|
|
22
|
+
import { asAppRegistryEntry, validateAppRegistryEntry } from "./registry-validation";
|
|
22
23
|
import { watchRegistryChanges } from "./registry-watch";
|
|
23
24
|
import { WidgetSettingsDeclarationSchema } from "./widget-settings";
|
|
24
25
|
|
|
@@ -150,7 +151,7 @@ export const readAppRegistrySnapshot = async (): Promise<AppRegistrySnapshot> =>
|
|
|
150
151
|
continue;
|
|
151
152
|
}
|
|
152
153
|
loggedInvalidReasons.delete(entry.key);
|
|
153
|
-
const value = entry.value
|
|
154
|
+
const value = asAppRegistryEntry(entry.value)!;
|
|
154
155
|
const updatedAt = entry.updatedAt.getTime();
|
|
155
156
|
apps.push({
|
|
156
157
|
...value,
|
|
@@ -194,7 +195,7 @@ export const getApp = async (appId: string): Promise<AppRegistryEntry | null> =>
|
|
|
194
195
|
: validateAppRegistryEntry(entry.value);
|
|
195
196
|
if (!reason) {
|
|
196
197
|
loggedInvalidReasons.delete(key);
|
|
197
|
-
return entry.value;
|
|
198
|
+
return asAppRegistryEntry(entry.value);
|
|
198
199
|
}
|
|
199
200
|
reportInvalidEntry({ key, version: entry.revision, reason });
|
|
200
201
|
return null;
|
|
@@ -237,6 +238,7 @@ const reportLeftOutEntries = (sent: unknown, manifest: CapabilityManifest): void
|
|
|
237
238
|
});
|
|
238
239
|
if (groups.calendarSource !== undefined && !manifest.calendarSource) leftOut.push("calendarSource");
|
|
239
240
|
if (groups.fileProvider !== undefined && !manifest.fileProvider) leftOut.push("fileProvider");
|
|
241
|
+
if (groups.discussions !== undefined && !manifest.discussions) leftOut.push("discussions");
|
|
240
242
|
if (leftOut.length === 0) return;
|
|
241
243
|
console.warn(
|
|
242
244
|
JSON.stringify({
|
|
@@ -320,6 +322,32 @@ const withValidCalendarSource = (manifest: CapabilityManifest): CapabilityManife
|
|
|
320
322
|
return rest;
|
|
321
323
|
};
|
|
322
324
|
|
|
325
|
+
const checkedDiscussions = new Map<string, { manifestHash: string; valid: boolean }>();
|
|
326
|
+
const withValidDiscussions = (manifest: CapabilityManifest): CapabilityManifest => {
|
|
327
|
+
if (!manifest.discussions) return manifest;
|
|
328
|
+
const checked = checkedDiscussions.get(manifest.appId);
|
|
329
|
+
let valid = checked?.manifestHash === manifest.manifestHash ? checked.valid : undefined;
|
|
330
|
+
if (valid === undefined) {
|
|
331
|
+
const issues = discussionsIssues(manifest);
|
|
332
|
+
valid = issues.length === 0;
|
|
333
|
+
checkedDiscussions.set(manifest.appId, { manifestHash: manifest.manifestHash, valid });
|
|
334
|
+
if (!valid)
|
|
335
|
+
console.error(
|
|
336
|
+
JSON.stringify({
|
|
337
|
+
level: "error",
|
|
338
|
+
source: "capability-registry",
|
|
339
|
+
message: "Ignored an incompatible discussions provider",
|
|
340
|
+
appId: manifest.appId,
|
|
341
|
+
manifestHash: manifest.manifestHash,
|
|
342
|
+
issues,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (valid) return manifest;
|
|
347
|
+
const { discussions: _ignored, ...rest } = manifest;
|
|
348
|
+
return rest;
|
|
349
|
+
};
|
|
350
|
+
|
|
323
351
|
/** Ignores record fields from a newer release; the endpoint always comes from the live app registry. */
|
|
324
352
|
export const resolveLiveCapabilityRegistryEntry = (
|
|
325
353
|
key: string,
|
|
@@ -334,7 +362,7 @@ export const resolveLiveCapabilityRegistryEntry = (
|
|
|
334
362
|
if (!endpoint) return null;
|
|
335
363
|
try {
|
|
336
364
|
const read = parseCapabilityManifest(record.manifest, app.id);
|
|
337
|
-
const supported = withValidCalendarSource(withValidFileProvider(read));
|
|
365
|
+
const supported = withValidDiscussions(withValidCalendarSource(withValidFileProvider(read)));
|
|
338
366
|
const manifest = supported.resourcePages
|
|
339
367
|
? {
|
|
340
368
|
...supported,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ResourcePrincipal } from "../contracts/capabilities";
|
|
2
|
+
import { resolveResourceReaders } from "../services/identity/resource-readers";
|
|
3
|
+
|
|
4
|
+
/** Apply the same current-account rules as provider-owned readability checks. */
|
|
5
|
+
export const activeResourcePrincipals = async (principals: ResourcePrincipal[]): Promise<ResourcePrincipal[]> =>
|
|
6
|
+
(await resolveResourceReaders(principals)).map((reader) => reader.principal);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { env } from "../config/env";
|
|
2
|
+
import { getProcessApplicationId } from "./process-identity";
|
|
3
|
+
import { getProcessNats } from "./process-sync";
|
|
4
|
+
import {
|
|
5
|
+
WORKLOAD_CHECK_INTERVAL_MS,
|
|
6
|
+
WORKLOAD_REQUEST_TIMEOUT_MS,
|
|
7
|
+
type WorkloadRequest,
|
|
8
|
+
WorkloadResponseSchema,
|
|
9
|
+
workloadSubject,
|
|
10
|
+
} from "./workload-protocol";
|
|
11
|
+
|
|
12
|
+
export type WorkloadCredentialHealth = { status: "ready"; source: "runtime" | "override" } | { status: "unavailable"; reason: string };
|
|
13
|
+
let active: { appId: string; token: string; expiresAt: number } | undefined;
|
|
14
|
+
let health: WorkloadCredentialHealth | undefined;
|
|
15
|
+
let refreshRuntime: { appId: string; refresh: () => Promise<void> } | undefined;
|
|
16
|
+
|
|
17
|
+
/** Refresh only this process's app authority; concurrent broker rejections share one request. */
|
|
18
|
+
export const refreshAppWorkloadCredential = async (appId: string): Promise<string | undefined> => {
|
|
19
|
+
if (appId !== getProcessApplicationId()) return undefined;
|
|
20
|
+
if (refreshRuntime?.appId === appId) await refreshRuntime.refresh();
|
|
21
|
+
return getAppWorkloadCredential();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** One server-side accessor for workers and app-authority invocation clients. Never send to a target app. */
|
|
25
|
+
export const getAppWorkloadCredential = (): string | undefined => {
|
|
26
|
+
const override = env.CLOUD_APP_CREDENTIAL?.trim();
|
|
27
|
+
if (override) return override;
|
|
28
|
+
if (active && active.appId === getProcessApplicationId() && active.expiresAt > Date.now()) return active.token;
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getAppWorkloadCredentialHealth = (): WorkloadCredentialHealth | undefined => {
|
|
33
|
+
if (env.CLOUD_APP_CREDENTIAL?.trim()) return { status: "ready", source: "override" };
|
|
34
|
+
if (active && active.expiresAt <= Date.now()) return { status: "unavailable", reason: "expired" };
|
|
35
|
+
return health;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const createAppWorkloadCredential = (options: {
|
|
39
|
+
appId: string;
|
|
40
|
+
instanceId: string;
|
|
41
|
+
request?: (request: WorkloadRequest) => Promise<unknown>;
|
|
42
|
+
onUnavailable?: (reason: string) => void;
|
|
43
|
+
onRecovered?: () => void;
|
|
44
|
+
}) => {
|
|
45
|
+
let credentialId: string | null = null;
|
|
46
|
+
let running = false;
|
|
47
|
+
let refreshFailed = false;
|
|
48
|
+
let timer: Timer | undefined;
|
|
49
|
+
let inFlight: Promise<void> | undefined;
|
|
50
|
+
const request =
|
|
51
|
+
options.request ??
|
|
52
|
+
(async (input: WorkloadRequest) => {
|
|
53
|
+
const response = await getProcessNats().request(workloadSubject(env.SYNC_NAMESPACE), JSON.stringify(input), {
|
|
54
|
+
timeout: WORKLOAD_REQUEST_TIMEOUT_MS,
|
|
55
|
+
});
|
|
56
|
+
return response.json<unknown>();
|
|
57
|
+
});
|
|
58
|
+
const unavailable = (reason: string, preserve = false) => {
|
|
59
|
+
refreshFailed = true;
|
|
60
|
+
if (preserve && active && active.expiresAt > Date.now()) return;
|
|
61
|
+
if (!preserve) {
|
|
62
|
+
active = undefined;
|
|
63
|
+
credentialId = null;
|
|
64
|
+
}
|
|
65
|
+
const changed = health?.status !== "unavailable" || health.reason !== reason;
|
|
66
|
+
health = { status: "unavailable", reason };
|
|
67
|
+
if (changed) options.onUnavailable?.(reason);
|
|
68
|
+
};
|
|
69
|
+
const recovered = () => {
|
|
70
|
+
health = { status: "ready", source: "runtime" };
|
|
71
|
+
if (refreshFailed) options.onRecovered?.();
|
|
72
|
+
refreshFailed = false;
|
|
73
|
+
};
|
|
74
|
+
const refresh = async (): Promise<void> => {
|
|
75
|
+
if (env.CLOUD_APP_CREDENTIAL?.trim()) {
|
|
76
|
+
active = undefined;
|
|
77
|
+
health = { status: "ready", source: "override" };
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const response = WorkloadResponseSchema.parse(
|
|
82
|
+
await request({ operation: "refresh", appId: options.appId, instanceId: options.instanceId, credentialId }),
|
|
83
|
+
);
|
|
84
|
+
if (!running) return;
|
|
85
|
+
if (response.status === "issued") {
|
|
86
|
+
if (
|
|
87
|
+
response.appId !== options.appId ||
|
|
88
|
+
response.instanceId !== options.instanceId ||
|
|
89
|
+
Date.parse(response.expiresAt) <= Date.now()
|
|
90
|
+
) {
|
|
91
|
+
unavailable("invalid_response");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
credentialId = response.credentialId;
|
|
95
|
+
active = { appId: options.appId, token: response.token, expiresAt: Date.parse(response.expiresAt) };
|
|
96
|
+
recovered();
|
|
97
|
+
} else if (response.status === "current" && response.credentialId === credentialId && active && active.expiresAt > Date.now()) {
|
|
98
|
+
recovered();
|
|
99
|
+
} else {
|
|
100
|
+
unavailable(response.status === "current" ? "invalid_response" : response.status, response.status === "unavailable");
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
if (running) {
|
|
104
|
+
unavailable("issuance_unavailable", true);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const refreshOnce = (): Promise<void> => {
|
|
109
|
+
if (inFlight) return inFlight;
|
|
110
|
+
const operation = refresh().finally(() => {
|
|
111
|
+
if (inFlight === operation) inFlight = undefined;
|
|
112
|
+
});
|
|
113
|
+
inFlight = operation;
|
|
114
|
+
return operation;
|
|
115
|
+
};
|
|
116
|
+
const schedule = () => {
|
|
117
|
+
if (!running) return;
|
|
118
|
+
timer = setTimeout(
|
|
119
|
+
() => {
|
|
120
|
+
void refreshOnce().finally(schedule);
|
|
121
|
+
},
|
|
122
|
+
refreshFailed || health?.status === "unavailable" ? 5_000 : WORKLOAD_CHECK_INTERVAL_MS,
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
start: async () => {
|
|
127
|
+
if (running) {
|
|
128
|
+
await inFlight;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
running = true;
|
|
132
|
+
refreshRuntime = { appId: options.appId, refresh: refreshOnce };
|
|
133
|
+
health = { status: "unavailable", reason: "starting" };
|
|
134
|
+
await refreshOnce();
|
|
135
|
+
schedule();
|
|
136
|
+
},
|
|
137
|
+
refresh: refreshOnce,
|
|
138
|
+
stop: async () => {
|
|
139
|
+
running = false;
|
|
140
|
+
if (refreshRuntime?.refresh === refreshOnce) refreshRuntime = undefined;
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
await inFlight;
|
|
143
|
+
if (!env.CLOUD_APP_CREDENTIAL?.trim()) {
|
|
144
|
+
await request({ operation: "release", appId: options.appId, instanceId: options.instanceId, credentialId }).catch(() => {
|
|
145
|
+
// Core's registry reconciliation revokes after disappearance even when this reply is lost.
|
|
146
|
+
options.onUnavailable?.("release_unavailable");
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
active = undefined;
|
|
150
|
+
health = undefined;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
4
|
+
|
|
5
|
+
// Core NATS request/reply only: no JetStream subject, KV value, or retained raw token.
|
|
6
|
+
export const workloadSubject = (namespace: string): string => `cloud.workload.${createHash("sha256").update(namespace).digest("hex")}.v1`;
|
|
7
|
+
export const WORKLOAD_CHECK_INTERVAL_MS = 60_000;
|
|
8
|
+
export const WORKLOAD_REQUEST_TIMEOUT_MS = 5_000;
|
|
9
|
+
export const WORKLOAD_LIFETIME_MS = 60 * 60_000;
|
|
10
|
+
export const WORKLOAD_ROTATE_BEFORE_MS = WORKLOAD_LIFETIME_MS / 2;
|
|
11
|
+
|
|
12
|
+
export const WorkloadRequestSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
operation: z.enum(["refresh", "release"]),
|
|
15
|
+
appId: CapabilityAppIdSchema,
|
|
16
|
+
instanceId: z.string().regex(/^[A-Za-z0-9_-]{16}$/),
|
|
17
|
+
credentialId: z.uuid().nullable(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export type WorkloadRequest = z.infer<typeof WorkloadRequestSchema>;
|
|
21
|
+
|
|
22
|
+
export const WorkloadResponseSchema = z.discriminatedUnion("status", [
|
|
23
|
+
z
|
|
24
|
+
.object({
|
|
25
|
+
status: z.literal("issued"),
|
|
26
|
+
appId: CapabilityAppIdSchema,
|
|
27
|
+
instanceId: z.string(),
|
|
28
|
+
credentialId: z.uuid(),
|
|
29
|
+
token: z.string().regex(/^cld_[a-f0-9]{24}_[a-f0-9]{64}$/),
|
|
30
|
+
expiresAt: z.iso.datetime(),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
z.object({ status: z.literal("current"), credentialId: z.uuid() }).strict(),
|
|
34
|
+
z.object({ status: z.enum(["unavailable", "revoked", "unregistered"]) }).strict(),
|
|
35
|
+
]);
|
|
36
|
+
export type WorkloadResponse = z.infer<typeof WorkloadResponseSchema>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
|
-
import { env } from "../config/env";
|
|
3
2
|
import type { AccessSubject } from "../server/services/access";
|
|
3
|
+
import { coreSettings } from "../services/settings/api";
|
|
4
4
|
import { WorkflowAiAdmissionError, type WorkflowAiScope, workflowAiHash, workflowAiModelHash } from "../workflows/ai/policy";
|
|
5
5
|
import { submitWorkflowAiTask } from "../workflows/ai/runtime";
|
|
6
6
|
import {
|
|
@@ -56,7 +56,10 @@ export const runApplicationAiTask = async (input: ApplicationAiTaskInput) => {
|
|
|
56
56
|
let task = await getWorkflowAiTaskByEffectKey(effectKey);
|
|
57
57
|
if (!task) {
|
|
58
58
|
const resolved = await resolveApplicationAiTaskModel(input.usageSubject, input.request.model);
|
|
59
|
-
const admissionSignal = AbortSignal.any([
|
|
59
|
+
const admissionSignal = AbortSignal.any([
|
|
60
|
+
input.signal,
|
|
61
|
+
AbortSignal.timeout(await coreSettings.get<number>("ai.workflow_queue_timeout_ms")),
|
|
62
|
+
]);
|
|
60
63
|
for (;;) {
|
|
61
64
|
input.signal.throwIfAborted();
|
|
62
65
|
if (admissionSignal.aborted) throw new WorkflowAiAdmissionError("WORKFLOW_AI_QUEUE_TIMEOUT");
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
4
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
5
5
|
import { capabilityReviewView } from "../_internal/capability-reviews";
|
|
6
6
|
import {
|
|
7
7
|
CAPABILITY_APPROVAL_REASON_FIELD,
|
|
@@ -164,7 +164,9 @@ export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[], locale
|
|
|
164
164
|
: app.appName;
|
|
165
165
|
return [
|
|
166
166
|
...app.manifest.actions.map((operation) => ({ app, appName, operation, kind: "action" as const })),
|
|
167
|
-
...app.manifest.queries
|
|
167
|
+
...app.manifest.queries
|
|
168
|
+
.filter(isPersonVisibleCapabilityQuery)
|
|
169
|
+
.map((operation) => ({ app, appName, operation, kind: "query" as const })),
|
|
168
170
|
];
|
|
169
171
|
})
|
|
170
172
|
.map(
|