@opengeni/api-router 0.3.0 → 0.4.1
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/dist/app.js +1 -1
- package/dist/{chunk-SVBM6RM6.js → chunk-2JL5OXRE.js} +81 -4
- package/dist/chunk-2JL5OXRE.js.map +1 -0
- package/dist/index.js +38 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +67 -1
- package/src/http/auth.ts +1 -1
- package/src/index.ts +9 -4
- package/src/mcp/server.ts +5 -0
- package/src/observability.ts +31 -0
- package/src/routes/billing.ts +19 -1
- package/src/sandbox/auth-callout.ts +5 -1
- package/dist/chunk-SVBM6RM6.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApp
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2JL5OXRE.js";
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { dbSearchPath, getSettings, resolveNatsCalloutConfig, resolveNatsControlPlaneAuth, retryStartupDependency, startupRetryOptions } from "@opengeni/config";
|
|
@@ -9,6 +9,34 @@ import { createNatsEventBus } from "@opengeni/events";
|
|
|
9
9
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
10
10
|
import { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from "@temporalio/client";
|
|
11
11
|
|
|
12
|
+
// src/observability.ts
|
|
13
|
+
function observabilityEventLogger(observability) {
|
|
14
|
+
return {
|
|
15
|
+
debug: (message, attributes) => observability.debug(message, eventAttributes(attributes)),
|
|
16
|
+
warn: (message, attributes) => observability.warn(message, eventAttributes(attributes))
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function eventAttributes(attributes) {
|
|
20
|
+
if (!attributes) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
const sanitized = {};
|
|
24
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
25
|
+
sanitized[key] = eventAttributeValue(value);
|
|
26
|
+
}
|
|
27
|
+
return sanitized;
|
|
28
|
+
}
|
|
29
|
+
function eventAttributeValue(value) {
|
|
30
|
+
if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
// src/sandbox/auth-callout.ts
|
|
13
41
|
import { resolveEnrollmentSigningSecret } from "@opengeni/config";
|
|
14
42
|
import { verifyEnrollmentBearer } from "@opengeni/contracts";
|
|
@@ -94,7 +122,10 @@ async function startAuthCalloutResponder(deps, natsUrl) {
|
|
|
94
122
|
{ kind: "user-password", user: deps.callout.user, pass: deps.callout.password },
|
|
95
123
|
AUTH_CALLOUT_SUBJECT,
|
|
96
124
|
(bytes) => handleAuthorizationRequest(deps, bytes),
|
|
97
|
-
{
|
|
125
|
+
{
|
|
126
|
+
name: "opengeni-auth-callout",
|
|
127
|
+
...deps.observability ? { logger: observabilityEventLogger(deps.observability) } : {}
|
|
128
|
+
}
|
|
98
129
|
);
|
|
99
130
|
deps.observability?.info?.("OpenGeni NATS auth-callout responder started", {
|
|
100
131
|
subject: AUTH_CALLOUT_SUBJECT
|
|
@@ -345,6 +376,9 @@ async function createTemporalWorkflowClient(settings) {
|
|
|
345
376
|
}
|
|
346
377
|
throw error;
|
|
347
378
|
}
|
|
379
|
+
},
|
|
380
|
+
check: async () => {
|
|
381
|
+
await connection.workflowService.getSystemInfo({});
|
|
348
382
|
}
|
|
349
383
|
};
|
|
350
384
|
const documentIndexer = {
|
|
@@ -383,7 +417,8 @@ async function startApi() {
|
|
|
383
417
|
"NATS",
|
|
384
418
|
() => createNatsEventBus(
|
|
385
419
|
settings.natsUrl,
|
|
386
|
-
controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : void 0
|
|
420
|
+
controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : void 0,
|
|
421
|
+
{ logger: observabilityEventLogger(observability) }
|
|
387
422
|
),
|
|
388
423
|
{
|
|
389
424
|
...retryOptions,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/sandbox/auth-callout.ts","../src/sandbox/metrics-ingestion.ts"],"sourcesContent":["import { dbSearchPath, getSettings, resolveNatsCalloutConfig, resolveNatsControlPlaneAuth, retryStartupDependency, startupRetryOptions } from \"@opengeni/config\";\nimport type { ScheduledTask, ScheduledTaskOverlapPolicy, ScheduledTaskScheduleSpec } from \"@opengeni/contracts\";\nimport { createDb } from \"@opengeni/db\";\nimport { createNatsEventBus, type ResponderConnection } from \"@opengeni/events\";\nimport { createObservability, logStartupDependencyRetry } from \"@opengeni/observability\";\nimport { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from \"@temporalio/client\";\nimport type { ScheduleOptions, ScheduleSpec, ScheduleUpdateOptions } from \"@temporalio/client\";\nimport { createApp, type DocumentIndexClient, type SessionWorkflowClient } from \"./app\";\nimport { startAuthCalloutResponder } from \"./sandbox/auth-callout\";\nimport { startHelloIngestion, startMetricsIngestion } from \"./sandbox/metrics-ingestion\";\n\n/**\n * A REJECT_DUPLICATE start collides on the deterministic workflowId when the\n * same manual trigger token fires twice. Temporal surfaces that as\n * WorkflowExecutionAlreadyStartedError; the caller treats it as an idempotent\n * no-op rather than a failure.\n */\nfunction isWorkflowAlreadyStarted(error: unknown): boolean {\n return error instanceof WorkflowExecutionAlreadyStartedError;\n}\n\nconst TEMPORAL_MONTHS = [\n \"JANUARY\",\n \"FEBRUARY\",\n \"MARCH\",\n \"APRIL\",\n \"MAY\",\n \"JUNE\",\n \"JULY\",\n \"AUGUST\",\n \"SEPTEMBER\",\n \"OCTOBER\",\n \"NOVEMBER\",\n \"DECEMBER\",\n] as const;\n\nexport async function createTemporalWorkflowClient(settings: ReturnType<typeof getSettings>): Promise<{\n client: SessionWorkflowClient;\n documentIndexer: DocumentIndexClient;\n close: () => Promise<void>;\n}> {\n const connection = await Connection.connect({ address: settings.temporalHost });\n const temporal = new TemporalClient({\n connection,\n namespace: settings.temporalNamespace,\n });\n const client: SessionWorkflowClient = {\n signalUserMessage: async ({ eventId, workflowId }) => {\n await temporal.workflow.getHandle(workflowId).signal(\"userMessage\", eventId);\n },\n wakeSessionWorkflow: async ({ accountId, workspaceId, sessionId, workflowId }) => {\n await temporal.workflow.signalWithStart(\"sessionWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n workflowIdReusePolicy: \"ALLOW_DUPLICATE\",\n args: [{ accountId, workspaceId, sessionId }],\n signal: \"queueChanged\",\n });\n },\n signalApprovalDecision: async ({ eventId, workflowId }) => {\n await temporal.workflow.getHandle(workflowId).signal(\"approvalDecision\", eventId);\n },\n signalInterrupt: async ({ accountId, workspaceId, sessionId, eventId, workflowId }) => {\n // Start-or-signal: an interrupt POSTed while the session is idle has no\n // running workflow execution to signal, and getHandle().signal() would\n // throw WorkflowNotFoundError -> a 500 (the operator-can't-stop bug). Like\n // wakeSessionWorkflow, signalWithStart delivers the signal to a live run\n // when one exists and otherwise starts a fresh sessionWorkflow that picks\n // the buffered `interrupt` up immediately. ALLOW_DUPLICATE matches the\n // wake path so a running execution is reused rather than rejected.\n await temporal.workflow.signalWithStart(\"sessionWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n workflowIdReusePolicy: \"ALLOW_DUPLICATE\",\n args: [{ accountId, workspaceId, sessionId }],\n signal: \"interrupt\",\n signalArgs: [eventId],\n });\n },\n syncScheduledTask: async ({ task }) => {\n const schedule = temporal.schedule.getHandle(task.temporalScheduleId);\n const options = temporalScheduleOptions(task, settings.temporalTaskQueue);\n try {\n await schedule.update(() => temporalScheduleUpdateOptions(options));\n } catch (error) {\n if (!shouldCreateScheduleAfterUpdateError(error)) {\n throw error;\n }\n await temporal.schedule.create(options);\n }\n },\n deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {\n await temporal.schedule.getHandle(temporalScheduleId).delete().catch(() => undefined);\n },\n\t triggerScheduledTask: async ({ task, agentRunUsageIdempotencyKey, triggerWorkflowId }) => {\n\t // Deterministic workflowId (derived from the trigger token by the\n\t // caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:\n\t // the second start collides on the id and is rejected instead of\n\t // spawning a second run. The shared idempotency key dedupes the charge.\n\t const workflowId = triggerWorkflowId ?? `scheduled-task-${task.id}-manual-${crypto.randomUUID()}`;\n\t try {\n\t await temporal.workflow.start(\"scheduledTaskFireWorkflow\", {\n\t taskQueue: settings.temporalTaskQueue,\n\t workflowId,\n\t workflowIdReusePolicy: \"REJECT_DUPLICATE\",\n\t args: [{\n\t accountId: task.accountId,\n\t workspaceId: task.workspaceId,\n\t taskId: task.id,\n\t triggerType: \"manual\",\n\t agentRunUsageIdempotencyKey,\n\t }],\n\t });\n\t } catch (error) {\n\t // A duplicate trigger token started this run already; treat the retry\n\t // as a no-op so the (idempotent) usage charge stays the only effect.\n\t if (isWorkflowAlreadyStarted(error)) {\n\t return;\n\t }\n\t throw error;\n\t }\n\t },\n };\n const documentIndexer: DocumentIndexClient = {\n indexDocument: async ({ accountId, workspaceId, documentId }) => {\n const workflowId = `document-index-${documentId}-${crypto.randomUUID()}`;\n await temporal.workflow.start(\"documentIndexWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n args: [{ accountId, workspaceId, documentId }],\n });\n },\n };\n return {\n client,\n documentIndexer,\n close: async () => {\n await connection.close();\n },\n };\n}\n\nexport async function startApi() {\n const settings = getSettings();\n const observability = createObservability(settings, { component: \"api\" });\n // Step I: standalone → dbSchema unset → searchPath undefined → today's plain\n // handle (public). Embedded → scoped to the dedicated schema + the host's RLS\n // strategy.\n const searchPath = dbSearchPath(settings);\n const dbClient = createDb(settings.databaseUrl, {\n ...(searchPath ? { searchPath } : {}),\n rlsStrategy: settings.rlsStrategy,\n });\n let bus: Awaited<ReturnType<typeof createNatsEventBus>> | undefined;\n let workflowClient: Awaited<ReturnType<typeof createTemporalWorkflowClient>> | undefined;\n const retryOptions = startupRetryOptions(settings);\n const onRetry = (event: Parameters<typeof logStartupDependencyRetry>[1]) => logStartupDependencyRetry(observability, event);\n // The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with\n // auth_callout, api/worker authenticate as a static account user permitted to\n // request `agent.*.rpc`. Null in local dev (anonymous connect — the bus default).\n const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);\n try {\n bus = await retryStartupDependency(\n \"NATS\",\n () =>\n createNatsEventBus(\n settings.natsUrl,\n controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : undefined,\n ),\n {\n ...retryOptions,\n onRetry,\n },\n );\n workflowClient = await retryStartupDependency(\"Temporal\", () => createTemporalWorkflowClient(settings), {\n ...retryOptions,\n onRetry,\n });\n } catch (error) {\n await Promise.allSettled([\n bus?.close(),\n workflowClient?.close(),\n dbClient.close(),\n ]);\n throw error;\n }\n if (!bus || !workflowClient) {\n await dbClient.close();\n throw new Error(\"OpenGeni API startup dependencies were not initialized\");\n }\n const app = createApp({\n settings,\n db: dbClient.db,\n bus,\n workflowClient: workflowClient.client,\n documentIndexer: workflowClient.documentIndexer,\n observability,\n });\n const server = Bun.serve({\n hostname: settings.apiHost,\n port: settings.apiPort,\n idleTimeout: 255,\n fetch: app.fetch,\n });\n // M10 — start the metrics-ingestion consumer (agent heartbeats → DB last-sample\n // + downsampled series), gated on the selfhosted flag. A no-op when disabled.\n let stopMetricsIngestion: (() => void) | undefined;\n // Reconcile enrollments.has_display to the LIVE capability the agent reports in\n // its connect Hello (has_display was frozen at the enroll-time snapshot). Gated\n // on the same selfhosted flag.\n let stopHelloIngestion: (() => void) | undefined;\n // M-AUTH — start the NATS auth-callout responder (the tenancy boundary): it\n // validates an agent's enrollment bearer presented at NATS connect and mints a\n // workspace-scoped user JWT. Gated on the selfhosted flag + a resolvable callout\n // config; without the callout plane it never starts (selfhosted agents simply\n // cannot connect — graceful). It runs on its OWN connection (the callout auth\n // user), separate from the privileged control-plane bus.\n let authCalloutResponder: ResponderConnection | undefined;\n if (settings.sandboxSelfhostedEnabled) {\n stopMetricsIngestion = startMetricsIngestion({ db: dbClient.db, bus, observability });\n stopHelloIngestion = startHelloIngestion({ db: dbClient.db, bus, observability });\n observability.info(\"OpenGeni machine-metrics + hello ingestion consumers started\", {});\n\n const callout = resolveNatsCalloutConfig(settings);\n if (callout) {\n try {\n authCalloutResponder = await startAuthCalloutResponder(\n { db: dbClient.db, settings, callout, observability },\n settings.natsUrl,\n );\n } catch (error) {\n // A responder start failure must not crash the API (other planes work); log\n // loudly — selfhosted agents will fail to connect until it is up.\n observability.error(\"OpenGeni NATS auth-callout responder failed to start\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n } else {\n observability.warn(\n \"OpenGeni selfhosted enabled but the NATS auth-callout plane is not configured; selfhosted agents cannot connect\",\n {},\n );\n }\n }\n observability.info(\"OpenGeni API listening\", {\n host: settings.apiHost,\n port: settings.apiPort,\n });\n return {\n server,\n close: async () => {\n server.stop(true);\n stopMetricsIngestion?.();\n stopHelloIngestion?.();\n await Promise.allSettled([\n authCalloutResponder?.close(),\n bus.close(),\n workflowClient.close(),\n dbClient.close(),\n ]);\n },\n };\n}\n\nif (import.meta.main) {\n await startApi();\n}\n\nexport function temporalOverlapPolicy(policy: ScheduledTaskOverlapPolicy): ScheduleOverlapPolicy {\n if (policy === \"skip\") {\n return ScheduleOverlapPolicy.SKIP;\n }\n if (policy === \"buffer_one\") {\n return ScheduleOverlapPolicy.BUFFER_ONE;\n }\n return ScheduleOverlapPolicy.ALLOW_ALL;\n}\n\nexport function shouldCreateScheduleAfterUpdateError(error: unknown): boolean {\n return error instanceof ScheduleNotFoundError;\n}\n\nexport function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): ScheduleSpec {\n if (schedule.type === \"interval\") {\n return {\n intervals: [{ every: `${schedule.everySeconds}s` }],\n ...(schedule.startAt ? { startAt: new Date(schedule.startAt) } : {}),\n ...(schedule.endAt ? { endAt: new Date(schedule.endAt) } : {}),\n };\n }\n if (schedule.type === \"calendar\") {\n return {\n calendars: [{\n hour: schedule.hour,\n minute: schedule.minute,\n second: 0,\n ...(schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}),\n }],\n timezone: schedule.timeZone,\n };\n }\n const runAt = new Date(schedule.runAt);\n return {\n calendars: [{\n year: runAt.getUTCFullYear(),\n month: temporalMonth(runAt.getUTCMonth()),\n dayOfMonth: runAt.getUTCDate(),\n hour: runAt.getUTCHours(),\n minute: runAt.getUTCMinutes(),\n second: runAt.getUTCSeconds(),\n }],\n timezone: \"UTC\",\n };\n}\n\nfunction temporalMonth(monthIndex: number) {\n return TEMPORAL_MONTHS[monthIndex]!;\n}\n\nfunction temporalScheduleOptions(task: ScheduledTask, taskQueue: string): ScheduleOptions {\n return {\n scheduleId: task.temporalScheduleId,\n spec: temporalScheduleSpec(task.schedule),\n action: {\n type: \"startWorkflow\",\n workflowType: \"scheduledTaskFireWorkflow\",\n taskQueue,\n args: [{\n accountId: task.accountId,\n workspaceId: task.workspaceId,\n taskId: task.id,\n triggerType: \"scheduled\",\n }],\n },\n policies: {\n overlap: temporalOverlapPolicy(task.overlapPolicy),\n catchupWindow: \"24h\",\n pauseOnFailure: false,\n },\n state: {\n paused: task.status === \"paused\",\n ...(task.schedule.type === \"once\" ? { remainingActions: 1 } : {}),\n },\n memo: {\n accountId: task.accountId,\n workspaceId: task.workspaceId,\n scheduledTaskId: task.id,\n name: task.name,\n },\n };\n}\n\nfunction temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdateOptions {\n return {\n spec: options.spec,\n action: options.action,\n ...(options.policies ? { policies: options.policies } : {}),\n state: options.state ?? {},\n ...(options.searchAttributes ? { searchAttributes: options.searchAttributes } : {}),\n ...(options.typedSearchAttributes ? { typedSearchAttributes: options.typedSearchAttributes } : {}),\n };\n}\n","// apps/api/src/sandbox/auth-callout.ts — the NATS AUTH-CALLOUT responder (the\n// bring-your-own-compute M-AUTH tenancy boundary; dossier §10.1 NATS Accounts per\n// workspace + §17 the isolation smoke + §19 the NATS-Accounts-misconfig leak risk).\n//\n// THE BOUNDARY THIS CLOSES: an external agent connects to NATS presenting its\n// `oge_` enrollment bearer as the connect auth-token. nats-server (configured with\n// `auth_callout`) issues an authorization request on $SYS.REQ.USER.AUTH. THIS\n// responder:\n// 1. decodes the authorization request (the `user_nkey` the response must scope\n// to, the `server_id` for the response `aud`, and the presented `auth_token`);\n// 2. VALIDATES the bearer with verifyEnrollmentBearer (HMAC, via\n// resolveEnrollmentSigningSecret) — an invalid/expired/forged bearer is denied;\n// 3. confirms the enrollment is still ACTIVE in the DB (a revoked machine is\n// denied even with a still-unexpired bearer);\n// 4. signs a NATS user JWT granting pub/sub ONLY `agent.<ws>.>` + `_INBOX.>`\n// (deny-all-else by an allow-list) and returns it inside a signed\n// authorization-response JWT.\n//\n// That per-subject scope is the per-workspace ISOLATION: workspace A's agent is\n// cryptographically incapable of pub/sub on `agent.B.>`. nats-server enforces the\n// signed permission set; the boundary does not rely on subject naming alone (the M4\n// transport test proved the CODE constructs scoped subjects; THIS proves the SERVER\n// refuses cross-workspace access).\n//\n// SECURITY (§18): the bearer value + the account signing seed are NEVER logged. A\n// validation failure → a DENIAL response (the server refuses the connection); a\n// responder error → the request is left UNANSWERED (fail-closed; the server denies\n// on its callout timeout). The bearer's `exp` caps the minted credential's life so a\n// revoked/expired enrollment cannot outlive its bearer.\n\nimport { resolveEnrollmentSigningSecret, type NatsCalloutConfig, type Settings } from \"@opengeni/config\";\nimport { verifyEnrollmentBearer } from \"@opengeni/contracts\";\nimport { getEnrollment, type Database } from \"@opengeni/db\";\nimport {\n createResponderConnection,\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n workspaceAgentPermissions,\n type ResponderConnection,\n} from \"@opengeni/events\";\nimport type { Observability } from \"@opengeni/observability\";\n\n/** The NATS subject nats-server publishes authorization requests on (ADR-26). */\nexport const AUTH_CALLOUT_SUBJECT = \"$SYS.REQ.USER.AUTH\";\n\nexport interface AuthCalloutDeps {\n db: Database;\n settings: Settings;\n callout: NatsCalloutConfig;\n observability?: Observability;\n}\n\n/**\n * The pure validate→scoped-JWT decision, isolated from the NATS transport so it is\n * unit-testable. Given the raw authorization-request JWT bytes, returns the signed\n * authorization-response JWT bytes to reply with — a GRANT (embedding a scoped user\n * JWT) on success, a DENIAL (carrying `nats.error`, no user JWT) otherwise. NEVER\n * throws on a bad/invalid request: every failure becomes a signed denial (the\n * server then refuses the connection cleanly).\n */\nexport async function handleAuthorizationRequest(\n deps: AuthCalloutDeps,\n requestBytes: Uint8Array,\n): Promise<Uint8Array> {\n const requestJwt = Buffer.from(requestBytes).toString(\"utf8\");\n const decoded = decodeAuthRequest(requestJwt);\n if (!decoded) {\n // A malformed request we cannot even read the user_nkey/server_id from — there\n // is nothing to scope a response to. Leave it for the server's timeout by\n // throwing (the transport leaves it unanswered, fail-closed).\n deps.observability?.warn?.(\"auth-callout: undecodable authorization request\", {});\n throw new Error(\"undecodable authorization request\");\n }\n\n const deny = (reason: string): Uint8Array => {\n // A SIGNED denial: the server reads `nats.error` and refuses the connection.\n const response = mintAuthResponse({\n userPublicKey: decoded.userNkey,\n serverId: decoded.serverId,\n accountSeed: deps.callout.accountSeed,\n error: reason,\n });\n return Buffer.from(response, \"utf8\");\n };\n\n const bearer = decoded.authToken;\n if (!bearer) {\n return deny(\"missing enrollment bearer\");\n }\n\n const secret = resolveEnrollmentSigningSecret(deps.settings);\n if (!secret) {\n // The credential plane is off for this deployment — deny rather than mint an\n // unscoped credential. (The responder should not even be running in this case,\n // but fail-closed regardless.)\n return deny(\"enrollment credential plane disabled\");\n }\n\n const claims = await verifyEnrollmentBearer(secret, bearer);\n if (!claims) {\n // Invalid signature / malformed / expired bearer. NEVER log the bearer value.\n deps.observability?.warn?.(\"auth-callout: rejected an invalid enrollment bearer\", {});\n return deny(\"invalid or expired enrollment bearer\");\n }\n\n // Confirm the enrollment is still ACTIVE — a revoked machine is denied even with a\n // still-unexpired bearer (the revoke path flips status; this re-checks at connect).\n const enrollment = await getEnrollment(deps.db, claims.workspaceId, claims.enrollmentId);\n if (!enrollment || enrollment.status !== \"active\") {\n deps.observability?.warn?.(\"auth-callout: denied a revoked or unknown enrollment\", {\n workspaceId: claims.workspaceId,\n agentId: claims.agentId,\n });\n return deny(\"enrollment is not active\");\n }\n\n // Belt-and-braces: the bearer's agentId/enrollmentId must match the row we found.\n // (verifyEnrollmentBearer already binds them; this guards a future schema where\n // agentId != enrollmentId.)\n if (enrollment.id !== claims.enrollmentId) {\n return deny(\"enrollment identity mismatch\");\n }\n\n // GRANT: a user JWT scoped to ONLY this workspace's agent subtree + the reply\n // inbox. This allow-list IS the per-workspace isolation boundary.\n const permissions = workspaceAgentPermissions(claims.workspaceId);\n const userJwt = mintUserJwt({\n userPublicKey: decoded.userNkey,\n accountSeed: deps.callout.accountSeed,\n name: claims.agentId,\n permissions,\n // Server-config-mode placement: the embedded user JWT's `aud` is the account\n // the user binds to (the configured `auth_callout.account`). All agents +\n // the privileged control plane share this account so subjects route; the\n // per-workspace isolation is carried by the subject permissions above.\n audienceAccount: deps.callout.accountName,\n // Tie the credential's life to the bearer's remaining life: a revoked/expired\n // enrollment cannot outlive its bearer at the NATS layer either.\n expiresAtSeconds: claims.exp,\n });\n const response = mintAuthResponse({\n userPublicKey: decoded.userNkey,\n serverId: decoded.serverId,\n accountSeed: deps.callout.accountSeed,\n userJwt,\n });\n deps.observability?.info?.(\"auth-callout: granted a workspace-scoped NATS credential\", {\n workspaceId: claims.workspaceId,\n agentId: claims.agentId,\n });\n return Buffer.from(response, \"utf8\");\n}\n\n/**\n * Start the auth-callout responder: open a SEPARATE NATS connection authenticated\n * as the callout `auth_users` user, subscribe $SYS.REQ.USER.AUTH, and answer every\n * authorization request via {@link handleAuthorizationRequest}. Returns a handle\n * whose `close()` drains the connection. Gated by the caller (sandboxSelfhostedEnabled\n * + a resolvable callout config); a deployment without the callout plane never starts\n * it.\n */\nexport async function startAuthCalloutResponder(\n deps: AuthCalloutDeps,\n natsUrl: string,\n): Promise<ResponderConnection> {\n const connection = await createResponderConnection(\n natsUrl,\n { kind: \"user-password\", user: deps.callout.user, pass: deps.callout.password },\n AUTH_CALLOUT_SUBJECT,\n (bytes) => handleAuthorizationRequest(deps, bytes),\n { name: \"opengeni-auth-callout\" },\n );\n deps.observability?.info?.(\"OpenGeni NATS auth-callout responder started\", {\n subject: AUTH_CALLOUT_SUBJECT,\n });\n return connection;\n}\n","// apps/api/src/sandbox/metrics-ingestion.ts — the M10 metrics INGESTION consumer\n// (dossier §10.7 + §10.6) + the connect-Hello DISPLAY-REFRESH consumer. The\n// enrolled agent piggybacks a `MetricsSample` on its ~5s heartbeat (an\n// `AgentEvent` published one-way on `agent.<ws>.<id>.events`) and publishes a\n// `Hello` (its live self-description) on `agent.<ws>.<id>.hello` on every connect\n// /reconnect. This module owns the two agent→control-plane inbound consumers:\n//\n// `agent.*.*.events` (heartbeat) →\n// 1. touchEnrollmentLastSeen — the liveness cursor (online/reconnecting/offline\n// derivation + the M3 probe disambiguation).\n// 2. ingestMachineMetricsSample — UPSERT machine_metrics_latest (the \"now\" row)\n// + APPEND a machine_metrics_series row downsampled to ~1/min.\n// A GOING-OFFLINE event is not a metrics point — liveness flips via the lease/\n// probe path; we skip it here (no-op).\n//\n// `agent.*.*.hello` (connect) →\n// refreshEnrollmentDisplay — reconcile `enrollments.has_display` to the LIVE\n// capability the Hello reports. `has_display` was previously FROZEN at the\n// enroll-time offer snapshot; a machine that GAINS a display later (a Mac that\n// grants Screen Recording, a box whose Xvfb starts) or LOSES one never\n// re-surfaced. Consuming the Hello's `capabilities.desktop` / `display` makes\n// `has_display` track reality (both directions), which the desktop-capability\n// gate (packages/runtime capabilities.ts) keys off.\n//\n// Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message\n// is logged + swallowed (the bus subscription already swallows handler throws) so\n// a metrics blip / a display-refresh write failure never tears down the consumer,\n// back-pressures the agent, or breaks its connect.\n\nimport {\n getEnrollment,\n ingestMachineMetricsSample,\n setEnrollmentHasDisplay,\n touchEnrollmentLastSeen,\n type Database,\n type MachineMetricsSample,\n} from \"@opengeni/db\";\nimport type { EventBus } from \"@opengeni/events\";\nimport type { Observability } from \"@opengeni/observability\";\nimport { AgentEvent, Hello, type MetricsSample } from \"@opengeni/agent-proto\";\n\n/** The wildcard subject the agent event plane publishes heartbeats on. */\nexport const AGENT_EVENTS_SUBJECT = \"agent.*.*.events\";\n\n/** The wildcard subject the agent publishes its connect Hello on. */\nexport const AGENT_HELLO_SUBJECT = \"agent.*.*.hello\";\n\n/**\n * Parse `agent.<ws>.<id>.<tail>` → `{ workspaceId, agentId }`, requiring the\n * expected tail token. Returns null for a subject that does not match the shape\n * (defensive — the subscription pattern already constrains it).\n */\nfunction parseAgentSubject(subject: string, tail: \"events\" | \"hello\"): { workspaceId: string; agentId: string } | null {\n const parts = subject.split(\".\");\n if (parts.length !== 4 || parts[0] !== \"agent\" || parts[3] !== tail) {\n return null;\n }\n return { workspaceId: parts[1]!, agentId: parts[2]! };\n}\n\n/** Parse `agent.<ws>.<id>.events` → `{ workspaceId, agentId }` (heartbeat plane). */\nexport function parseAgentEventSubject(subject: string): { workspaceId: string; agentId: string } | null {\n return parseAgentSubject(subject, \"events\");\n}\n\n/** Parse `agent.<ws>.<id>.hello` → `{ workspaceId, agentId }` (connect plane). */\nexport function parseAgentHelloSubject(subject: string): { workspaceId: string; agentId: string } | null {\n return parseAgentSubject(subject, \"hello\");\n}\n\n/**\n * Project a wire `MetricsSample` (proto, ms-stamped, GPU as a repeated list) to\n * the DB `MachineMetricsSample`. The proto byte/count fields are protobuf-encoded\n * as decimal strings (uint64) on the TS side (ts-proto `string`); coerce to\n * numbers. The DB carries a single `gpuUtilPercent` + `gpuMemUsedBytes`/Total —\n * we take the FIRST GPU (the dashboard surfaces the primary accelerator); absent\n * GPUs stay null (the not-reported contract). A zero on a non-GPU field is the\n * agent's \"not reported\" (we keep it null-friendly via `nullIfZero` only for the\n * GPU plane; cpu/mem/disk 0 is a legitimate reading the dashboard shows as 0).\n */\nexport function wireSampleToDbSample(wire: MetricsSample): MachineMetricsSample {\n const num = (v: string | number): number => (typeof v === \"number\" ? v : Number(v));\n const firstGpu = wire.gpus[0];\n return {\n cpuPercent: wire.cpuPercent,\n load1: wire.load1,\n load5: wire.load5,\n load15: wire.load15,\n memUsedBytes: num(wire.memUsedBytes),\n memTotalBytes: num(wire.memTotalBytes),\n diskUsedBytes: num(wire.diskUsedBytes),\n diskTotalBytes: num(wire.diskTotalBytes),\n gpuUtilPercent: firstGpu ? firstGpu.utilPercent : null,\n gpuMemUsedBytes: firstGpu ? num(firstGpu.memUsedBytes) : null,\n gpuMemTotalBytes: firstGpu ? num(firstGpu.memTotalBytes) : null,\n contention: wire.runQueue,\n // The sample carries its own wall-clock stamp (epoch ms); fall back to now on\n // a missing/zero stamp so a series row is never NULL-dated.\n sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : new Date(),\n };\n}\n\n/**\n * Ingest ONE decoded heartbeat for an enrolled machine. Resolves the enrollment's\n * accountId (needed for the RLS-scoped writes) from the enrollment row; an\n * unknown/cross-workspace agentId is ignored (no row → no write). Touches\n * last-seen + upserts latest + downsamples the series.\n */\nexport async function ingestHeartbeat(\n db: Database,\n input: { workspaceId: string; agentId: string; sample: MetricsSample },\n): Promise<{ ingested: boolean; seriesAppended: boolean }> {\n // The enrollment row is the source of the accountId (the RLS principal) and the\n // existence check. A revoked machine still reports its accountId, so we ingest\n // (the dashboard shows its last sample); a truly unknown id is a no-op.\n const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);\n if (!enrollment) {\n return { ingested: false, seriesAppended: false };\n }\n const sample = wireSampleToDbSample(input.sample);\n await touchEnrollmentLastSeen(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n });\n const result = await ingestMachineMetricsSample(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n sample,\n });\n return { ingested: true, seriesAppended: result.seriesAppended };\n}\n\n/**\n * Decode a raw `AgentEvent` payload + ingest it (the per-message handler). A\n * heartbeat carrying a metrics sample is ingested; a going-offline (or a\n * heartbeat without metrics) is a no-op. Decode failures are reported + swallowed.\n */\nexport async function handleAgentEventPayload(\n db: Database,\n observability: Observability | undefined,\n payload: Uint8Array,\n subject: string,\n): Promise<void> {\n const ids = parseAgentEventSubject(subject);\n if (!ids) {\n return;\n }\n let event: AgentEvent;\n try {\n event = AgentEvent.decode(payload);\n } catch (error) {\n observability?.warn?.(\"Failed to decode an agent event for metrics ingestion\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n return;\n }\n if (event.event?.$case !== \"heartbeat\") {\n return; // going-offline / unknown → not a metrics point.\n }\n const metrics = event.event.heartbeat.metrics;\n if (!metrics) {\n return; // a heartbeat without a sample → liveness already touched elsewhere.\n }\n try {\n await ingestHeartbeat(db, { workspaceId: ids.workspaceId, agentId: ids.agentId, sample: metrics });\n } catch (error) {\n observability?.warn?.(\"Failed to ingest a machine metrics heartbeat\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\n/**\n * Start the metrics-ingestion consumer: subscribe `agent.*.*.events` and ingest\n * every heartbeat. Gated by sandboxSelfhostedEnabled (the caller checks the flag;\n * a disabled deployment never starts the consumer). Returns the unsubscribe fn.\n */\nexport function startMetricsIngestion(deps: {\n db: Database;\n bus: EventBus;\n observability?: Observability;\n}): () => void {\n return deps.bus.subscribeAgentEvents(AGENT_EVENTS_SUBJECT, (payload, subject) =>\n handleAgentEventPayload(deps.db, deps.observability, payload, subject),\n );\n}\n\n// ── Connect-Hello display refresh ─────────────────────────────────────────────\n\n/**\n * The LIVE display presence the agent's Hello reports: a desktop framebuffer is\n * available (`capabilities.desktop`, which the agent sets true only when a display\n * probes AND it can stream it) OR a `Display` detail is present. An unset\n * Capabilities (or a headless machine) → false. This is what `has_display` should\n * track, replacing the enroll-time snapshot.\n */\nexport function helloReportsDisplay(hello: Hello): boolean {\n const caps = hello.capabilities;\n if (!caps) {\n return false;\n }\n return caps.desktop === true || caps.display != null;\n}\n\n/**\n * Reconcile `enrollments.has_display` to the display presence a Hello reports.\n * Resolves the enrollment (the accountId is the RLS principal + the existence\n * check + the current value). A no-change Hello short-circuits BEFORE issuing any\n * write (and the DB writer is itself change-guarded as a backstop), so a steady\n * state never churns. An unknown/cross-workspace agentId is a no-op.\n */\nexport async function refreshEnrollmentDisplay(\n db: Database,\n input: { workspaceId: string; agentId: string; hasDisplay: boolean },\n): Promise<{ updated: boolean }> {\n const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);\n if (!enrollment) {\n return { updated: false };\n }\n if (enrollment.hasDisplay === input.hasDisplay) {\n // Unchanged — do not even issue the UPDATE (no churn on a steady-state Hello).\n return { updated: false };\n }\n return await setEnrollmentHasDisplay(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n hasDisplay: input.hasDisplay,\n });\n}\n\n/**\n * Decode a raw `Hello` payload + refresh the enrollment's display cursor (the\n * per-message handler for the hello plane). Decode failures + write failures are\n * reported + swallowed — a display refresh must NEVER break the agent's connect.\n */\nexport async function handleHelloPayload(\n db: Database,\n observability: Observability | undefined,\n payload: Uint8Array,\n subject: string,\n): Promise<void> {\n const ids = parseAgentHelloSubject(subject);\n if (!ids) {\n return;\n }\n let hello: Hello;\n try {\n hello = Hello.decode(payload);\n } catch (error) {\n observability?.warn?.(\"Failed to decode an agent Hello for display refresh\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n return;\n }\n try {\n await refreshEnrollmentDisplay(db, {\n workspaceId: ids.workspaceId,\n agentId: ids.agentId,\n hasDisplay: helloReportsDisplay(hello),\n });\n } catch (error) {\n observability?.warn?.(\"Failed to refresh an enrollment's display from a Hello\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\n/**\n * Start the Hello display-refresh consumer: subscribe `agent.*.*.hello` and\n * reconcile `has_display` to the live capability the agent reports on every\n * connect. Gated by sandboxSelfhostedEnabled (the caller checks the flag). Returns\n * the unsubscribe fn.\n */\nexport function startHelloIngestion(deps: {\n db: Database;\n bus: EventBus;\n observability?: Observability;\n}): () => void {\n return deps.bus.subscribeAgentEvents(AGENT_HELLO_SUBJECT, (payload, subject) =>\n handleHelloPayload(deps.db, deps.observability, payload, subject),\n );\n}\n"],"mappings":"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","names":["response","getEnrollment"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/observability.ts","../src/sandbox/auth-callout.ts","../src/sandbox/metrics-ingestion.ts"],"sourcesContent":["import { dbSearchPath, getSettings, resolveNatsCalloutConfig, resolveNatsControlPlaneAuth, retryStartupDependency, startupRetryOptions } from \"@opengeni/config\";\nimport type { ScheduledTask, ScheduledTaskOverlapPolicy, ScheduledTaskScheduleSpec } from \"@opengeni/contracts\";\nimport { createDb } from \"@opengeni/db\";\nimport { createNatsEventBus, type ResponderConnection } from \"@opengeni/events\";\nimport { createObservability, logStartupDependencyRetry } from \"@opengeni/observability\";\nimport { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from \"@temporalio/client\";\nimport type { ScheduleOptions, ScheduleSpec, ScheduleUpdateOptions } from \"@temporalio/client\";\nimport { createApp, type DocumentIndexClient, type SessionWorkflowClient } from \"./app\";\nimport { observabilityEventLogger } from \"./observability\";\nimport { startAuthCalloutResponder } from \"./sandbox/auth-callout\";\nimport { startHelloIngestion, startMetricsIngestion } from \"./sandbox/metrics-ingestion\";\n\n/**\n * A REJECT_DUPLICATE start collides on the deterministic workflowId when the\n * same manual trigger token fires twice. Temporal surfaces that as\n * WorkflowExecutionAlreadyStartedError; the caller treats it as an idempotent\n * no-op rather than a failure.\n */\nfunction isWorkflowAlreadyStarted(error: unknown): boolean {\n return error instanceof WorkflowExecutionAlreadyStartedError;\n}\n\nconst TEMPORAL_MONTHS = [\n \"JANUARY\",\n \"FEBRUARY\",\n \"MARCH\",\n \"APRIL\",\n \"MAY\",\n \"JUNE\",\n \"JULY\",\n \"AUGUST\",\n \"SEPTEMBER\",\n \"OCTOBER\",\n \"NOVEMBER\",\n \"DECEMBER\",\n] as const;\n\nexport async function createTemporalWorkflowClient(settings: ReturnType<typeof getSettings>): Promise<{\n client: SessionWorkflowClient;\n documentIndexer: DocumentIndexClient;\n close: () => Promise<void>;\n}> {\n const connection = await Connection.connect({ address: settings.temporalHost });\n const temporal = new TemporalClient({\n connection,\n namespace: settings.temporalNamespace,\n });\n const client: SessionWorkflowClient = {\n signalUserMessage: async ({ eventId, workflowId }) => {\n await temporal.workflow.getHandle(workflowId).signal(\"userMessage\", eventId);\n },\n wakeSessionWorkflow: async ({ accountId, workspaceId, sessionId, workflowId }) => {\n await temporal.workflow.signalWithStart(\"sessionWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n workflowIdReusePolicy: \"ALLOW_DUPLICATE\",\n args: [{ accountId, workspaceId, sessionId }],\n signal: \"queueChanged\",\n });\n },\n signalApprovalDecision: async ({ eventId, workflowId }) => {\n await temporal.workflow.getHandle(workflowId).signal(\"approvalDecision\", eventId);\n },\n signalInterrupt: async ({ accountId, workspaceId, sessionId, eventId, workflowId }) => {\n // Start-or-signal: an interrupt POSTed while the session is idle has no\n // running workflow execution to signal, and getHandle().signal() would\n // throw WorkflowNotFoundError -> a 500 (the operator-can't-stop bug). Like\n // wakeSessionWorkflow, signalWithStart delivers the signal to a live run\n // when one exists and otherwise starts a fresh sessionWorkflow that picks\n // the buffered `interrupt` up immediately. ALLOW_DUPLICATE matches the\n // wake path so a running execution is reused rather than rejected.\n await temporal.workflow.signalWithStart(\"sessionWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n workflowIdReusePolicy: \"ALLOW_DUPLICATE\",\n args: [{ accountId, workspaceId, sessionId }],\n signal: \"interrupt\",\n signalArgs: [eventId],\n });\n },\n syncScheduledTask: async ({ task }) => {\n const schedule = temporal.schedule.getHandle(task.temporalScheduleId);\n const options = temporalScheduleOptions(task, settings.temporalTaskQueue);\n try {\n await schedule.update(() => temporalScheduleUpdateOptions(options));\n } catch (error) {\n if (!shouldCreateScheduleAfterUpdateError(error)) {\n throw error;\n }\n await temporal.schedule.create(options);\n }\n },\n deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {\n await temporal.schedule.getHandle(temporalScheduleId).delete().catch(() => undefined);\n },\n triggerScheduledTask: async ({ task, agentRunUsageIdempotencyKey, triggerWorkflowId }) => {\n\t // Deterministic workflowId (derived from the trigger token by the\n\t // caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:\n\t // the second start collides on the id and is rejected instead of\n\t // spawning a second run. The shared idempotency key dedupes the charge.\n\t const workflowId = triggerWorkflowId ?? `scheduled-task-${task.id}-manual-${crypto.randomUUID()}`;\n\t try {\n\t await temporal.workflow.start(\"scheduledTaskFireWorkflow\", {\n\t taskQueue: settings.temporalTaskQueue,\n\t workflowId,\n\t workflowIdReusePolicy: \"REJECT_DUPLICATE\",\n\t args: [{\n\t accountId: task.accountId,\n\t workspaceId: task.workspaceId,\n\t taskId: task.id,\n\t triggerType: \"manual\",\n\t agentRunUsageIdempotencyKey,\n\t }],\n\t });\n\t } catch (error) {\n\t // A duplicate trigger token started this run already; treat the retry\n\t // as a no-op so the (idempotent) usage charge stays the only effect.\n\t if (isWorkflowAlreadyStarted(error)) {\n\t return;\n\t }\n throw error;\n }\n },\n check: async () => {\n await connection.workflowService.getSystemInfo({});\n },\n };\n const documentIndexer: DocumentIndexClient = {\n indexDocument: async ({ accountId, workspaceId, documentId }) => {\n const workflowId = `document-index-${documentId}-${crypto.randomUUID()}`;\n await temporal.workflow.start(\"documentIndexWorkflow\", {\n taskQueue: settings.temporalTaskQueue,\n workflowId,\n args: [{ accountId, workspaceId, documentId }],\n });\n },\n };\n return {\n client,\n documentIndexer,\n close: async () => {\n await connection.close();\n },\n };\n}\n\nexport async function startApi() {\n const settings = getSettings();\n const observability = createObservability(settings, { component: \"api\" });\n // Step I: standalone → dbSchema unset → searchPath undefined → today's plain\n // handle (public). Embedded → scoped to the dedicated schema + the host's RLS\n // strategy.\n const searchPath = dbSearchPath(settings);\n const dbClient = createDb(settings.databaseUrl, {\n ...(searchPath ? { searchPath } : {}),\n rlsStrategy: settings.rlsStrategy,\n });\n let bus: Awaited<ReturnType<typeof createNatsEventBus>> | undefined;\n let workflowClient: Awaited<ReturnType<typeof createTemporalWorkflowClient>> | undefined;\n const retryOptions = startupRetryOptions(settings);\n const onRetry = (event: Parameters<typeof logStartupDependencyRetry>[1]) => logStartupDependencyRetry(observability, event);\n // The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with\n // auth_callout, api/worker authenticate as a static account user permitted to\n // request `agent.*.rpc`. Null in local dev (anonymous connect — the bus default).\n const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);\n try {\n bus = await retryStartupDependency(\n \"NATS\",\n () =>\n createNatsEventBus(\n settings.natsUrl,\n controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : undefined,\n { logger: observabilityEventLogger(observability) },\n ),\n {\n ...retryOptions,\n onRetry,\n },\n );\n workflowClient = await retryStartupDependency(\"Temporal\", () => createTemporalWorkflowClient(settings), {\n ...retryOptions,\n onRetry,\n });\n } catch (error) {\n await Promise.allSettled([\n bus?.close(),\n workflowClient?.close(),\n dbClient.close(),\n ]);\n throw error;\n }\n if (!bus || !workflowClient) {\n await dbClient.close();\n throw new Error(\"OpenGeni API startup dependencies were not initialized\");\n }\n const app = createApp({\n settings,\n db: dbClient.db,\n bus,\n workflowClient: workflowClient.client,\n documentIndexer: workflowClient.documentIndexer,\n observability,\n });\n const server = Bun.serve({\n hostname: settings.apiHost,\n port: settings.apiPort,\n idleTimeout: 255,\n fetch: app.fetch,\n });\n // M10 — start the metrics-ingestion consumer (agent heartbeats → DB last-sample\n // + downsampled series), gated on the selfhosted flag. A no-op when disabled.\n let stopMetricsIngestion: (() => void) | undefined;\n // Reconcile enrollments.has_display to the LIVE capability the agent reports in\n // its connect Hello (has_display was frozen at the enroll-time snapshot). Gated\n // on the same selfhosted flag.\n let stopHelloIngestion: (() => void) | undefined;\n // M-AUTH — start the NATS auth-callout responder (the tenancy boundary): it\n // validates an agent's enrollment bearer presented at NATS connect and mints a\n // workspace-scoped user JWT. Gated on the selfhosted flag + a resolvable callout\n // config; without the callout plane it never starts (selfhosted agents simply\n // cannot connect — graceful). It runs on its OWN connection (the callout auth\n // user), separate from the privileged control-plane bus.\n let authCalloutResponder: ResponderConnection | undefined;\n if (settings.sandboxSelfhostedEnabled) {\n stopMetricsIngestion = startMetricsIngestion({ db: dbClient.db, bus, observability });\n stopHelloIngestion = startHelloIngestion({ db: dbClient.db, bus, observability });\n observability.info(\"OpenGeni machine-metrics + hello ingestion consumers started\", {});\n\n const callout = resolveNatsCalloutConfig(settings);\n if (callout) {\n try {\n authCalloutResponder = await startAuthCalloutResponder(\n { db: dbClient.db, settings, callout, observability },\n settings.natsUrl,\n );\n } catch (error) {\n // A responder start failure must not crash the API (other planes work); log\n // loudly — selfhosted agents will fail to connect until it is up.\n observability.error(\"OpenGeni NATS auth-callout responder failed to start\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n } else {\n observability.warn(\n \"OpenGeni selfhosted enabled but the NATS auth-callout plane is not configured; selfhosted agents cannot connect\",\n {},\n );\n }\n }\n observability.info(\"OpenGeni API listening\", {\n host: settings.apiHost,\n port: settings.apiPort,\n });\n return {\n server,\n close: async () => {\n server.stop(true);\n stopMetricsIngestion?.();\n stopHelloIngestion?.();\n await Promise.allSettled([\n authCalloutResponder?.close(),\n bus.close(),\n workflowClient.close(),\n dbClient.close(),\n ]);\n },\n };\n}\n\nif (import.meta.main) {\n await startApi();\n}\n\nexport function temporalOverlapPolicy(policy: ScheduledTaskOverlapPolicy): ScheduleOverlapPolicy {\n if (policy === \"skip\") {\n return ScheduleOverlapPolicy.SKIP;\n }\n if (policy === \"buffer_one\") {\n return ScheduleOverlapPolicy.BUFFER_ONE;\n }\n return ScheduleOverlapPolicy.ALLOW_ALL;\n}\n\nexport function shouldCreateScheduleAfterUpdateError(error: unknown): boolean {\n return error instanceof ScheduleNotFoundError;\n}\n\nexport function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): ScheduleSpec {\n if (schedule.type === \"interval\") {\n return {\n intervals: [{ every: `${schedule.everySeconds}s` }],\n ...(schedule.startAt ? { startAt: new Date(schedule.startAt) } : {}),\n ...(schedule.endAt ? { endAt: new Date(schedule.endAt) } : {}),\n };\n }\n if (schedule.type === \"calendar\") {\n return {\n calendars: [{\n hour: schedule.hour,\n minute: schedule.minute,\n second: 0,\n ...(schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}),\n }],\n timezone: schedule.timeZone,\n };\n }\n const runAt = new Date(schedule.runAt);\n return {\n calendars: [{\n year: runAt.getUTCFullYear(),\n month: temporalMonth(runAt.getUTCMonth()),\n dayOfMonth: runAt.getUTCDate(),\n hour: runAt.getUTCHours(),\n minute: runAt.getUTCMinutes(),\n second: runAt.getUTCSeconds(),\n }],\n timezone: \"UTC\",\n };\n}\n\nfunction temporalMonth(monthIndex: number) {\n return TEMPORAL_MONTHS[monthIndex]!;\n}\n\nfunction temporalScheduleOptions(task: ScheduledTask, taskQueue: string): ScheduleOptions {\n return {\n scheduleId: task.temporalScheduleId,\n spec: temporalScheduleSpec(task.schedule),\n action: {\n type: \"startWorkflow\",\n workflowType: \"scheduledTaskFireWorkflow\",\n taskQueue,\n args: [{\n accountId: task.accountId,\n workspaceId: task.workspaceId,\n taskId: task.id,\n triggerType: \"scheduled\",\n }],\n },\n policies: {\n overlap: temporalOverlapPolicy(task.overlapPolicy),\n catchupWindow: \"24h\",\n pauseOnFailure: false,\n },\n state: {\n paused: task.status === \"paused\",\n ...(task.schedule.type === \"once\" ? { remainingActions: 1 } : {}),\n },\n memo: {\n accountId: task.accountId,\n workspaceId: task.workspaceId,\n scheduledTaskId: task.id,\n name: task.name,\n },\n };\n}\n\nfunction temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdateOptions {\n return {\n spec: options.spec,\n action: options.action,\n ...(options.policies ? { policies: options.policies } : {}),\n state: options.state ?? {},\n ...(options.searchAttributes ? { searchAttributes: options.searchAttributes } : {}),\n ...(options.typedSearchAttributes ? { typedSearchAttributes: options.typedSearchAttributes } : {}),\n };\n}\n","import type { EventLogger } from \"@opengeni/events\";\nimport type { Attributes, AttributeValue, Observability } from \"@opengeni/observability\";\n\nexport function observabilityEventLogger(observability: Observability): EventLogger {\n return {\n debug: (message, attributes) => observability.debug(message, eventAttributes(attributes)),\n warn: (message, attributes) => observability.warn(message, eventAttributes(attributes)),\n };\n}\n\nfunction eventAttributes(attributes: Record<string, unknown> | undefined): Attributes | undefined {\n if (!attributes) {\n return undefined;\n }\n const sanitized: Attributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n sanitized[key] = eventAttributeValue(value);\n }\n return sanitized;\n}\n\nfunction eventAttributeValue(value: unknown): AttributeValue {\n if (value === null || value === undefined || typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n return value;\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n","// apps/api/src/sandbox/auth-callout.ts — the NATS AUTH-CALLOUT responder (the\n// bring-your-own-compute M-AUTH tenancy boundary; dossier §10.1 NATS Accounts per\n// workspace + §17 the isolation smoke + §19 the NATS-Accounts-misconfig leak risk).\n//\n// THE BOUNDARY THIS CLOSES: an external agent connects to NATS presenting its\n// `oge_` enrollment bearer as the connect auth-token. nats-server (configured with\n// `auth_callout`) issues an authorization request on $SYS.REQ.USER.AUTH. THIS\n// responder:\n// 1. decodes the authorization request (the `user_nkey` the response must scope\n// to, the `server_id` for the response `aud`, and the presented `auth_token`);\n// 2. VALIDATES the bearer with verifyEnrollmentBearer (HMAC, via\n// resolveEnrollmentSigningSecret) — an invalid/expired/forged bearer is denied;\n// 3. confirms the enrollment is still ACTIVE in the DB (a revoked machine is\n// denied even with a still-unexpired bearer);\n// 4. signs a NATS user JWT granting pub/sub ONLY `agent.<ws>.>` + `_INBOX.>`\n// (deny-all-else by an allow-list) and returns it inside a signed\n// authorization-response JWT.\n//\n// That per-subject scope is the per-workspace ISOLATION: workspace A's agent is\n// cryptographically incapable of pub/sub on `agent.B.>`. nats-server enforces the\n// signed permission set; the boundary does not rely on subject naming alone (the M4\n// transport test proved the CODE constructs scoped subjects; THIS proves the SERVER\n// refuses cross-workspace access).\n//\n// SECURITY (§18): the bearer value + the account signing seed are NEVER logged. A\n// validation failure → a DENIAL response (the server refuses the connection); a\n// responder error → the request is left UNANSWERED (fail-closed; the server denies\n// on its callout timeout). The bearer's `exp` caps the minted credential's life so a\n// revoked/expired enrollment cannot outlive its bearer.\n\nimport { resolveEnrollmentSigningSecret, type NatsCalloutConfig, type Settings } from \"@opengeni/config\";\nimport { verifyEnrollmentBearer } from \"@opengeni/contracts\";\nimport { getEnrollment, type Database } from \"@opengeni/db\";\nimport {\n createResponderConnection,\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n workspaceAgentPermissions,\n type ResponderConnection,\n} from \"@opengeni/events\";\nimport type { Observability } from \"@opengeni/observability\";\nimport { observabilityEventLogger } from \"../observability\";\n\n/** The NATS subject nats-server publishes authorization requests on (ADR-26). */\nexport const AUTH_CALLOUT_SUBJECT = \"$SYS.REQ.USER.AUTH\";\n\nexport interface AuthCalloutDeps {\n db: Database;\n settings: Settings;\n callout: NatsCalloutConfig;\n observability?: Observability;\n}\n\n/**\n * The pure validate→scoped-JWT decision, isolated from the NATS transport so it is\n * unit-testable. Given the raw authorization-request JWT bytes, returns the signed\n * authorization-response JWT bytes to reply with — a GRANT (embedding a scoped user\n * JWT) on success, a DENIAL (carrying `nats.error`, no user JWT) otherwise. NEVER\n * throws on a bad/invalid request: every failure becomes a signed denial (the\n * server then refuses the connection cleanly).\n */\nexport async function handleAuthorizationRequest(\n deps: AuthCalloutDeps,\n requestBytes: Uint8Array,\n): Promise<Uint8Array> {\n const requestJwt = Buffer.from(requestBytes).toString(\"utf8\");\n const decoded = decodeAuthRequest(requestJwt);\n if (!decoded) {\n // A malformed request we cannot even read the user_nkey/server_id from — there\n // is nothing to scope a response to. Leave it for the server's timeout by\n // throwing (the transport leaves it unanswered, fail-closed).\n deps.observability?.warn?.(\"auth-callout: undecodable authorization request\", {});\n throw new Error(\"undecodable authorization request\");\n }\n\n const deny = (reason: string): Uint8Array => {\n // A SIGNED denial: the server reads `nats.error` and refuses the connection.\n const response = mintAuthResponse({\n userPublicKey: decoded.userNkey,\n serverId: decoded.serverId,\n accountSeed: deps.callout.accountSeed,\n error: reason,\n });\n return Buffer.from(response, \"utf8\");\n };\n\n const bearer = decoded.authToken;\n if (!bearer) {\n return deny(\"missing enrollment bearer\");\n }\n\n const secret = resolveEnrollmentSigningSecret(deps.settings);\n if (!secret) {\n // The credential plane is off for this deployment — deny rather than mint an\n // unscoped credential. (The responder should not even be running in this case,\n // but fail-closed regardless.)\n return deny(\"enrollment credential plane disabled\");\n }\n\n const claims = await verifyEnrollmentBearer(secret, bearer);\n if (!claims) {\n // Invalid signature / malformed / expired bearer. NEVER log the bearer value.\n deps.observability?.warn?.(\"auth-callout: rejected an invalid enrollment bearer\", {});\n return deny(\"invalid or expired enrollment bearer\");\n }\n\n // Confirm the enrollment is still ACTIVE — a revoked machine is denied even with a\n // still-unexpired bearer (the revoke path flips status; this re-checks at connect).\n const enrollment = await getEnrollment(deps.db, claims.workspaceId, claims.enrollmentId);\n if (!enrollment || enrollment.status !== \"active\") {\n deps.observability?.warn?.(\"auth-callout: denied a revoked or unknown enrollment\", {\n workspaceId: claims.workspaceId,\n agentId: claims.agentId,\n });\n return deny(\"enrollment is not active\");\n }\n\n // Belt-and-braces: the bearer's agentId/enrollmentId must match the row we found.\n // (verifyEnrollmentBearer already binds them; this guards a future schema where\n // agentId != enrollmentId.)\n if (enrollment.id !== claims.enrollmentId) {\n return deny(\"enrollment identity mismatch\");\n }\n\n // GRANT: a user JWT scoped to ONLY this workspace's agent subtree + the reply\n // inbox. This allow-list IS the per-workspace isolation boundary.\n const permissions = workspaceAgentPermissions(claims.workspaceId);\n const userJwt = mintUserJwt({\n userPublicKey: decoded.userNkey,\n accountSeed: deps.callout.accountSeed,\n name: claims.agentId,\n permissions,\n // Server-config-mode placement: the embedded user JWT's `aud` is the account\n // the user binds to (the configured `auth_callout.account`). All agents +\n // the privileged control plane share this account so subjects route; the\n // per-workspace isolation is carried by the subject permissions above.\n audienceAccount: deps.callout.accountName,\n // Tie the credential's life to the bearer's remaining life: a revoked/expired\n // enrollment cannot outlive its bearer at the NATS layer either.\n expiresAtSeconds: claims.exp,\n });\n const response = mintAuthResponse({\n userPublicKey: decoded.userNkey,\n serverId: decoded.serverId,\n accountSeed: deps.callout.accountSeed,\n userJwt,\n });\n deps.observability?.info?.(\"auth-callout: granted a workspace-scoped NATS credential\", {\n workspaceId: claims.workspaceId,\n agentId: claims.agentId,\n });\n return Buffer.from(response, \"utf8\");\n}\n\n/**\n * Start the auth-callout responder: open a SEPARATE NATS connection authenticated\n * as the callout `auth_users` user, subscribe $SYS.REQ.USER.AUTH, and answer every\n * authorization request via {@link handleAuthorizationRequest}. Returns a handle\n * whose `close()` drains the connection. Gated by the caller (sandboxSelfhostedEnabled\n * + a resolvable callout config); a deployment without the callout plane never starts\n * it.\n */\nexport async function startAuthCalloutResponder(\n deps: AuthCalloutDeps,\n natsUrl: string,\n): Promise<ResponderConnection> {\n const connection = await createResponderConnection(\n natsUrl,\n { kind: \"user-password\", user: deps.callout.user, pass: deps.callout.password },\n AUTH_CALLOUT_SUBJECT,\n (bytes) => handleAuthorizationRequest(deps, bytes),\n {\n name: \"opengeni-auth-callout\",\n ...(deps.observability ? { logger: observabilityEventLogger(deps.observability) } : {}),\n },\n );\n deps.observability?.info?.(\"OpenGeni NATS auth-callout responder started\", {\n subject: AUTH_CALLOUT_SUBJECT,\n });\n return connection;\n}\n","// apps/api/src/sandbox/metrics-ingestion.ts — the M10 metrics INGESTION consumer\n// (dossier §10.7 + §10.6) + the connect-Hello DISPLAY-REFRESH consumer. The\n// enrolled agent piggybacks a `MetricsSample` on its ~5s heartbeat (an\n// `AgentEvent` published one-way on `agent.<ws>.<id>.events`) and publishes a\n// `Hello` (its live self-description) on `agent.<ws>.<id>.hello` on every connect\n// /reconnect. This module owns the two agent→control-plane inbound consumers:\n//\n// `agent.*.*.events` (heartbeat) →\n// 1. touchEnrollmentLastSeen — the liveness cursor (online/reconnecting/offline\n// derivation + the M3 probe disambiguation).\n// 2. ingestMachineMetricsSample — UPSERT machine_metrics_latest (the \"now\" row)\n// + APPEND a machine_metrics_series row downsampled to ~1/min.\n// A GOING-OFFLINE event is not a metrics point — liveness flips via the lease/\n// probe path; we skip it here (no-op).\n//\n// `agent.*.*.hello` (connect) →\n// refreshEnrollmentDisplay — reconcile `enrollments.has_display` to the LIVE\n// capability the Hello reports. `has_display` was previously FROZEN at the\n// enroll-time offer snapshot; a machine that GAINS a display later (a Mac that\n// grants Screen Recording, a box whose Xvfb starts) or LOSES one never\n// re-surfaced. Consuming the Hello's `capabilities.desktop` / `display` makes\n// `has_display` track reality (both directions), which the desktop-capability\n// gate (packages/runtime capabilities.ts) keys off.\n//\n// Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message\n// is logged + swallowed (the bus subscription already swallows handler throws) so\n// a metrics blip / a display-refresh write failure never tears down the consumer,\n// back-pressures the agent, or breaks its connect.\n\nimport {\n getEnrollment,\n ingestMachineMetricsSample,\n setEnrollmentHasDisplay,\n touchEnrollmentLastSeen,\n type Database,\n type MachineMetricsSample,\n} from \"@opengeni/db\";\nimport type { EventBus } from \"@opengeni/events\";\nimport type { Observability } from \"@opengeni/observability\";\nimport { AgentEvent, Hello, type MetricsSample } from \"@opengeni/agent-proto\";\n\n/** The wildcard subject the agent event plane publishes heartbeats on. */\nexport const AGENT_EVENTS_SUBJECT = \"agent.*.*.events\";\n\n/** The wildcard subject the agent publishes its connect Hello on. */\nexport const AGENT_HELLO_SUBJECT = \"agent.*.*.hello\";\n\n/**\n * Parse `agent.<ws>.<id>.<tail>` → `{ workspaceId, agentId }`, requiring the\n * expected tail token. Returns null for a subject that does not match the shape\n * (defensive — the subscription pattern already constrains it).\n */\nfunction parseAgentSubject(subject: string, tail: \"events\" | \"hello\"): { workspaceId: string; agentId: string } | null {\n const parts = subject.split(\".\");\n if (parts.length !== 4 || parts[0] !== \"agent\" || parts[3] !== tail) {\n return null;\n }\n return { workspaceId: parts[1]!, agentId: parts[2]! };\n}\n\n/** Parse `agent.<ws>.<id>.events` → `{ workspaceId, agentId }` (heartbeat plane). */\nexport function parseAgentEventSubject(subject: string): { workspaceId: string; agentId: string } | null {\n return parseAgentSubject(subject, \"events\");\n}\n\n/** Parse `agent.<ws>.<id>.hello` → `{ workspaceId, agentId }` (connect plane). */\nexport function parseAgentHelloSubject(subject: string): { workspaceId: string; agentId: string } | null {\n return parseAgentSubject(subject, \"hello\");\n}\n\n/**\n * Project a wire `MetricsSample` (proto, ms-stamped, GPU as a repeated list) to\n * the DB `MachineMetricsSample`. The proto byte/count fields are protobuf-encoded\n * as decimal strings (uint64) on the TS side (ts-proto `string`); coerce to\n * numbers. The DB carries a single `gpuUtilPercent` + `gpuMemUsedBytes`/Total —\n * we take the FIRST GPU (the dashboard surfaces the primary accelerator); absent\n * GPUs stay null (the not-reported contract). A zero on a non-GPU field is the\n * agent's \"not reported\" (we keep it null-friendly via `nullIfZero` only for the\n * GPU plane; cpu/mem/disk 0 is a legitimate reading the dashboard shows as 0).\n */\nexport function wireSampleToDbSample(wire: MetricsSample): MachineMetricsSample {\n const num = (v: string | number): number => (typeof v === \"number\" ? v : Number(v));\n const firstGpu = wire.gpus[0];\n return {\n cpuPercent: wire.cpuPercent,\n load1: wire.load1,\n load5: wire.load5,\n load15: wire.load15,\n memUsedBytes: num(wire.memUsedBytes),\n memTotalBytes: num(wire.memTotalBytes),\n diskUsedBytes: num(wire.diskUsedBytes),\n diskTotalBytes: num(wire.diskTotalBytes),\n gpuUtilPercent: firstGpu ? firstGpu.utilPercent : null,\n gpuMemUsedBytes: firstGpu ? num(firstGpu.memUsedBytes) : null,\n gpuMemTotalBytes: firstGpu ? num(firstGpu.memTotalBytes) : null,\n contention: wire.runQueue,\n // The sample carries its own wall-clock stamp (epoch ms); fall back to now on\n // a missing/zero stamp so a series row is never NULL-dated.\n sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : new Date(),\n };\n}\n\n/**\n * Ingest ONE decoded heartbeat for an enrolled machine. Resolves the enrollment's\n * accountId (needed for the RLS-scoped writes) from the enrollment row; an\n * unknown/cross-workspace agentId is ignored (no row → no write). Touches\n * last-seen + upserts latest + downsamples the series.\n */\nexport async function ingestHeartbeat(\n db: Database,\n input: { workspaceId: string; agentId: string; sample: MetricsSample },\n): Promise<{ ingested: boolean; seriesAppended: boolean }> {\n // The enrollment row is the source of the accountId (the RLS principal) and the\n // existence check. A revoked machine still reports its accountId, so we ingest\n // (the dashboard shows its last sample); a truly unknown id is a no-op.\n const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);\n if (!enrollment) {\n return { ingested: false, seriesAppended: false };\n }\n const sample = wireSampleToDbSample(input.sample);\n await touchEnrollmentLastSeen(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n });\n const result = await ingestMachineMetricsSample(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n sample,\n });\n return { ingested: true, seriesAppended: result.seriesAppended };\n}\n\n/**\n * Decode a raw `AgentEvent` payload + ingest it (the per-message handler). A\n * heartbeat carrying a metrics sample is ingested; a going-offline (or a\n * heartbeat without metrics) is a no-op. Decode failures are reported + swallowed.\n */\nexport async function handleAgentEventPayload(\n db: Database,\n observability: Observability | undefined,\n payload: Uint8Array,\n subject: string,\n): Promise<void> {\n const ids = parseAgentEventSubject(subject);\n if (!ids) {\n return;\n }\n let event: AgentEvent;\n try {\n event = AgentEvent.decode(payload);\n } catch (error) {\n observability?.warn?.(\"Failed to decode an agent event for metrics ingestion\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n return;\n }\n if (event.event?.$case !== \"heartbeat\") {\n return; // going-offline / unknown → not a metrics point.\n }\n const metrics = event.event.heartbeat.metrics;\n if (!metrics) {\n return; // a heartbeat without a sample → liveness already touched elsewhere.\n }\n try {\n await ingestHeartbeat(db, { workspaceId: ids.workspaceId, agentId: ids.agentId, sample: metrics });\n } catch (error) {\n observability?.warn?.(\"Failed to ingest a machine metrics heartbeat\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\n/**\n * Start the metrics-ingestion consumer: subscribe `agent.*.*.events` and ingest\n * every heartbeat. Gated by sandboxSelfhostedEnabled (the caller checks the flag;\n * a disabled deployment never starts the consumer). Returns the unsubscribe fn.\n */\nexport function startMetricsIngestion(deps: {\n db: Database;\n bus: EventBus;\n observability?: Observability;\n}): () => void {\n return deps.bus.subscribeAgentEvents(AGENT_EVENTS_SUBJECT, (payload, subject) =>\n handleAgentEventPayload(deps.db, deps.observability, payload, subject),\n );\n}\n\n// ── Connect-Hello display refresh ─────────────────────────────────────────────\n\n/**\n * The LIVE display presence the agent's Hello reports: a desktop framebuffer is\n * available (`capabilities.desktop`, which the agent sets true only when a display\n * probes AND it can stream it) OR a `Display` detail is present. An unset\n * Capabilities (or a headless machine) → false. This is what `has_display` should\n * track, replacing the enroll-time snapshot.\n */\nexport function helloReportsDisplay(hello: Hello): boolean {\n const caps = hello.capabilities;\n if (!caps) {\n return false;\n }\n return caps.desktop === true || caps.display != null;\n}\n\n/**\n * Reconcile `enrollments.has_display` to the display presence a Hello reports.\n * Resolves the enrollment (the accountId is the RLS principal + the existence\n * check + the current value). A no-change Hello short-circuits BEFORE issuing any\n * write (and the DB writer is itself change-guarded as a backstop), so a steady\n * state never churns. An unknown/cross-workspace agentId is a no-op.\n */\nexport async function refreshEnrollmentDisplay(\n db: Database,\n input: { workspaceId: string; agentId: string; hasDisplay: boolean },\n): Promise<{ updated: boolean }> {\n const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);\n if (!enrollment) {\n return { updated: false };\n }\n if (enrollment.hasDisplay === input.hasDisplay) {\n // Unchanged — do not even issue the UPDATE (no churn on a steady-state Hello).\n return { updated: false };\n }\n return await setEnrollmentHasDisplay(db, {\n accountId: enrollment.accountId,\n workspaceId: input.workspaceId,\n enrollmentId: input.agentId,\n hasDisplay: input.hasDisplay,\n });\n}\n\n/**\n * Decode a raw `Hello` payload + refresh the enrollment's display cursor (the\n * per-message handler for the hello plane). Decode failures + write failures are\n * reported + swallowed — a display refresh must NEVER break the agent's connect.\n */\nexport async function handleHelloPayload(\n db: Database,\n observability: Observability | undefined,\n payload: Uint8Array,\n subject: string,\n): Promise<void> {\n const ids = parseAgentHelloSubject(subject);\n if (!ids) {\n return;\n }\n let hello: Hello;\n try {\n hello = Hello.decode(payload);\n } catch (error) {\n observability?.warn?.(\"Failed to decode an agent Hello for display refresh\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n return;\n }\n try {\n await refreshEnrollmentDisplay(db, {\n workspaceId: ids.workspaceId,\n agentId: ids.agentId,\n hasDisplay: helloReportsDisplay(hello),\n });\n } catch (error) {\n observability?.warn?.(\"Failed to refresh an enrollment's display from a Hello\", {\n subject,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\n/**\n * Start the Hello display-refresh consumer: subscribe `agent.*.*.hello` and\n * reconcile `has_display` to the live capability the agent reports on every\n * connect. Gated by sandboxSelfhostedEnabled (the caller checks the flag). Returns\n * the unsubscribe fn.\n */\nexport function startHelloIngestion(deps: {\n db: Database;\n bus: EventBus;\n observability?: Observability;\n}): () => void {\n return deps.bus.subscribeAgentEvents(AGENT_HELLO_SUBJECT, (payload, subject) =>\n handleHelloPayload(deps.db, deps.observability, payload, subject),\n );\n}\n"],"mappings":"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","names":["response","getEnrollment"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
47
|
"@opengeni/agent-proto": "^0.2.1",
|
|
48
48
|
"@opengeni/codex": "^0.2.1",
|
|
49
|
-
"@opengeni/config": "^0.2.
|
|
50
|
-
"@opengeni/contracts": "^0.
|
|
51
|
-
"@opengeni/core": "^0.
|
|
52
|
-
"@opengeni/db": "^0.
|
|
53
|
-
"@opengeni/documents": "^0.2.
|
|
54
|
-
"@opengeni/events": "^0.2.
|
|
55
|
-
"@opengeni/github": "^0.2.
|
|
49
|
+
"@opengeni/config": "^0.2.5",
|
|
50
|
+
"@opengeni/contracts": "^0.7.0",
|
|
51
|
+
"@opengeni/core": "^0.4.1",
|
|
52
|
+
"@opengeni/db": "^0.4.1",
|
|
53
|
+
"@opengeni/documents": "^0.2.5",
|
|
54
|
+
"@opengeni/events": "^0.2.5",
|
|
55
|
+
"@opengeni/github": "^0.2.5",
|
|
56
56
|
"@opengeni/observability": "^0.2.1",
|
|
57
|
-
"@opengeni/runtime": "^0.
|
|
58
|
-
"@opengeni/storage": "^0.2.
|
|
57
|
+
"@opengeni/runtime": "^0.3.1",
|
|
58
|
+
"@opengeni/storage": "^0.2.5",
|
|
59
59
|
"@temporalio/client": "^1.17.0",
|
|
60
60
|
"better-auth": "^1.6.14",
|
|
61
61
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@opengeni/config";
|
|
6
6
|
import { ClientConfig } from "@opengeni/contracts";
|
|
7
7
|
import { createDocumentServices, indexDocumentNow, type DocumentServices } from "@opengeni/documents";
|
|
8
|
+
import { dbSql } from "@opengeni/db";
|
|
8
9
|
import { createObservability } from "@opengeni/observability";
|
|
9
10
|
import { createObjectStorage } from "@opengeni/storage";
|
|
10
11
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -166,15 +167,22 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
166
167
|
service: deps.settings.serviceName,
|
|
167
168
|
environment: deps.settings.environment,
|
|
168
169
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
170
|
+
...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
|
|
169
171
|
ok: true,
|
|
170
172
|
}));
|
|
171
173
|
|
|
172
|
-
app.get("/
|
|
174
|
+
app.get("/readyz", async (c) => {
|
|
175
|
+
const result = await runReadinessChecks(readinessChecks(deps), 2_000);
|
|
176
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.get("/metrics", async (c) => c.text(await observability.prometheusMetrics(), 200, {
|
|
173
180
|
"content-type": "text/plain; version=0.0.4; charset=utf-8",
|
|
174
181
|
}));
|
|
175
182
|
|
|
176
183
|
app.get("/v1/config/client", (c) => c.json(ClientConfig.parse({
|
|
177
184
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
185
|
+
...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
|
|
178
186
|
defaultModel: deps.settings.openaiModel,
|
|
179
187
|
allowedModels: configuredAllowedModels(deps.settings),
|
|
180
188
|
// Provider-grouped model list for the picker. configuredModels() carries the
|
|
@@ -265,8 +273,66 @@ export function httpStatusForError(error: unknown): number {
|
|
|
265
273
|
return 500;
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
type ReadinessCheckName = "db" | "nats" | "temporal";
|
|
277
|
+
type ReadinessChecks = Record<ReadinessCheckName, () => Promise<void> | void>;
|
|
278
|
+
|
|
279
|
+
function readinessChecks(deps: AppDependencies): ReadinessChecks {
|
|
280
|
+
return {
|
|
281
|
+
db: deps.readinessChecks?.db ?? (async () => {
|
|
282
|
+
await deps.db.execute(dbSql`select 1`);
|
|
283
|
+
}),
|
|
284
|
+
nats: deps.readinessChecks?.nats ?? (() => {
|
|
285
|
+
if (deps.bus.isConnected && !deps.bus.isConnected()) {
|
|
286
|
+
throw new Error("NATS is not connected");
|
|
287
|
+
}
|
|
288
|
+
}),
|
|
289
|
+
temporal: deps.readinessChecks?.temporal ?? deps.workflowClient.check ?? (() => {
|
|
290
|
+
throw new Error("Temporal readiness check unavailable");
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function runReadinessChecks(checks: ReadinessChecks, timeoutMs: number): Promise<{
|
|
296
|
+
ok: boolean;
|
|
297
|
+
checks: Record<ReadinessCheckName, { ok: boolean; error?: string }>;
|
|
298
|
+
}> {
|
|
299
|
+
const entries = await Promise.all(
|
|
300
|
+
(Object.entries(checks) as Array<[ReadinessCheckName, () => Promise<void> | void]>)
|
|
301
|
+
.map(async ([name, check]) => {
|
|
302
|
+
try {
|
|
303
|
+
await withTimeout(Promise.resolve().then(check), timeoutMs);
|
|
304
|
+
return [name, { ok: true }] as const;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return [name, { ok: false, error: error instanceof Error ? error.message : String(error) }] as const;
|
|
307
|
+
}
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
const result = Object.fromEntries(entries) as Record<ReadinessCheckName, { ok: boolean; error?: string }>;
|
|
311
|
+
return {
|
|
312
|
+
ok: Object.values(result).every((check) => check.ok),
|
|
313
|
+
checks: result,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
318
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
319
|
+
try {
|
|
320
|
+
return await Promise.race([
|
|
321
|
+
promise,
|
|
322
|
+
new Promise<never>((_, reject) => {
|
|
323
|
+
timer = setTimeout(() => reject(new Error(`readiness check timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
324
|
+
}),
|
|
325
|
+
]);
|
|
326
|
+
} finally {
|
|
327
|
+
if (timer) {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
268
333
|
const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
269
334
|
{ pattern: /^\/healthz$/, label: "/healthz" },
|
|
335
|
+
{ pattern: /^\/readyz$/, label: "/readyz" },
|
|
270
336
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/, label: "/v1/workspaces/:workspaceId/codex/connect/start" },
|
|
271
337
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/poll$/, label: "/v1/workspaces/:workspaceId/codex/connect/poll" },
|
|
272
338
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/, label: "/v1/workspaces/:workspaceId/codex/status" },
|
package/src/http/auth.ts
CHANGED
|
@@ -58,7 +58,7 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
|
|
|
58
58
|
if (installExactPaths.has(path) || isInstallRedirectPath(path)) {
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
61
|
-
if (settings.authAllowHealth && path === "/healthz") {
|
|
61
|
+
if (settings.authAllowHealth && (path === "/healthz" || path === "/readyz")) {
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
64
|
if (settings.authAllowMetrics && path === "/metrics") {
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createObservability, logStartupDependencyRetry } from "@opengeni/observ
|
|
|
6
6
|
import { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from "@temporalio/client";
|
|
7
7
|
import type { ScheduleOptions, ScheduleSpec, ScheduleUpdateOptions } from "@temporalio/client";
|
|
8
8
|
import { createApp, type DocumentIndexClient, type SessionWorkflowClient } from "./app";
|
|
9
|
+
import { observabilityEventLogger } from "./observability";
|
|
9
10
|
import { startAuthCalloutResponder } from "./sandbox/auth-callout";
|
|
10
11
|
import { startHelloIngestion, startMetricsIngestion } from "./sandbox/metrics-ingestion";
|
|
11
12
|
|
|
@@ -92,7 +93,7 @@ export async function createTemporalWorkflowClient(settings: ReturnType<typeof g
|
|
|
92
93
|
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
93
94
|
await temporal.schedule.getHandle(temporalScheduleId).delete().catch(() => undefined);
|
|
94
95
|
},
|
|
95
|
-
|
|
96
|
+
triggerScheduledTask: async ({ task, agentRunUsageIdempotencyKey, triggerWorkflowId }) => {
|
|
96
97
|
// Deterministic workflowId (derived from the trigger token by the
|
|
97
98
|
// caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:
|
|
98
99
|
// the second start collides on the id and is rejected instead of
|
|
@@ -117,9 +118,12 @@ export async function createTemporalWorkflowClient(settings: ReturnType<typeof g
|
|
|
117
118
|
if (isWorkflowAlreadyStarted(error)) {
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
check: async () => {
|
|
125
|
+
await connection.workflowService.getSystemInfo({});
|
|
126
|
+
},
|
|
123
127
|
};
|
|
124
128
|
const documentIndexer: DocumentIndexClient = {
|
|
125
129
|
indexDocument: async ({ accountId, workspaceId, documentId }) => {
|
|
@@ -166,6 +170,7 @@ export async function startApi() {
|
|
|
166
170
|
createNatsEventBus(
|
|
167
171
|
settings.natsUrl,
|
|
168
172
|
controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : undefined,
|
|
173
|
+
{ logger: observabilityEventLogger(observability) },
|
|
169
174
|
),
|
|
170
175
|
{
|
|
171
176
|
...retryOptions,
|
package/src/mcp/server.ts
CHANGED
|
@@ -629,6 +629,11 @@ function registerWorkspaceOrchestrationTools(
|
|
|
629
629
|
description: "Spawn a new agent session (a worker) with an initial message and optional goal, resources (e.g. repositories from github_repositories_list), tools, and workspace environment attachment. Environment attachment happens at creation only — it cannot be added to a running session — and requires the environments:use permission. When targetSandboxId names a machine, workingDir sets the working directory (cwd) the spawned session runs under on that machine.",
|
|
630
630
|
inputSchema: {
|
|
631
631
|
initialMessage: z4.string().min(1),
|
|
632
|
+
// Per-session agent persona/system instructions for the spawned worker
|
|
633
|
+
// (a per-agent-type prompt). Delivered system-level, composed AFTER the
|
|
634
|
+
// workspace persona; never shown in the worker's timeline. Trimmed,
|
|
635
|
+
// non-empty, max 32768 chars (re-validated by the contracts schema).
|
|
636
|
+
instructions: z4.string().min(1).max(32768).optional(),
|
|
632
637
|
goal: z4.unknown().optional(),
|
|
633
638
|
resources: z4.array(z4.unknown()).optional(),
|
|
634
639
|
tools: z4.array(z4.unknown()).optional(),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EventLogger } from "@opengeni/events";
|
|
2
|
+
import type { Attributes, AttributeValue, Observability } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
export function observabilityEventLogger(observability: Observability): EventLogger {
|
|
5
|
+
return {
|
|
6
|
+
debug: (message, attributes) => observability.debug(message, eventAttributes(attributes)),
|
|
7
|
+
warn: (message, attributes) => observability.warn(message, eventAttributes(attributes)),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function eventAttributes(attributes: Record<string, unknown> | undefined): Attributes | undefined {
|
|
12
|
+
if (!attributes) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const sanitized: Attributes = {};
|
|
16
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
17
|
+
sanitized[key] = eventAttributeValue(value);
|
|
18
|
+
}
|
|
19
|
+
return sanitized;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function eventAttributeValue(value: unknown): AttributeValue {
|
|
23
|
+
if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/routes/billing.ts
CHANGED
|
@@ -268,6 +268,7 @@ async function handleCheckoutSessionCompleted(deps: ApiRouteDeps, event: Stripe.
|
|
|
268
268
|
stripeCreditAmountUsd: credit.amountUsd,
|
|
269
269
|
},
|
|
270
270
|
});
|
|
271
|
+
recordCreditMicrosMetric(deps, "topup", credit.amountMicros);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
async function mirrorPaymentIntentCustomer(deps: ApiRouteDeps, event: Stripe.Event): Promise<void> {
|
|
@@ -304,18 +305,23 @@ async function applyRefundDebit(deps: ApiRouteDeps, stripe: Stripe, refund: Stri
|
|
|
304
305
|
if (!accountId) {
|
|
305
306
|
return;
|
|
306
307
|
}
|
|
308
|
+
const idempotencyKey = `stripe:refund:${refund.id}`;
|
|
309
|
+
if (await hasCreditLedgerEntry(deps.db, accountId, idempotencyKey)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
307
312
|
await applyCreditLedgerEntry(deps.db, {
|
|
308
313
|
accountId,
|
|
309
314
|
type: "credit_refund",
|
|
310
315
|
amountMicros: -centsToMicros(refund.amount),
|
|
311
316
|
sourceType: "stripe_refund",
|
|
312
317
|
sourceId: refund.id,
|
|
313
|
-
idempotencyKey
|
|
318
|
+
idempotencyKey,
|
|
314
319
|
metadata: {
|
|
315
320
|
stripeRefundId: refund.id,
|
|
316
321
|
stripePaymentIntentId: paymentIntentId(refund.payment_intent),
|
|
317
322
|
},
|
|
318
323
|
});
|
|
324
|
+
recordCreditMicrosMetric(deps, "refund", centsToMicros(refund.amount));
|
|
319
325
|
}
|
|
320
326
|
|
|
321
327
|
async function holdDisputedCredits(deps: ApiRouteDeps, stripe: Stripe, event: Stripe.Event): Promise<void> {
|
|
@@ -374,6 +380,18 @@ async function mirrorCustomer(deps: ApiRouteDeps, event: Stripe.Event, customer:
|
|
|
374
380
|
});
|
|
375
381
|
}
|
|
376
382
|
|
|
383
|
+
function recordCreditMicrosMetric(deps: ApiRouteDeps, kind: "grant" | "topup" | "refund", amountMicros: number): void {
|
|
384
|
+
if (amountMicros <= 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
deps.observability?.incrementCounter({
|
|
388
|
+
name: "opengeni_credit_micros_total",
|
|
389
|
+
help: "Total credit micros recorded by kind.",
|
|
390
|
+
labels: { kind },
|
|
391
|
+
amount: amountMicros,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
377
395
|
async function metadataForRefund(stripe: Stripe, refund: Stripe.Refund): Promise<Stripe.Metadata | null> {
|
|
378
396
|
if (Object.keys(refund.metadata ?? {}).length > 0) {
|
|
379
397
|
return refund.metadata;
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type ResponderConnection,
|
|
41
41
|
} from "@opengeni/events";
|
|
42
42
|
import type { Observability } from "@opengeni/observability";
|
|
43
|
+
import { observabilityEventLogger } from "../observability";
|
|
43
44
|
|
|
44
45
|
/** The NATS subject nats-server publishes authorization requests on (ADR-26). */
|
|
45
46
|
export const AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
|
|
@@ -169,7 +170,10 @@ export async function startAuthCalloutResponder(
|
|
|
169
170
|
{ kind: "user-password", user: deps.callout.user, pass: deps.callout.password },
|
|
170
171
|
AUTH_CALLOUT_SUBJECT,
|
|
171
172
|
(bytes) => handleAuthorizationRequest(deps, bytes),
|
|
172
|
-
{
|
|
173
|
+
{
|
|
174
|
+
name: "opengeni-auth-callout",
|
|
175
|
+
...(deps.observability ? { logger: observabilityEventLogger(deps.observability) } : {}),
|
|
176
|
+
},
|
|
173
177
|
);
|
|
174
178
|
deps.observability?.info?.("OpenGeni NATS auth-callout responder started", {
|
|
175
179
|
subject: AUTH_CALLOUT_SUBJECT,
|