mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(release): reuse validation evidence after tooling updates (#130850)
* fix(release): trust ancestor validation evidence * fix(release): read candidate auth schema
This commit is contained in:
@@ -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}`, {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user