Files
pikasTech-HWLAB/internal/db/runtime-store.ts
T

1768 lines
57 KiB
TypeScript

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.ts";
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_SSL_BLOCKED = "runtime_durable_adapter_ssl_blocked";
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 function buildPostgresPoolConfig({ dbUrl, sslMode = "require", timeoutMs } = {}) {
const normalizedSslMode = normalizePostgresSslMode(sslMode);
return {
connectionString: normalizePostgresConnectionStringForSslMode(dbUrl, normalizedSslMode),
ssl: postgresSslOption(normalizedSslMode),
connectionTimeoutMillis: normalizeRuntimeTimeoutMs(timeoutMs)
};
}
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({
ssl: readyGate(),
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);
const migrationReadBlocker = [
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
].includes(classified.blocker)
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED;
this.lastReadiness = this.blockedSummary({
blocker: migrationReadBlocker,
reason: migrationReadBlocker === classified.blocker
? 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: migrationReadBlocker === classified.blocker
? classified.connection?.queryResult
: "migration_blocked",
errorCode: classified.connection?.errorCode ?? null
},
gates: runtimeGates({
ssl: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? notCheckedGate()
: readyGate(),
schema: readyGate(),
migration: blockedGate({
checked: true,
blocker: migrationReadBlocker
}),
durability: blockedGate({ checked: false, blocker: migrationReadBlocker })
})
});
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({
ssl: readyGate(),
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({
ssl: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? notCheckedGate()
: 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({
ssl: readyGate(),
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 = {}) {
await this.assertReadyForDurableReads("audit.event.query");
const result = await this.queryDurableReadRows(
"audit.event.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 = {}) {
await this.assertReadyForDurableReads("evidence.record.query");
const result = await this.queryDurableReadRows(
"evidence.record.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 assertReadyForDurableReads(method) {
const readiness = await this.readiness();
if (readiness.ready !== true || readiness.durable !== true || readiness.liveRuntimeEvidence !== true) {
throw durableRuntimeReadBlockedError(method, readiness);
}
}
async queryDurableReadRows(method, sql, params = []) {
try {
return await this.query(sql, params);
} catch (error) {
this.lastReadiness = this.blockedSummary({
...classifyRuntimeDbError(error),
reason: `Postgres durable runtime adapter could not complete ${method} read query`
});
throw durableRuntimeReadBlockedError(method, this.summary());
}
}
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(buildPostgresPoolConfig({
dbUrl: this.dbUrl,
sslMode: this.sslMode,
timeoutMs: 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({
...defaultRuntimeGates({ blocker, connection, schema: blockedSchema, migration: blockedMigration }),
schema: defaultSchemaGate({ blocker, schema: blockedSchema }),
migration: defaultMigrationGate({ blocker, migration: blockedMigration }),
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 normalizePostgresSslMode(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (["disable", "false", "0", "off", "no"].includes(normalized)) {
return "disable";
}
return "require";
}
function postgresSslOption(sslMode) {
return sslMode === "disable" ? false : { rejectUnauthorized: false };
}
function normalizePostgresConnectionStringForSslMode(dbUrl, sslMode) {
if (typeof dbUrl !== "string" || dbUrl.trim() === "") {
return dbUrl;
}
let url;
try {
url = new URL(dbUrl);
} catch {
return dbUrl;
}
if (!["postgres:", "postgresql:"].includes(url.protocol)) {
return dbUrl;
}
for (const key of ["ssl", "sslmode", "sslcert", "sslkey", "sslrootcert"]) {
url.searchParams.delete(key);
}
if (sslMode !== "disable") {
url.searchParams.set("sslmode", "require");
}
return url.toString();
}
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_SSL_BLOCKED) {
return "ssl";
}
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 (isRuntimeDbSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
reason: "Postgres runtime adapter reached the DB driver but SSL negotiation or SSL mode compatibility blocked the schema query",
connection: {
queryAttempted: true,
queryResult: "ssl_negotiation_blocked",
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 durableRuntimeReadBlockedError(method, readiness = {}) {
return new HwlabProtocolError("Postgres durable runtime adapter is blocked for runtime evidence queries", {
code: ERROR_CODES.internalError,
data: durableRuntimeReadBlockedData(method, readiness),
context: {
result: "failed"
}
});
}
function durableRuntimeReadBlockedData(method, readiness = {}) {
const durability = readiness.durabilityContract ?? {};
const connection = readiness.connection ?? {};
const blockedLayer = durability.blockedLayer ?? durabilityBlockedLayer(readiness);
const queryResult = connection.queryResult ?? (blockedLayer === "durability_query" ? "query_blocked" : "not_ready");
return pruneUndefined({
method,
adapter: readiness.adapter ?? RUNTIME_STORE_KIND_POSTGRES,
status: "blocked",
blocker: readiness.blocker ?? RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
durable: false,
durableRequested: true,
liveRuntimeEvidence: false,
queryResult,
blockedLayer,
requiredEvidence: durability.requiredEvidence ?? RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
dbLiveEvidenceIsDurabilityEvidence: false,
secretMaterialRead: false,
valueRedacted: true,
endpointRedacted: true,
errorCode: connection.errorCode ?? undefined,
redaction: {
valueRedacted: true,
valuesRedacted: true,
endpointRedacted: true,
secretMaterialRead: false
}
});
}
function isRuntimeDbSslError(error) {
const code = typeof error?.code === "string" ? error.code : "";
if (code.startsWith("ERR_SSL") || code.startsWith("ERR_TLS")) {
return true;
}
if ([
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED"
].includes(code)) {
return true;
}
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
return [
"does not support ssl",
"ssl is not enabled",
"ssl negotiation",
"ssl off",
"ssl required",
"requires ssl",
"no ssl encryption",
"no encryption",
"hostssl",
"server requires ssl",
"before secure tls connection",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
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({
ssl = notCheckedGate(),
auth = notCheckedGate(),
schema = notCheckedGate(),
migration = notCheckedGate(),
durability = notCheckedGate()
} = {}) {
return {
ssl,
auth,
schema,
migration,
durability
};
}
function defaultRuntimeGates({ blocker, connection, schema, migration } = {}) {
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return {
ssl: blockedGate({ checked: true, blocker }),
auth: notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return {
ssl: readyGate(),
auth: blockedGate({ checked: true, blocker })
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return {
ssl: connection?.queryAttempted ? readyGate() : notCheckedGate(),
auth: connection?.queryAttempted ? readyGate() : notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED && schema?.checked && migration?.checked) {
return {
ssl: readyGate(),
auth: readyGate()
};
}
return {
ssl: notCheckedGate(),
auth: notCheckedGate()
};
}
function defaultSchemaGate({ blocker, schema } = {}) {
if (schema?.checked) {
return gateFromReadiness(schema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
function defaultMigrationGate({ blocker, migration } = {}) {
if (migration?.checked) {
return gateFromReadiness(migration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
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);
}