@k2b/cloud 0.46.0 → 0.48.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 +8 -4
- package/scripts/build.ts +5 -1
- package/scripts/preload.ts +5 -1
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +8 -0
- package/src/_internal/define-app.ts +69 -2
- package/src/_internal/heartbeat.ts +21 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +15 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/resource-principals.ts +4 -34
- package/src/_internal/workload-credential.ts +154 -0
- package/src/_internal/workload-protocol.ts +36 -0
- package/src/ai/application-tasks.ts +5 -2
- package/src/ai/chart-block.ts +36 -711
- package/src/ai/default-tool-contracts.ts +21 -259
- package/src/api/admin-app-domains.ts +62 -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/app-bootstrap.ts +131 -0
- package/src/api/capabilities.ts +17 -4
- package/src/api/index.ts +8 -0
- package/src/api/settings-ops.ts +35 -0
- package/src/app-key.ts +125 -0
- package/src/browser/CloudResourceSearch.tsx +48 -41
- package/src/capabilities/discussion-providers.ts +3 -2
- package/src/capabilities/resource-readers.ts +13 -2
- package/src/capabilities/response.ts +6 -2
- package/src/capabilities/server-invoke.ts +73 -29
- package/src/capabilities/server.ts +1 -1
- package/src/capabilities/types.ts +1 -1
- package/src/cli/admin/app-credentials.ts +32 -7
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +226 -0
- package/src/cli/admin/index.ts +109 -84
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +34 -22
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +22 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/index.ts +10 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +60 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +46 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/client-ip.ts +1 -1
- package/src/server/index.ts +1 -0
- package/src/services/app-access-management.ts +1 -8
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-bootstrap.ts +134 -0
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-keys.ts +301 -0
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/gateway.ts +6 -0
- package/src/services/identity/index.ts +3 -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 +8 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +7 -5
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +8 -8
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- package/src/services/request-cache-redis.ts +2 -2
- 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 +119 -1
- package/src/services/settings/crypto.ts +2 -2
- 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 +78 -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/admin-navigation.ts +0 -1
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +57 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { EphemeralConfig, MutexConfig } from "@k2b/sync";
|
|
3
3
|
import type { AppAppearanceColor } from "../contracts/app";
|
|
4
|
+
import { appStatusIssues } from "../contracts/app-status";
|
|
4
5
|
import { calendarSourceIssues } from "../contracts/calendar-source";
|
|
5
6
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
|
|
6
7
|
import { discussionsIssues } from "../contracts/discussions";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
WIDGET_TITLE_MAX_LENGTH,
|
|
17
18
|
type WidgetSettingsDeclaration,
|
|
18
19
|
} from "../contracts/widgets";
|
|
20
|
+
import type { PlatformBootstrapMetadata } from "../runtime/connections";
|
|
19
21
|
import { resolveAppPresentations } from "../shared/app-presentation";
|
|
20
22
|
import { compileCapabilityPresentation, parseCapabilityManifest } from "./capabilities";
|
|
21
23
|
import { lazySync } from "./process-sync";
|
|
@@ -63,6 +65,8 @@ export type AppInstanceRecord = {
|
|
|
63
65
|
hostname: string;
|
|
64
66
|
startedAt: number;
|
|
65
67
|
entries: Record<string, string>;
|
|
68
|
+
/** Missing on older instances means unknown, never env or successful bootstrap. */
|
|
69
|
+
bootstrap?: PlatformBootstrapMetadata;
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
// The id and owner are a cross-version contract: all replicas must use the same lock.
|
|
@@ -103,6 +107,8 @@ export type AppRegistryDetail = AppRegistryEntry & {
|
|
|
103
107
|
updatedAt: number;
|
|
104
108
|
expiresAt: number;
|
|
105
109
|
version: string;
|
|
110
|
+
/** Per-process startup metadata for admin readers. null means an older/unknown producer. */
|
|
111
|
+
instances?: { instanceId: string; hostname: string; startedAt: number; bootstrap: PlatformBootstrapMetadata | null }[];
|
|
106
112
|
};
|
|
107
113
|
|
|
108
114
|
export type AppRegistryIssue = {
|
|
@@ -238,6 +244,7 @@ const reportLeftOutEntries = (sent: unknown, manifest: CapabilityManifest): void
|
|
|
238
244
|
});
|
|
239
245
|
if (groups.calendarSource !== undefined && !manifest.calendarSource) leftOut.push("calendarSource");
|
|
240
246
|
if (groups.fileProvider !== undefined && !manifest.fileProvider) leftOut.push("fileProvider");
|
|
247
|
+
if (groups.appStatus !== undefined && !manifest.appStatus) leftOut.push("appStatus");
|
|
241
248
|
if (groups.discussions !== undefined && !manifest.discussions) leftOut.push("discussions");
|
|
242
249
|
if (leftOut.length === 0) return;
|
|
243
250
|
console.warn(
|
|
@@ -348,6 +355,32 @@ const withValidDiscussions = (manifest: CapabilityManifest): CapabilityManifest
|
|
|
348
355
|
return rest;
|
|
349
356
|
};
|
|
350
357
|
|
|
358
|
+
const checkedAppStatus = new Map<string, { manifestHash: string; valid: boolean }>();
|
|
359
|
+
const withValidAppStatus = (manifest: CapabilityManifest): CapabilityManifest => {
|
|
360
|
+
if (!manifest.appStatus) return manifest;
|
|
361
|
+
const checked = checkedAppStatus.get(manifest.appId);
|
|
362
|
+
let valid = checked?.manifestHash === manifest.manifestHash ? checked.valid : undefined;
|
|
363
|
+
if (valid === undefined) {
|
|
364
|
+
const issues = appStatusIssues(manifest);
|
|
365
|
+
valid = issues.length === 0;
|
|
366
|
+
checkedAppStatus.set(manifest.appId, { manifestHash: manifest.manifestHash, valid });
|
|
367
|
+
if (!valid)
|
|
368
|
+
console.error(
|
|
369
|
+
JSON.stringify({
|
|
370
|
+
level: "error",
|
|
371
|
+
source: "capability-registry",
|
|
372
|
+
message: "Ignored an incompatible appStatus provider",
|
|
373
|
+
appId: manifest.appId,
|
|
374
|
+
manifestHash: manifest.manifestHash,
|
|
375
|
+
issues,
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (valid) return manifest;
|
|
380
|
+
const { appStatus: _ignored, ...rest } = manifest;
|
|
381
|
+
return rest;
|
|
382
|
+
};
|
|
383
|
+
|
|
351
384
|
/** Ignores record fields from a newer release; the endpoint always comes from the live app registry. */
|
|
352
385
|
export const resolveLiveCapabilityRegistryEntry = (
|
|
353
386
|
key: string,
|
|
@@ -362,7 +395,7 @@ export const resolveLiveCapabilityRegistryEntry = (
|
|
|
362
395
|
if (!endpoint) return null;
|
|
363
396
|
try {
|
|
364
397
|
const read = parseCapabilityManifest(record.manifest, app.id);
|
|
365
|
-
const supported = withValidDiscussions(withValidCalendarSource(withValidFileProvider(read)));
|
|
398
|
+
const supported = withValidAppStatus(withValidDiscussions(withValidCalendarSource(withValidFileProvider(read))));
|
|
366
399
|
const manifest = supported.resourcePages
|
|
367
400
|
? {
|
|
368
401
|
...supported,
|
|
@@ -411,8 +444,21 @@ export const getCapability = async (appId: string): Promise<CapabilityRegistryEn
|
|
|
411
444
|
* Same as `listApps` but returns registry metadata for admin observability.
|
|
412
445
|
*/
|
|
413
446
|
export const listAppsDetailed = async (): Promise<AppRegistryDetail[]> => {
|
|
414
|
-
const snapshot = await
|
|
415
|
-
|
|
447
|
+
const [snapshot, instances] = await Promise.all([
|
|
448
|
+
readAppRegistrySnapshot(),
|
|
449
|
+
appInstanceRegistry().snapshot({ prefix: APP_INSTANCE_PREFIX }),
|
|
450
|
+
]);
|
|
451
|
+
return snapshot.apps.map((app) => ({
|
|
452
|
+
...app,
|
|
453
|
+
instances: instances.entries
|
|
454
|
+
.filter(({ value }) => value.appId === app.id)
|
|
455
|
+
.map(({ value }) => ({
|
|
456
|
+
instanceId: value.instanceId,
|
|
457
|
+
hostname: value.hostname,
|
|
458
|
+
startedAt: value.startedAt,
|
|
459
|
+
bootstrap: value.bootstrap ?? null,
|
|
460
|
+
})),
|
|
461
|
+
}));
|
|
416
462
|
};
|
|
417
463
|
|
|
418
464
|
/**
|
|
@@ -1,36 +1,6 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { ResourcePrincipal } from "../contracts/capabilities";
|
|
3
|
-
import {
|
|
4
|
-
import { toPgTextArray } from "../services/postgres";
|
|
2
|
+
import { resolveResourceReaders } from "../services/identity/resource-readers";
|
|
5
3
|
|
|
6
|
-
/**
|
|
7
|
-
export const activeResourcePrincipals = async (principals: ResourcePrincipal[]): Promise<ResourcePrincipal[]> =>
|
|
8
|
-
|
|
9
|
-
const users = principals.flatMap((principal) => (principal.type === "user" ? [principal.userId] : []));
|
|
10
|
-
const accounts = principals.flatMap((principal) => (principal.type === "service_account" ? [principal.serviceAccountId] : []));
|
|
11
|
-
const rows = await sql<{ type: ResourcePrincipal["type"]; id: string; category_enabled: string | null }[]>`
|
|
12
|
-
SELECT 'user' AS type, u.id, category.value AS category_enabled
|
|
13
|
-
FROM auth.users u
|
|
14
|
-
LEFT JOIN settings.entries category ON category.key = 'user.category.' ||
|
|
15
|
-
CASE WHEN u.provider = 'ipa' THEN 'freeipa' WHEN u.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
|
|
16
|
-
WHERE u.id = ANY(${toPgTextArray(users)}::uuid[])
|
|
17
|
-
AND (u.account_expires IS NULL OR u.account_expires > clock_timestamp())
|
|
18
|
-
UNION ALL
|
|
19
|
-
SELECT 'service_account', sa.id, category.value
|
|
20
|
-
FROM auth.service_accounts sa
|
|
21
|
-
LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
|
|
22
|
-
LEFT JOIN settings.entries category ON sa.kind = 'user_delegated' AND category.key = 'user.category.' ||
|
|
23
|
-
CASE WHEN du.provider = 'ipa' THEN 'freeipa' WHEN du.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
|
|
24
|
-
WHERE sa.id = ANY(${toPgTextArray(accounts)}::uuid[]) AND sa.status = 'active'
|
|
25
|
-
AND (sa.kind <> 'user_delegated' OR (du.id IS NOT NULL AND (du.account_expires IS NULL OR du.account_expires > clock_timestamp())))
|
|
26
|
-
`;
|
|
27
|
-
const enabled = new Map<string | null, boolean>();
|
|
28
|
-
const active = new Set<string>();
|
|
29
|
-
for (const row of rows) {
|
|
30
|
-
if (!enabled.has(row.category_enabled)) enabled.set(row.category_enabled, await decodeAccountCategoryEnabled(row.category_enabled));
|
|
31
|
-
if (enabled.get(row.category_enabled)) active.add(`${row.type}:${row.id}`);
|
|
32
|
-
}
|
|
33
|
-
return principals.filter((principal) =>
|
|
34
|
-
active.has(`${principal.type}:${(principal.type === "user" ? principal.userId : principal.serviceAccountId).toLowerCase()}`),
|
|
35
|
-
);
|
|
36
|
-
};
|
|
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,154 @@
|
|
|
1
|
+
import { env } from "../config/env";
|
|
2
|
+
import { connections } from "../runtime/connections";
|
|
3
|
+
import { getProcessApplicationId } from "./process-identity";
|
|
4
|
+
import { getProcessNats } from "./process-sync";
|
|
5
|
+
import {
|
|
6
|
+
WORKLOAD_CHECK_INTERVAL_MS,
|
|
7
|
+
WORKLOAD_REQUEST_TIMEOUT_MS,
|
|
8
|
+
type WorkloadRequest,
|
|
9
|
+
WorkloadResponseSchema,
|
|
10
|
+
workloadSubject,
|
|
11
|
+
} from "./workload-protocol";
|
|
12
|
+
|
|
13
|
+
export type WorkloadCredentialHealth = { status: "ready"; source: "runtime" | "override" } | { status: "unavailable"; reason: string };
|
|
14
|
+
let active: { appId: string; token: string; expiresAt: number } | undefined;
|
|
15
|
+
let health: WorkloadCredentialHealth | undefined;
|
|
16
|
+
let refreshRuntime: { appId: string; refresh: () => Promise<void> } | undefined;
|
|
17
|
+
|
|
18
|
+
/** Refresh only this process's app authority; concurrent broker rejections share one request. */
|
|
19
|
+
export const refreshAppWorkloadCredential = async (appId: string): Promise<string | undefined> => {
|
|
20
|
+
if (appId !== getProcessApplicationId()) return undefined;
|
|
21
|
+
if (refreshRuntime?.appId === appId) await refreshRuntime.refresh();
|
|
22
|
+
return getAppWorkloadCredential();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** One server-side accessor for workers and app-authority invocation clients. Never send to a target app. */
|
|
26
|
+
export const getAppWorkloadCredential = (): string | undefined => {
|
|
27
|
+
const override = env.CLOUD_APP_CREDENTIAL?.trim();
|
|
28
|
+
if (override) return override;
|
|
29
|
+
if (active && active.appId === getProcessApplicationId() && active.expiresAt > Date.now()) return active.token;
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const getAppWorkloadCredentialHealth = (): WorkloadCredentialHealth | undefined => {
|
|
34
|
+
if (env.CLOUD_APP_CREDENTIAL?.trim()) return { status: "ready", source: "override" };
|
|
35
|
+
if (active && active.expiresAt <= Date.now()) return { status: "unavailable", reason: "expired" };
|
|
36
|
+
return health;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const createAppWorkloadCredential = (options: {
|
|
40
|
+
appId: string;
|
|
41
|
+
instanceId: string;
|
|
42
|
+
request?: (request: WorkloadRequest) => Promise<unknown>;
|
|
43
|
+
onUnavailable?: (reason: string) => void;
|
|
44
|
+
onRecovered?: () => void;
|
|
45
|
+
}) => {
|
|
46
|
+
let credentialId: string | null = null;
|
|
47
|
+
let running = false;
|
|
48
|
+
let refreshFailed = false;
|
|
49
|
+
let timer: Timer | undefined;
|
|
50
|
+
let inFlight: Promise<void> | undefined;
|
|
51
|
+
const request =
|
|
52
|
+
options.request ??
|
|
53
|
+
(async (input: WorkloadRequest) => {
|
|
54
|
+
const response = await getProcessNats().request(workloadSubject(connections().nats.namespace), JSON.stringify(input), {
|
|
55
|
+
timeout: WORKLOAD_REQUEST_TIMEOUT_MS,
|
|
56
|
+
});
|
|
57
|
+
return response.json<unknown>();
|
|
58
|
+
});
|
|
59
|
+
const unavailable = (reason: string, preserve = false) => {
|
|
60
|
+
refreshFailed = true;
|
|
61
|
+
if (preserve && active && active.expiresAt > Date.now()) return;
|
|
62
|
+
if (!preserve) {
|
|
63
|
+
active = undefined;
|
|
64
|
+
credentialId = null;
|
|
65
|
+
}
|
|
66
|
+
const changed = health?.status !== "unavailable" || health.reason !== reason;
|
|
67
|
+
health = { status: "unavailable", reason };
|
|
68
|
+
if (changed) options.onUnavailable?.(reason);
|
|
69
|
+
};
|
|
70
|
+
const recovered = () => {
|
|
71
|
+
health = { status: "ready", source: "runtime" };
|
|
72
|
+
if (refreshFailed) options.onRecovered?.();
|
|
73
|
+
refreshFailed = false;
|
|
74
|
+
};
|
|
75
|
+
const refresh = async (): Promise<void> => {
|
|
76
|
+
if (env.CLOUD_APP_CREDENTIAL?.trim()) {
|
|
77
|
+
active = undefined;
|
|
78
|
+
health = { status: "ready", source: "override" };
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const response = WorkloadResponseSchema.parse(
|
|
83
|
+
await request({ operation: "refresh", appId: options.appId, instanceId: options.instanceId, credentialId }),
|
|
84
|
+
);
|
|
85
|
+
if (!running) return;
|
|
86
|
+
if (response.status === "issued") {
|
|
87
|
+
if (
|
|
88
|
+
response.appId !== options.appId ||
|
|
89
|
+
response.instanceId !== options.instanceId ||
|
|
90
|
+
Date.parse(response.expiresAt) <= Date.now()
|
|
91
|
+
) {
|
|
92
|
+
unavailable("invalid_response");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
credentialId = response.credentialId;
|
|
96
|
+
active = { appId: options.appId, token: response.token, expiresAt: Date.parse(response.expiresAt) };
|
|
97
|
+
recovered();
|
|
98
|
+
} else if (response.status === "current" && response.credentialId === credentialId && active && active.expiresAt > Date.now()) {
|
|
99
|
+
recovered();
|
|
100
|
+
} else {
|
|
101
|
+
unavailable(response.status === "current" ? "invalid_response" : response.status, response.status === "unavailable");
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
if (running) {
|
|
105
|
+
unavailable("issuance_unavailable", true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const refreshOnce = (): Promise<void> => {
|
|
110
|
+
if (inFlight) return inFlight;
|
|
111
|
+
const operation = refresh().finally(() => {
|
|
112
|
+
if (inFlight === operation) inFlight = undefined;
|
|
113
|
+
});
|
|
114
|
+
inFlight = operation;
|
|
115
|
+
return operation;
|
|
116
|
+
};
|
|
117
|
+
const schedule = () => {
|
|
118
|
+
if (!running) return;
|
|
119
|
+
timer = setTimeout(
|
|
120
|
+
() => {
|
|
121
|
+
void refreshOnce().finally(schedule);
|
|
122
|
+
},
|
|
123
|
+
refreshFailed || health?.status === "unavailable" ? 5_000 : WORKLOAD_CHECK_INTERVAL_MS,
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
start: async () => {
|
|
128
|
+
if (running) {
|
|
129
|
+
await inFlight;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
running = true;
|
|
133
|
+
refreshRuntime = { appId: options.appId, refresh: refreshOnce };
|
|
134
|
+
health = { status: "unavailable", reason: "starting" };
|
|
135
|
+
await refreshOnce();
|
|
136
|
+
schedule();
|
|
137
|
+
},
|
|
138
|
+
refresh: refreshOnce,
|
|
139
|
+
stop: async () => {
|
|
140
|
+
running = false;
|
|
141
|
+
if (refreshRuntime?.refresh === refreshOnce) refreshRuntime = undefined;
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
await inFlight;
|
|
144
|
+
if (!env.CLOUD_APP_CREDENTIAL?.trim()) {
|
|
145
|
+
await request({ operation: "release", appId: options.appId, instanceId: options.instanceId, credentialId }).catch(() => {
|
|
146
|
+
// Core's registry reconciliation revokes after disappearance even when this reply is lost.
|
|
147
|
+
options.onUnavailable?.("release_unavailable");
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
active = undefined;
|
|
151
|
+
health = undefined;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
@@ -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");
|