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"; export const RUNTIME_STORE_KIND = "memory"; export function createCloudRuntimeStore(options = {}) { return new CloudRuntimeStore(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 { adapter: RUNTIME_STORE_KIND, durable: false, status: "degraded", reason: "live DB persistence is not connected; L1 runtime writes are process-local only", 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; } } 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); }