fix: require durable AgentRun dispatch admission
Move fresh and retry runner dispatch into AgentRun command admission, remove process-local job creation and secret persistence, and split oversized modules by responsibility. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
* In-memory Cloud runtime store implementation.
|
||||
* Public construction remains in runtime-store.ts.
|
||||
*/
|
||||
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";
|
||||
|
||||
|
||||
import * as runtimeCore from "./runtime-store-core.ts";
|
||||
|
||||
const {
|
||||
RUNTIME_STORE_KIND,
|
||||
RUNTIME_STORE_KIND_POSTGRES,
|
||||
RUNTIME_DURABLE_ADAPTER_MISSING,
|
||||
RUNTIME_DURABLE_ADAPTER_UNCONFIGURED,
|
||||
RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
|
||||
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
|
||||
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
|
||||
RUNTIME_ADAPTER_ENV,
|
||||
RUNTIME_DURABLE_ENV,
|
||||
RUNTIME_DB_POOL_MAX_ENV,
|
||||
requiredPostgresSchema,
|
||||
postgresCountTables,
|
||||
postgresRuntimeReadIndexes,
|
||||
buildPostgresPoolConfig,
|
||||
RUNTIME_DB_QUERY_TIMEOUT_MS_ENV,
|
||||
RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS_ENV,
|
||||
RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS_ENV,
|
||||
RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS_ENV,
|
||||
RUNTIME_DB_POOL_RESET_COOLDOWN_MS_ENV,
|
||||
DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS,
|
||||
DEFAULT_RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS,
|
||||
DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS,
|
||||
DEFAULT_RUNTIME_DB_POOL_RESET_COOLDOWN_MS,
|
||||
normalizePositiveInteger,
|
||||
delayRuntimeDbQueryRetry,
|
||||
isReadyForDurableRuntimeRead,
|
||||
normalizeCapabilityInputs,
|
||||
normalizeShellInput,
|
||||
normalizeAgentTraceEvent,
|
||||
normalizeWorkbenchProjectionState,
|
||||
normalizeWorkbenchProjectionAllocation,
|
||||
normalizeWorkbenchFacts,
|
||||
normalizeWorkbenchAggregateEventsForFacts,
|
||||
appendWorkbenchFactEvent,
|
||||
workbenchAggregateForFact,
|
||||
normalizeWorkbenchAggregateEvent,
|
||||
workbenchAggregateEventType,
|
||||
workbenchInputDelivery,
|
||||
workbenchInputStatus,
|
||||
indexWorkbenchAggregateEvents,
|
||||
normalizeWorkbenchSessionInputFact,
|
||||
memoryWorkbenchSessionInputWithSeq,
|
||||
inputFactWithAggregateSeq,
|
||||
workbenchSessionInputWithSeq,
|
||||
workbenchSessionInputPromotedSeq,
|
||||
normalizeWorkbenchSessionFact,
|
||||
normalizeWorkbenchMessageFact,
|
||||
normalizeWorkbenchPartFact,
|
||||
normalizeWorkbenchTurnFact,
|
||||
assertWorkbenchTurnFinalResponseInvariant,
|
||||
normalizeWorkbenchFactStatus,
|
||||
workbenchFactFinalResponseText,
|
||||
normalizeWorkbenchTraceEventFact,
|
||||
normalizeWorkbenchProjectionCheckpoint,
|
||||
arrayInput,
|
||||
stableWorkbenchFactId,
|
||||
requiredWorkbenchFactError,
|
||||
compareTraceEventRecords,
|
||||
compareProjectionStateRecords,
|
||||
compareWorkbenchFactRecords,
|
||||
compareWorkbenchInputFactRecords,
|
||||
compareWorkbenchPartFactRecords,
|
||||
compareWorkbenchTraceEventFacts,
|
||||
sortWorkbenchFactRows,
|
||||
WORKBENCH_FACT_FAMILIES,
|
||||
workbenchFactFamilySet,
|
||||
workbenchFactFamilyForTable,
|
||||
workbenchSessionSummarySelectClause,
|
||||
workbenchSessionSummaryFactFromRow,
|
||||
workbenchFactOrder,
|
||||
traceEventLevel,
|
||||
positiveIntegerOrNull,
|
||||
nonNegativeInteger,
|
||||
nonNegativeIntegerOrNull,
|
||||
projectionStatusOr,
|
||||
projectionHealthOr,
|
||||
resultSyncStateOr,
|
||||
timestampOr,
|
||||
textOr,
|
||||
textList,
|
||||
asObject,
|
||||
requireString,
|
||||
requireProtocolId,
|
||||
normalizeJsonObject,
|
||||
pruneUndefined,
|
||||
makeId,
|
||||
stableJson,
|
||||
sha256Hex,
|
||||
sortJson,
|
||||
matchesQuery,
|
||||
matchesWorkbenchSessionFactQuery,
|
||||
limitResults,
|
||||
normalizeRuntimeAdapter,
|
||||
normalizePostgresSslMode,
|
||||
postgresSslModeFromConnectionString,
|
||||
postgresSslOption,
|
||||
normalizePostgresConnectionStringForSslMode,
|
||||
postgresAdapterContract,
|
||||
addRuntimeDurabilityContract,
|
||||
durabilityBlockedLayer,
|
||||
runtimeSafety,
|
||||
classifyRuntimeDbError,
|
||||
isRuntimeDbConnectTimeout,
|
||||
durableRuntimeReadBlockedError,
|
||||
durableRuntimeReadBlockedData,
|
||||
isRuntimeDbSslError,
|
||||
summarizeRuntimeSchema,
|
||||
notCheckedRuntimeMigration,
|
||||
blockedRuntimeMigration,
|
||||
runtimeGates,
|
||||
defaultRuntimeGates,
|
||||
defaultSchemaGate,
|
||||
defaultMigrationGate,
|
||||
readyGate,
|
||||
blockedGate,
|
||||
notCheckedGate,
|
||||
gateFromReadiness,
|
||||
snapshotMemory,
|
||||
newRecords,
|
||||
changedRecords,
|
||||
withPersistence,
|
||||
parseJsonColumn,
|
||||
postgresPoolStats,
|
||||
toCountKey,
|
||||
normalizeRuntimeTimeoutMs,
|
||||
normalizeRuntimePoolMax
|
||||
} = runtimeCore;
|
||||
|
||||
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();
|
||||
this.agentTraceEvents = new Map();
|
||||
this.workbenchProjectionStates = new Map();
|
||||
this.workbenchSessions = new Map();
|
||||
this.workbenchMessages = new Map();
|
||||
this.workbenchParts = new Map();
|
||||
this.workbenchTurns = new Map();
|
||||
this.workbenchTraceEvents = new Map();
|
||||
this.workbenchEventSequences = new Map();
|
||||
this.workbenchEvents = new Map();
|
||||
this.workbenchSessionInputs = new Map();
|
||||
this.workbenchProjectionCheckpoints = 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,
|
||||
agentTraceEvents: this.agentTraceEvents.size,
|
||||
workbenchProjectionStates: this.workbenchProjectionStates.size,
|
||||
workbenchSessions: this.workbenchSessions.size,
|
||||
workbenchMessages: this.workbenchMessages.size,
|
||||
workbenchParts: this.workbenchParts.size,
|
||||
workbenchTurns: this.workbenchTurns.size,
|
||||
workbenchTraceEvents: this.workbenchTraceEvents.size,
|
||||
workbenchEventSequences: this.workbenchEventSequences.size,
|
||||
workbenchEvents: this.workbenchEvents.size,
|
||||
workbenchSessionInputs: this.workbenchSessionInputs.size,
|
||||
workbenchProjectionCheckpoints: this.workbenchProjectionCheckpoints.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",
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
writeAgentTraceEvent(params = {}, requestMeta = {}) {
|
||||
const traceEvent = normalizeAgentTraceEvent(params.traceEvent ?? params.event ?? params, requestMeta, this.now());
|
||||
this.agentTraceEvents.set(traceEvent.id, traceEvent);
|
||||
return {
|
||||
written: true,
|
||||
traceEvent,
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
queryAgentTraceEvents(params = {}) {
|
||||
const records = [...this.agentTraceEvents.values()].filter((record) => matchesQuery(record, params, [
|
||||
"id",
|
||||
"traceId",
|
||||
"agentSessionId",
|
||||
"workerSessionId",
|
||||
"level"
|
||||
]));
|
||||
records.sort(compareTraceEventRecords);
|
||||
return {
|
||||
events: limitResults(records.map((record) => record.event), params.limit),
|
||||
count: records.length,
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
writeWorkbenchProjectionState(params = {}, requestMeta = {}) {
|
||||
const state = normalizeWorkbenchProjectionState(params.projectionState ?? params.state ?? params, requestMeta, this.now());
|
||||
this.workbenchProjectionStates.set(state.traceId, state);
|
||||
return {
|
||||
written: true,
|
||||
projectionState: state,
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
getWorkbenchProjectionState(params = {}) {
|
||||
const traceId = textOr(params.traceId, "");
|
||||
const state = traceId ? this.workbenchProjectionStates.get(traceId) ?? null : null;
|
||||
return {
|
||||
projectionState: state,
|
||||
found: Boolean(state),
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
allocateWorkbenchProjectedSeq(params = {}, requestMeta = {}) {
|
||||
const allocation = normalizeWorkbenchProjectionAllocation(params, requestMeta, this.now());
|
||||
const existing = [...this.workbenchTraceEvents.values()].find((record) =>
|
||||
record.traceId === allocation.traceId && record.sourceEventId === allocation.sourceEventId
|
||||
);
|
||||
if (existing) {
|
||||
return {
|
||||
...allocation,
|
||||
projectedSeq: nonNegativeInteger(existing.projectedSeq),
|
||||
reused: true,
|
||||
persistence: this.summary(),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
const eventMax = [...this.workbenchTraceEvents.values()]
|
||||
.filter((record) => record.traceId === allocation.traceId)
|
||||
.reduce((max, record) => Math.max(max, nonNegativeInteger(record.projectedSeq)), 0);
|
||||
const checkpointMax = nonNegativeInteger(this.workbenchProjectionCheckpoints.get(allocation.traceId)?.projectedSeq);
|
||||
return {
|
||||
...allocation,
|
||||
projectedSeq: Math.max(eventMax, checkpointMax) + 1,
|
||||
reused: false,
|
||||
persistence: this.summary(),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
queryWorkbenchProjectionStates(params = {}) {
|
||||
const dueAt = textOr(params.dueAt, "");
|
||||
const statuses = Array.isArray(params.projectionStatuses) ? new Set(params.projectionStatuses.map((item) => textOr(item, "")).filter(Boolean)) : null;
|
||||
const records = [...this.workbenchProjectionStates.values()]
|
||||
.filter((record) => matchesQuery(record, params, ["traceId", "sessionId", "runId", "commandId", "sourceRunId", "sourceCommandId"]))
|
||||
.filter((record) => !statuses || statuses.has(record.projectionStatus))
|
||||
.filter((record) => !dueAt || !record.nextRetryAt || String(record.nextRetryAt) <= dueAt)
|
||||
.sort(compareProjectionStateRecords);
|
||||
return {
|
||||
states: limitResults(records, params.limit),
|
||||
count: records.length,
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
writeWorkbenchFacts(params = {}, requestMeta = {}) {
|
||||
const facts = normalizeWorkbenchFacts(params.facts ?? params, requestMeta, this.now());
|
||||
const inputFacts = facts.inputs.map((fact) => memoryWorkbenchSessionInputWithSeq(fact, this.workbenchSessionInputs));
|
||||
for (const fact of inputFacts) this.workbenchSessionInputs.set(fact.inputId, fact);
|
||||
for (const fact of facts.sessions) this.workbenchSessions.set(fact.sessionId, fact);
|
||||
for (const fact of facts.messages) this.workbenchMessages.set(fact.messageId, fact);
|
||||
for (const fact of facts.parts) this.workbenchParts.set(fact.partId, fact);
|
||||
for (const fact of facts.turns) this.workbenchTurns.set(fact.turnId, fact);
|
||||
for (const fact of facts.traceEvents) this.workbenchTraceEvents.set(fact.id, fact);
|
||||
for (const fact of facts.checkpoints) {
|
||||
const previous = this.workbenchProjectionCheckpoints.get(fact.traceId) ?? null;
|
||||
if (!previous || nonNegativeInteger(fact.projectedSeq) >= nonNegativeInteger(previous.projectedSeq)) {
|
||||
this.workbenchProjectionCheckpoints.set(fact.traceId, fact);
|
||||
}
|
||||
}
|
||||
return {
|
||||
written: true,
|
||||
facts: { ...facts, inputs: inputFacts },
|
||||
persistence: this.summary()
|
||||
};
|
||||
}
|
||||
|
||||
queryWorkbenchFacts(params = {}) {
|
||||
const families = workbenchFactFamilySet(params);
|
||||
const sessions = [...this.workbenchSessions.values()].filter((record) => matchesWorkbenchSessionFactQuery(record, params));
|
||||
const inputs = [...this.workbenchSessionInputs.values()].filter((record) => matchesQuery(record, params, ["inputId", "sessionId", "turnId", "traceId", "messageId", "commandId", "delivery", "status"]));
|
||||
const messages = [...this.workbenchMessages.values()].filter((record) => matchesQuery(record, params, ["messageId", "sessionId", "turnId", "traceId", "role", "status"]));
|
||||
const parts = [...this.workbenchParts.values()].filter((record) => matchesQuery(record, params, ["partId", "messageId", "sessionId", "turnId", "traceId", "partType", "status"]));
|
||||
const turns = [...this.workbenchTurns.values()].filter((record) => matchesQuery(record, params, ["turnId", "sessionId", "traceId", "messageId", "status"]));
|
||||
const afterProjectedSeq = nonNegativeInteger(params.afterProjectedSeq);
|
||||
const traceEvents = [...this.workbenchTraceEvents.values()].filter((record) => matchesQuery(record, params, ["id", "traceId", "sessionId", "turnId", "messageId", "eventType"]) && (afterProjectedSeq <= 0 || nonNegativeInteger(record.projectedSeq) > afterProjectedSeq));
|
||||
const checkpoints = [...this.workbenchProjectionCheckpoints.values()].filter((record) => matchesQuery(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"]));
|
||||
const facts = {
|
||||
inputs: families.has("inputs") ? limitResults(sortWorkbenchFactRows(inputs, params, "inputs", compareWorkbenchFactRecords), params.limit) : [],
|
||||
sessions: families.has("sessions") ? limitResults(sortWorkbenchFactRows(sessions, params, "sessions", compareWorkbenchFactRecords), params.limit) : [],
|
||||
messages: families.has("messages") ? limitResults(sortWorkbenchFactRows(messages, params, "messages", compareWorkbenchFactRecords), params.limit) : [],
|
||||
parts: families.has("parts") ? limitResults(sortWorkbenchFactRows(parts, params, "parts", compareWorkbenchPartFactRecords), params.limit) : [],
|
||||
turns: families.has("turns") ? limitResults(sortWorkbenchFactRows(turns, params, "turns", compareWorkbenchFactRecords), params.limit) : [],
|
||||
traceEvents: families.has("traceEvents") ? limitResults(sortWorkbenchFactRows(traceEvents, params, "traceEvents", compareWorkbenchTraceEventFacts), params.limit) : [],
|
||||
checkpoints: families.has("checkpoints") ? limitResults(sortWorkbenchFactRows(checkpoints, params, "checkpoints", compareWorkbenchFactRecords), params.limit) : []
|
||||
};
|
||||
return {
|
||||
facts,
|
||||
count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0),
|
||||
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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+28
-3585
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user