Files
yoi/docs/report/2026-10-10-repository-key-migration-timestamp-mismatch.md

6.5 KiB
Raw Permalink Blame History

schema86→87のSSH移行: 異なる時刻形式の文字列比較でfingerprintがNULLになる

調査日: 2026-10-10 報告された起動エラー:

yoi-server: store error: workspace schema migration failed: sqlite error: NOT NULL constraint failed: workdir_create_credential_candidates.credential_fingerprint

原因

crates/workspace-server/src/frozen_repository_key_migration.rs:262–266のcredential candidate移行は、旧credential IDと保存counterの組からrepository_key_cutoverへLEFT JOINし、m.fingerprintを新しいNOT NULL列に挿入する。

JOIN条件のm.created_at <= o.updated_atが、RFC3339の鍵作成時刻とepoch-millis文字列のWorkdir更新時刻を、そのままTEXTとして比較している。日時としての順序を判定していないため、有効な古い鍵のJOINが外れ、fingerprintがNULLになる。

  • 鍵作成時刻: repository_access.rs:2256のUtc::now().to_rfc3339_opts(SecondsFormat::Millis, true)。
  • Workdir作成・更新時刻: server.rs:30936–30940のnow_registry_timestamp()はepochミリ秒の文字列。reserveでもこれを使用する(server.rs:24272–24308)。

同じ異形式比較はoperation本体のcredential/host trust移行にもある(migration L260–261)。candidateだけ直してもoperation snapshotのfingerprintが欠落する問題が残る。

実データによる確認

ユーザーから共有済みのyoi.local/server/server.dbをNodeのDatabaseSync({readOnly: true})とPRAGMA query_only=ONで照合した。秘密鍵、暗号化envelope、master keyは取得していない。migrationやDB更新も実行していない。

  • DBはschema86のまま。
  • 新candidateへ移される5件すべてがfailed Workdir createのcredential候補で、旧retentionが各1件あるため成功済み・unretainedのarchive経路には入らない。
  • 5件とも参照先の鍵は存在する。対応するmutation receiptも各1件あり、証拠欠落ではない。
  • 5件ともSQLiteのTEXT比較はfalse、実際の時刻比較はtrue。

代表例:

項目 値
operation call_mgmyCNofdQMZ8uyI5tg0mBhv
credential workspace-default、旧保存counter 1
鍵created_at 2026-09-16T18:44:56.736Z
Workdir updated_at 1790173821030
Workdir updated_atを日時に変換 2026-09-23T14:30:21.030Z
SQLiteの文字列比較 false
日時としての比較 true

他の4件では鍵作成が2026-09-10T13:09:52.113Z、Workdir更新は9/23または9/29で、同じ問題になる。

修正方針(この調査では未実装)

  • frozen migration内で旧データの時刻形式を明示的に読み、同じ時刻型/epoch-millisへ正規化して比較する。文字列比較やSQLの安易なCASTに頼らない。
  • created_at <= updated_atの安全確認そのものは削除しない。旧IDと保存counterが削除後に再利用された鍵を、過去のcandidateに誤って結び付けることを防ぐ条件である。
  • candidateだけでなくoperation本体のcredential/host trust等、同じ境界の比較を確認する。
  • 正規化不能・不正な時刻や真に必要な証拠が欠落した場合は、NOT NULLエラーになる前に文脈付きで拒否し、transactionをrollbackする。

既存のfrozen_repository_key_tests.rs:35–42の共通fixtureは、鍵とWorkdirの時刻をすべてcreatedに揃えており、実データの異形式を再現していない。RFC3339鍵時刻+epoch-millis Workdir時刻での移行成功と、実時刻が操作より後の再作成鍵を拒否するケースが必要。テスト追加時はdocs/development/rust-testing-strategy.mdに従う。

影響・実施範囲

schema86→87はtransaction内で実行され、version87の記録とcommitは最後に行う(migration L29、L349–350)。今回のDBでもschema86と旧テーブルを確認した。ただし、それより前のschema更新まで含めた全起動処理が一括で元に戻るという意味ではない。

NOT NULLを外す、適当なfingerprintを入れる、candidate/retentionを消して通す、といったDB修復は行うべきではない。正しい既存データをmigrationが誤判定しているので、移行コード側を修正する。

調査時点で追加したのは本報告のみ。以下は、その後のユーザー指示による修正記録。

修正と検証(2026-10-10)

  • RFC3339/epoch-millisをRustで読み、temporary tableのUTC秒・ナノ秒へ正規化して比較するよう修正。credential/host trustのoperation snapshot、candidate、旧receiptの時刻条件に適用した。
  • タイムゾーン・sub-millisecondの順序を維持し、元の履歴時刻やreceipt照合用の文字列、暗号化AADは書き換えない。永続schemaの変更・version bumpは不要。
  • 不正な時刻、欠けた鍵、操作より新しい再利用鍵は文脈付きで拒否しrollback。NOT NULLを緩めたり候補を削除したりしない。
  • 独立コード確認で見つかった、親operationのない旧candidateがINNER JOINから落ちて消える経路も、コピー前の拒否とrollbackテストで保護した。追修正に追加のblocking findingなし。
  • 偽時刻createdや日付だけのfixtureは実際のRFC3339へ修正。新規回帰は混在形式・同時刻・offset・ナノ秒精度、各SSH参照での新しい鍵拒否、不正時刻、孤立候補の保持を検証する。
  • 最小の混在形式テスト、migrationモジュール、公開migration dry-run/applyテスト、孤立候補テストを順に実行して成功。最終のcargo test -p yoi-workspace-serverはlibrary 815 passed / 1 ignored、binary 11 passed、doctest失敗なし。
  • root cargo check、cargo fmt --all -- --check、変更したincludeファイルの明示的rustfmt --check、git diff --check HEADも成功。
  • 稼働DBでのmigration、ビルド済みバイナリの配備、サービスの停止・再起動は行っていない。更新はユーザー操作のまま。

作業中、テスト担当SubWorkerはcwd基準の読取で、scopeに付与した別ディレクトリのテスト規則を参照できなかった(絶対pathもinvalid logical filesystem pathで拒否)。編集なしを確認して停止し、親でテストを実装した。読取専用レビュー担当はroot cwdで規則・ソースを読めた。cwdとscopeにまたがる論理pathの案内・検証がツール上の改善点。