Merge pull request #197 from pikasTech/fix/trusted-record-runtime-postgres-store

fix: gate durable runtime readiness
This commit is contained in:
Lyon
2026-05-23 07:45:46 +08:00
committed by GitHub
12 changed files with 430 additions and 98 deletions
+16 -5
View File
@@ -48,11 +48,22 @@ non-secret env flags:
The Postgres adapter uses `HWLAB_CLOUD_DB_URL` from
`hwlab-cloud-api-dev-db/database-url`; source records only the Secret reference
and non-secret DNS contract. The adapter reports `runtime.ready: true` only
after a live schema query proves the required runtime tables and columns exist.
If DB auth or schema is missing, `/health/live` stays degraded and reports a
runtime blocker such as `runtime_durable_adapter_auth_blocked` or
`runtime_durable_adapter_schema_blocked`. Fixture data and env presence are
and non-secret DNS contract. The adapter reports `runtime.durable: true` only
after the configured adapter proves all durable gates through read-only runtime
checks:
- auth/query access to the configured DB path;
- required runtime table and column schema;
- the source-controlled migration ledger row for
`0001_cloud_core_skeleton` / `runtime-durable-postgres-v1`;
- read readiness against the runtime tables used by audit and evidence
queries.
If DB auth, schema, migration, or read readiness is missing, `/health/live`
stays degraded with `runtime.durable: false`, `runtime.durableRequested: true`,
and a runtime blocker such as `runtime_durable_adapter_auth_blocked`,
`runtime_durable_adapter_schema_blocked`, or
`runtime_durable_adapter_migration_blocked`. Fixture data and env presence are
never promoted to `runtime.liveRuntimeEvidence`.
If `HWLAB_CLOUD_DB_URL` and `HWLAB_CLOUD_DB_SSL_MODE` are absent, health
+6 -4
View File
@@ -168,12 +168,14 @@ Readiness reports must keep these dimensions distinct:
| Dimension | Green condition | Degraded/blocking condition |
| --- | --- | --- |
| DB live | `db.ready=true`, `db.connected=true`, `db.liveDbEvidence=true` | Missing env, failed connection, disabled probe, or no `liveDbEvidence` |
| Runtime durability | `runtime.durable=true` with a durable adapter | `runtime.adapter="memory"` or `runtime.durable=false` |
| Runtime durability | `runtime.adapter="postgres"`, `runtime.durable=true`, `runtime.ready=true`, `runtime.liveRuntimeEvidence=true`, and runtime auth/schema/migration/read gates are all ready | `runtime.adapter="memory"`, `runtime.durable=false`, missing migration ledger, failed schema/auth/readiness, or no `liveRuntimeEvidence` |
Do not declare DEV-LIVE complete while `runtime.adapter="memory"` or
`runtime.durable=false`, even when DB live readiness is connected. The next
fix boundary is a durable runtime store and migration path; documenting or
testing this contract must not mutate runtime state or PROD.
`runtime.durable=false`, even when DB live readiness is connected. A selected
Postgres adapter may report `runtime.durableRequested=true`, but it still stays
non-durable until the schema, migration ledger, and read readiness gates are
proven through the configured adapter. Documenting or testing this contract
must not mutate runtime state or PROD.
## Runtime Substitution Ban
+17 -5
View File
@@ -1,4 +1,7 @@
import { RUNTIME_DURABLE_ADAPTER_MISSING } from "../db/runtime-store.mjs";
import {
RUNTIME_DURABLE_ADAPTER_MISSING,
RUNTIME_STORE_KIND_POSTGRES
} from "../db/runtime-store.mjs";
export const CLOUD_API_HEALTH_CONTRACT_VERSION = "v2";
@@ -46,6 +49,9 @@ function isRuntimeReady(runtime) {
if (runtime?.durable !== true) {
return false;
}
if (runtime?.ready !== true || runtime?.liveRuntimeEvidence !== true) {
return false;
}
return !["blocked", "degraded", "failed"].includes(runtime.status);
}
@@ -87,31 +93,37 @@ function codeAgentBlocker(codeAgent) {
function runtimeBlocker(runtime) {
const durable = runtime?.durable === true;
const unhealthy = durable && ["blocked", "degraded", "failed"].includes(runtime?.status);
const durableRequested = runtime?.durableRequested === true || runtime?.adapter === RUNTIME_STORE_KIND_POSTGRES;
const unhealthy = (durable || durableRequested) && ["blocked", "degraded", "failed"].includes(runtime?.status);
const blocker = runtime?.blocker;
return {
code: durable && unhealthy ? blocker ?? "runtime_durable_adapter_unhealthy" : RUNTIME_DURABLE_ADAPTER_MISSING,
code: durableRequested && unhealthy ? blocker ?? "runtime_durable_adapter_unhealthy" : RUNTIME_DURABLE_ADAPTER_MISSING,
type: "runtime_blocker",
scope: "runtime-durable-adapter",
status: "open",
sourceIssue: "pikasTech/HWLAB#164",
summary: durable && unhealthy
summary: durableRequested && unhealthy
? runtime?.reason ?? "cloud-api durable runtime adapter is configured but not ready"
: "cloud-api DB is live, but L1 runtime writes still use process-local memory because no DB-backed durable adapter is configured",
evidence: {
adapter: runtime?.adapter ?? "unknown",
durable: Boolean(runtime?.durable),
durableRequested: Boolean(runtime?.durableRequested),
durableCapable: Boolean(runtime?.durableCapable),
runtimeStatus: runtime?.status ?? "unknown",
runtimeReady: Boolean(runtime?.ready),
liveRuntimeEvidence: Boolean(runtime?.liveRuntimeEvidence),
schemaReady: Boolean(runtime?.schema?.ready),
schemaChecked: Boolean(runtime?.schema?.checked),
migrationReady: Boolean(runtime?.migration?.ready),
migrationChecked: Boolean(runtime?.migration?.checked),
gates: runtime?.gates ?? null,
queryAttempted: Boolean(runtime?.connection?.queryAttempted),
queryResult: runtime?.connection?.queryResult ?? "unknown",
dbBackedAdapterRequired: true,
secretMaterialRead: false
},
nextTask: durable
nextTask: durableRequested
? "Apply/repair the runtime DB schema or DB auth, then rerun /health/live without printing DB secret material."
: "Set HWLAB_CLOUD_RUNTIME_ADAPTER=postgres after the runtime DB schema is available; keep HWLAB_CLOUD_DB_URL injected from Secret."
};
+10 -2
View File
@@ -200,7 +200,9 @@ test("cloud api health separates DB connected from durable runtime schema readin
async readiness() {
return {
adapter: "postgres",
durable: true,
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
@@ -219,6 +221,11 @@ test("cloud api health separates DB connected from durable runtime schema readin
missingTables: ["gateway_sessions"],
missingColumns: ["gateway_sessions.gateway_session_json"]
},
migration: {
checked: false,
ready: false,
missing: true
},
counts: {}
};
}
@@ -234,7 +241,8 @@ test("cloud api health separates DB connected from durable runtime schema readin
assert.equal(payload.status, "degraded");
assert.equal(payload.db.connected, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.durable, true);
assert.equal(payload.runtime.durable, false);
assert.equal(payload.runtime.durableRequested, true);
assert.equal(payload.runtime.ready, false);
assert.equal(payload.runtime.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
assert.equal(payload.readiness.components.db, "ready");
@@ -115,3 +115,21 @@ CREATE TABLE IF NOT EXISTS evidence_records (
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS hwlab_schema_migrations (
id TEXT PRIMARY KEY,
schema_version TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
migration_json TEXT NOT NULL DEFAULT '{}'
);
INSERT INTO hwlab_schema_migrations (id, schema_version, applied_at, migration_json)
VALUES (
'0001_cloud_core_skeleton',
'runtime-durable-postgres-v1',
CURRENT_TIMESTAMP,
'{"path":"internal/db/migrations/0001_cloud_core_skeleton.sql","runtime":"cloud-api"}'
)
ON CONFLICT (id) DO UPDATE SET
schema_version = EXCLUDED.schema_version,
migration_json = EXCLUDED.migration_json;
+224 -71
View File
@@ -11,6 +11,12 @@ import {
createProtocolAuditEvent,
deriveProtocolActorFromMeta
} from "../audit/index.mjs";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.mjs";
export const RUNTIME_STORE_KIND = "memory";
export const RUNTIME_STORE_KIND_POSTGRES = "postgres";
@@ -19,68 +25,13 @@ export const RUNTIME_DURABLE_ADAPTER_UNCONFIGURED = "runtime_durable_adapter_unc
export const RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING = "runtime_durable_adapter_driver_missing";
export const RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED = "runtime_durable_adapter_auth_blocked";
export const RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED = "runtime_durable_adapter_schema_blocked";
export const RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED = "runtime_durable_adapter_migration_blocked";
export const RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED = "runtime_durable_adapter_query_blocked";
export const RUNTIME_ADAPTER_ENV = "HWLAB_CLOUD_RUNTIME_ADAPTER";
export const RUNTIME_DURABLE_ENV = "HWLAB_CLOUD_RUNTIME_DURABLE";
const requiredPostgresSchema = Object.freeze({
gateway_sessions: Object.freeze([
"id",
"project_id",
"gateway_service_id",
"status",
"started_at",
"ended_at",
"gateway_session_json"
]),
box_resources: Object.freeze([
"id",
"project_id",
"gateway_session_id",
"resource_state",
"labels_json",
"resource_json",
"updated_at"
]),
box_capabilities: Object.freeze([
"id",
"box_resource_id",
"capability_type",
"capability_json",
"updated_at"
]),
hardware_operations: Object.freeze([
"id",
"project_id",
"requested_by",
"operation_type",
"operation_json",
"status",
"requested_at",
"updated_at"
]),
audit_events: Object.freeze([
"id",
"request_id",
"actor",
"source",
"operation",
"target",
"result",
"timestamp",
"event_json"
]),
evidence_records: Object.freeze([
"id",
"project_id",
"operation_id",
"evidence_type",
"uri",
"metadata_json",
"created_at"
])
});
const requiredPostgresSchema = CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS;
const postgresCountTables = Object.freeze([
"gateway_sessions",
@@ -631,7 +582,7 @@ export class PostgresCloudRuntimeStore {
this.pool = null;
this.memory = new CloudRuntimeStore({ now });
this.lastReadiness = this.blockedSummary({
blocker: dbUrl || queryClient ? "runtime durable adapter has not completed schema readiness" : RUNTIME_DURABLE_ADAPTER_UNCONFIGURED,
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"
@@ -671,31 +622,131 @@ export class PostgresCloudRuntimeStore {
this.lastReadiness = this.blockedSummary({
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
reason: "Postgres runtime adapter connected, but required runtime tables or columns are missing",
schema
schema,
connection: {
queryAttempted: true,
queryResult: "schema_blocked"
},
gates: runtimeGates({
auth: readyGate(),
schema: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED }),
migration: notCheckedGate(),
durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED })
})
});
return this.summary();
}
let migration;
try {
migration = await this.readMigrationReadiness();
} catch (error) {
const classified = classifyRuntimeDbError(error);
this.lastReadiness = this.blockedSummary({
blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
reason: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? classified.reason
: "Postgres runtime schema is present, but the durable runtime migration ledger could not be proven",
schema,
migration: blockedRuntimeMigration({ errorCode: classified.connection?.errorCode }),
connection: {
queryAttempted: true,
queryResult: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? "auth_blocked" : "migration_blocked",
errorCode: classified.connection?.errorCode ?? null
},
gates: runtimeGates({
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
schema: readyGate(),
migration: blockedGate({
checked: true,
blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED
}),
durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED })
})
});
return this.summary();
}
if (!migration.ready) {
this.lastReadiness = this.blockedSummary({
blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
reason: "Postgres runtime schema is present, but the required source migration is not recorded",
schema,
migration,
connection: {
queryAttempted: true,
queryResult: "migration_blocked"
},
gates: runtimeGates({
auth: readyGate(),
schema: readyGate(),
migration: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }),
durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED })
})
});
return this.summary();
}
let counts;
try {
counts = await this.readCounts();
} catch (error) {
const classified = classifyRuntimeDbError(error);
this.lastReadiness = this.blockedSummary({
...classified,
schema,
migration,
connection: {
queryAttempted: true,
queryResult: classified.connection?.queryResult ?? "query_blocked",
errorCode: classified.connection?.errorCode ?? null
},
gates: runtimeGates({
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
schema: readyGate(),
migration: readyGate(),
durability: blockedGate({ checked: true, blocker: classified.blocker })
})
});
return this.summary();
}
const counts = await this.readCounts().catch(() => null);
this.lastReadiness = {
adapter: RUNTIME_STORE_KIND_POSTGRES,
durable: true,
durableRequested: true,
durableCapable: true,
ready: true,
status: "ready",
blocker: null,
reason: "Postgres durable runtime adapter completed a live schema readiness query",
reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries",
liveRuntimeEvidence: true,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult: "schema_ready",
queryResult: "durable_readiness_ready",
endpointRedacted: true,
valueRedacted: true
},
schema,
migration,
gates: runtimeGates({
auth: readyGate(),
schema: readyGate(),
migration: readyGate(),
durability: readyGate()
}),
safety: runtimeSafety(),
adapterContract: postgresAdapterContract(),
counts: counts ?? this.memory.summary().counts
counts
};
return this.summary();
}
@@ -992,6 +1043,27 @@ export class PostgresCloudRuntimeStore {
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);
@@ -1033,10 +1105,19 @@ export class PostgresCloudRuntimeStore {
return this.pool;
}
blockedSummary({ blocker, reason, schema, connection }) {
blockedSummary({ blocker, reason, schema, migration, connection, gates }) {
const blockedSchema = schema ?? {
ready: false,
checked: false,
missingTables: [],
missingColumns: []
};
const blockedMigration = migration ?? notCheckedRuntimeMigration();
return {
adapter: RUNTIME_STORE_KIND_POSTGRES,
durable: true,
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker,
@@ -1050,12 +1131,18 @@ export class PostgresCloudRuntimeStore {
valueRedacted: true,
errorCode: connection?.errorCode ?? null
},
schema: schema ?? {
ready: false,
checked: false,
missingTables: [],
missingColumns: []
},
schema: blockedSchema,
migration: blockedMigration,
gates: gates ?? runtimeGates({
auth: connection?.queryAttempted ? blockedGate({ checked: true, blocker }) : notCheckedGate(),
schema: blockedSchema.checked
? gateFromReadiness(blockedSchema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED })
: notCheckedGate(),
migration: blockedMigration.checked
? gateFromReadiness(blockedMigration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED })
: notCheckedGate(),
durability: blockedGate({ checked: false, blocker })
}),
safety: runtimeSafety(),
adapterContract: postgresAdapterContract()
};
@@ -1289,6 +1376,72 @@ function summarizeRuntimeSchema(rows) {
};
}
function notCheckedRuntimeMigration() {
return {
checked: false,
ready: false,
table: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
requiredMigrationId: CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
appliedMigrationId: null,
appliedSchemaVersion: null,
missing: true
};
}
function blockedRuntimeMigration({ errorCode = null } = {}) {
return {
...notCheckedRuntimeMigration(),
checked: true,
errorCode
};
}
function runtimeGates({
auth = notCheckedGate(),
schema = notCheckedGate(),
migration = notCheckedGate(),
durability = notCheckedGate()
} = {}) {
return {
auth,
schema,
migration,
durability
};
}
function readyGate() {
return {
checked: true,
ready: true,
status: "ready",
blocker: null
};
}
function blockedGate({ checked = true, blocker = "runtime_durable_adapter_blocked" } = {}) {
return {
checked,
ready: false,
status: checked ? "blocked" : "not_checked",
blocker
};
}
function notCheckedGate() {
return {
checked: false,
ready: false,
status: "not_checked",
blocker: null
};
}
function gateFromReadiness(ready, { blocker } = {}) {
return ready ? readyGate() : blockedGate({ checked: true, blocker });
}
function snapshotMemory(memory) {
return {
gatewaySessions: new Map(memory.gatewaySessions),
+57 -5
View File
@@ -4,12 +4,17 @@ import test from "node:test";
import { handleJsonRpcRequest } from "../cloud/json-rpc.mjs";
import { validateResponse } from "../protocol/index.mjs";
import {
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_STORE_KIND_POSTGRES,
createCloudRuntimeStore,
createConfiguredCloudRuntimeStore
} from "./runtime-store.mjs";
import { CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS } from "./schema.mjs";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.mjs";
test("memory runtime store is never marked durable", () => {
const store = createCloudRuntimeStore();
@@ -39,7 +44,8 @@ test("configured postgres runtime reports schema blocker without green readiness
const readiness = await store.readiness();
assert.equal(readiness.adapter, RUNTIME_STORE_KIND_POSTGRES);
assert.equal(readiness.durable, true);
assert.equal(readiness.durable, false);
assert.equal(readiness.durableRequested, true);
assert.equal(readiness.ready, false);
assert.equal(readiness.status, "blocked");
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
@@ -47,11 +53,40 @@ test("configured postgres runtime reports schema blocker without green readiness
assert.equal(readiness.fixtureEvidence, false);
assert.equal(readiness.schema.checked, true);
assert.ok(readiness.schema.missingTables.includes("gateway_sessions"));
assert.equal(readiness.migration.checked, false);
assert.equal(readiness.gates.schema.status, "blocked");
assert.equal(readiness.gates.migration.status, "not_checked");
assert.equal(JSON.stringify(readiness).includes("redacted@db.example"), false);
});
test("configured postgres runtime keeps durable false when migration ledger is missing", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
queryClient: createFakePostgresClient({ migrationReady: false })
});
const readiness = await store.readiness();
assert.equal(readiness.adapter, RUNTIME_STORE_KIND_POSTGRES);
assert.equal(readiness.durable, false);
assert.equal(readiness.durableRequested, true);
assert.equal(readiness.ready, false);
assert.equal(readiness.status, "blocked");
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED);
assert.equal(readiness.schema.ready, true);
assert.equal(readiness.migration.checked, true);
assert.equal(readiness.migration.ready, false);
assert.equal(readiness.migration.missing, true);
assert.equal(readiness.gates.schema.status, "ready");
assert.equal(readiness.gates.migration.status, "blocked");
assert.equal(readiness.liveRuntimeEvidence, false);
});
test("configured postgres runtime persists and queries records through query client", async () => {
const queryClient = createFakePostgresClient();
const queryClient = createFakePostgresClient({ migrationReady: true });
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
@@ -88,9 +123,15 @@ test("configured postgres runtime persists and queries records through query cli
const health = await rpc("req_01J00000000000000000001000", "system.health");
assert.equal(health.runtime.adapter, "postgres");
assert.equal(health.runtime.durable, true);
assert.equal(health.runtime.durableRequested, true);
assert.equal(health.runtime.durableCapable, true);
assert.equal(health.runtime.ready, true);
assert.equal(health.runtime.liveRuntimeEvidence, true);
assert.equal(health.runtime.fixtureEvidence, false);
assert.equal(health.runtime.schema.ready, true);
assert.equal(health.runtime.migration.ready, true);
assert.equal(health.runtime.gates.migration.status, "ready");
assert.equal(health.runtime.gates.durability.status, "ready");
assert.equal(health.readiness.components.runtime, "ready");
await rpc("req_01J00000000000000000001001", "gateway.session.register", {
@@ -141,15 +182,22 @@ test("configured postgres runtime persists and queries records through query cli
assert.equal(evidence.records[0].operationId, invoke.operationId);
});
function createFakePostgresClient() {
function createFakePostgresClient({ migrationReady = true } = {}) {
const state = {
gateway_sessions: new Map(),
box_resources: new Map(),
box_capabilities: new Map(),
hardware_operations: new Map(),
audit_events: new Map(),
evidence_records: new Map()
evidence_records: new Map(),
hwlab_schema_migrations: new Map()
};
if (migrationReady) {
state.hwlab_schema_migrations.set(CLOUD_CORE_MIGRATION_ID, {
id: CLOUD_CORE_MIGRATION_ID,
schema_version: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION
});
}
const calls = [];
return {
@@ -163,6 +211,10 @@ function createFakePostgresClient() {
const table = sql.match(/FROM ([a-z_]+)/u)?.[1];
return { rows: [{ count: state[table]?.size ?? 0 }] };
}
if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) {
const row = state.hwlab_schema_migrations.get(params[0]);
return { rows: row ? [row] : [] };
}
if (sql.startsWith("SELECT gateway_session_json FROM gateway_sessions")) {
return jsonSelect(state.gateway_sessions, params[0], "gateway_session_json");
}
+10 -1
View File
@@ -2,9 +2,17 @@ import { TABLES } from "../protocol/index.mjs";
export const CLOUD_CORE_MIGRATION_ID = "0001_cloud_core_skeleton";
export const CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION = "runtime-durable-postgres-v1";
export const CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE = "hwlab_schema_migrations";
export const FROZEN_CLOUD_CORE_TABLES = Object.freeze([...TABLES]);
export const CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS = Object.freeze([
"id",
"schema_version",
"applied_at",
"migration_json"
]);
export const CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS = Object.freeze({
gateway_sessions: Object.freeze([
"id",
@@ -69,7 +77,8 @@ export const CLOUD_CORE_MIGRATIONS = Object.freeze([
path: "internal/db/migrations/0001_cloud_core_skeleton.sql",
tables: FROZEN_CLOUD_CORE_TABLES,
connectsToDatabase: false,
runtimeDurableAdapterSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION
runtimeDurableAdapterSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
runtimeDurableMigrationTable: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE
}
]);
+15
View File
@@ -7,6 +7,10 @@ import { fileURLToPath } from "node:url";
import { TABLES, assertProtocolRecord, assertProtocolRecords } from "../protocol/index.mjs";
import {
CLOUD_CORE_MIGRATIONS,
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS,
FROZEN_CLOUD_CORE_TABLES,
assertFrozenCloudCoreTables,
@@ -48,6 +52,17 @@ test("initial migration exposes columns required by the durable runtime adapter"
assert.match(tableMatch[1], new RegExp(`\\b${column}\\b`), `missing ${table}.${column}`);
}
}
const migrationMatch = sql.match(
new RegExp(`CREATE TABLE IF NOT EXISTS ${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}\\s*\\(([\\s\\S]*?)\\);`, "u")
);
assert.ok(migrationMatch, `missing ${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}`);
for (const column of CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS) {
assert.match(migrationMatch[1], new RegExp(`\\b${column}\\b`), `missing migration ledger column ${column}`);
}
assert.match(sql, new RegExp(`'${CLOUD_CORE_MIGRATION_ID}'`, "u"));
assert.match(sql, new RegExp(`'${CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION}'`, "u"));
assert.equal(CLOUD_CORE_MIGRATIONS[0].runtimeDurableMigrationTable, CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE);
});
test("protocol record guards catch schema drift before runtime writes", () => {
+3
View File
@@ -868,6 +868,9 @@ function liveRecordTone(record, live) {
function liveRecordDurabilityBlocker(live) {
const runtimeSummary = runtimeSummaryFrom(live);
if (isDurableRuntimeReady(runtimeSummary)) return null;
if (runtimeSummary?.durableRequested === true || runtimeSummary?.adapter === "postgres") {
return `runtime durable adapter 未 readystatus=${runtimeSummary?.status ?? "unknown"}durable=${runtimeSummary?.durable === true});可信记录保持 BLOCKED。`;
}
if (runtimeSummary?.durable !== true) {
return "runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。";
}
@@ -55,7 +55,8 @@ export function runWorkbenchHardwarePanelContract() {
assert.equal(blockedModel.chain.toResourceId, "res_boxsimu_2");
assert.equal(blockedModel.chain.toPort, "DI1");
assert.match(blockedModel.chain.detail, /BLOCKED/u);
assert.match(blockedModel.chain.detail, /waiting for DEV-LIVE M3 loop/u);
assert.match(blockedModel.chain.detail, /runtime\.durable=false/u);
assert.equal(blockedModel.trustedLoop.runtimeDurability.ready, false);
assert.ok(
blockedModel.statuses.some((item) => item.kind === "BOX-SIMU" && item.id === "res_boxsimu_1"),
@@ -110,7 +111,7 @@ export function runWorkbenchHardwarePanelContract() {
assert.equal(misleadingGreenModel.chain.status, "blocked", "chain must be blocked without DEV-LIVE evidence");
assert.notEqual(misleadingGreenModel.statuses[0].status, "healthy", "healthy source-only status must be downgraded");
const trustedModel = createHardwareEvidencePanelModel({
const devLiveEvidenceWithMemoryRuntime = {
chain: {
...M3_TRUSTED_CHAIN,
status: "verified",
@@ -142,6 +143,21 @@ export function runWorkbenchHardwarePanelContract() {
summary: "Trusted M3 loop evidence is already recorded."
}
]
}
};
const nonDurableModel = createHardwareEvidencePanelModel(devLiveEvidenceWithMemoryRuntime);
assert.equal(nonDurableModel.trustedLoop.status, "blocked", "DEV-LIVE-looking records cannot promote without durable runtime");
assert.equal(nonDurableModel.sourceKind, "BLOCKED");
assert.match(nonDurableModel.trustedLoop.reason, /runtime\.durable=false/u);
const trustedModel = createHardwareEvidencePanelModel({
...devLiveEvidenceWithMemoryRuntime,
runtime: {
adapter: "postgres",
durable: true,
ready: true,
status: "ready",
liveRuntimeEvidence: true
},
controls: [
{
@@ -127,15 +127,19 @@ export function createHardwareEvidencePanelModel(data = {}) {
const records = normalizeRecords(data.records ?? defaultHardwareEvidencePanelData.records);
const controls = normalizeControls(data.controls ?? defaultHardwareEvidencePanelData.controls);
const chain = normalizeChain(data.chain ?? defaultHardwareEvidencePanelData.chain);
const runtimeDurability = normalizeRuntimeDurability(data.runtime ?? data.runtimeSummary);
const hasTrustedDevLiveEvidence = records.evidence.some(
(record) =>
record.sourceKind === "DEV-LIVE" &&
["pass", "passed", "verified", "succeeded", "trusted"].includes(record.status)
);
const chainMatches = isRequiredM3Chain(chain);
const trustStatus = hasTrustedDevLiveEvidence && chainMatches ? "verified" : "blocked";
const trustStatus = runtimeDurability.ready && hasTrustedDevLiveEvidence && chainMatches ? "verified" : "blocked";
const trustTone = trustStatus === "verified" ? "trusted" : "degraded";
const sourceKind = trustStatus === "verified" ? "DEV-LIVE" : "BLOCKED";
const blockedReason = runtimeDurability.ready
? "Current data lacks real DEV-LIVE evidence; the workbench must render BLOCKED/degraded, never green."
: runtimeDurability.reason;
const guardedChain = {
...chain,
status: trustStatus === "verified" ? normalizeStatus(chain.status, "verified") : "blocked",
@@ -143,7 +147,7 @@ export function createHardwareEvidencePanelModel(data = {}) {
detail:
trustStatus === "verified"
? chain.detail
: "BLOCKED: waiting for DEV-LIVE M3 loop evidence for res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1."
: `BLOCKED: ${blockedReason}`
};
return {
@@ -157,10 +161,11 @@ export function createHardwareEvidencePanelModel(data = {}) {
status: trustStatus,
tone: trustTone,
sourceKind,
runtimeDurability,
reason:
trustStatus === "verified"
? "DEV-LIVE evidence is attached to the required M3 chain."
: "Current data lacks real DEV-LIVE evidence; the workbench must render BLOCKED/degraded, never green."
: blockedReason
},
statuses: statuses.map((status) => ({
...status,
@@ -220,6 +225,34 @@ function normalizeRecords(records) {
};
}
function normalizeRuntimeDurability(runtime) {
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
return {
ready: false,
adapter: "memory",
durable: false,
liveRuntimeEvidence: false,
reason: "runtime.durable=false; memory/non-durable runtime cannot render DEV-LIVE or durable evidence."
};
}
const ready =
runtime.durable === true &&
runtime.ready !== false &&
runtime.liveRuntimeEvidence === true &&
!["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase());
return {
ready,
adapter: stringOr(runtime.adapter, "unknown"),
durable: runtime.durable === true,
durableRequested: runtime.durableRequested === true,
liveRuntimeEvidence: runtime.liveRuntimeEvidence === true,
reason: ready
? "Durable runtime readiness is proven."
: `runtime durable adapter is not ready (adapter=${stringOr(runtime.adapter, "unknown")}, durable=${runtime.durable === true}); trusted records stay BLOCKED.`
};
}
function normalizeRecordList(records) {
return arrayFrom(records).map((record) => ({
id: stringOr(record.id ?? record.operationId ?? record.auditId ?? record.evidenceId, "record"),