import { createHash, randomUUID } from "node:crypto"; import { ENVIRONMENT_DEV, ERROR_CODES, HwlabProtocolError, assertProtocolRecord } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID, createProtocolAuditEvent, deriveProtocolActorFromMeta } from "../audit/index.mjs"; import { CLOUD_CORE_MIGRATION_ID, CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION, CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE, CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS } from "./schema.mjs"; export const RUNTIME_STORE_KIND = "memory"; export const RUNTIME_STORE_KIND_POSTGRES = "postgres"; export const RUNTIME_DURABLE_ADAPTER_MISSING = "runtime_durable_adapter_missing"; export const RUNTIME_DURABLE_ADAPTER_UNCONFIGURED = "runtime_durable_adapter_unconfigured"; export const RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING = "runtime_durable_adapter_driver_missing"; export const RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED = "runtime_durable_adapter_auth_blocked"; export const RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED = "runtime_durable_adapter_schema_blocked"; export const RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED = "runtime_durable_adapter_migration_blocked"; export const RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED = "runtime_durable_adapter_query_blocked"; export const RUNTIME_DURABILITY_REQUIRED_EVIDENCE = "runtime_adapter_schema_migration_read_query"; export const RUNTIME_ADAPTER_ENV = "HWLAB_CLOUD_RUNTIME_ADAPTER"; export const RUNTIME_DURABLE_ENV = "HWLAB_CLOUD_RUNTIME_DURABLE"; const requiredPostgresSchema = CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS; const postgresCountTables = Object.freeze([ "gateway_sessions", "box_resources", "box_capabilities", "hardware_operations", "audit_events", "evidence_records" ]); export function createCloudRuntimeStore(options = {}) { return new CloudRuntimeStore(options); } export function createConfiguredCloudRuntimeStore(options = {}) { const env = options.env ?? process.env; const adapter = normalizeRuntimeAdapter(options.adapter ?? env?.[RUNTIME_ADAPTER_ENV]); const durableRequested = options.durable === true || env?.[RUNTIME_DURABLE_ENV] === "1" || env?.[RUNTIME_DURABLE_ENV] === "true"; if (adapter === RUNTIME_STORE_KIND_POSTGRES || durableRequested) { return new PostgresCloudRuntimeStore({ ...options, env, dbUrl: options.dbUrl ?? env?.HWLAB_CLOUD_DB_URL, sslMode: options.sslMode ?? env?.HWLAB_CLOUD_DB_SSL_MODE }); } return createCloudRuntimeStore(options); } export class CloudRuntimeStore { constructor({ now = () => new Date().toISOString() } = {}) { this.now = now; this.gatewaySessions = new Map(); this.boxResources = new Map(); this.boxCapabilities = new Map(); this.hardwareOperations = new Map(); this.auditEvents = new Map(); this.evidenceRecords = new Map(); } summary() { return addRuntimeDurabilityContract({ adapter: RUNTIME_STORE_KIND, durable: false, status: "degraded", blocker: RUNTIME_DURABLE_ADAPTER_MISSING, reason: "L1 runtime writes are process-local only; no DB-backed durable runtime adapter is configured", adapterContract: { required: "DB-backed durable runtime adapter using HWLAB_CLOUD_DB_URL", sourceIssue: "pikasTech/HWLAB#164", secretMaterialRequiredInSource: false }, counts: { gatewaySessions: this.gatewaySessions.size, boxResources: this.boxResources.size, boxCapabilities: this.boxCapabilities.size, hardwareOperations: this.hardwareOperations.size, auditEvents: this.auditEvents.size, evidenceRecords: this.evidenceRecords.size } }); } registerGatewaySession(params = {}, requestMeta = {}) { const now = this.now(); const input = asObject(params.gatewaySession ?? params, "gatewaySession"); const gatewaySession = pruneUndefined({ gatewaySessionId: input.gatewaySessionId || makeId("gws"), projectId: requireProtocolId(input, "projectId"), serviceId: input.serviceId || "hwlab-gateway-simu", gatewayId: input.gatewayId || makeId("gtw"), endpoint: input.endpoint, status: input.status || "connected", environment: ENVIRONMENT_DEV, startedAt: input.startedAt || now, lastSeenAt: input.lastSeenAt || now, stoppedAt: input.stoppedAt, labels: normalizeJsonObject(input.labels) }); assertProtocolRecord("gatewaySession", gatewaySession); this.gatewaySessions.set(gatewaySession.gatewaySessionId, gatewaySession); const auditEvent = this.recordAuditEvent({ requestMeta, params, action: "gateway.session.register", targetType: "gateway_session", targetId: gatewaySession.gatewaySessionId, projectId: gatewaySession.projectId, gatewaySessionId: gatewaySession.gatewaySessionId, outcome: "accepted", metadata: { gatewayId: gatewaySession.gatewayId, serviceId: gatewaySession.serviceId } }); return { registered: true, gatewaySession, auditEvent, persistence: this.summary() }; } registerBoxResource(params = {}, requestMeta = {}) { const now = this.now(); const input = asObject(params.resource ?? params.boxResource ?? params, "boxResource"); const gatewaySessionId = requireProtocolId(input, "gatewaySessionId"); const gatewaySession = this.requireGatewaySession(gatewaySessionId); const projectId = input.projectId || gatewaySession.projectId; const resource = pruneUndefined({ resourceId: input.resourceId || makeId("res"), projectId, gatewaySessionId, boxId: input.boxId || makeId("box"), resourceType: input.resourceType || "simulator_endpoint", name: input.name, state: input.state || "available", environment: ENVIRONMENT_DEV, metadata: normalizeJsonObject(input.metadata), createdAt: input.createdAt || now, updatedAt: input.updatedAt || now }); assertProtocolRecord("boxResource", resource); this.boxResources.set(resource.resourceId, resource); const auditEvent = this.recordAuditEvent({ requestMeta, params, action: "box.resource.register", targetType: "box_resource", targetId: resource.resourceId, projectId: resource.projectId, gatewaySessionId: resource.gatewaySessionId, outcome: "accepted", metadata: { boxId: resource.boxId, resourceType: resource.resourceType } }); return { registered: true, resource, auditEvent, persistence: this.summary() }; } reportBoxCapabilities(params = {}, requestMeta = {}) { const now = this.now(); const inputs = normalizeCapabilityInputs(params); const capabilities = []; for (const input of inputs) { const resourceId = requireProtocolId(input, "resourceId"); const resource = this.requireBoxResource(resourceId); const capability = pruneUndefined({ capabilityId: input.capabilityId || makeId("cap"), resourceId, projectId: input.projectId || resource.projectId, name: requireString(input, "name"), description: input.description, direction: input.direction || "bidirectional", valueType: input.valueType || "object", unit: input.unit, constraints: normalizeJsonObject(input.constraints), mutatesState: input.mutatesState ?? true, createdAt: input.createdAt || now, updatedAt: input.updatedAt || now }); assertProtocolRecord("boxCapability", capability); this.boxCapabilities.set(capability.capabilityId, capability); capabilities.push(capability); } const first = capabilities[0]; const auditEvent = this.recordAuditEvent({ requestMeta, params, action: "box.capability.report", targetType: "box_capability", targetId: first.capabilityId, projectId: first.projectId, gatewaySessionId: this.boxResources.get(first.resourceId)?.gatewaySessionId, outcome: "accepted", metadata: { capabilityIds: capabilities.map((capability) => capability.capabilityId) } }); return { reported: true, capabilities, auditEvent, persistence: this.summary() }; } requestHardwareOperation(params = {}, requestMeta = {}) { const refs = this.requireOperationRefs(params, requestMeta); const now = this.now(); const operation = pruneUndefined({ operationId: params.operationId || makeId("op"), projectId: refs.projectId, gatewaySessionId: refs.gatewaySession.gatewaySessionId, agentSessionId: params.agentSessionId, workerSessionId: params.workerSessionId, resourceId: refs.resource.resourceId, capabilityId: refs.capability.capabilityId, requestedBy: refs.actor.actorId, input: normalizeJsonObject(params.input), status: "accepted", environment: ENVIRONMENT_DEV, requestedAt: params.requestedAt || now, updatedAt: now }); assertProtocolRecord("hardwareOperation", operation); this.hardwareOperations.set(operation.operationId, operation); const auditEvent = this.recordAuditEvent({ requestMeta, params, action: "hardware.operation.request", targetType: "hardware_operation", targetId: operation.operationId, projectId: operation.projectId, gatewaySessionId: operation.gatewaySessionId, workerSessionId: operation.workerSessionId, operationId: operation.operationId, outcome: "accepted", metadata: { resourceId: operation.resourceId, capabilityId: operation.capabilityId } }); return { accepted: true, status: operation.status, operationId: operation.operationId, operation, auditEvent, persistence: this.summary() }; } invokeHardwareShell(params = {}, requestMeta = {}) { const shellInput = normalizeShellInput(params); const operationResult = this.requestHardwareOperation( { ...params, input: { ...normalizeJsonObject(params.input), shell: shellInput } }, requestMeta ); const operation = { ...operationResult.operation, output: { shellExecuted: false, dispatchStatus: "not_connected", message: "cloud-api recorded the shell invoke request; no gateway shell adapter is connected in L1" }, updatedAt: this.now() }; assertProtocolRecord("hardwareOperation", operation); this.hardwareOperations.set(operation.operationId, operation); const evidenceRecord = this.writeEvidenceRecord( { projectId: operation.projectId, operationId: operation.operationId, kind: "trace", mimeType: "application/json", serviceId: CLOUD_API_SERVICE_ID, metadata: { traceId: requestMeta.traceId, gatewaySessionId: operation.gatewaySessionId, resourceId: operation.resourceId, capabilityId: operation.capabilityId, shellExecuted: false, dispatchStatus: "not_connected" }, content: { operationId: operation.operationId, shell: shellInput, dispatchStatus: "not_connected" } }, requestMeta ).evidenceRecord; const auditEvent = this.recordAuditEvent({ requestMeta, params, action: "hardware.invoke.shell", targetType: "hardware_operation", targetId: operation.operationId, projectId: operation.projectId, gatewaySessionId: operation.gatewaySessionId, workerSessionId: operation.workerSessionId, operationId: operation.operationId, outcome: "accepted", metadata: { evidenceId: evidenceRecord.evidenceId, shellExecuted: false, dispatchStatus: "not_connected" } }); return { accepted: true, status: operation.status, operationId: operation.operationId, operation, auditEvent, evidenceRecord, dispatch: operation.output, persistence: this.summary() }; } writeAuditEvent(params = {}, requestMeta = {}) { const eventInput = params.event ?? params.auditEvent ?? params; const auditEvent = eventInput.auditId ? pruneUndefined({ ...eventInput, environment: ENVIRONMENT_DEV }) : this.buildAuditEventFromInput(eventInput, requestMeta); assertProtocolRecord("auditEvent", auditEvent); this.auditEvents.set(auditEvent.auditId, auditEvent); return { written: true, auditEvent, persistence: this.summary() }; } queryAuditEvents(params = {}) { const events = [...this.auditEvents.values()].filter((event) => matchesQuery(event, params, [ "auditId", "traceId", "projectId", "gatewaySessionId", "operationId", "action", "targetId" ])); return { events: limitResults(events, params.limit), count: events.length, persistence: this.summary() }; } writeEvidenceRecord(params = {}, requestMeta = {}) { const now = this.now(); const input = asObject(params.record ?? params.evidenceRecord ?? params, "evidenceRecord"); const content = input.content ?? input.metadata ?? {}; const sha256 = input.sha256 || sha256Hex(content); const evidenceId = input.evidenceId || makeId("evd"); const evidenceRecord = pruneUndefined({ evidenceId, projectId: requireProtocolId(input, "projectId"), operationId: requireProtocolId(input, "operationId"), agentSessionId: input.agentSessionId, workerSessionId: input.workerSessionId, kind: input.kind || "trace", uri: input.uri || `memory://hwlab/evidence/${evidenceId}`, mimeType: input.mimeType, sha256, sizeBytes: input.sizeBytes ?? Buffer.byteLength(stableJson(content), "utf8"), serviceId: input.serviceId || CLOUD_API_SERVICE_ID, environment: ENVIRONMENT_DEV, metadata: { ...normalizeJsonObject(input.metadata), ...(requestMeta.traceId ? { traceId: requestMeta.traceId } : {}) }, createdAt: input.createdAt || now }); assertProtocolRecord("evidenceRecord", evidenceRecord); this.evidenceRecords.set(evidenceRecord.evidenceId, evidenceRecord); return { written: true, evidenceRecord, persistence: this.summary() }; } queryEvidenceRecords(params = {}) { const records = [...this.evidenceRecords.values()].filter((record) => matchesQuery(record, params, [ "evidenceId", "projectId", "operationId", "agentSessionId", "workerSessionId", "kind", "serviceId" ])); return { records: limitResults(records, params.limit), count: records.length, persistence: this.summary() }; } recordAuditEvent(input) { return this.writeAuditEvent(this.buildAuditEventFromInput(input, input.requestMeta), input.requestMeta).auditEvent; } buildAuditEventFromInput(input = {}, requestMeta = {}) { const actor = deriveProtocolActorFromMeta(requestMeta, input.params?.audit ?? input); return createProtocolAuditEvent({ auditId: input.auditId, traceId: requestMeta.traceId || input.traceId, actorType: actor.actorType, actorId: actor.actorId, action: input.action, targetType: input.targetType, targetId: input.targetId, projectId: input.projectId, gatewaySessionId: input.gatewaySessionId, workerSessionId: input.workerSessionId, operationId: input.operationId, serviceId: CLOUD_API_SERVICE_ID, outcome: input.outcome, reason: input.reason || input.params?.audit?.reason, metadata: input.metadata, occurredAt: input.occurredAt }); } requireOperationRefs(params, requestMeta = {}) { const projectId = requireProtocolId(params, "projectId"); const gatewaySession = this.requireGatewaySession(requireProtocolId(params, "gatewaySessionId")); const resource = this.requireBoxResource(requireProtocolId(params, "resourceId")); const capability = this.requireBoxCapability(requireProtocolId(params, "capabilityId")); if (gatewaySession.projectId !== projectId || resource.projectId !== projectId || capability.projectId !== projectId) { throw new HwlabProtocolError("hardware operation references must belong to one project", { code: ERROR_CODES.invalidParams, data: { projectId, gatewaySessionId: gatewaySession.gatewaySessionId, resourceId: resource.resourceId, capabilityId: capability.capabilityId } }); } if (resource.gatewaySessionId !== gatewaySession.gatewaySessionId) { throw new HwlabProtocolError("resource is not registered under the requested gateway session", { code: ERROR_CODES.resourceLocked, data: { gatewaySessionId: gatewaySession.gatewaySessionId, resourceGatewaySessionId: resource.gatewaySessionId, resourceId: resource.resourceId } }); } if (capability.resourceId !== resource.resourceId) { throw new HwlabProtocolError("capability is not registered on the requested resource", { code: ERROR_CODES.capabilityUnavailable, data: { resourceId: resource.resourceId, capabilityId: capability.capabilityId } }); } return { projectId, gatewaySession, resource, capability, actor: deriveProtocolActorFromMeta(requestMeta, params.audit ?? {}) }; } requireGatewaySession(gatewaySessionId) { const gatewaySession = this.gatewaySessions.get(gatewaySessionId); if (!gatewaySession) { throw new HwlabProtocolError(`gateway session ${gatewaySessionId} is not registered`, { code: ERROR_CODES.sessionNotFound, data: { gatewaySessionId } }); } return gatewaySession; } requireBoxResource(resourceId) { const resource = this.boxResources.get(resourceId); if (!resource) { throw new HwlabProtocolError(`box resource ${resourceId} is not registered`, { code: ERROR_CODES.capabilityUnavailable, data: { resourceId } }); } return resource; } requireBoxCapability(capabilityId) { const capability = this.boxCapabilities.get(capabilityId); if (!capability) { throw new HwlabProtocolError(`capability ${capabilityId} is not reported`, { code: ERROR_CODES.capabilityUnavailable, data: { capabilityId } }); } return capability; } } export class PostgresCloudRuntimeStore { constructor({ env = process.env, dbUrl, sslMode = "require", now = () => new Date().toISOString(), queryClient, pgModuleLoader } = {}) { this.env = env; this.dbUrl = dbUrl; this.sslMode = sslMode; this.now = now; this.queryClient = queryClient; this.pgModuleLoader = pgModuleLoader; this.pool = null; this.memory = new CloudRuntimeStore({ now }); this.lastReadiness = this.blockedSummary({ blocker: dbUrl || queryClient ? RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED : RUNTIME_DURABLE_ADAPTER_UNCONFIGURED, reason: dbUrl || queryClient ? "Postgres runtime adapter is configured but has not completed a live schema query" : "Postgres runtime adapter requires HWLAB_CLOUD_DB_URL or an injected query client" }); } summary() { return { ...this.lastReadiness, counts: this.lastReadiness.counts ?? this.memory.summary().counts }; } async readiness() { if (!this.dbUrl && !this.queryClient) { this.lastReadiness = this.blockedSummary({ blocker: RUNTIME_DURABLE_ADAPTER_UNCONFIGURED, reason: "Postgres runtime adapter is selected but HWLAB_CLOUD_DB_URL is not injected" }); return this.summary(); } let rows; try { const result = await this.query( "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ANY($1::text[])", [Object.keys(requiredPostgresSchema)] ); rows = Array.isArray(result?.rows) ? result.rows : []; } catch (error) { this.lastReadiness = this.blockedSummary(classifyRuntimeDbError(error)); return this.summary(); } const schema = summarizeRuntimeSchema(rows); if (!schema.ready) { this.lastReadiness = this.blockedSummary({ blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED, reason: "Postgres runtime adapter connected, but required runtime tables or columns are missing", schema, connection: { queryAttempted: true, queryResult: "schema_blocked" }, gates: runtimeGates({ auth: readyGate(), schema: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED }), migration: notCheckedGate(), durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED }) }) }); return this.summary(); } let migration; try { migration = await this.readMigrationReadiness(); } catch (error) { const classified = classifyRuntimeDbError(error); this.lastReadiness = this.blockedSummary({ blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? classified.blocker : RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED, reason: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? classified.reason : "Postgres runtime schema is present, but the durable runtime migration ledger could not be proven", schema, migration: blockedRuntimeMigration({ errorCode: classified.connection?.errorCode }), connection: { queryAttempted: true, queryResult: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? "auth_blocked" : "migration_blocked", errorCode: classified.connection?.errorCode ?? null }, gates: runtimeGates({ auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? blockedGate({ checked: true, blocker: classified.blocker }) : readyGate(), schema: readyGate(), migration: blockedGate({ checked: true, blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? classified.blocker : RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }), durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }) }) }); return this.summary(); } if (!migration.ready) { this.lastReadiness = this.blockedSummary({ blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED, reason: "Postgres runtime schema is present, but the required source migration is not recorded", schema, migration, connection: { queryAttempted: true, queryResult: "migration_blocked" }, gates: runtimeGates({ auth: readyGate(), schema: readyGate(), migration: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }), durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }) }) }); return this.summary(); } let counts; try { counts = await this.readCounts(); } catch (error) { const classified = classifyRuntimeDbError(error); this.lastReadiness = this.blockedSummary({ ...classified, schema, migration, connection: { queryAttempted: true, queryResult: classified.connection?.queryResult ?? "query_blocked", errorCode: classified.connection?.errorCode ?? null }, gates: runtimeGates({ auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? blockedGate({ checked: true, blocker: classified.blocker }) : readyGate(), schema: readyGate(), migration: readyGate(), durability: blockedGate({ checked: true, blocker: classified.blocker }) }) }); return this.summary(); } this.lastReadiness = addRuntimeDurabilityContract({ adapter: RUNTIME_STORE_KIND_POSTGRES, durable: true, durableRequested: true, durableCapable: true, ready: true, status: "ready", blocker: null, reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries", liveRuntimeEvidence: true, fixtureEvidence: false, connection: { queryAttempted: true, queryResult: "durable_readiness_ready", endpointRedacted: true, valueRedacted: true }, schema, migration, gates: runtimeGates({ auth: readyGate(), schema: readyGate(), migration: readyGate(), durability: readyGate() }), safety: runtimeSafety(), adapterContract: postgresAdapterContract(), counts }); return this.summary(); } async registerGatewaySession(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); const before = snapshotMemory(this.memory); const result = this.memory.registerGatewaySession(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async registerBoxResource(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); const input = asObject(params.resource ?? params.boxResource ?? params, "boxResource"); await this.hydrateGatewaySession(input.gatewaySessionId); const before = snapshotMemory(this.memory); const result = this.memory.registerBoxResource(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async reportBoxCapabilities(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); const inputs = normalizeCapabilityInputs(params); for (const input of inputs) { await this.hydrateBoxResource(input.resourceId); } const before = snapshotMemory(this.memory); const result = this.memory.reportBoxCapabilities(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async requestHardwareOperation(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); await this.hydrateOperationRefs(params); const before = snapshotMemory(this.memory); const result = this.memory.requestHardwareOperation(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async invokeHardwareShell(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); await this.hydrateOperationRefs(params); const before = snapshotMemory(this.memory); const result = this.memory.invokeHardwareShell(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async writeAuditEvent(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); const before = snapshotMemory(this.memory); const result = this.memory.writeAuditEvent(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async queryAuditEvents(params = {}) { const result = await this.query( "SELECT event_json FROM audit_events ORDER BY timestamp ASC", [] ); const events = result.rows .map((row) => parseJsonColumn(row.event_json, null)) .filter(Boolean) .filter((event) => matchesQuery(event, params, [ "auditId", "traceId", "projectId", "gatewaySessionId", "operationId", "action", "targetId" ])); return { events: limitResults(events, params.limit), count: events.length, persistence: this.summary() }; } async writeEvidenceRecord(params = {}, requestMeta = {}) { await this.assertReadyForWrites(); const before = snapshotMemory(this.memory); const result = this.memory.writeEvidenceRecord(params, requestMeta); await this.persistChanges(before); return withPersistence(result, this.summary()); } async queryEvidenceRecords(params = {}) { const result = await this.query( "SELECT metadata_json FROM evidence_records ORDER BY created_at ASC", [] ); const records = result.rows .map((row) => parseJsonColumn(row.metadata_json, null)) .filter(Boolean) .filter((record) => matchesQuery(record, params, [ "evidenceId", "projectId", "operationId", "agentSessionId", "workerSessionId", "kind", "serviceId" ])); return { records: limitResults(records, params.limit), count: records.length, persistence: this.summary() }; } async hydrateOperationRefs(params = {}) { await this.hydrateGatewaySession(params.gatewaySessionId); await this.hydrateBoxResource(params.resourceId); await this.hydrateBoxCapability(params.capabilityId); } async hydrateGatewaySession(gatewaySessionId) { if (!gatewaySessionId || this.memory.gatewaySessions.has(gatewaySessionId)) return; const result = await this.query("SELECT gateway_session_json FROM gateway_sessions WHERE id = $1 LIMIT 1", [ gatewaySessionId ]); const record = parseJsonColumn(result.rows?.[0]?.gateway_session_json, null); if (record) this.memory.gatewaySessions.set(record.gatewaySessionId, record); } async hydrateBoxResource(resourceId) { if (!resourceId || this.memory.boxResources.has(resourceId)) return; const result = await this.query("SELECT resource_json FROM box_resources WHERE id = $1 LIMIT 1", [ resourceId ]); const record = parseJsonColumn(result.rows?.[0]?.resource_json, null); if (record) { await this.hydrateGatewaySession(record.gatewaySessionId); this.memory.boxResources.set(record.resourceId, record); } } async hydrateBoxCapability(capabilityId) { if (!capabilityId || this.memory.boxCapabilities.has(capabilityId)) return; const result = await this.query("SELECT capability_json FROM box_capabilities WHERE id = $1 LIMIT 1", [ capabilityId ]); const record = parseJsonColumn(result.rows?.[0]?.capability_json, null); if (record) { await this.hydrateBoxResource(record.resourceId); this.memory.boxCapabilities.set(record.capabilityId, record); } } async persistChanges(before) { for (const record of newRecords(this.memory.gatewaySessions, before.gatewaySessions)) { await this.persistGatewaySession(record); } for (const record of newRecords(this.memory.boxResources, before.boxResources)) { await this.persistBoxResource(record); } for (const record of newRecords(this.memory.boxCapabilities, before.boxCapabilities)) { await this.persistBoxCapability(record); } for (const record of changedRecords(this.memory.hardwareOperations, before.hardwareOperations)) { await this.persistHardwareOperation(record); } for (const record of newRecords(this.memory.auditEvents, before.auditEvents)) { await this.persistAuditEvent(record); } for (const record of newRecords(this.memory.evidenceRecords, before.evidenceRecords)) { await this.persistEvidenceRecord(record); } } async assertReadyForWrites() { const readiness = await this.readiness(); if (readiness.ready !== true) { throw new HwlabProtocolError("Postgres durable runtime adapter is not ready for writes", { code: ERROR_CODES.internalError, data: { adapter: RUNTIME_STORE_KIND_POSTGRES, blocker: readiness.blocker, status: readiness.status, schemaReady: Boolean(readiness.schema?.ready) }, context: { result: "failed" } }); } } async persistGatewaySession(record) { await this.query( "INSERT INTO gateway_sessions (id, project_id, gateway_service_id, status, started_at, ended_at, gateway_session_json) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, gateway_service_id = EXCLUDED.gateway_service_id, status = EXCLUDED.status, started_at = EXCLUDED.started_at, ended_at = EXCLUDED.ended_at, gateway_session_json = EXCLUDED.gateway_session_json", [ record.gatewaySessionId, record.projectId, record.serviceId, record.status, record.startedAt, record.stoppedAt ?? null, stableJson(record) ] ); } async persistBoxResource(record) { await this.query( "INSERT INTO box_resources (id, project_id, gateway_session_id, resource_state, labels_json, resource_json, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, gateway_session_id = EXCLUDED.gateway_session_id, resource_state = EXCLUDED.resource_state, labels_json = EXCLUDED.labels_json, resource_json = EXCLUDED.resource_json, updated_at = EXCLUDED.updated_at", [ record.resourceId, record.projectId, record.gatewaySessionId, record.state, stableJson(record.metadata ?? {}), stableJson(record), record.updatedAt ] ); } async persistBoxCapability(record) { await this.query( "INSERT INTO box_capabilities (id, box_resource_id, capability_type, capability_json, updated_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO UPDATE SET box_resource_id = EXCLUDED.box_resource_id, capability_type = EXCLUDED.capability_type, capability_json = EXCLUDED.capability_json, updated_at = EXCLUDED.updated_at", [ record.capabilityId, record.resourceId, record.name, stableJson(record), record.updatedAt ] ); } async persistHardwareOperation(record) { await this.query( "INSERT INTO hardware_operations (id, project_id, requested_by, operation_type, operation_json, status, requested_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, requested_by = EXCLUDED.requested_by, operation_type = EXCLUDED.operation_type, operation_json = EXCLUDED.operation_json, status = EXCLUDED.status, requested_at = EXCLUDED.requested_at, updated_at = EXCLUDED.updated_at", [ record.operationId, record.projectId, record.requestedBy, record.input?.shell ? "hardware.invoke.shell" : "hardware.operation.request", stableJson(record), record.status, record.requestedAt, record.updatedAt ] ); } async persistAuditEvent(record) { await this.query( "INSERT INTO audit_events (id, request_id, actor, source, operation, target, result, timestamp, event_json) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO UPDATE SET request_id = EXCLUDED.request_id, actor = EXCLUDED.actor, source = EXCLUDED.source, operation = EXCLUDED.operation, target = EXCLUDED.target, result = EXCLUDED.result, timestamp = EXCLUDED.timestamp, event_json = EXCLUDED.event_json", [ record.auditId, record.traceId, stableJson({ type: record.actorType, id: record.actorId }), stableJson({ serviceId: record.serviceId, environment: record.environment }), record.action, stableJson({ type: record.targetType, id: record.targetId }), record.outcome ?? "accepted", record.occurredAt, stableJson(record) ] ); } async persistEvidenceRecord(record) { await this.query( "INSERT INTO evidence_records (id, project_id, operation_id, evidence_type, uri, metadata_json, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, operation_id = EXCLUDED.operation_id, evidence_type = EXCLUDED.evidence_type, uri = EXCLUDED.uri, metadata_json = EXCLUDED.metadata_json, created_at = EXCLUDED.created_at", [ record.evidenceId, record.projectId, record.operationId, record.kind, record.uri, stableJson(record), record.createdAt ] ); } async readCounts() { const counts = {}; for (const table of postgresCountTables) { const result = await this.query(`SELECT COUNT(*)::int AS count FROM ${table}`, []); counts[toCountKey(table)] = Number(result.rows?.[0]?.count ?? 0); } return counts; } async readMigrationReadiness() { const result = await this.query( `SELECT id, schema_version FROM ${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE} WHERE id = $1 LIMIT 1`, [CLOUD_CORE_MIGRATION_ID] ); const row = result.rows?.[0] ?? null; const ready = row?.id === CLOUD_CORE_MIGRATION_ID && row?.schema_version === CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION; return { checked: true, ready, table: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE, requiredMigrationId: CLOUD_CORE_MIGRATION_ID, requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION, appliedMigrationId: row?.id ?? null, appliedSchemaVersion: row?.schema_version ?? null, missing: !row }; } async query(sql, params = []) { const client = await this.getQueryClient(); return client.query(sql, params); } async getQueryClient() { if (this.queryClient) { return this.queryClient; } if (this.pool) { return this.pool; } let pg; try { pg = await (this.pgModuleLoader ? this.pgModuleLoader() : import("pg")); } catch (error) { if (error?.code === "ERR_MODULE_NOT_FOUND") { const driverError = new Error("Postgres runtime adapter requires the pg package"); driverError.code = "HWLAB_PG_DRIVER_MISSING"; throw driverError; } throw error; } const Pool = pg.Pool ?? pg.default?.Pool; if (typeof Pool !== "function") { const driverError = new Error("Postgres runtime adapter could not load pg.Pool"); driverError.code = "HWLAB_PG_DRIVER_MISSING"; throw driverError; } this.pool = new Pool({ connectionString: this.dbUrl, ssl: this.sslMode === "require" ? { rejectUnauthorized: false } : false, connectionTimeoutMillis: normalizeRuntimeTimeoutMs(this.env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS) }); return this.pool; } blockedSummary({ blocker, reason, schema, migration, connection, gates }) { const blockedSchema = schema ?? { ready: false, checked: false, missingTables: [], missingColumns: [] }; const blockedMigration = migration ?? notCheckedRuntimeMigration(); return addRuntimeDurabilityContract({ adapter: RUNTIME_STORE_KIND_POSTGRES, durable: false, durableRequested: true, durableCapable: false, ready: false, status: "blocked", blocker, reason, liveRuntimeEvidence: false, fixtureEvidence: false, connection: { queryAttempted: Boolean(connection?.queryAttempted), queryResult: connection?.queryResult ?? "not_ready", endpointRedacted: true, valueRedacted: true, errorCode: connection?.errorCode ?? null }, schema: blockedSchema, migration: blockedMigration, gates: gates ?? runtimeGates({ auth: connection?.queryAttempted ? blockedGate({ checked: true, blocker }) : notCheckedGate(), schema: blockedSchema.checked ? gateFromReadiness(blockedSchema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED }) : notCheckedGate(), migration: blockedMigration.checked ? gateFromReadiness(blockedMigration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }) : notCheckedGate(), durability: blockedGate({ checked: false, blocker }) }), safety: runtimeSafety(), adapterContract: postgresAdapterContract() }); } } function normalizeCapabilityInputs(params) { const value = params.capabilities ?? params.capability ?? params; if (Array.isArray(value)) { if (value.length === 0) { throw new HwlabProtocolError("capabilities must not be empty", { code: ERROR_CODES.invalidParams }); } return value.map((item) => asObject(item, "capability")); } return [asObject(value, "capability")]; } function normalizeShellInput(params) { const input = normalizeJsonObject(params.input); const command = params.command ?? input.command; if (command === undefined || command === null || command === "") { throw new HwlabProtocolError("hardware.invoke.shell requires input.command or command", { code: ERROR_CODES.invalidParams, data: { required: ["projectId", "gatewaySessionId", "resourceId", "capabilityId", "input.command"] } }); } if (Array.isArray(command)) { return { command: command.map((item) => String(item)), cwd: input.cwd, timeoutMs: input.timeoutMs }; } return { command: String(command), cwd: input.cwd, timeoutMs: input.timeoutMs }; } function asObject(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new HwlabProtocolError(`${label} must be a JSON object`, { code: ERROR_CODES.invalidParams }); } return value; } function requireString(input, field) { if (typeof input[field] !== "string" || input[field].trim() === "") { throw new HwlabProtocolError(`${field} is required`, { code: ERROR_CODES.invalidParams, data: { field } }); } return input[field]; } function requireProtocolId(input, field) { const value = requireString(input, field); return value; } function normalizeJsonObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return {}; } return { ...value }; } function pruneUndefined(input) { return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined)); } function makeId(prefix) { return `${prefix}_${randomUUID()}`; } function stableJson(value) { return JSON.stringify(sortJson(value)); } function sha256Hex(value) { return createHash("sha256").update(stableJson(value)).digest("hex"); } function sortJson(value) { if (Array.isArray(value)) { return value.map(sortJson); } if (value && typeof value === "object") { return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])])); } return value; } function matchesQuery(record, params, fields) { for (const field of fields) { if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) { return false; } } return true; } function limitResults(records, limit) { const parsed = Number.parseInt(limit ?? "", 10); if (!Number.isInteger(parsed) || parsed <= 0) { return records; } return records.slice(0, parsed); } function normalizeRuntimeAdapter(value) { if (typeof value !== "string" || value.trim() === "") { return RUNTIME_STORE_KIND; } const normalized = value.trim().toLowerCase(); if (["postgres", "postgresql", "pg", "db"].includes(normalized)) { return RUNTIME_STORE_KIND_POSTGRES; } return RUNTIME_STORE_KIND; } function postgresAdapterContract() { return { required: "Postgres-backed durable runtime adapter using HWLAB_CLOUD_DB_URL", adapterEnv: RUNTIME_ADAPTER_ENV, adapterEnvValue: RUNTIME_STORE_KIND_POSTGRES, sourceIssue: "pikasTech/HWLAB#164", secretMaterialRequiredInSource: false, fixtureEvidenceAllowed: false }; } function addRuntimeDurabilityContract(summary) { return { ...summary, durabilityContract: { ready: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true, status: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true ? "ready" : "blocked", requiredEvidence: RUNTIME_DURABILITY_REQUIRED_EVIDENCE, dbLiveEvidenceIsDurabilityEvidence: false, liveRuntimeEvidence: Boolean(summary.liveRuntimeEvidence), adapterQueryRequired: true, blockedLayer: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true ? null : durabilityBlockedLayer(summary), blocker: summary.blocker ?? null, secretMaterialRead: false } }; } function durabilityBlockedLayer(summary = {}) { const blocker = summary.blocker; if ( blocker === RUNTIME_DURABLE_ADAPTER_MISSING || blocker === RUNTIME_DURABLE_ADAPTER_UNCONFIGURED || blocker === RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING ) { return "adapter"; } if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) { return "auth"; } if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) { return "schema"; } if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) { return "migration"; } if (summary.connection?.queryAttempted || blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED) { return "durability_query"; } return "adapter"; } function runtimeSafety() { return { devOnly: true, secretsRead: false, secretMaterialRead: false, valuesRedacted: true, endpointRedacted: true }; } function classifyRuntimeDbError(error) { const code = typeof error?.code === "string" ? error.code : "UNKNOWN"; if (code === "HWLAB_PG_DRIVER_MISSING") { return { blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING, reason: "Postgres runtime adapter is selected, but the pg package is not installed in the runtime image", connection: { queryAttempted: false, queryResult: "driver_missing", errorCode: code } }; } if (["28P01", "28000", "42501"].includes(code)) { return { blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED, reason: "Postgres runtime adapter reached the DB driver but authentication or authorization blocked the schema query", connection: { queryAttempted: true, queryResult: "auth_blocked", errorCode: code } }; } if (["42P01", "42703", "3F000"].includes(code)) { return { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED, reason: "Postgres runtime adapter reached the database, but the runtime schema is missing or incompatible", connection: { queryAttempted: true, queryResult: "schema_blocked", errorCode: code } }; } return { blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED, reason: "Postgres runtime adapter could not complete a live runtime schema query", connection: { queryAttempted: true, queryResult: "query_blocked", errorCode: code } }; } function summarizeRuntimeSchema(rows) { const columnsByTable = new Map(); for (const row of rows) { const table = row.table_name; const column = row.column_name; if (!table || !column) continue; if (!columnsByTable.has(table)) { columnsByTable.set(table, new Set()); } columnsByTable.get(table).add(column); } const missingTables = []; const missingColumns = []; for (const [table, columns] of Object.entries(requiredPostgresSchema)) { const observed = columnsByTable.get(table); if (!observed) { missingTables.push(table); missingColumns.push(...columns.map((column) => `${table}.${column}`)); continue; } for (const column of columns) { if (!observed.has(column)) { missingColumns.push(`${table}.${column}`); } } } return { ready: missingTables.length === 0 && missingColumns.length === 0, checked: true, requiredTables: Object.keys(requiredPostgresSchema), missingTables, missingColumns }; } function notCheckedRuntimeMigration() { return { checked: false, ready: false, table: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE, requiredMigrationId: CLOUD_CORE_MIGRATION_ID, requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION, appliedMigrationId: null, appliedSchemaVersion: null, missing: true }; } function blockedRuntimeMigration({ errorCode = null } = {}) { return { ...notCheckedRuntimeMigration(), checked: true, errorCode }; } function runtimeGates({ auth = notCheckedGate(), schema = notCheckedGate(), migration = notCheckedGate(), durability = notCheckedGate() } = {}) { return { auth, schema, migration, durability }; } function readyGate() { return { checked: true, ready: true, status: "ready", blocker: null }; } function blockedGate({ checked = true, blocker = "runtime_durable_adapter_blocked" } = {}) { return { checked, ready: false, status: checked ? "blocked" : "not_checked", blocker }; } function notCheckedGate() { return { checked: false, ready: false, status: "not_checked", blocker: null }; } function gateFromReadiness(ready, { blocker } = {}) { return ready ? readyGate() : blockedGate({ checked: true, blocker }); } function snapshotMemory(memory) { return { gatewaySessions: new Map(memory.gatewaySessions), boxResources: new Map(memory.boxResources), boxCapabilities: new Map(memory.boxCapabilities), hardwareOperations: new Map(memory.hardwareOperations), auditEvents: new Map(memory.auditEvents), evidenceRecords: new Map(memory.evidenceRecords) }; } function newRecords(current, before) { return [...current.entries()] .filter(([id]) => !before.has(id)) .map(([, record]) => record); } function changedRecords(current, before) { return [...current.entries()] .filter(([id, record]) => stableJson(before.get(id)) !== stableJson(record)) .map(([, record]) => record); } function withPersistence(result, persistence) { return { ...result, persistence }; } function parseJsonColumn(value, fallback) { if (!value) return fallback; if (typeof value === "object") return value; try { return JSON.parse(String(value)); } catch { return fallback; } } function toCountKey(table) { if (table === "gateway_sessions") return "gatewaySessions"; if (table === "box_resources") return "boxResources"; if (table === "box_capabilities") return "boxCapabilities"; if (table === "hardware_operations") return "hardwareOperations"; if (table === "audit_events") return "auditEvents"; if (table === "evidence_records") return "evidenceRecords"; return table; } function normalizeRuntimeTimeoutMs(value) { const parsed = Number.parseInt(String(value ?? ""), 10); if (!Number.isInteger(parsed) || parsed <= 0) { return 1200; } return Math.min(parsed, 5000); }