1007 lines
37 KiB
JavaScript
1007 lines
37 KiB
JavaScript
const PASS_STATUSES = Object.freeze(["ok", "ready", "pass", "passed", "healthy", "available", "live"]);
|
|
const ERROR_STATUSES = Object.freeze(["error", "failed", "failure"]);
|
|
const RAW_DEGRADED_STATUSES = Object.freeze(["degraded"]);
|
|
const BLOCKED_STATUSES = Object.freeze(["blocked", "unavailable", "rejected"]);
|
|
const UNVERIFIED_STATUSES = Object.freeze(["", "unknown", "pending", "loading", "probing", "unverified", "not_observed"]);
|
|
const RUNTIME_DURABLE_READY_QUERY_RESULTS = Object.freeze([
|
|
"durable_readiness_ready",
|
|
"runtime_durable_ready",
|
|
"query_ready",
|
|
"readiness_ready",
|
|
"ready",
|
|
"ok",
|
|
"pass",
|
|
"passed"
|
|
]);
|
|
const RUNTIME_DURABLE_BLOCKED_QUERY_RESULTS = Object.freeze({
|
|
driver_missing: "runtime_durable_adapter_driver_missing",
|
|
ssl_negotiation_blocked: "runtime_durable_adapter_ssl_blocked",
|
|
auth_blocked: "runtime_durable_adapter_auth_blocked",
|
|
schema_blocked: "runtime_durable_adapter_schema_blocked",
|
|
migration_blocked: "runtime_durable_adapter_migration_blocked",
|
|
query_blocked: "runtime_durable_adapter_query_blocked",
|
|
not_ready: "runtime_durable_adapter_query_blocked"
|
|
});
|
|
const RUNTIME_DURABLE_GATE_LAYERS = Object.freeze(["ssl", "auth", "schema", "migration", "durability"]);
|
|
const READ_ONLY_REASON_CODES = Object.freeze([
|
|
"read_only",
|
|
"read-only",
|
|
"readonly",
|
|
"permission",
|
|
"permissions",
|
|
"forbidden",
|
|
"unauthorized",
|
|
"runtime_durable",
|
|
"runtime-durable",
|
|
"durability",
|
|
"durable",
|
|
"db_persistence",
|
|
"persistence",
|
|
"not-codex-stdio",
|
|
"not-write-capable",
|
|
"process-local-session-registry",
|
|
"text-chat-only",
|
|
"fallback:text-chat-only",
|
|
"codex_stdio_blocked_readonly_available",
|
|
"codex_stdio_blocked_readonly_session_available",
|
|
"controlled_readonly_not_long_lived_stdio",
|
|
"codex_cli_binary_missing",
|
|
"runner_lifecycle_missing",
|
|
"stdio_protocol_not_wired",
|
|
"codex_stdio_supervisor_disabled",
|
|
"security_blocked"
|
|
]);
|
|
|
|
export function classifyWorkbenchLiveStatus(live = {}) {
|
|
const probes = collectLiveStatusProbes(live);
|
|
const reachable = probes.some((probe) => probe.reachable);
|
|
if (!reachable) {
|
|
const failedProbe = probes.find((probe) => !probe.reachable) ?? probes[0];
|
|
return statusResult({
|
|
kind: "unverified",
|
|
label: "等待验证",
|
|
tone: "pending",
|
|
serviceId: failedProbe?.serviceId ?? "hwlab-cloud-api",
|
|
apiPath: failedProbe?.apiPath ?? "/health/live",
|
|
reasonCode: failedProbe?.reasonCode ?? "api_unverified",
|
|
detail: failedProbe
|
|
? `等待验证:${probeAttribution(failedProbe)}。`
|
|
: "等待同源 API 验证;主工作台不会把未探测状态当作通过。",
|
|
probes
|
|
});
|
|
}
|
|
|
|
const errorProbe = probes.find((probe) => probe.kind === "error");
|
|
if (errorProbe) {
|
|
return statusResult({
|
|
kind: "error",
|
|
label: "API 错误",
|
|
tone: "blocked",
|
|
serviceId: errorProbe.serviceId,
|
|
apiPath: errorProbe.apiPath,
|
|
reasonCode: errorProbe.reasonCode,
|
|
traceId: errorProbe.traceId,
|
|
detail: `错误:${probeAttribution(errorProbe)}。`,
|
|
probes
|
|
});
|
|
}
|
|
|
|
const readOnlyProbe = probes.find((probe) => probe.kind === "readonly");
|
|
if (readOnlyProbe) {
|
|
return statusResult({
|
|
kind: "readonly",
|
|
label: "只读模式",
|
|
tone: "source",
|
|
serviceId: readOnlyProbe.serviceId,
|
|
apiPath: readOnlyProbe.apiPath,
|
|
reasonCode: readOnlyProbe.reasonCode,
|
|
traceId: readOnlyProbe.traceId,
|
|
detail: `只读:${probeAttribution(readOnlyProbe)}。`,
|
|
probes
|
|
});
|
|
}
|
|
|
|
const pendingProbe = probes.find((probe) => probe.kind === "unverified");
|
|
if (pendingProbe) {
|
|
return statusResult({
|
|
kind: "unverified",
|
|
label: "等待验证",
|
|
tone: "pending",
|
|
serviceId: pendingProbe.serviceId,
|
|
apiPath: pendingProbe.apiPath,
|
|
reasonCode: pendingProbe.reasonCode,
|
|
traceId: pendingProbe.traceId,
|
|
detail: `等待验证:${probeAttribution(pendingProbe)}。`,
|
|
probes
|
|
});
|
|
}
|
|
|
|
return statusResult({
|
|
kind: "pass",
|
|
label: "API 正常",
|
|
tone: "dev-live",
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/health/live",
|
|
reasonCode: "ready",
|
|
detail: "通过:hwlab-cloud-api /health/live ready;关键同源 API 已完成当前工作台只读验证。",
|
|
probes
|
|
});
|
|
}
|
|
|
|
export function collectLiveStatusProbes(live = {}) {
|
|
return [
|
|
classifyApiProbe(live.healthLive, {
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/health/live",
|
|
fallbackReasonCode: "health_live_unverified"
|
|
}),
|
|
classifyApiProbe(live.restIndex, {
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/v1",
|
|
fallbackReasonCode: "rest_index_unverified"
|
|
}),
|
|
classifyApiProbe(live.health, {
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/json-rpc system.health",
|
|
fallbackReasonCode: "system_health_unverified"
|
|
}),
|
|
classifyApiProbe(live.adapter, {
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/json-rpc cloud.adapter.describe",
|
|
fallbackReasonCode: "adapter_describe_unverified"
|
|
}),
|
|
classifyCodeAgentProbe(live),
|
|
classifyDevicePodProbe(live)
|
|
].filter(Boolean);
|
|
}
|
|
|
|
function classifyDevicePodProbe(live = {}) {
|
|
const probe = live.devicePodStatus ?? live.devicePods;
|
|
const context = {
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/v1/device-pods",
|
|
fallbackReasonCode: "device_pod_unverified"
|
|
};
|
|
if (!probe) {
|
|
return unverifiedProbe({
|
|
...context,
|
|
reasonCode: context.fallbackReasonCode,
|
|
reason: "Device Pod status not observed"
|
|
});
|
|
}
|
|
if (probe.ok !== true) {
|
|
return errorProbe({
|
|
...context,
|
|
reasonCode: probe.timeout ? "timeout" : probe.status ? `HTTP ${probe.status}` : "device_pod_request_failed",
|
|
reason: probe.error ?? "Device Pod status request failed",
|
|
httpStatus: probe.status,
|
|
traceId: traceIdFrom(probe),
|
|
evidenceSummary: evidenceSummaryFrom(probe)
|
|
});
|
|
}
|
|
const data = probe.data ?? {};
|
|
const pod = data.devicePod ?? data.devicePods?.[0] ?? data.summary ?? {};
|
|
const blocker = data.summary?.blocker ?? pod.blocker ?? null;
|
|
if (blocker) {
|
|
return errorProbe({
|
|
...context,
|
|
reasonCode: blocker.code ?? "device_pod_blocked",
|
|
reason: blocker.message ?? "Device Pod blocked",
|
|
traceId: data.summary?.latestEvent?.refs?.traceId ?? data.events?.[0]?.refs?.traceId,
|
|
evidenceSummary: devicePodEvidenceSummary(data)
|
|
});
|
|
}
|
|
if (["ok", "ready", "live", "pass", "healthy"].includes(String(data.status ?? pod.status ?? "").toLowerCase())) {
|
|
return passProbe({
|
|
...context,
|
|
serviceId: data.serviceId ?? "hwlab-device-pod",
|
|
reasonCode: "device_pod_ready",
|
|
reason: `Device Pod ${pod.devicePodId ?? data.summary?.devicePodId ?? "unknown"} status ready`,
|
|
evidenceSummary: devicePodEvidenceSummary(data)
|
|
});
|
|
}
|
|
return unverifiedProbe({
|
|
...context,
|
|
reasonCode: data.status ?? "device_pod_unverified",
|
|
reason: "Device Pod status is not ready yet",
|
|
evidenceSummary: devicePodEvidenceSummary(data)
|
|
});
|
|
}
|
|
|
|
function devicePodEvidenceSummary(data = {}) {
|
|
const summary = data.summary ?? {};
|
|
const pod = data.devicePod ?? data.devicePods?.[0] ?? {};
|
|
const refs = summary.latestEvent?.refs ?? data.events?.[0]?.refs ?? pod.refs ?? {};
|
|
return [
|
|
summary.devicePodId ?? pod.devicePodId ? `pod=${summary.devicePodId ?? pod.devicePodId}` : null,
|
|
summary.targetId ?? pod.targetId ?? pod.target?.targetId ? `target=${summary.targetId ?? pod.targetId ?? pod.target?.targetId}` : null,
|
|
summary.profileHash ?? pod.profileHash ?? pod.profile?.profileHash ? `profile=${shortEvidenceToken(summary.profileHash ?? pod.profileHash ?? pod.profile?.profileHash)}` : null,
|
|
refs.operationId ? `operation=${refs.operationId}` : null,
|
|
refs.traceId ? `trace=${refs.traceId}` : null,
|
|
refs.evidenceId ? `evidence=${refs.evidenceId}` : null
|
|
].filter(Boolean).join(" / ");
|
|
}
|
|
|
|
function shortEvidenceToken(value) {
|
|
const text = String(value ?? "").trim();
|
|
if (!text) return "unknown";
|
|
return text.length > 16 ? text.slice(0, 16) : text;
|
|
}
|
|
|
|
function classifyApiProbe(probe, context) {
|
|
if (!probe) {
|
|
return unverifiedProbe({
|
|
...context,
|
|
reasonCode: context.fallbackReasonCode,
|
|
reason: "not_observed"
|
|
});
|
|
}
|
|
if (probe.ok !== true) {
|
|
return errorProbe({
|
|
...context,
|
|
reasonCode: probe.timeout ? "timeout" : probe.status ? `HTTP ${probe.status}` : context.fallbackReasonCode,
|
|
reason: probe.error ?? "request_failed",
|
|
httpStatus: probe.status,
|
|
traceId: traceIdFrom(probe),
|
|
evidenceSummary: evidenceSummaryFrom(probe)
|
|
});
|
|
}
|
|
|
|
const payload = probe.data;
|
|
const serviceId = payload?.serviceId ?? context.serviceId;
|
|
if ((context.apiPath === "/health/live" || context.apiPath === "/v1") && payload?.readiness?.codeAgent) {
|
|
const codeAgentProbe = classifyStructuredCodeAgentProbe(payload.readiness.codeAgent);
|
|
if (codeAgentProbe && codeAgentProbe.kind !== "pass" && codeAgentProbe.kind !== "unverified") {
|
|
return {
|
|
...codeAgentProbe,
|
|
rawStatus: rawStatusFrom(payload)
|
|
};
|
|
}
|
|
}
|
|
const durableReason = durableRuntimeReason(payload);
|
|
if (durableReason) {
|
|
return readOnlyProbe({
|
|
...context,
|
|
serviceId,
|
|
...durableReason,
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
|
|
if (context.apiPath === "/v1" && payload?.codeAgent) {
|
|
const codeAgentProbe = classifyCodeAgentProbe({ restIndex: probe });
|
|
if (codeAgentProbe && codeAgentProbe.kind !== "pass" && codeAgentProbe.kind !== "unverified") {
|
|
return {
|
|
...codeAgentProbe,
|
|
rawStatus: rawStatusFrom(payload)
|
|
};
|
|
}
|
|
}
|
|
|
|
const blocker = firstBlocker(payload);
|
|
if (blocker) {
|
|
const reasonCode = String(blocker.code ?? blocker.reason ?? "blocked");
|
|
const reason = blocker.summary ?? blocker.message ?? blocker.reason ?? reasonCode;
|
|
if (isReadOnlyReason(reasonCode) || isReadOnlyReason(reason)) {
|
|
return readOnlyProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode,
|
|
reason,
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
return errorProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode,
|
|
reason,
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
|
|
const status = normalizedStatus(rawStatusFrom(payload));
|
|
if (ERROR_STATUSES.includes(status)) {
|
|
return errorProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode: status,
|
|
reason: "API returned error status",
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
if (RAW_DEGRADED_STATUSES.includes(status) || BLOCKED_STATUSES.includes(status) || readinessFalse(payload)) {
|
|
const reasonCode = readinessFalse(payload)
|
|
? "ready_false_without_blocker"
|
|
: status === "degraded"
|
|
? "api_degraded_without_attribution"
|
|
: status;
|
|
return errorProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode,
|
|
reason: "API readiness is not pass and no concrete blocker was provided",
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
if (UNVERIFIED_STATUSES.includes(status)) {
|
|
return unverifiedProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode: context.fallbackReasonCode,
|
|
reason: "status_not_observed",
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
return passProbe({
|
|
...context,
|
|
serviceId,
|
|
reasonCode: status || "ready",
|
|
reason: "ready",
|
|
rawStatus: rawStatusFrom(payload),
|
|
traceId: traceIdFrom(payload),
|
|
evidenceSummary: evidenceSummaryFrom(payload)
|
|
});
|
|
}
|
|
|
|
function classifyCodeAgentProbe(live = {}) {
|
|
const availability = live.restIndex?.data?.codeAgent ?? live.healthLive?.data?.codeAgent;
|
|
const structured = live.restIndex?.data?.readiness?.codeAgent ?? live.healthLive?.data?.readiness?.codeAgent;
|
|
if (structured) return classifyStructuredCodeAgentProbe(structured);
|
|
if (!availability) {
|
|
return unverifiedProbe({
|
|
serviceId: "hwlab-cloud-api",
|
|
apiPath: "/v1/agent/chat",
|
|
reasonCode: "code_agent_unverified",
|
|
reason: "availability_not_observed"
|
|
});
|
|
}
|
|
const serviceId = "hwlab-cloud-api";
|
|
const apiPath = availability.endpoint ?? "POST /v1/agent/chat";
|
|
const provider = availability.provider ?? "not_observed";
|
|
const backend = availability.backend ?? "not_observed";
|
|
const capability = availability.capabilityLevel ?? availability.runner?.capabilityLevel;
|
|
const providerStatus = availability.error?.providerStatus ?? availability.providerStatus;
|
|
const gate = availability.longLivedSessionGate;
|
|
const stdio = availability.codexStdio ?? availability.codexStdioFeasibility;
|
|
if (gate?.status === "pass" && stdio?.canStartLongLivedCodexStdio === true && availability.ready === true) {
|
|
return passProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "codex_stdio_ready",
|
|
reason: `provider=codex-stdio; backend=${availability.codexStdio?.backend ?? backend}; capability=long-lived-codex-stdio-session`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider: "codex-stdio", backend: availability.codexStdio?.backend ?? backend, capability: "long-lived-codex-stdio-session" }
|
|
});
|
|
}
|
|
if (capability === "text-chat-only" || availability.runner?.kind === "openai-responses-fallback") {
|
|
return readOnlyProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "fallback_text_chat_only",
|
|
reason: `provider=${provider}; backend=${backend}; capability=text-chat-only`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (availability.agentKind === "controlled-readonly-session-registry" || availability.partialReady === true) {
|
|
return readOnlyProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "codex_stdio_blocked_readonly_session_available",
|
|
reason: `${availability.summary ?? "controlled-readonly-session-registry available; long-lived Codex stdio blocked"}; blockers=${blockerCodes(availability).join(",") || "not_observed"}`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (capability === "read-only-session-tools") {
|
|
return readOnlyProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "code_agent_read_only_session_tools",
|
|
reason: codeAgentReadOnlySessionToolsReason(availability),
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (availability.status === "blocked" || availability.ready === false) {
|
|
return errorProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: providerStatus ? `provider_http_${providerStatus}` : availability.reason ?? availability.blocker ?? "code_agent_blocked",
|
|
reason: availability.summary ?? `provider=${provider}; backend=${backend}; capability=${capability ?? "unknown"}`,
|
|
traceId: traceIdFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (availability.runner?.ready === true && availability.runner?.writeCapable === false) {
|
|
return readOnlyProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "codex_readonly_runner",
|
|
reason: `runner=${availability.runner.kind ?? "hwlab-readonly-runner"}; capability=${capability ?? "read-only-tools"}`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (availability.status === "available" || availability.ready === true) {
|
|
return passProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: capability ?? "available",
|
|
reason: `provider=${provider}; backend=${backend}`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
return unverifiedProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "code_agent_unverified",
|
|
reason: `provider=${provider}; backend=${backend}`,
|
|
traceId: traceIdFrom(availability),
|
|
evidenceSummary: evidenceSummaryFrom(availability),
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
|
|
function classifyStructuredCodeAgentProbe(readiness = {}) {
|
|
const serviceId = "hwlab-cloud-api";
|
|
const apiPath = "POST /v1/agent/chat";
|
|
const provider = readiness.provider?.provider ?? "not_observed";
|
|
const backend = readiness.provider?.backend ?? "not_observed";
|
|
const capability = readiness.sessionRunner?.capabilityLevel ?? "not_observed";
|
|
if (structuredCodexStdioReady(readiness)) {
|
|
return passProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: "codex_stdio_ready",
|
|
reason: `provider=${provider}; backend=${backend}; capability=long-lived-codex-stdio-session`,
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
if (readiness.sessionRunnerReady === true) {
|
|
return readOnlyProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: readiness.codexStdio?.blocker ?? "codex_stdio_blocked_readonly_session_available",
|
|
reason: `providerReady=${Boolean(readiness.providerReady)}; durableDbReady=${Boolean(readiness.durableDbReady)}; sessionRunner=${readiness.sessionRunner?.status ?? "unknown"}; codexStdio=${readiness.codexStdio?.status ?? "unknown"}; current=${currentStructuredBlockers(readiness).join(",") || "not_observed"}`,
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
const blocker = readiness.currentBlocker ?? currentStructuredBlockers(readiness)[0] ?? "code_agent_blocked";
|
|
return errorProbe({
|
|
serviceId,
|
|
apiPath,
|
|
reasonCode: blocker,
|
|
reason: `providerReady=${Boolean(readiness.providerReady)}; durableDbReady=${Boolean(readiness.durableDbReady)}; sessionRunnerReady=${Boolean(readiness.sessionRunnerReady)}; codexStdioFeasible=${Boolean(readiness.codexStdioFeasible)}`,
|
|
meta: { provider, backend, capability }
|
|
});
|
|
}
|
|
|
|
function structuredCodexStdioReady(readiness = {}) {
|
|
return Boolean(
|
|
readiness.sessionRunnerReady === true &&
|
|
readiness.codexStdioFeasible === true &&
|
|
readiness.sessionRunner?.codexStdio === true &&
|
|
readiness.sessionRunner?.writeCapable === true &&
|
|
readiness.sessionRunner?.longLivedSession === true &&
|
|
readiness.sessionRunner?.capabilityLevel === "long-lived-codex-stdio-session"
|
|
);
|
|
}
|
|
|
|
function currentStructuredBlockers(readiness = {}) {
|
|
return Array.isArray(readiness.currentBlockers)
|
|
? readiness.currentBlockers.filter(Boolean)
|
|
: [readiness.currentBlocker].filter(Boolean);
|
|
}
|
|
|
|
function blockerCodes(availability) {
|
|
const values = [
|
|
...(Array.isArray(availability?.blockerCodes) ? availability.blockerCodes : []),
|
|
...(Array.isArray(availability?.blockers) ? availability.blockers.map((blocker) => blocker?.code) : []),
|
|
...(Array.isArray(availability?.longLivedSessionGate?.blockers) ? availability.longLivedSessionGate.blockers.map((blocker) => blocker?.code) : []),
|
|
...(Array.isArray(availability?.codexStdioFeasibility?.blockers) ? availability.codexStdioFeasibility.blockers.map((blocker) => blocker?.code) : []),
|
|
availability?.runtimeContract?.binary?.status === "missing" ? "codex_cli_binary_missing" : null,
|
|
availability?.runtimeContract?.lifecycleSupervisor?.status === "blocked" ? "runner_lifecycle_missing" : null,
|
|
availability?.runtimeContract?.stdioProtocol?.status === "blocked" ? "stdio_protocol_not_wired" : null
|
|
].filter(Boolean);
|
|
return [...new Set(values.map(String))];
|
|
}
|
|
|
|
|
|
function durableRuntimeReason(payload) {
|
|
if (!hasRuntimeDurableSignal(payload)) return null;
|
|
const classification = classifyRuntimeDurableReadiness(payload);
|
|
if (classification.ready) return null;
|
|
if (
|
|
classification.status === "blocked" &&
|
|
(
|
|
isReadOnlyReason(classification.reasonCode) ||
|
|
String(classification.reasonCode).endsWith("_blocked") ||
|
|
normalizedStatus(classification.evidence?.blockedLayer).includes("durability")
|
|
)
|
|
) {
|
|
return {
|
|
reasonCode: classification.reasonCode,
|
|
reason: classification.reason,
|
|
layer: classification.evidence?.blockedLayer || "runtime-durable",
|
|
classification
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hasRuntimeDurableSignal(payload = {}) {
|
|
const runtime = payload?.runtime;
|
|
const durability = payload?.readiness?.durability;
|
|
const dbRuntimeReadiness = payload?.db?.runtimeReadiness;
|
|
return Boolean(
|
|
blockerCodeStartingWith(payload, "runtime_durable_adapter_") ||
|
|
hasOwnNonEmpty(runtime, ["adapter", "durable", "durableRequested", "ready", "blocker", "liveRuntimeEvidence", "connection", "durabilityContract", "gates"]) ||
|
|
hasOwnNonEmpty(durability, ["adapter", "durable", "ready", "blocker", "blockedLayer", "queryResult", "liveRuntimeEvidence", "gates"]) ||
|
|
hasOwnNonEmpty(dbRuntimeReadiness, ["adapter", "durable", "ready", "blocker", "blockedLayer", "queryResult", "liveRuntimeEvidence"])
|
|
);
|
|
}
|
|
|
|
function hasOwnNonEmpty(object, fields) {
|
|
if (!object || typeof object !== "object") return false;
|
|
return fields.some((field) => Object.hasOwn(object, field) && object[field] !== undefined && object[field] !== null && object[field] !== "");
|
|
}
|
|
|
|
export function classifyRuntimeDurableReadiness(payload = {}, options = {}) {
|
|
const runtime = payload?.runtime ?? {};
|
|
const durability = payload?.readiness?.durability ?? {};
|
|
const dbRuntimeReadiness = payload?.db?.runtimeReadiness ?? {};
|
|
const gates = runtime?.gates ?? durability?.gates ?? {};
|
|
const gateBlocker = firstBlockedRuntimeGate(gates);
|
|
const queryResult = firstNonEmpty(
|
|
runtime?.connection?.queryResult,
|
|
durability?.queryResult,
|
|
dbRuntimeReadiness?.queryResult
|
|
);
|
|
const queryBlocker = runtimeDurableBlockerFromQueryResult(queryResult);
|
|
const readyQueryResult = isRuntimeDurableReadyQueryResult(queryResult);
|
|
const explicitDurabilityReady = durabilityReadySignal({
|
|
runtime,
|
|
durability,
|
|
dbRuntimeReadiness,
|
|
gateBlocker,
|
|
queryResult,
|
|
readyQueryResult,
|
|
queryBlocker
|
|
});
|
|
const directBlocker = firstNonEmpty(
|
|
explicitDurabilityReady ? "" : runtime?.blocker,
|
|
durability?.blocker,
|
|
dbRuntimeReadiness?.blocker,
|
|
explicitDurabilityReady ? "" : blockerCodeStartingWith(payload, "runtime_durable_adapter_")
|
|
);
|
|
const reasonCode = firstNonEmpty(directBlocker, gateBlocker?.blocker, queryBlocker);
|
|
const blockedLayer = firstNonEmpty(
|
|
runtime?.durabilityContract?.blockedLayer,
|
|
durability?.blockedLayer,
|
|
dbRuntimeReadiness?.blockedLayer,
|
|
gateBlocker?.layer,
|
|
runtimeDurableLayerFromBlocker(reasonCode),
|
|
runtimeDurableLayerFromQueryResult(queryResult)
|
|
);
|
|
const adapter = firstNonEmpty(runtime?.adapter, durability?.adapter, dbRuntimeReadiness?.adapter, "unknown");
|
|
const runtimeDurable = runtime?.durable === true || durability?.durable === true || dbRuntimeReadiness?.durable === true;
|
|
const runtimeExplicitlyNonDurable = runtime?.durable === false || durability?.durable === false || dbRuntimeReadiness?.durable === false;
|
|
const runtimeReady = runtime?.ready === true || durability?.runtimeReady === true || dbRuntimeReadiness?.ready === true;
|
|
const durabilityReady = durability?.ready === true || dbRuntimeReadiness?.ready === true || runtimeReady;
|
|
const liveRuntimeEvidence =
|
|
runtime?.liveRuntimeEvidence === true ||
|
|
durability?.liveRuntimeEvidence === true ||
|
|
dbRuntimeReadiness?.liveRuntimeEvidence === true;
|
|
const rawStatus = rawStatusFrom(payload);
|
|
const postgresRequired = options.requirePostgresAdapter === true;
|
|
const adapterReady = !postgresRequired || adapter === "postgres";
|
|
const ready = adapterReady &&
|
|
runtimeDurable &&
|
|
runtimeReady &&
|
|
durabilityReady &&
|
|
liveRuntimeEvidence &&
|
|
!reasonCode &&
|
|
!blockedLayer &&
|
|
(!queryResult || readyQueryResult);
|
|
|
|
if (ready) {
|
|
return {
|
|
status: "ready",
|
|
ready: true,
|
|
classification: "runtime_durable_ready",
|
|
reasonCode: "runtime_durable_ready",
|
|
reason: `runtime durable ready; adapter=${adapter}; queryResult=${queryResult || "not_observed"}`,
|
|
impact: "Device Pod readiness checks may rely on durable runtime evidence when persistence readiness is green.",
|
|
safeNextAction: "Continue with repo-owned DEV CD postflight; do not read or print Secret values.",
|
|
retryable: true,
|
|
readonly: false,
|
|
evidence: runtimeDurableEvidence({
|
|
adapter,
|
|
runtimeDurable,
|
|
runtimeReady,
|
|
durabilityReady,
|
|
liveRuntimeEvidence,
|
|
blockedLayer: null,
|
|
queryResult,
|
|
readyQueryResult,
|
|
reasonCode: null,
|
|
gates,
|
|
rawStatus,
|
|
postgresRequired
|
|
})
|
|
};
|
|
}
|
|
|
|
const nonReadyReasonCode = firstNonEmpty(
|
|
reasonCode,
|
|
postgresRequired && adapter !== "postgres" ? "runtime_durable_adapter_not_postgres" : "",
|
|
runtimeExplicitlyNonDurable ? "runtime_durable_false" : "",
|
|
runtimeDurable ? "" : "runtime_durable_adapter_missing",
|
|
runtimeReady ? "" : "runtime_durable_runtime_not_ready",
|
|
liveRuntimeEvidence ? "" : "runtime_durable_live_evidence_missing",
|
|
isBlockedRuntimeStatus(rawStatus) ? rawStatus : "",
|
|
"runtime_durable_adapter_not_ready"
|
|
);
|
|
const layer = firstNonEmpty(
|
|
blockedLayer,
|
|
runtimeDurableLayerFromBlocker(nonReadyReasonCode),
|
|
runtimeExplicitlyNonDurable ? "runtime-durable" : "",
|
|
"runtime-durable"
|
|
);
|
|
return {
|
|
status: "blocked",
|
|
ready: false,
|
|
classification: nonReadyReasonCode,
|
|
reasonCode: nonReadyReasonCode,
|
|
reason: runtimeDurableBlockedReason({ reasonCode: nonReadyReasonCode, adapter, layer, queryResult }),
|
|
impact: "Device Pod readiness remains source-level while durable runtime evidence is blocked.",
|
|
safeNextAction: runtimeDurableSafeNextAction(nonReadyReasonCode, layer),
|
|
retryable: true,
|
|
readonly: true,
|
|
evidence: runtimeDurableEvidence({
|
|
adapter,
|
|
runtimeDurable,
|
|
runtimeReady,
|
|
durabilityReady,
|
|
liveRuntimeEvidence,
|
|
blockedLayer: layer,
|
|
queryResult,
|
|
readyQueryResult,
|
|
reasonCode: nonReadyReasonCode,
|
|
gates,
|
|
rawStatus,
|
|
postgresRequired
|
|
})
|
|
};
|
|
}
|
|
|
|
function durabilityReadySignal({
|
|
runtime,
|
|
durability,
|
|
dbRuntimeReadiness,
|
|
gateBlocker,
|
|
queryResult,
|
|
readyQueryResult,
|
|
queryBlocker
|
|
}) {
|
|
if (gateBlocker || queryBlocker) return false;
|
|
if (queryResult && !readyQueryResult) return false;
|
|
const ready = durability?.ready === true ||
|
|
dbRuntimeReadiness?.ready === true ||
|
|
runtime?.durabilityContract?.ready === true;
|
|
if (!ready) return false;
|
|
return !firstNonEmpty(
|
|
durability?.blocker,
|
|
dbRuntimeReadiness?.blocker,
|
|
runtime?.durabilityContract?.blocker,
|
|
durability?.blockedLayer,
|
|
dbRuntimeReadiness?.blockedLayer,
|
|
runtime?.durabilityContract?.blockedLayer
|
|
);
|
|
}
|
|
|
|
function isBlockedRuntimeStatus(status) {
|
|
return [...ERROR_STATUSES, ...BLOCKED_STATUSES, ...RAW_DEGRADED_STATUSES].includes(normalizedStatus(status));
|
|
}
|
|
|
|
function firstBlockedRuntimeGate(gates = {}) {
|
|
for (const layer of RUNTIME_DURABLE_GATE_LAYERS) {
|
|
const gate = gates?.[layer];
|
|
const status = normalizedStatus(gate?.status);
|
|
if (gate?.ready === false || status === "blocked" || status === "failed") {
|
|
return {
|
|
layer,
|
|
blocker: firstNonEmpty(gate?.blocker, runtimeDurableBlockerForLayer(layer))
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function runtimeDurableBlockerForLayer(layer) {
|
|
const text = normalizedStatus(layer);
|
|
if (text === "ssl") return "runtime_durable_adapter_ssl_blocked";
|
|
if (text === "auth") return "runtime_durable_adapter_auth_blocked";
|
|
if (text === "schema") return "runtime_durable_adapter_schema_blocked";
|
|
if (text === "migration") return "runtime_durable_adapter_migration_blocked";
|
|
if (text === "durability" || text === "durability_query") return "runtime_durable_adapter_query_blocked";
|
|
return "runtime_durable_adapter_blocked";
|
|
}
|
|
|
|
function runtimeDurableLayerFromBlocker(blocker) {
|
|
const text = normalizedStatus(blocker);
|
|
if (!text) return "";
|
|
if (text.includes("ssl")) return "ssl";
|
|
if (text.includes("auth")) return "auth";
|
|
if (text.includes("schema")) return "schema";
|
|
if (text.includes("migration")) return "migration";
|
|
if (text.includes("query")) return "durability_query";
|
|
if (text.includes("missing") || text.includes("unconfigured") || text.includes("driver")) return "adapter";
|
|
return "";
|
|
}
|
|
|
|
function runtimeDurableLayerFromQueryResult(queryResult) {
|
|
const text = normalizedStatus(queryResult);
|
|
if (!text || isRuntimeDurableReadyQueryResult(text)) return "";
|
|
if (text.includes("ssl")) return "ssl";
|
|
if (text.includes("auth")) return "auth";
|
|
if (text.includes("schema")) return "schema";
|
|
if (text.includes("migration")) return "migration";
|
|
if (text.includes("query")) return "durability_query";
|
|
if (text.includes("driver")) return "adapter";
|
|
return "";
|
|
}
|
|
|
|
function runtimeDurableBlockerFromQueryResult(queryResult) {
|
|
const text = normalizedStatus(queryResult);
|
|
if (!text || isRuntimeDurableReadyQueryResult(text)) return "";
|
|
return RUNTIME_DURABLE_BLOCKED_QUERY_RESULTS[text] ?? "";
|
|
}
|
|
|
|
function isRuntimeDurableReadyQueryResult(queryResult) {
|
|
const text = normalizedStatus(queryResult);
|
|
return RUNTIME_DURABLE_READY_QUERY_RESULTS.includes(text);
|
|
}
|
|
|
|
function runtimeDurableEvidence({
|
|
adapter,
|
|
runtimeDurable,
|
|
runtimeReady,
|
|
durabilityReady,
|
|
liveRuntimeEvidence,
|
|
blockedLayer,
|
|
queryResult,
|
|
readyQueryResult,
|
|
reasonCode,
|
|
gates,
|
|
rawStatus,
|
|
postgresRequired
|
|
}) {
|
|
return {
|
|
adapter,
|
|
durable: runtimeDurable,
|
|
runtimeReady,
|
|
durabilityReady,
|
|
liveRuntimeEvidence,
|
|
blocker: reasonCode,
|
|
blockedLayer,
|
|
queryResult: queryResult || null,
|
|
readyQueryResult,
|
|
rawStatus: rawStatus || null,
|
|
postgresRequired,
|
|
gates: summarizeRuntimeGateStatuses(gates),
|
|
secretMaterialRead: false
|
|
};
|
|
}
|
|
|
|
function summarizeRuntimeGateStatuses(gates = {}) {
|
|
return Object.fromEntries(RUNTIME_DURABLE_GATE_LAYERS.map((layer) => [
|
|
layer,
|
|
{
|
|
checked: gates?.[layer]?.checked === true,
|
|
ready: gates?.[layer]?.ready === true,
|
|
status: gates?.[layer]?.status ?? "unknown",
|
|
blocker: gates?.[layer]?.blocker ?? null
|
|
}
|
|
]));
|
|
}
|
|
|
|
function runtimeDurableBlockedReason({ reasonCode, adapter, layer, queryResult }) {
|
|
if (reasonCode === "runtime_durable_false") {
|
|
return `runtime durable=false; adapter=${adapter}; queryResult=${queryResult || "not_observed"}`;
|
|
}
|
|
if (reasonCode === "runtime_durable_adapter_not_postgres") {
|
|
return `runtime durable adapter is not postgres; adapter=${adapter}; queryResult=${queryResult || "not_observed"}`;
|
|
}
|
|
return `runtime durable blocked at ${layer || "runtime-durable"}; blocker=${reasonCode}; queryResult=${queryResult || "unknown"}`;
|
|
}
|
|
|
|
function runtimeDurableSafeNextAction(reasonCode, layer) {
|
|
const text = normalizedStatus(`${reasonCode} ${layer}`);
|
|
if (text.includes("auth")) return "Repair DEV DB auth/SecretRef metadata through repo-owned provisioning, then rerun postflight without printing Secret values.";
|
|
if (text.includes("schema") || text.includes("migration")) return "Run repo-owned DEV runtime provisioning and migration, then rerun postflight.";
|
|
if (text.includes("ssl")) return "Align DEV DB SSL mode with the repo-owned DEV contract, then rerun postflight.";
|
|
if (text.includes("query")) return "Inspect durable runtime query readiness, preserve evidence persistence, and rerun postflight.";
|
|
if (text.includes("postgres")) return "Run DEV with HWLAB_CLOUD_RUNTIME_ADAPTER=postgres and DB URLs injected only through SecretRefs.";
|
|
return "Restore durable runtime readiness through repo-owned DEV CD steps, then rerun postflight.";
|
|
}
|
|
|
|
function firstBlocker(payload) {
|
|
const blockers = [
|
|
...(Array.isArray(payload?.blockers) ? payload.blockers : []),
|
|
...(Array.isArray(payload?.readiness?.blockers) ? payload.readiness.blockers : [])
|
|
].filter(Boolean);
|
|
if (blockers.length > 0) return blockers[0];
|
|
const code = firstNonEmpty(
|
|
...(Array.isArray(payload?.blockerCodes) ? payload.blockerCodes : []),
|
|
...(Array.isArray(payload?.readiness?.blockerCodes) ? payload.readiness.blockerCodes : [])
|
|
);
|
|
return code ? { code, summary: code } : null;
|
|
}
|
|
|
|
function blockerCodeStartingWith(payload, prefix) {
|
|
return [
|
|
...(Array.isArray(payload?.blockerCodes) ? payload.blockerCodes : []),
|
|
...(Array.isArray(payload?.readiness?.blockerCodes) ? payload.readiness.blockerCodes : [])
|
|
].find((code) => String(code ?? "").startsWith(prefix));
|
|
}
|
|
|
|
function readinessFalse(payload) {
|
|
return [
|
|
payload?.ready,
|
|
payload?.readiness?.ready,
|
|
payload?.readiness?.durability?.ready,
|
|
payload?.runtime?.ready
|
|
].some((value) => value === false);
|
|
}
|
|
|
|
function rawStatusFrom(payload) {
|
|
return payload?.status ?? payload?.readiness?.status ?? payload?.runtime?.status ?? payload?.readiness?.durability?.status ?? "";
|
|
}
|
|
|
|
function statusResult(result) {
|
|
return {
|
|
serviceId: null,
|
|
apiPath: null,
|
|
reasonCode: null,
|
|
traceId: null,
|
|
internalRawStatuses: result.probes
|
|
.map((probe) => probe.rawStatus)
|
|
.filter((status) => status !== undefined && status !== null && String(status).trim() !== ""),
|
|
...result
|
|
};
|
|
}
|
|
|
|
function passProbe(options) {
|
|
return probe({ kind: "pass", reachable: true, ...options });
|
|
}
|
|
|
|
function readOnlyProbe(options) {
|
|
return probe({ kind: "readonly", reachable: true, ...options });
|
|
}
|
|
|
|
function errorProbe(options) {
|
|
return probe({ kind: "error", reachable: Boolean(options.httpStatus ? options.httpStatus >= 200 && options.httpStatus < 600 : true), ...options });
|
|
}
|
|
|
|
function unverifiedProbe(options) {
|
|
return probe({ kind: "unverified", reachable: false, ...options });
|
|
}
|
|
|
|
function probe(options) {
|
|
return {
|
|
kind: options.kind,
|
|
reachable: options.reachable,
|
|
serviceId: options.serviceId,
|
|
apiPath: options.apiPath,
|
|
reasonCode: String(options.reasonCode ?? options.kind),
|
|
reason: String(options.reason ?? options.reasonCode ?? options.kind),
|
|
httpStatus: options.httpStatus ?? null,
|
|
rawStatus: options.rawStatus ?? null,
|
|
traceId: options.traceId ?? null,
|
|
evidenceSummary: options.evidenceSummary ?? null,
|
|
meta: options.meta ?? null
|
|
};
|
|
}
|
|
|
|
function probeAttribution(probe) {
|
|
return [
|
|
`${probe.serviceId} ${probe.apiPath}`,
|
|
probe.httpStatus ? `HTTP ${probe.httpStatus}` : null,
|
|
displayReasonCode(probe.reasonCode),
|
|
probe.reason && probe.reason !== probe.reasonCode ? displayReasonText(probe.reason) : null,
|
|
probe.traceId ? `trace=${probe.traceId}` : null,
|
|
probe.evidenceSummary
|
|
].filter(Boolean).join(" / ");
|
|
}
|
|
|
|
function displayReasonCode(reasonCode) {
|
|
return String(reasonCode ?? "");
|
|
}
|
|
|
|
function displayReasonText(reason) {
|
|
return String(reason ?? "").replace(/\bruntime_durable_adapter_[a-z0-9_]+\b/giu, "runtime durable");
|
|
}
|
|
|
|
function normalizedStatus(value) {
|
|
return String(value ?? "").trim().toLowerCase();
|
|
}
|
|
|
|
function isReadOnlyReason(value) {
|
|
const text = normalizedStatus(value);
|
|
return READ_ONLY_REASON_CODES.some((code) => text.includes(code));
|
|
}
|
|
|
|
function codeAgentReadOnlySessionToolsReason(availability) {
|
|
const flags = [
|
|
...(Array.isArray(availability?.runnerLimitations) ? availability.runnerLimitations : []),
|
|
...(Array.isArray(availability?.runner?.limitations) ? availability.runner.limitations : [])
|
|
].filter(Boolean);
|
|
const issue = firstNonEmpty(availability?.issue, availability?.trackingIssue, "#317");
|
|
return `capability=read-only-session-tools; ${issue} full pass not complete${flags.length > 0 ? `; ${flags.join(",")}` : ""}`;
|
|
}
|
|
|
|
function traceIdFrom(value) {
|
|
return firstNonEmpty(
|
|
value?.traceId,
|
|
value?.meta?.traceId,
|
|
value?.data?.traceId,
|
|
value?.data?.meta?.traceId,
|
|
value?.data?.trust?.traceId,
|
|
value?.request?.traceId,
|
|
value?.operation?.traceId,
|
|
value?.result?.traceId,
|
|
value?.trust?.traceId,
|
|
value?.evidenceState?.traceId,
|
|
value?.metadata?.traceId,
|
|
value?.error?.traceId,
|
|
value?.session?.lastTraceId,
|
|
value?.session?.currentTraceId,
|
|
value?.runnerTrace?.traceId,
|
|
value?.providerTrace?.traceId
|
|
);
|
|
}
|
|
|
|
function evidenceSummaryFrom(value) {
|
|
const payload = value?.data && typeof value.data === "object" ? value.data : value;
|
|
const parts = [
|
|
["operation", firstNonEmpty(payload?.operationId, payload?.operation?.operationId, payload?.result?.operationId, payload?.trust?.operationId)],
|
|
["audit", firstNonEmpty(payload?.auditId, payload?.audit?.auditId, payload?.auditEvent?.auditId, payload?.trust?.auditId)],
|
|
["evidence", firstNonEmpty(payload?.evidenceId, payload?.evidence?.evidenceId, payload?.evidenceRecord?.evidenceId, payload?.trust?.evidenceId)],
|
|
["session", firstNonEmpty(payload?.session?.sessionId, payload?.sessionId)]
|
|
]
|
|
.filter(([, id]) => id)
|
|
.slice(0, 4)
|
|
.map(([label, id]) => `${label}=${id}`);
|
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
}
|
|
|
|
|
|
function firstNonEmpty(...values) {
|
|
for (const value of values) {
|
|
if (value !== undefined && value !== null && String(value).trim() !== "") return String(value).trim();
|
|
}
|
|
return "";
|
|
}
|