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 between
9a93a52a8a and 473962b7de, 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:
Peter Steinberger
2026-08-27 00:26:14 -07:00
committed by GitHub
parent dff2d26820
commit 1ea2640f54
113 changed files with 3505 additions and 3481 deletions
+27 -27
View File
@@ -203,7 +203,7 @@ without exceptions outside doctor/import/export/debug boundaries.
- No active session files.
- No fake JSONL test fixtures except doctor legacy migration tests.
- No raw SQLite access where Kysely is expected.
- No new file-era runtime stores. The current global schema is version `12`, and
- No new file-era runtime stores. The current global schema is version `13`, and
the current per-agent schema is version `17`; older supported databases move
through the bounded forward migrations listed in
[Database schemas](/reference/database-schemas).
@@ -310,20 +310,19 @@ The branch already has a real shared SQLite base:
- Runtime stores derive selected and inserted row types from those generated
Kysely `DB` interfaces instead of shadowing SQLite row shapes by hand. Raw SQL
remains limited to schema application, pragmas, and migration-only DDL.
- The global SQLite schema is at `user_version = 12`. The per-agent schema is at
- The global SQLite schema is at `user_version = 13`. The per-agent schema is at
version `17`; their openers apply bounded forward migrations from supported
older schemas. File-to-database import remains in Doctor code.
- Relational ownership is enforced where the ownership boundary is canonical:
source migration rows cascade from `migration_runs`, task delivery state
cascades from `task_runs`, and transcript identity rows cascade from
transcript events.
- Current shared tables include `agent_databases`,
`auth_profile_stores`, `auth_profile_state`,
- Current shared tables include `config_machine_state`, `agent_databases`,
`plugin_state_entries`, `plugin_blob_entries`,
`skill_uploads`, `capture_sessions`, `capture_events`, `capture_blobs`,
`sandbox_registry_entries`, `cron_jobs`,
`delivery_queue_entries`,
`workspace_setup_state`, `workspace_path_aliases`, `workspace_attestations`,
`workspace_setup_state`, `workspace_path_aliases`,
`workspace_generated_bootstrap_hashes`, `native_hook_relay_bridges`,
`current_conversation_bindings`, `plugin_binding_approvals`,
`acp_sessions`, `acp_replay_sessions`,
@@ -349,7 +348,7 @@ The branch already has a real shared SQLite base:
site.
- Global and per-agent databases record a `schema_meta` row with database role,
schema version, timestamps, and agent id for agent databases. The global DB
currently uses `user_version = 9`; per-agent DBs use version `17`.
currently uses `user_version = 13`; per-agent DBs use version `17`.
- Per-agent session identity now has a canonical `sessions` root table keyed by
`session_id`, with `session_key`, `session_scope`, `account_id`,
`primary_conversation_id`, timestamps, display fields, model metadata,
@@ -443,8 +442,8 @@ The branch already has a real shared SQLite base:
legacy path resolver lives in the doctor migration module.
- Secret target metadata now talks about stores instead of pretending every
credential target is a config file. `openclaw.json` remains the config store;
auth-profile targets use typed SQLite `auth_profile_stores` rows with
provider-shaped credentials kept as JSON payloads.
shared auth-profile snapshots use the `authProfiles.store` and
`authProfiles.state` keys in `config_machine_state`.
- Secret audit no longer scans retired per-agent `auth.json` files. Doctor owns
warning about, importing, and removing that legacy file.
- Legacy auth profile path helpers now live in doctor legacy code. Core auth
@@ -465,9 +464,9 @@ The branch already has a real shared SQLite base:
define VFS, tool-artifact, or run-artifact tables.
- Workspace bootstrap completion, attestation recency, and generated bootstrap
hashes now live in typed shared `workspace_setup_state`,
`workspace_path_aliases`, `workspace_attestations`, and
`workspace_generated_bootstrap_hashes` rows keyed by canonical workspace
identity. Persisted lexical and real-path aliases keep vanished-workspace
`workspace_path_aliases`, and `workspace_generated_bootstrap_hashes` rows
keyed by canonical workspace identity. Attestation timestamps are owned by
`workspace_setup_state`. Persisted lexical and real-path aliases keep vanished-workspace
protection stable after a configured symlink disappears; repointed aliases
fail closed. Runtime no longer reads or writes
`openclaw-workspace-state.json`, `.openclaw/workspace-state.json`, state-dir
@@ -482,7 +481,7 @@ The branch already has a real shared SQLite base:
`device_identities` and `device_auth_tokens` rows. Gateway startup may import
a valid retired primary identity under the startup migration lease; invalid
canonical identity repair remains Doctor-only. Gateway-origin-scoped tokens
use the lazy additive `gateway_origin_device_tokens` table.
use the canonical `gateway_origin_device_tokens` table.
- GitHub Copilot token exchange cache uses the shared SQLite plugin-state table
under `github-copilot/token-cache/default`. It is provider-owned cache state,
so it intentionally does not add a host schema table.
@@ -1299,10 +1298,10 @@ sessionId})`; create, branch, continue, list, and fork flows live in their
- Hermes secret migration plans and applies imported API-key profiles directly
into the SQLite auth-profile store. It no longer writes or verifies
`auth-profiles.json` as an intermediate target.
- User-facing auth docs now describe
`state/openclaw.sqlite#table/auth_profile_stores/<agentDir>` instead of
telling users to inspect or copy `auth-profiles.json`; legacy OAuth/auth JSON
names remain documented only as doctor-import inputs.
- User-facing auth docs distinguish shared auth snapshots in
`config_machine_state` under `authProfiles.store` and `authProfiles.state`
from the per-agent `auth_profile_store` and `auth_profile_state` tables.
Legacy OAuth/auth JSON names remain documented only as doctor-import inputs.
- MCP OAuth sessions now use versioned `mcp_oauth_stores` rows in shared
`state/openclaw.sqlite`. SDK-owned token, client-registration, and discovery
objects remain one validated JSON payload so dependency extension fields
@@ -1353,9 +1352,9 @@ sessionId})`; create, branch, continue, list, and fork flows live in their
state objects rather than file-shaped lockfile/origin abstractions. Doctor
imports the legacy sidecars from configured agent workspaces and removes them
after a clean import.
- The installed plugin index now reads and writes the typed shared SQLite
`installed_plugin_index` singleton row instead of `plugins/installs.json`; the
legacy JSON file is only a doctor migration input and is removed after import.
- The installed plugin index now reads and writes `config_machine_state` under
`plugins.installedIndex` instead of `plugins/installs.json`; the legacy JSON
file is only a doctor migration input and is removed after import.
- The legacy `plugins/installs.json` path helper now lives in doctor legacy
code. Runtime plugin-index modules expose only SQLite-backed persistence
options, not a JSON file path.
@@ -1504,7 +1503,7 @@ agent_databases(agent_id, path, schema_version, last_seen_at, size_bytes)
task_runs(...)
task_delivery_state(...)
flow_runs(...)
subagent_runs(run_id, child_session_key, requester_session_key, controller_session_key, created_at, ended_at, cleanup_handled, payload_json)
subagent_runs(run_id, child_session_key, controller_session_key, requester_session_key, created_at, payload_json)
current_conversation_bindings(binding_key, binding_id, target_agent_id, target_session_id, target_session_key, channel, account_id, conversation_kind, parent_conversation_id, conversation_id, target_kind, status, bound_at, expires_at, metadata_json, updated_at)
plugin_binding_approvals(plugin_root, channel, account_id, plugin_id, plugin_name, approved_at)
plugin_state_entries(plugin_id, namespace, entry_key, value_json, created_at, expires_at)
@@ -1517,9 +1516,8 @@ apns_registration_tombstones(node_id, deleted_at_ms)
device_identities(identity_key, device_id, public_key_pem, private_key_pem, created_at_ms, updated_at_ms)
device_auth_tokens(device_id, role, token, scopes_json, updated_at_ms)
macos_port_guardian_records(pid, port, command, mode, timestamp)
workspace_setup_state(workspace_key, workspace_path, version, bootstrap_seeded_at, setup_completed_at, updated_at)
workspace_setup_state(workspace_key, workspace_path, version, bootstrap_seeded_at, setup_completed_at, updated_at, attested_at_ms, attestation_updated_at_ms)
workspace_path_aliases(alias_key, alias_path, workspace_key, workspace_path, updated_at_ms)
workspace_attestations(workspace_key, attested_at_ms, updated_at_ms)
workspace_generated_bootstrap_hashes(workspace_key, filename, sha256)
native_hook_relay_bridges(relay_id, pid, hostname, port, token, expires_at_ms, updated_at_ms)
managed_outgoing_image_records(attachment_id, session_key, agent_id, message_id, created_at, updated_at, retention_class, alt, original_media_id, original_media_subdir, original_content_type, original_width, original_height, original_size_bytes, original_filename, record_json, cleanup_pending)
@@ -1528,19 +1526,21 @@ channel_pairing_requests(channel_key, account_id, request_id, code, created_at,
channel_pairing_allow_entries(channel_key, account_id, entry, sort_order, updated_at)
config_health_entries(config_path, last_known_good_json, last_promoted_good_json, last_observed_suspicious_signature, updated_at_ms)
sandbox_registry_entries(registry_kind, container_name, session_key, backend_id, runtime_label, image, created_at_ms, last_used_at_ms, config_label_kind, config_hash, cdp_port, no_vnc_port, entry_json, updated_at)
cron_jobs(store_key, job_id, name, description, enabled, delete_after_run, created_at_ms, agent_id, session_key, schedule_kind, schedule_expr, schedule_tz, every_ms, anchor_ms, at, stagger_ms, session_target, wake_mode, payload_kind, payload_message, payload_model, payload_fallbacks_json, payload_thinking, payload_timeout_seconds, payload_allow_unsafe_external_content, payload_external_content_source_json, payload_light_context, payload_tools_allow_json, delivery_mode, delivery_channel, delivery_to, delivery_thread_id, delivery_account_id, delivery_best_effort, failure_delivery_mode, failure_delivery_channel, failure_delivery_to, failure_delivery_account_id, failure_alert_disabled, failure_alert_after, failure_alert_channel, failure_alert_to, failure_alert_cooldown_ms, failure_alert_include_skipped, failure_alert_mode, failure_alert_account_id, next_run_at_ms, running_at_ms, last_run_at_ms, last_run_status, last_error, last_duration_ms, consecutive_errors, consecutive_skipped, schedule_error_count, last_delivery_status, last_delivery_error, last_delivered, last_failure_alert_at_ms, job_json, state_json, runtime_updated_at_ms, schedule_identity, sort_order, updated_at)
cron_jobs(store_key, job_id, declaration_key, owner_agent_id, name, description, enabled, agent_id, payload_kind, job_json, state_json, runtime_updated_at_ms, schedule_identity, sort_order, updated_at)
delivery_queue_entries(queue_name, id, status, entry_kind, session_key, channel, target, account_id, retry_count, last_attempt_at, last_error, recovery_state, platform_send_started_at, entry_json, enqueued_at, updated_at, failed_at)
migration_runs(id, started_at, finished_at, status, report_json)
migration_sources(source_key, migration_kind, source_path, target_table, source_sha256, source_size_bytes, source_record_count, last_run_id, status, imported_at, removed_source, report_json)
backup_runs(id, created_at, archive_path, status, manifest_json)
```
`config_machine_state` owns the `skills.curatorState`, `update.checkState`,
`config_machine_state` owns the `authProfiles.store`, `authProfiles.state`,
`plugins.installedIndex`, `skills.curatorState`, `update.checkState`,
`clawhub.promotionsFeed`, `modelCatalog.remote`, `voicewake.triggers`,
`voicewake.routing`, `onboarding.recommendations.<workspaceKey>`,
`tui.lastSession.<scopeKey>`, `sidebar.sectionOrder`, `nodeHost.config`, and
`webPush.vapidKeys` snapshots. Secret-excluding Git backups omit the `nodeHost.`
and `webPush.vapidKeys` key prefixes without dropping other machine state.
`webPush.vapidKeys` snapshots. Secret-excluding Git backups omit the
`authProfiles.`, `nodeHost.`, and `webPush.vapidKeys` key prefixes without
dropping other machine state.
Agent database:
@@ -1956,7 +1956,7 @@ manifest asset from the verified extracted payload.
1. Add database registry APIs.
- Resolve global DB and per-agent DB paths.
- The global schema now uses `user_version = 9`; per-agent DBs use version
- The global schema now uses `user_version = 13`; per-agent DBs use version
`17`, with bounded forward migrations from supported older versions.
- Add close/checkpoint/integrity helpers used by tests, backup, and doctor.