diff --git a/scripts/e2e/lib/auth-profile-store-assertions.mjs b/scripts/e2e/lib/auth-profile-store-assertions.mjs index 1cef42818607..5b3d75115aa3 100644 --- a/scripts/e2e/lib/auth-profile-store-assertions.mjs +++ b/scripts/e2e/lib/auth-profile-store-assertions.mjs @@ -4,6 +4,8 @@ import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import { isRecord } from "../../lib/record-shared.mjs"; +const AUTH_PROFILE_MACHINE_STATE_SCHEMA_VERSION = 13; + export function readSharedAuthProfileStoreText(stateDir) { const dbPath = path.join(stateDir, "state", "openclaw.sqlite"); if (!fs.existsSync(dbPath)) { @@ -12,19 +14,38 @@ export function readSharedAuthProfileStoreText(stateDir) { let db; try { db = new DatabaseSync(dbPath, { readOnly: true }); + const schemaVersion = db.prepare("PRAGMA user_version").get()?.user_version; + if (!Number.isInteger(schemaVersion)) { + throw new Error(`invalid state schema version ${String(schemaVersion)}`); + } + // Release candidates own their persisted schema. Never let a retired row + // mask a missing canonical row once the v13 fold has occurred. + const storage = + schemaVersion >= AUTH_PROFILE_MACHINE_STATE_SCHEMA_VERSION + ? { + column: "value_json", + key: "authProfiles.store", + query: "SELECT value_json FROM config_machine_state WHERE state_key = ?", + table: "config_machine_state", + } + : { + column: "store_json", + key: "shared", + query: "SELECT store_json FROM auth_profile_stores WHERE store_key = ?", + table: "auth_profile_stores", + }; const schema = db .prepare("SELECT type FROM sqlite_schema WHERE name = ? LIMIT 1") - .get("config_machine_state"); + .get(storage.table); if (!schema) { return ""; } if (schema.type !== "table") { - throw new Error(`config_machine_state is ${String(schema.type)}, not a table`); + throw new Error(`${storage.table} is ${String(schema.type)}, not a table`); } - const row = db - .prepare("SELECT value_json FROM config_machine_state WHERE state_key = ?") - .get("authProfiles.store"); - return typeof row?.value_json === "string" ? row.value_json : ""; + const row = db.prepare(storage.query).get(storage.key); + const value = row?.[storage.column]; + return typeof value === "string" ? value : ""; } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`could not read the shared auth profile store: ${detail}`, { diff --git a/scripts/release-ci-summary.mjs b/scripts/release-ci-summary.mjs index caef6f0e9e52..3cd42f05af62 100755 --- a/scripts/release-ci-summary.mjs +++ b/scripts/release-ci-summary.mjs @@ -1359,6 +1359,7 @@ export function validateTrustedProducerIdentity( // Keep this predicate local: verifier source identity covers this file only. const shaPinned = SHA_PINNED_BRANCH_PATTERN.test(manifest.workflowRef ?? ""); const protectedTagRoute = trustedIdentity.type === "tag"; + let protectedTagWorkflowRefProof = "manifest-v3-protected-tag-exact-sha"; if (protectedTagRoute) { let liveTag; try { @@ -1378,7 +1379,16 @@ export function validateTrustedProducerIdentity( throw new Error("protected-tag release evidence must use a canonical release-ci branch"); } if (manifest.workflowSha !== trustedIdentity.sha) { - throw new Error("protected-tag release evidence workflow SHA does not match trusted tooling"); + const comparison = client.compareCommitLineage(manifest.workflowSha, trustedIdentity.sha); + if ( + !["ahead", "identical"].includes(String(comparison.status)) || + comparison.merge_base_commit?.sha !== manifest.workflowSha + ) { + throw new Error( + "protected-tag release evidence producer is not on the trusted tooling lineage", + ); + } + protectedTagWorkflowRefProof = "manifest-v3-protected-tag-tooling-lineage"; } } else if (manifest.workflowRef !== trustedWorkflowRef && !shaPinned) { throw new Error( @@ -1412,7 +1422,7 @@ export function validateTrustedProducerIdentity( throw new Error("release evidence producer workflow full ref is not trusted"); } workflowRefProof = protectedTagRoute - ? "manifest-v3-protected-tag-exact-sha" + ? protectedTagWorkflowRefProof : shaPinned ? "manifest-v3-sha-pinned-main-ancestry" : "manifest-v3-branch"; diff --git a/test/scripts/auth-profile-store-assertions.test.ts b/test/scripts/auth-profile-store-assertions.test.ts index b6315845b3ee..2a48412dc011 100644 --- a/test/scripts/auth-profile-store-assertions.test.ts +++ b/test/scripts/auth-profile-store-assertions.test.ts @@ -17,28 +17,72 @@ function makeStateDir(): string { function writeSharedDatabase( stateDir: string, - options: { asView?: boolean; storeJson?: string } = {}, + options: { + asView?: boolean; + legacyStoreJson?: string; + schemaVersion?: 12 | 13; + storeJson?: string; + } = {}, ): string { const dbPath = path.join(stateDir, "state", "openclaw.sqlite"); mkdirSync(path.dirname(dbPath), { recursive: true }); const db = new DatabaseSync(dbPath); try { + const schemaVersion = options.schemaVersion ?? 13; + db.exec(`PRAGMA user_version = ${schemaVersion};`); + const table = schemaVersion >= 13 ? "config_machine_state" : "auth_profile_stores"; if (options.asView) { - db.exec(` - CREATE VIEW config_machine_state AS - SELECT 'authProfiles.store' AS state_key, '{}' AS value_json, 1 AS updated_at_ms; - `); + if (table === "config_machine_state") { + db.exec(` + CREATE VIEW config_machine_state AS + SELECT 'authProfiles.store' AS state_key, '{}' AS value_json, 1 AS updated_at_ms; + `); + } else { + db.exec(` + CREATE VIEW auth_profile_stores AS + SELECT 'shared' AS store_key, '{}' AS store_json, 1 AS updated_at; + `); + } } else { + if (table === "config_machine_state") { + db.exec(` + CREATE TABLE config_machine_state ( + state_key TEXT NOT NULL PRIMARY KEY, + value_json TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL + ) STRICT; + `); + db.prepare("INSERT INTO config_machine_state VALUES (?, ?, ?)").run( + "authProfiles.store", + options.storeJson ?? "{}", + Date.now(), + ); + } else { + db.exec(` + CREATE TABLE auth_profile_stores ( + store_key TEXT NOT NULL PRIMARY KEY, + store_json TEXT NOT NULL, + updated_at INTEGER NOT NULL + ) STRICT; + `); + db.prepare("INSERT INTO auth_profile_stores VALUES (?, ?, ?)").run( + "shared", + options.storeJson ?? "{}", + Date.now(), + ); + } + } + if (options.legacyStoreJson !== undefined) { db.exec(` - CREATE TABLE config_machine_state ( - state_key TEXT NOT NULL PRIMARY KEY, - value_json TEXT NOT NULL, - updated_at_ms INTEGER NOT NULL + CREATE TABLE auth_profile_stores ( + store_key TEXT NOT NULL PRIMARY KEY, + store_json TEXT NOT NULL, + updated_at INTEGER NOT NULL ) STRICT; `); - db.prepare("INSERT INTO config_machine_state VALUES (?, ?, ?)").run( - "authProfiles.store", - options.storeJson ?? "{}", + db.prepare("INSERT INTO auth_profile_stores VALUES (?, ?, ?)").run( + "shared", + options.legacyStoreJson, Date.now(), ); } @@ -101,6 +145,26 @@ describe("auth profile store E2E assertions", () => { expect(readSharedAuthProfileStoreText(stateDir)).toBe('{"version":1}'); }); + it("reads the release-owned shared row before schema v13", () => { + const stateDir = makeStateDir(); + writeSharedDatabase(stateDir, { + schemaVersion: 12, + storeJson: '{"version":1,"schema":12}', + }); + + expect(readSharedAuthProfileStoreText(stateDir)).toBe('{"version":1,"schema":12}'); + }); + + it("does not accept a retired shared row for schema v13", () => { + const stateDir = makeStateDir(); + writeSharedDatabase(stateDir, { + legacyStoreJson: '{"version":1,"schema":12}', + storeJson: "", + }); + + expect(readSharedAuthProfileStoreText(stateDir)).toBe(""); + }); + it("returns empty when the shared database or table is absent", () => { const stateDir = makeStateDir(); diff --git a/test/scripts/codex-install-assertions.test.ts b/test/scripts/codex-install-assertions.test.ts index 19d79b68caa5..64a9d050fbcd 100644 --- a/test/scripts/codex-install-assertions.test.ts +++ b/test/scripts/codex-install-assertions.test.ts @@ -57,6 +57,7 @@ function writeAuthProfileStoreSqlite(stateDir: string) { const db = new DatabaseSync(databasePath); try { db.exec(` + PRAGMA user_version = 13; CREATE TABLE IF NOT EXISTS config_machine_state ( state_key TEXT NOT NULL PRIMARY KEY, value_json TEXT NOT NULL, diff --git a/test/scripts/npm-onboard-channel-agent-assertions.test.ts b/test/scripts/npm-onboard-channel-agent-assertions.test.ts index 6d2b70374b3e..f71657ff9ecf 100644 --- a/test/scripts/npm-onboard-channel-agent-assertions.test.ts +++ b/test/scripts/npm-onboard-channel-agent-assertions.test.ts @@ -43,6 +43,7 @@ function writeSharedAuthProfileStoreSqlite(home: string, store: unknown): void { const db = new DatabaseSync(path.join(stateDir, "openclaw.sqlite")); try { db.exec(` + PRAGMA user_version = 13; CREATE TABLE IF NOT EXISTS config_machine_state ( state_key TEXT NOT NULL PRIMARY KEY, value_json TEXT NOT NULL, diff --git a/test/scripts/release-ci-summary.test.ts b/test/scripts/release-ci-summary.test.ts index df0fca154477..3dded3820e95 100644 --- a/test/scripts/release-ci-summary.test.ts +++ b/test/scripts/release-ci-summary.test.ts @@ -1376,7 +1376,55 @@ describe("release CI summary child correlation", () => { }); }); - it("rejects protected-tag evidence from a same-name branch or older ancestor", () => { + it("accepts protected-tag evidence from an older trusted tooling ancestor", () => { + const trustedWorkflowSha = "7".repeat(40); + const trustedWorkflowRef = `release-publish/${trustedWorkflowSha.slice(0, 12)}-123`; + const olderWorkflowSha = "6".repeat(40); + const olderWorkflowRef = `release-ci/${olderWorkflowSha.slice(0, 12)}-1783705000000`; + const olderFixture = trustedMainPackageFixture({ + manifestVersion: 3, + targetSha: "8".repeat(40), + workflowFullRef: `refs/heads/${olderWorkflowRef}`, + workflowRef: olderWorkflowRef, + workflowSha: olderWorkflowSha, + }); + olderFixture.manifest.targetRef = olderFixture.targetSha; + olderFixture.client.getRef = (fullRef: string) => ({ + object: { sha: trustedWorkflowSha }, + ref: fullRef, + }); + olderFixture.client.compareCommitLineage = (base: string, head: string) => { + expect(base).toBe(olderWorkflowSha); + expect(head).toBe(trustedWorkflowSha); + return { + merge_base_commit: { sha: olderWorkflowSha }, + status: "ahead", + }; + }; + + expect( + validateReleaseRunEvidence( + { + repository: "openclaw/openclaw", + runId: olderFixture.runId, + trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`, + trustedWorkflowRef, + trustedWorkflowSha, + verifierSourceContent: readFileSync(SCRIPT), + verifierSourceSha: "c".repeat(40), + }, + olderFixture.client, + ), + ).toMatchObject({ + root: { + workflowRef: olderWorkflowRef, + workflowRefProof: "manifest-v3-protected-tag-tooling-lineage", + workflowSha: olderWorkflowSha, + }, + }); + }); + + it("rejects protected-tag evidence from a same-name branch or unrelated producer", () => { const trustedWorkflowSha = "7".repeat(40); const trustedWorkflowRef = `release-publish/${trustedWorkflowSha.slice(0, 12)}-123`; const validFixture = trustedMainPackageFixture({ @@ -1399,34 +1447,38 @@ describe("release CI summary child correlation", () => { ), ).toThrow("must be a protected tag"); - const olderWorkflowSha = "6".repeat(40); - const olderWorkflowRef = `release-ci/${olderWorkflowSha.slice(0, 12)}-1783705000000`; - const olderFixture = trustedMainPackageFixture({ + const unrelatedWorkflowSha = "6".repeat(40); + const unrelatedWorkflowRef = `release-ci/${unrelatedWorkflowSha.slice(0, 12)}-1783705000000`; + const unrelatedFixture = trustedMainPackageFixture({ manifestVersion: 3, targetSha: "8".repeat(40), - workflowFullRef: `refs/heads/${olderWorkflowRef}`, - workflowRef: olderWorkflowRef, - workflowSha: olderWorkflowSha, + workflowFullRef: `refs/heads/${unrelatedWorkflowRef}`, + workflowRef: unrelatedWorkflowRef, + workflowSha: unrelatedWorkflowSha, }); - olderFixture.manifest.targetRef = olderFixture.targetSha; - olderFixture.client.getRef = (fullRef: string) => ({ + unrelatedFixture.manifest.targetRef = unrelatedFixture.targetSha; + unrelatedFixture.client.getRef = (fullRef: string) => ({ object: { sha: trustedWorkflowSha }, ref: fullRef, }); + unrelatedFixture.client.compareCommitLineage = () => ({ + merge_base_commit: { sha: "5".repeat(40) }, + status: "diverged", + }); expect(() => validateReleaseRunEvidence( { repository: "openclaw/openclaw", - runId: olderFixture.runId, + runId: unrelatedFixture.runId, trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`, trustedWorkflowRef, trustedWorkflowSha, verifierSourceContent: readFileSync(SCRIPT), verifierSourceSha: "c".repeat(40), }, - olderFixture.client, + unrelatedFixture.client, ), - ).toThrow("does not match trusted tooling"); + ).toThrow("not on the trusted tooling lineage"); const sameNameFixture = trustedMainPackageFixture({ manifestVersion: 3, diff --git a/test/scripts/release-scenarios-assertions.test.ts b/test/scripts/release-scenarios-assertions.test.ts index d44f5f64c8ae..7ce7924cc402 100644 --- a/test/scripts/release-scenarios-assertions.test.ts +++ b/test/scripts/release-scenarios-assertions.test.ts @@ -38,6 +38,7 @@ function writeAuthProfileStoreSqlite(stateDir: string, store: unknown) { const db = new DatabaseSync(databasePath); try { db.exec(` + PRAGMA user_version = 13; CREATE TABLE IF NOT EXISTS config_machine_state ( state_key TEXT NOT NULL PRIMARY KEY, value_json TEXT NOT NULL,