Skip to main content

agentmux_srv/migrations/
mod.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Data migration framework.
5//!
6//! Migrations are versioned steps that run once, either via `agentmux-srv migrate`
7//! or in-process at startup via `run_pending_migrations` (called unconditionally;
8//! fast-paths to `Ok(0)` when already current). The in-process path runs before
9//! any store is opened for normal operation so that `id_store` always binds to a
10//! fully-backfilled shared store.
11//!
12//! Adding a migration: implement [`Migration`], add it to [`REGISTRY`], add a
13//! file under this module. The ID must be unique and lexicographically ordered
14//! (use the `NNNN_slug` convention). Retiring old migrations: remove from
15//! [`REGISTRY`] once the minimum supported upgrade path passes the migration's
16//! origin version — the `db_migrations` row stays as a permanent record.
17//!
18//! # No private marker files
19//!
20//! `db_migrations` (plus a [`Migration::verify`] post-condition) is the sole
21//! source of truth for "has this run" — SPEC_MIGRATION_SYSTEM_HARDENING_2026_08_03
22//! Phase 2. A new migration must not introduce its own flag file, and no
23//! migration may treat the mere *existence* of a marker as proof of
24//! completion: that is the shape that left `0007`'s target table empty
25//! while its flag said "done" (§1.2, F1). The three legacy markers that
26//! remain are evidence only — `0007`'s and `0002`'s helpers decide from the
27//! data (`consolidate_looks_incomplete`, `block_zones_look_incomplete`) and
28//! are content-idempotent, `0003`'s helper ignores its marker and re-asserts
29//! its invariant every run — and `m0000_bootstrap` consults them only
30//! together with those content checks when stamping a pre-framework install.
31//!
32//! # Migrations freeze copies of live logic on purpose
33//!
34//! Some migrations carry verbatim copies of helpers that also exist in live
35//! code: `m0017_ambient_login_grandfather` mirrors ~35 lines of
36//! `backend/blockcontroller/core.rs`; `m0020_agent_color_backfill` and
37//! `m0021_backfill_agent_bundles` mirror 25–30 lines each of
38//! `backend/storage/mcp_servers.rs` and `def_registry_mirror.rs`. That is
39//! deliberate, not accidental duplication. A migration must produce the same
40//! rows on every machine it ever runs on, so it cannot call a live helper
41//! whose behaviour a later release may change; the copy pins the logic as it
42//! was at the migration's origin version. Do not "deduplicate" a migration
43//! against live code. When you copy logic into a new migration, say so next
44//! to the copy — `// frozen copy of <path>::<fn> as of <version>` — so the
45//! next reader can tell a freeze from a drift.
46
47mod m0000_bootstrap;
48mod m0001_legacy_data_dir;
49mod m0002_block_zones_v1;
50mod m0003_template_sessions_v1;
51mod m0004_registry_from_sqlite;
52mod m0005_registry_source_bases;
53mod m0006_definitions_global;
54mod m0007_agents_consolidate;
55mod m0008_default_bundle;
56mod m0009_transcript_backfill;
57mod m0010_session_ids;
58mod m0011_shared_store_backfill;
59mod m0012_dedup_identity_accounts;
60mod m0013_agent_direct_bindings;
61mod m0014_agent_direct_bindings_rerun;
62mod m0015_seed_starter_skills;
63mod m0016_seed_starter_mcp_servers;
64mod m0017_ambient_login_grandfather;
65mod m0018_ambient_login_registry;
66mod m0019_repair_malformed_secret_ref;
67mod m0020_agent_color_backfill;
68mod m0021_backfill_agent_bundles;
69mod m0022_identity_store_links_backfill;
70mod m0023_native_memory_versions_backfill;
71mod m0024_native_memory_backfill_from_fs;
72mod m0025_agents_launch_state_backfill;
73mod m0026_registry_agent_id_rekey;
74mod m0027_agents_workspace_backfill;
75mod m0028_agent_child_tables_repoint_fk;
76mod m0029_drop_legacy_agent_tables;
77mod m0030_backfill_bundle_component_refs;
78mod m0031_carry_skills_and_mcp_servers_to_identity_store;
79mod m0032_drop_catalog_fk_from_ref_tables;
80mod m0033_narrow_skill_global_uniqueness_index;
81mod m0034_identity_history_backfill;
82mod m0035_agent_owns_its_provider;
83mod m0036_drop_bundle_mcp;
84mod m0037_retire_old_pane_names;
85mod runner;
86#[cfg(test)]
87mod phase5_tests;
88
89pub use runner::count_pending_migrations;
90pub use runner::doctor_report_for_instance;
91pub use runner::run_migrate_command;
92pub use runner::run_pending_migrations;
93pub use runner::try_count_pending_migrations;
94pub use runner::PendingCountError;
95
96use std::path::PathBuf;
97
98// ── Public types ─────────────────────────────────────────────────────────────
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum MigrationScope {
102    /// Touches `~/.agentmux/shared/` — runs once regardless of channel.
103    Global,
104    /// Touches the current channel's data dir.
105    Channel,
106}
107
108impl MigrationScope {
109    pub fn as_str(self) -> &'static str {
110        match self {
111            Self::Global => "global",
112            Self::Channel => "channel",
113        }
114    }
115}
116
117/// Context passed to every migration's `up()` call.
118pub struct MigrationContext {
119    /// `~/.agentmux` (or wherever AGENTMUX_SHARED_DIR's parent lives).
120    pub home: PathBuf,
121    /// Current channel's data directory (passed via `--wavedata`).
122    pub data_dir: PathBuf,
123    /// Path to the shared store (`~/.agentmux/shared/store.db`).
124    pub shared_store_path: PathBuf,
125    /// Path to the channel store (`<data_dir>/db/objects.db`).
126    pub channel_store_path: PathBuf,
127}
128
129#[derive(Debug)]
130pub struct MigrationError(pub String);
131
132impl std::fmt::Display for MigrationError {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        write!(f, "{}", self.0)
135    }
136}
137
138impl From<String> for MigrationError {
139    fn from(s: String) -> Self {
140        Self(s)
141    }
142}
143
144impl From<&str> for MigrationError {
145    fn from(s: &str) -> Self {
146        Self(s.to_owned())
147    }
148}
149
150/// Result of an applied migration's post-condition check
151/// (`agentmux-srv migrate --verify`, SPEC_MIGRATION_SYSTEM_HARDENING_2026_08_03
152/// Phase 1). `db_migrations` records only that `up()` returned `Ok` — never
153/// that it wrote what it was meant to. A migration that can state its own
154/// effect as a cheap query implements [`Migration::verify`]; the rest report
155/// `NotVerifiable` and the doctor pass says so rather than guessing.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum VerifyOutcome {
158    /// The migration declares no checkable post-condition (the default).
159    NotVerifiable,
160    /// The post-condition holds. The string is the evidence (counts, paths).
161    Ok(String),
162    /// Recorded applied, but the effect is not there — the F1/F2 "marker
163    /// present, wrote nothing" shape the spec was written about.
164    Mismatch(String),
165    /// The check itself could not run (store unreadable, etc.). Treated
166    /// like a mismatch for exit-code purposes: an unverifiable applied
167    /// migration is not a passing one.
168    Error(String),
169}
170
171impl VerifyOutcome {
172    /// `true` for `Mismatch` and `Error` — the two outcomes that fail the pass.
173    pub fn is_failure(&self) -> bool {
174        matches!(self, Self::Mismatch(_) | Self::Error(_))
175    }
176
177    /// Short status word for CLI / NDJSON output.
178    pub fn label(&self) -> &'static str {
179        match self {
180            Self::NotVerifiable => "n/a",
181            Self::Ok(_) => "ok",
182            Self::Mismatch(_) => "MISMATCH",
183            Self::Error(_) => "error",
184        }
185    }
186
187    /// The detail string, if any.
188    pub fn detail(&self) -> &str {
189        match self {
190            Self::NotVerifiable => "",
191            Self::Ok(s) | Self::Mismatch(s) | Self::Error(s) => s,
192        }
193    }
194}
195
196pub trait Migration: Send + Sync {
197    fn id(&self) -> &'static str;
198    fn scope(&self) -> MigrationScope;
199    fn description(&self) -> &'static str;
200    fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError>;
201
202    /// Post-condition check for an APPLIED migration. Must be read-only and
203    /// cheap (a handful of queries / `exists()` calls) — it runs from a CLI
204    /// doctor pass, never from startup. Default: nothing checkable. Override
205    /// where the effect can be stated as a query; see `m0007` for the
206    /// row-count shape and `m0002` for the marker-file shape.
207    fn verify(&self, _ctx: &MigrationContext) -> VerifyOutcome {
208        VerifyOutcome::NotVerifiable
209    }
210}
211
212// ── Registry ─────────────────────────────────────────────────────────────────
213//
214// Ordered list of all migrations. Applied in order; each runs at most once
215// (guarded by `db_migrations` in the shared store). Add new migrations at the
216// end. Do NOT reorder or remove applied migrations — removals belong in a
217// separate "retire" pass once the minimum supported upgrade path clears them.
218
219static REGISTRY: &[&(dyn Migration + Sync)] = &[
220    &m0000_bootstrap::M0000Bootstrap,
221    &m0001_legacy_data_dir::M0001LegacyDataDir,
222    &m0002_block_zones_v1::M0002BlockZonesV1,
223    &m0003_template_sessions_v1::M0003TemplateSessionsV1,
224    &m0004_registry_from_sqlite::M0004RegistryFromSqlite,
225    &m0005_registry_source_bases::M0005RegistrySourceBases,
226    &m0006_definitions_global::M0006DefinitionsGlobal,
227    &m0007_agents_consolidate::M0007AgentsConsolidate,
228    &m0008_default_bundle::M0008DefaultBundle,
229    &m0009_transcript_backfill::M0009TranscriptBackfill,
230    &m0010_session_ids::M0010SessionIds,
231    &m0011_shared_store_backfill::M0011SharedStoreBackfill,
232    &m0012_dedup_identity_accounts::M0012DedupIdentityAccounts,
233    &m0013_agent_direct_bindings::M0013AgentDirectBindings,
234    &m0014_agent_direct_bindings_rerun::M0014AgentDirectBindingsRerun,
235    &m0015_seed_starter_skills::M0015SeedStarterSkills,
236    &m0016_seed_starter_mcp_servers::M0016SeedStarterMcpServers,
237    &m0017_ambient_login_grandfather::M0017AmbientLoginGrandfather,
238    &m0018_ambient_login_registry::M0018AmbientLoginRegistry,
239    &m0019_repair_malformed_secret_ref::M0019RepairMalformedSecretRef,
240    &m0020_agent_color_backfill::M0020AgentColorBackfill,
241    &m0021_backfill_agent_bundles::M0021BackfillAgentBundles,
242    &m0022_identity_store_links_backfill::M0022IdentityStoreLinksBackfill,
243    &m0023_native_memory_versions_backfill::M0023NativeMemoryVersionsBackfill,
244    &m0024_native_memory_backfill_from_fs::M0024NativeMemoryBackfillFromFs,
245    &m0025_agents_launch_state_backfill::M0025AgentsLaunchStateBackfill,
246    &m0026_registry_agent_id_rekey::M0026RegistryAgentIdRekey,
247    &m0027_agents_workspace_backfill::M0027AgentsWorkspaceBackfill,
248    &m0028_agent_child_tables_repoint_fk::M0028AgentChildTablesRepointFk,
249    &m0029_drop_legacy_agent_tables::M0029DropLegacyAgentTables,
250    &m0030_backfill_bundle_component_refs::M0030BackfillBundleComponentRefs,
251    &m0031_carry_skills_and_mcp_servers_to_identity_store::M0031CarrySkillsAndMcpServersToIdentityStore,
252    &m0032_drop_catalog_fk_from_ref_tables::M0032DropCatalogFkFromRefTables,
253    &m0033_narrow_skill_global_uniqueness_index::M0033NarrowSkillGlobalUniquenessIndex,
254    &m0034_identity_history_backfill::M0034IdentityHistoryBackfill,
255    &m0035_agent_owns_its_provider::M0035AgentOwnsItsProvider,
256    &m0036_drop_bundle_mcp::M0036DropBundleMcp,
257    &m0037_retire_old_pane_names::M0037RetireOldPaneNames,
258];