merge: compose current auth migration with Ticket case schema 91

This commit is contained in:
2026-10-11 02:21:52 +09:00
64 changed files with 5196 additions and 4551 deletions
@@ -0,0 +1,70 @@
# 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の案内・検証がツール上の改善点。
@@ -0,0 +1,88 @@
# Workdir 削除のマウント残留と結果不明の診断
## 調査範囲
ユーザーの「workdirを消せない原因を調査して」という依頼に対し、保存済み Server DB を
`mode=ro` / `PRAGMA query_only=ON` で照合し、対象ディレクトリ・マウント・現行実装を
観察した。削除再実行、アンマウント、DB 更新、サービス/プロセス操作、実装修正は行っていない。
調査中にも operation の記録が更新されたため、以下は最後に読んだ状態であり、過去の全試行に
同じ原因があったと断定するものではない。
Workspace: `0197a949-4b6b-7f2a-9d9a-1f87e3a4c5b6`、Runtime: `arcadia`。
表示に含まれる次の2つの Workdir ID は別の対象である。
## T-721: 残留した JuiceFS マウントによる拒否(確認済み)
- Workdir: `001a11ab3e830000007` / `T-721 JuiceFS Drive foundation`
- operation: `wdr_1c0840acc66f27b7e048bdfff8f80b76`
- 最後に読んだ attempt: `01a12682-3b33-7e43-b4c6-4e57eb469912`
- 更新時刻: `2026-10-10T15:50:31.515343065+00:00`
- 結果: `failed` / `mount_present` / `retryable=1`
`findmnt` で、次の2つが `JuiceFS:yoi-t721` / `fuse.juicefs` のマウントとして残っていることを
確認した(パスは `yoi.local` 以下の論理パス)。
```text
workdirs/001a11ab3e830000007/checkout/.yoi/dev/juicefs-t721/mount
workdirs/001a11ab3e830000007/checkout/.yoi/dev/juicefs-t721/mount2
```
両エントリの属性参照は `Transport endpoint is not connected` で失敗した。
`ps -C juicefs` に現在のプロセスはなかった。マウント解除されていない FUSE の残留状態と
整合するが、プロセスが終了した経緯までは今回確認していない。
Registry の cleanliness は `clean` だが、これはマウントを安全に削除できる証明ではない。
実ディレクトリの `materialization.json` は `cleanup_pending`。
現行 `crates/worker-runtime/src/working_directory/cleanup.rs` は、削除前にマウント境界を
検査し、外部/未知のマウントを自動解除しない。Server は
`working_directory_cleanup_mount_present` を `mount_present` として扱う。
これは安全性を維持する拒否であり、cleanliness 判定の不具合とは異なる。
解消には、所有者が保存先と利用状況を確認して両マウントを安全に解除することが必要。
その後、マウントがないことを確認して通常の Workdir 削除を再実行する。
マウント先を含む `rm -rf`、強制的なメタデータ改変、未知のプロセス停止は解消手段にしない。
マウント解除後も ignored な実験データ等の保護により別の拒否が起こる可能性があり、今回の
確認は次回の削除成功を保証しない。
## 別 ID: HTTP タイムアウトと物理削除の時間差が疑われる(未確定)
- Workdir: `001a111ca9d33000002`
- operation: `wdr_fccf7c6e9ee6cbb931ea2f9b82198650`
- attempt: `01a12682-411e-7720-b829-f9c9c0148886`
- created: `2026-10-10T15:50:32.984845992+00:00`
- updated: `2026-10-10T15:50:43.018819445+00:00`
- 結果: `failed` / `provider_cleanup_failed` / `retryable=1`
要求から失敗まで約10.034秒。Registry には `corrupted` / cleanliness `unknown` として
残っている一方、調査時点で `yoi.local/workdirs/001a111ca9d33000002` は存在しない。
`.cleanup-authority` にも残存ファイルはなかった。
現行実装では `RemoteRuntimeConfig::new` の timeout は10秒で、DELETE はその共通
blocking HTTP client を使う。Runtime の HTTP handler は `spawn_blocking` で同期的な
検査/削除の完了を待つ。クライアントのタイムアウトは、この blocking 削除処理の取り消しや
物理削除の失敗を意味しない。
このため「Server が10秒で応答待ちを打ち切った後、Runtime の物理削除が完了した」という
説明と整合する。ただし、元の `remote_runtime_timeout` 等の診断を今回の観察面で取得できず、
別の操作でディレクトリがなくなった可能性も排除できない。物理パスがないだけで Backend の
削除完了を捏造してはいけない。通常の再試行で Runtime の厳密な not-found 応答を確認できれば、
現行 Server は registry 削除を完了する経路を持つ。今回、その再試行は行っていない。
## 診断面の障壁と改善案
`crates/workspace-server/src/server.rs` の `workdir_cleanup_failure_category` は、既知の
cleanup code を分類し、それ以外を `provider_cleanup_failed` に落とす。
`crates/workspace-server/src/hosts.rs` は transport timeout を `remote_runtime_timeout`
に区別するが、上記分類では失敗結果不明との区別が失われる。
また removal operation は bounded category のみを永続化し、元の diagnostic code を残さない。
これは秘密情報を含み得る message を保存しない方針とは両立するが、現行の失敗ログも
Workdir/operation/runtime/category のみで、ユーザー案内の「相関 Runtime diagnostics を調べる」
を保存済み情報だけでは完遂できなかった。`runtime.json` の diagnostics にも対象 ID / operation
を含む記録は見つからなかった。
改善案は、秘密を含む raw message を公開・永続化せず、transport timeout を「削除結果不明」と
して区別し、許可された診断コードと operation/attempt/Workdir の相関を残すこと。
さらに、完了まで時間のかかる guarded cleanup を通常の10秒 control request と同じ同期要求で
扱う契約を見直すこと。単にタイムアウトを延長するだけでは結果不明の状態はなくならない。
今回は調査報告のみで、これらの実装変更はしていない。