fix: reconcile Ticket tags with current develop contracts and Runtime auth

This commit is contained in:
2026-10-11 02:03:44 +09:00
67 changed files with 5629 additions and 4519 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/revisionから`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`、旧revision 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/revisionが削除後に再利用された鍵を、過去の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 と同じ同期要求で
扱う契約を見直すこと。単にタイムアウトを延長するだけでは結果不明の状態はなくならない。
今回は調査報告のみで、これらの実装変更はしていない。
+49
View File
@@ -0,0 +1,49 @@
# T-740: composition with develop 71ab4c06
## Source reconciliation
The existing `work/T-740-workspace-ticket-tags` source at `e9397fa0` was clean. The requested develop commit `71ab4c06b9ae24ca4daa88516d1e436c5a31c779` was fetched and merged normally into this same branch (no replacement MR or integration branch). Generated conflicts in OpenAPI and companion/repository/runtime/ticket/worker-launch types were resolved by regeneration, not by selecting one side's contract. Workdir types were regenerated as well because the shared contract digest changed.
The merged Rust contract retains both classification operations/projections and the develop Runtime binding/per-request authentication model. Workspace schema remains 90; Ticket classification schema remains 8. The Runtime migration archives historical verification metadata, does not make it a grant, and does not rebuild Ticket/tag tables. Shared server/store/schema-equivalence/purge inventory changes were inspected against develop: both Runtime archives and all three classification tables are included.
Official generators rerun:
- canonical `export_openapi`;
- repository/runtime/ticket/workdir/worker-launch/companion OpenAPI schema-closure generators;
- feature-gated memory/skill/auth/repository-access/Drive/legacy Server type catalogs. Their checked-in representation conventions are preserved (auth uses the raw normalized generator output, not Deno reformatting).
## Composition defect fixed
New signed-Runtime router tests found that classification handlers discarded the verified Runtime Worker identity when optional transport source-hint headers were absent. This produced empty attribution in durable tag receipts and allowed another authenticated Worker to replay the same operation key.
Classification now takes the verified Runtime source from `ServerRequestContext`, retains the existing source-hint verification path, rejects mismatched identities, and carries the actual Worker identity into event/audit and exact-replay input. Runtime-only claims without a Worker identity cannot impersonate a human classification actor. Request proof tokens are not operation identity; exact replay with a new valid signature from the same Worker is accepted, while another Worker conflicts. Other Ticket operation contracts are not respecified by this correction.
## Executed validation
All fixtures are hermetic, temporary, in-process boundaries following `docs/development/rust-testing-strategy.md`.
- `cargo test -p yoi-workspace-server --lib ticket_tags_tests`: 6 passed (original 3 plus signed Runtime lifecycle/replay, identity-bound replay, and rejection matrix).
- Rejection coverage: unsigned hints, forged signing key, unknown Worker, other Workspace claim, body tampering, stale pinned key, and administratively revoked binding, for both catalog mutation and Ticket membership endpoints; no rejected-operation writes or execution effects.
- `current_workspace90_without_classification_upgrades_ticket7_without_rewriting_ticket_history`: passed. A current Workspace-90/pre-classification Ticket-7 fixture upgrades to Ticket-8 without rewriting an existing closed Ticket, labels, decision/history, or Workspace auth schema; arbitrary `ready` classification preserves the decision.
- `workspace89_to90_runtime_rebuild_preserves_tag_links_tombstones_receipts_and_audit`: passed. Real retained migration plus tagged data preserves exact current links, tombstones/recreated identities, receipt/context/audit rows and thread snapshots; replay and FK/deleted-reference rejection remain intact.
- `fresh_and_upgraded_state_contract_schemas_enforce_the_same_constraints`: passed, including classification tables and Runtime archives.
- `cargo test -p ticket --lib tags`: 14 passed.
- `cargo test -p worker --lib feature::builtin::ticket::tags`: 7 passed, covering normal tools/native WIP, permission, route binding and durable mutation/replay contracts after composition.
- `cargo test -p worker-runtime --lib workspace_issuer`: 12 passed.
- Full `cargo test -p ticket -p server-api -p client -p worker -p yoi-workspace-server`: passed including integration/doc targets. Unit suites: client 57, server-api 92, ticket 120, worker 975, Server 830 passed/1 existing ignored. The compaction integration target's 25 tests also passed.
- `cargo test -p server-api --features typescript`: 101 passed; all feature-catalog artifact tests passed.
- Root `cargo check`: passed, including normal TUI/Server closure.
- Canonical OpenAPI and all six OpenAPI-based generator `--check` runs: passed.
- `cargo fmt --all -- --check`, `git diff --check HEAD`: passed.
Initial signed-source tests failed on attribution/replay and passed after the fix. An attempted Deno format of auth catalog output failed its exact generated-artifact test; regenerating it in the documented raw form restored the artifact and all 101 feature tests passed.
## Naming audit limitation inherited from target
The required state-contract-name command was executed. It reports **30 unapproved lines and 9 stale exception hashes** in develop's frozen SSH migration/fixtures, migration-entrypoint fixture and timestamp-mismatch report. Every affected file and the exception inventory are byte-for-byte unchanged from `71ab4c06` (`git diff --exit-code 71ab4c06 -- <affected paths>` passed). The T-740 integration additions introduce no such finding. The inventory was not expanded, hashes were not silently refreshed, and no upstream historical contracts were renamed merely to make the checker green. This audit is **not reported as passing** and remains an Orchestrator/user follow-up outside the classification correction.
## Limits / handoff
T-739 is not integrated: its future case-state/close/migration composition is **not verified** by these tests. T-743 final Web UI, live production DB migration and full end-to-end UI/Runtime execution are not claimed. The existing ignored Server Drive Web-adapter roundtrip was not executed. The current-DB upgrade test covers a seeded schema-90 fixture, not live user data.
Only the existing source branch is to be published non-force. Source movement requires fresh trusted review on the same explicit MR `01a126a0-b4b1-7a13-ba61-5b2c06a3bca4` and current Ticket digest. Target publication, MR integration and Ticket completion/close are not performed here.