mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
refactor(state): consolidate wide rows, plugin index, workspace attestations, and shared auth singletons at schema v13 (#130466)
* refactor(state): make cron and subagent rows JSON-canonical * refactor(state): make gateway origin device tokens canonical at v13 The lazy ensure predates the table joining the canonical schema; at the v13 bump the schema owns creation, so the feature-local DDL, WeakSet dedupe, and lazy-list entry retire. The legacy-file guard the ensure carried stays at each call site. * test: drop obsolete lazy-ensure coverage for origin device tokens The table is canonical at v13; same-version lazy creation no longer exists to protect. Origin CRUD, isolation, and rotation coverage remains in the surviving cases. * refactor(state): fold installed_plugin_index into config_machine_state The singleton index row becomes one JSON value under plugins.installedIndex with its rollback-fencing revision inside the value; reads, CAS restore, and the lease-held write transactions use direct Kysely on config_machine_state so the state_leases assertion stays in-transaction. The v13 migration imports the row and drops the table; the additive workspace_dir entry folds with it. Doctor guidance, docker staging, and the e2e probes name the machine-state row. * refactor(state): merge workspace_attestations into workspace_setup_state One row per workspace now carries both setup milestones and the attestation clock: nullable setup columns represent attestation-only workspaces (replaceWorkspaceAttestation can precede any setup write) and setupExists derives from a non-null version. The bootstrap-hash FK repoints to the merged table; migration receipts keep the historical workspace_attestations discriminator string. The v13 migration grows and rebuilds the table, merges attestation rows (orphans without a path alias drop — their hashes re-derive at the next bootstrap attestation), and the consolidation kind is renamed state-consolidation-v13 to cover the batch. * test(state): cover the workspace merge and consolidation fallout The v12-to-v13 regression seeds merged, attestation-only, and orphan attestation workspaces; the 13-to-12 downgrade fixture recreates workspace_attestations and installed_plugin_index from the folded data; the fold-in migration gates the additive workspace_dir column for pre-additive rows; the workspace merge now triggers on the setup table's own shape so stable-era databases without an attestations table still reshape; the consolidation applied-message covers the batch. * refactor(state): fold shared auth profile singletons into config_machine_state The shared-state auth_profile_stores/auth_profile_state rows (fixed key 'shared') become authProfiles.store/authProfiles.state machine-state values; the agent-DB tables of the same names are untouched. Git-backup redaction moves from table-drop to the authProfiles. secret prefix with seeded-secret absence proof; migration receipts keep the historical table-name discriminators; the shared-auth relocation and receipt verification project the KV cells back to the receipt-era row shapes so persisted digests stay byte-compatible. mcp_oauth_stores stays a table — its multi-key fold is a named follow-up. * test(state): finish shared-auth fold coverage and annotate boundary casts Auth seeders and assertions across the e2e/scripts/secrets suites target the authProfiles machine-state cells; the v12-to-v13 regression proves payload-byte fidelity, non-shared-row drop, and insert-if-absent precedence; the downgrade fixture recreates and repopulates both v12 tables. Boundary type assertions in the plugin-index store carry SAFETY invariants per the ratchet. * chore: shrink assertion-safety baseline for plugin-index store * refactor(doctor): delete the dead onboarding-recommendations migration Its input — the unscoped 'primary' onboarding row — existed only between9a93a52a8aand473962b7de, a two-day beta window; no shipped stable can produce it and the runtime table folded away at v12. The audit backup list keeps recognizing system-agent.jsonl artifacts because beta installs that ran that import may still carry its backups. * docs: sync the 13-to-12 downgrade example with the executable fixture * style: format the synced downgrade example * style: drop unused import and duplicate union constituent * fix(state): keep orphan attestations across the v13 workspace merge The merged workspace_setup_state required a workspace path, but legacy orphan hashed-key attestations never recorded one. workspace_path is now nullable (setup rows still enforce it via CHECK), the v13 migration and the doctor file import keep orphans with a NULL path that heals on the next live access, and the 13-to-12 downgrade keeps attestation-owned hashes. Doctor test seeds move to the folded KV row. * perf(state): retire unused cron indexes * fix(state): preserve v13 migration recovery * fix(state): preserve v12 lazy-table upgrade * docs(state): document v13 auth relocation --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
committed by
GitHub
parent
dff2d26820
commit
1ea2640f54
@@ -25,19 +25,19 @@ function writeSharedDatabase(
|
||||
try {
|
||||
if (options.asView) {
|
||||
db.exec(`
|
||||
CREATE VIEW auth_profile_stores AS
|
||||
SELECT 'shared' AS store_key, '{}' AS store_json, 1 AS updated_at;
|
||||
CREATE VIEW config_machine_state AS
|
||||
SELECT 'authProfiles.store' AS state_key, '{}' AS value_json, 1 AS updated_at_ms;
|
||||
`);
|
||||
} 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
|
||||
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 auth_profile_stores VALUES (?, ?, ?)").run(
|
||||
"shared",
|
||||
db.prepare("INSERT INTO config_machine_state VALUES (?, ?, ?)").run(
|
||||
"authProfiles.store",
|
||||
options.storeJson ?? "{}",
|
||||
Date.now(),
|
||||
);
|
||||
@@ -127,7 +127,7 @@ describe("auth profile store E2E assertions", () => {
|
||||
writeSharedDatabase(stateDir, { asView: true });
|
||||
|
||||
expect(() => readSharedAuthProfileStoreText(stateDir)).toThrow(
|
||||
"auth_profile_stores is view, not a table",
|
||||
"config_machine_state is view, not a table",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OPENCLAW_STATE_SCHEMA_VERSION } from "../../src/state/openclaw-state-db
|
||||
|
||||
describe("native state schema version guard", () => {
|
||||
it("keeps the checked-in Swift and TypeScript contracts aligned", () => {
|
||||
expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(12);
|
||||
expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(13);
|
||||
expect(checkNativeStateSchemaVersion()).toBe(OPENCLAW_STATE_SCHEMA_VERSION);
|
||||
});
|
||||
|
||||
|
||||
@@ -57,19 +57,19 @@ function writeAuthProfileStoreSqlite(stateDir: string) {
|
||||
const db = new DatabaseSync(databasePath);
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS auth_profile_stores (
|
||||
store_key TEXT NOT NULL PRIMARY KEY,
|
||||
store_json TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS config_machine_state (
|
||||
state_key TEXT NOT NULL PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO auth_profile_stores (store_key, store_json, updated_at)
|
||||
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
"shared",
|
||||
"authProfiles.store",
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
|
||||
@@ -51,7 +51,9 @@ describe("live Docker state staging", () => {
|
||||
expect(script).toContain("--exclude=sandboxes");
|
||||
expect(script).toContain("--exclude=plugins/installs.json");
|
||||
expect(script).toContain("--exclude=plugins/installs.json.migrated");
|
||||
expect(script).toContain("DELETE FROM installed_plugin_index");
|
||||
expect(script).toContain(
|
||||
`db.prepare("DELETE FROM config_machine_state WHERE state_key = ?").run("plugins.installedIndex");`,
|
||||
);
|
||||
expect(script).toContain("PRAGMA secure_delete = ON");
|
||||
expect(script).toContain("VACUUM");
|
||||
expect(script).toContain("host-absolute paths");
|
||||
|
||||
@@ -43,18 +43,18 @@ function writeSharedAuthProfileStoreSqlite(home: string, store: unknown): void {
|
||||
const db = new DatabaseSync(path.join(stateDir, "openclaw.sqlite"));
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS auth_profile_stores (
|
||||
store_key TEXT NOT NULL PRIMARY KEY,
|
||||
store_json TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS config_machine_state (
|
||||
state_key TEXT NOT NULL PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO auth_profile_stores (store_key, store_json, updated_at)
|
||||
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
).run("shared", JSON.stringify(store), Date.now());
|
||||
).run("authProfiles.store", JSON.stringify(store), Date.now());
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
@@ -32,44 +32,30 @@ function writeSqliteIndex(root: string, installRecordsJson: string) {
|
||||
const db = new DatabaseSync(dbPath);
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE installed_plugin_index (
|
||||
index_key TEXT NOT NULL PRIMARY KEY,
|
||||
version INTEGER NOT NULL,
|
||||
host_contract_version TEXT NOT NULL,
|
||||
compat_registry_version TEXT NOT NULL,
|
||||
migration_version INTEGER NOT NULL,
|
||||
policy_hash TEXT NOT NULL,
|
||||
generated_at_ms INTEGER NOT NULL,
|
||||
refresh_reason TEXT,
|
||||
install_records_json TEXT NOT NULL,
|
||||
plugins_json TEXT NOT NULL,
|
||||
diagnostics_json TEXT NOT NULL,
|
||||
warning TEXT,
|
||||
CREATE TABLE config_machine_state (
|
||||
state_key TEXT NOT NULL PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO installed_plugin_index (
|
||||
index_key, version, host_contract_version, compat_registry_version,
|
||||
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
||||
install_records_json, plugins_json, diagnostics_json, warning, updated_at_ms
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
"INSERT INTO config_machine_state (state_key, value_json, updated_at_ms) VALUES (?, ?, ?)",
|
||||
).run(
|
||||
"installed-plugin-index",
|
||||
1,
|
||||
"1",
|
||||
"1",
|
||||
1,
|
||||
"hash",
|
||||
Date.now(),
|
||||
null,
|
||||
installRecordsJson,
|
||||
"{}",
|
||||
"{}",
|
||||
null,
|
||||
"plugins.installedIndex",
|
||||
JSON.stringify({
|
||||
revision: Date.now(),
|
||||
index: {
|
||||
version: 1,
|
||||
hostContractVersion: "1",
|
||||
compatRegistryVersion: "1",
|
||||
migrationVersion: 1,
|
||||
policyHash: "hash",
|
||||
generatedAtMs: Date.now(),
|
||||
installRecords: JSON.parse(installRecordsJson) as unknown,
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
}),
|
||||
Date.now(),
|
||||
);
|
||||
} finally {
|
||||
@@ -139,7 +125,7 @@ describe("plugin index SQLite E2E helpers", () => {
|
||||
|
||||
expect(() =>
|
||||
readPluginInstallIndex({ stateDir: root, configPath: configPath(root) }),
|
||||
).toThrow("plugin index install_records_json exceeded 64 bytes");
|
||||
).toThrow("plugin index value_json exceeded 64 bytes");
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
@@ -23,46 +23,37 @@ function writeIndex(
|
||||
const db = new DatabaseSync(databasePath);
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS installed_plugin_index (
|
||||
index_key TEXT NOT NULL PRIMARY KEY,
|
||||
version INTEGER NOT NULL,
|
||||
host_contract_version TEXT NOT NULL,
|
||||
compat_registry_version TEXT NOT NULL,
|
||||
migration_version INTEGER NOT NULL,
|
||||
policy_hash TEXT NOT NULL,
|
||||
generated_at_ms INTEGER NOT NULL,
|
||||
refresh_reason TEXT,
|
||||
install_records_json TEXT NOT NULL,
|
||||
plugins_json TEXT NOT NULL,
|
||||
diagnostics_json TEXT NOT NULL,
|
||||
warning TEXT,
|
||||
CREATE TABLE IF NOT EXISTS config_machine_state (
|
||||
state_key TEXT NOT NULL PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
const now = Date.now();
|
||||
const valueJson = JSON.stringify({
|
||||
revision: now,
|
||||
index: {
|
||||
version: 1,
|
||||
hostContractVersion: "test",
|
||||
compatRegistryVersion: "test",
|
||||
migrationVersion: 1,
|
||||
policyHash: "test",
|
||||
generatedAtMs: now,
|
||||
refreshReason: "source-changed",
|
||||
installRecords: { [pluginId]: record },
|
||||
plugins: [{ pluginId, packageVersion }],
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
db.prepare(
|
||||
`
|
||||
INSERT OR REPLACE INTO installed_plugin_index (
|
||||
index_key, version, host_contract_version, compat_registry_version,
|
||||
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
||||
install_records_json, plugins_json, diagnostics_json, warning, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms)
|
||||
VALUES ('plugins.installedIndex', ?, ?)
|
||||
ON CONFLICT(state_key) DO UPDATE SET
|
||||
value_json = excluded.value_json,
|
||||
updated_at_ms = excluded.updated_at_ms
|
||||
`,
|
||||
).run(
|
||||
"installed-plugin-index",
|
||||
1,
|
||||
"test",
|
||||
"test",
|
||||
1,
|
||||
"test",
|
||||
now,
|
||||
"source-changed",
|
||||
JSON.stringify({ [pluginId]: record }),
|
||||
JSON.stringify([{ pluginId, packageVersion }]),
|
||||
"[]",
|
||||
null,
|
||||
now,
|
||||
);
|
||||
).run(valueJson, now);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -120,13 +111,16 @@ function clearMarketplaceIndex(home: string) {
|
||||
try {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE installed_plugin_index
|
||||
SET install_records_json = ?,
|
||||
plugins_json = ?,
|
||||
UPDATE config_machine_state
|
||||
SET value_json = json_set(
|
||||
value_json,
|
||||
'$.index.installRecords', json('{}'),
|
||||
'$.index.plugins', json('[]')
|
||||
),
|
||||
updated_at_ms = ?
|
||||
WHERE index_key = ?
|
||||
WHERE state_key = 'plugins.installedIndex'
|
||||
`,
|
||||
).run("{}", "[]", Date.now(), "installed-plugin-index");
|
||||
).run(Date.now());
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
@@ -38,18 +38,18 @@ function writeAuthProfileStoreSqlite(stateDir: string, store: unknown) {
|
||||
const db = new DatabaseSync(databasePath);
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS auth_profile_stores (
|
||||
store_key TEXT NOT NULL PRIMARY KEY,
|
||||
store_json TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS config_machine_state (
|
||||
state_key TEXT NOT NULL PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO auth_profile_stores (store_key, store_json, updated_at)
|
||||
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
).run("shared", JSON.stringify(store), Date.now());
|
||||
).run("authProfiles.store", JSON.stringify(store), Date.now());
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user