From 6a12b6cc346402b7a10208685038967171b09cb3 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 10 Oct 2026 17:48:59 +0900 Subject: [PATCH] fix: complete state-contract naming cleanup --- AGENTS.md | 8 + crates/api-macros/src/openapi.rs | 2 +- crates/job/tests/contract.rs | 8 +- crates/manifest/src/lib.rs | 26 +- crates/merge-request/src/lib.rs | 6 +- crates/merge-request/tests/store.rs | 9 +- crates/protocol/src/lib.rs | 46 +- crates/server-api/src/lib.rs | 65 +- crates/session-store/src/system_item.rs | 12 +- .../src/job_store/legacy_migrations.rs | 3 +- crates/standalone/src/store.rs | 8 +- crates/subjektiv/src/legacy_migrations.rs | 21 +- crates/subjektiv/src/lib.rs | 8 + crates/ticket/src/lib.rs | 4 + crates/worker-runtime/src/fs_store.rs | 8 +- crates/worker-runtime/src/main.rs | 59 +- crates/worker-runtime/src/worker_source.rs | 1 - .../worker-runtime/src/working_directory.rs | 35 +- crates/worker-runtime/src/workspace_issuer.rs | 70 +- .../src/feature/builtin/manage_worker.rs | 49 +- .../feature/builtin/resource_projection.rs | 8 +- crates/worker/src/prompt/catalog.rs | 1 - crates/worker/src/shared_state.rs | 13 +- crates/worker/src/worker.rs | 3 +- crates/workspace-drive/src/lib.rs | 3 + crates/workspace-drive/src/migrations.rs | 160 +- crates/workspace-drive/src/tests.rs | 5 +- crates/workspace-server/src/authority.rs | 71 +- crates/workspace-server/src/backend_job.rs | 16 +- .../src/frozen_content_state_migration.rs | 4 +- .../src/frozen_content_state_tests.rs | 4 +- .../src/frozen_flow_content_migration.rs | 4 +- .../src/frozen_flow_content_tests.rs | 4 +- .../src/frozen_repository_key_migration.rs | 38 +- .../src/frozen_repository_key_tests.rs | 28 +- .../src/frozen_runtime_binding_migration.rs | 2 +- crates/workspace-server/src/latest_schema.sql | 6 +- .../src/legacy_evidence_column_migration.rs | 24 + .../src/legacy_evidence_column_tests.rs | 124 + crates/workspace-server/src/lib.rs | 2 +- crates/workspace-server/src/main.rs | 4 +- .../workspace-server/src/repository_access.rs | 8 +- .../src/schema_equivalence_tests.rs | 1 + crates/workspace-server/src/server.rs | 177 +- .../src/server/subjektiv_job.rs | 2 +- .../src/server/tests/ticket_evidence_tests.rs | 32 +- .../server/tests/ticket_worker_claim_tests.rs | 6 +- .../src/server/tests/value_profiles_tests.rs | 4 +- .../src/server/worker_operations.rs | 2 +- crates/workspace-server/src/skills.rs | 4 +- crates/workspace-server/src/store.rs | 61 +- .../src/workdir_create_operations.rs | 6 +- .../src/workspace_signing_identity.rs | 9 +- docs/design/standalone-subjektiv.md | 4 +- .../ui-ux/subject-memory-information.md | 26 +- docs/development/web-state-preconditions.md | 20 + .../workdir-refusal-diagnostics.md | 2 +- .../workspace-request-admission.md | 4 +- .../workspace-schema-migrations.md | 2 +- ...26-08-06-ticket-search-audit-visibility.md | 33 +- ...-11-flow-verifier-stale-session-capture.md | 6 +- ...remote-worker-repository-access-restore.md | 4 +- ...9-24-worker-lifecycle-restore-atomicity.md | 6 +- .../2026-10-05-review-verdict-validation.md | 4 +- ...5-reviewer-subworker-stopped-before-run.md | 4 +- .../2026-10-05-subworker-ticket-context.md | 6 +- docs/report/2026-10-06-t704-validation.md | 6 +- docs/report/T-711-import-cycle.md | 4 +- ...te-contract-cleanup-recovery-checkpoint.md | 128 +- docs/report/state-contract-name-audit.md | 126 + docs/report/t718-subworker-path-context.md | 4 +- docs/report/t724-validation.md | 14 +- .../t731-review-verdict-registration-401.md | 6 +- docs/report/test-code-audit-20260806.md | 6 +- openapi/server-api.json | 3 +- tests/e2e/src/worker_environment.rs | 19 +- tools/check-state-contract-names.test.ts | 90 + tools/check-state-contract-names.ts | 132 + tools/state-contract-name-exceptions.json | 2186 +++++++++++++++++ tools/web-ux/README.md | 2 +- .../memory_document_shell_test.ts | 2 +- .../src/lib/generated/companion-api.ts | 2 +- .../src/lib/generated/legacy-server-api.ts | 1 - .../src/lib/generated/protocol-validator.ts | 2 +- .../src/lib/generated/repository-api.ts | 2 +- .../src/lib/generated/runtime-api.ts | 2 +- web/workspace/src/lib/generated/ticket-api.ts | 2 +- .../src/lib/generated/workdir-api.ts | 2 +- .../src/lib/generated/worker-launch-api.ts | 2 +- .../src/lib/workspace/api/workspace-model.ts | 9 +- .../console/worker-console.ui.test.ts | 1 - .../workspace/merge-request-status.test.ts | 2 +- .../src/lib/workspace/merge-request-status.ts | 2 +- .../src/lib/workspace/resource-links.ts | 2 +- .../src/lib/workspace/settings/profile-api.ts | 4 +- .../settings/workdir-checkout.test.ts | 4 +- .../sidebar/worker-workdir-meta.test.ts | 13 +- .../workspace/sidebar/worker-workdir-meta.ts | 8 +- .../memory/memory-page.browser.spec.ts | 2 +- .../settings/settings.browser.spec.ts | 1 - web/workspace/test/config-source/api.test.ts | 4 +- .../test/config-source/wasm-parity.test.ts | 2 - .../test/repository-access/model.test.ts | 2 +- web/workspace/test/visual-review/README.md | 2 +- web/workspace/tests/memory-api.test.ts | 7 +- web/workspace/tests/profile-api.test.ts | 18 +- .../tests/runtime-management.test.ts | 10 +- web/workspace/tests/skill-api.test.ts | 8 +- web/workspace/tests/workspace-model.test.ts | 23 +- 109 files changed, 3696 insertions(+), 576 deletions(-) create mode 100644 crates/workspace-server/src/legacy_evidence_column_migration.rs create mode 100644 crates/workspace-server/src/legacy_evidence_column_tests.rs create mode 100644 docs/development/web-state-preconditions.md create mode 100644 docs/report/state-contract-name-audit.md create mode 100644 tools/check-state-contract-names.test.ts create mode 100644 tools/check-state-contract-names.ts create mode 100644 tools/state-contract-name-exceptions.json diff --git a/AGENTS.md b/AGENTS.md index d78ac0109..1a9ae9cfc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,14 @@ - プロンプトはすべて`resources/prompts`に集約している。管理効率の向上のためであると同時に、ユーザーがオーバーライドする形式でもある。 - 変更量を最小にするために設計を歪めたり、設計問題に対して不必要な後方互換性を作らない。長期的なメンテナンスと型安全性を追求すること。 +### 状態契約の命名・競合制御 + +- `revision` という名前を、大小文字や複合語を含め、新しい型・フィールド・変数・API・DB列・文書に導入しない。 +- 不透明な更新番号を別の汎用名へ言い換えて済ませない。何を、どの根拠と比較し、どの操作の前/中/後に確認するかを、コードの名前と境界処理で示す。 +- 到着順、操作時刻、先勝ち/後勝ちだけで成立する処理はそれで実装する。内容の一致、鍵の一致、同じ操作の再送、同じ接続の生存期間を確認する必要がある場合は、その対象を明示し、順序や時刻だけでは足りない理由を残す。 +- 旧保存データを読むための綴りは現行契約と分離する。削除によるデータ喪失を避けるために残す場合も、必要な行と理由を明示して検査対象にする。文字列分割・符号化や広いディレクトリ除外で検査を逃れない。 +- 完了前に `env -u LD_LIBRARY_PATH deno run --allow-read --allow-run=git tools/check-state-contract-names.ts` を実行する。例外一覧を機械的に増やして合格させない。 + ### LLM コンテキストの加工原則 LLM に投げる context はappend-onlyが基本であり、またその永続化形式からAPIコールの形式を純粋に再現可能である必要が有る。 diff --git a/crates/api-macros/src/openapi.rs b/crates/api-macros/src/openapi.rs index 9fc172d09..6c08815f8 100644 --- a/crates/api-macros/src/openapi.rs +++ b/crates/api-macros/src/openapi.rs @@ -65,7 +65,7 @@ impl OpenApiSchema for BTreeMap {} pub struct OpenApiInfo<'a> { pub title: &'a str, pub version: &'a str, - /// Opaque digest of the source revision used to build the contract. + /// Source-content digest used to identify the exported contract's build inputs. pub source_digest: &'a str, } diff --git a/crates/job/tests/contract.rs b/crates/job/tests/contract.rs index 141224323..6c445a395 100644 --- a/crates/job/tests/contract.rs +++ b/crates/job/tests/contract.rs @@ -15,7 +15,7 @@ fn request() -> JobRequest { } #[test] -fn input_content_identity_is_computed_and_old_counter_is_not_accepted() { +fn input_content_identity_is_computed_and_unknown_fields_are_rejected() { let original = request(); let mut changed = original.clone(); changed.input = json!({"title": "changed"}); @@ -28,9 +28,9 @@ fn input_content_identity_is_computed_and_old_counter_is_not_accepted() { changed.input_digest().unwrap(), fingerprint(&changed.input).unwrap() ); - let mut old = serde_json::to_value(&original).unwrap(); - old["input_revision"] = json!(7); - assert!(serde_json::from_value::(old).is_err()); + let mut unknown = serde_json::to_value(&original).unwrap(); + unknown["unexpected_state_guard"] = json!(7); + assert!(serde_json::from_value::(unknown).is_err()); } #[test] diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 938a3b62c..bb59430ed 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -1141,7 +1141,8 @@ struct LegacyMemoryConfig { _query_excerpt_lines: Option, inject_summary: Option, workspace_id: Option, - settings_revision: Option, + #[serde(rename = "settings_revision")] + legacy_settings_counter: Option, language: Option, extract_model: Option, extract_threshold: Option, @@ -1241,8 +1242,9 @@ fn validate_persisted_worker_manifest( Ok(manifest) } -/// Frozen migration for schema 2/3 resolved manifests. Keep actual Workspace -/// identity and language unchanged; the obsolete sequence has no execution role. +/// Frozen migration for schema 2/3 resolved manifests. During persisted snapshot +/// decoding, reject an invalid old counter; do not compare it for execution freshness. +/// Keep actual Workspace identity and language unchanged; the sequence has no execution role. fn migrate_legacy_memory_settings_counter( manifest: &mut serde_json::Value, ) -> Result<(), serde_json::Error> { @@ -1342,14 +1344,16 @@ fn migrate_legacy_resolved_manifest_snapshot( serde_json::from_value(legacy_memory.unwrap_or_else(|| serde_json::json!({})))?; let mut workspace_settings = match ( legacy_memory.workspace_id, - legacy_memory.settings_revision, + legacy_memory.legacy_settings_counter, legacy_memory.language, ) { - (Some(workspace_id), Some(settings_revision), Some(language)) => Some(serde_json::json!({ - "workspace_id": workspace_id, - "settings_revision": settings_revision, - "language": language, - })), + (Some(workspace_id), Some(legacy_settings_counter), Some(language)) => { + Some(serde_json::json!({ + "workspace_id": workspace_id, + "settings_revision": legacy_settings_counter, + "language": language, + })) + } (None, None, None) => None, _ => { return Err(serde_json::Error::io(std::io::Error::new( @@ -1891,10 +1895,10 @@ model_id = "claude-sonnet-4-20250514" } #[test] - fn current_settings_snapshot_rejects_removed_counter_outside_migration() { + fn current_settings_snapshot_rejects_unknown_fields_and_invalid_language() { assert!( serde_json::from_value::(serde_json::json!({ - "workspace_id": "workspace-1", "language": "English", "settings_revision": 9 + "workspace_id": "workspace-1", "language": "English", "unexpected_state_guard": 9 })) .is_err() ); diff --git a/crates/merge-request/src/lib.rs b/crates/merge-request/src/lib.rs index 2d4f602ac..21054a748 100644 --- a/crates/merge-request/src/lib.rs +++ b/crates/merge-request/src/lib.rs @@ -606,8 +606,10 @@ pub struct CompleteMergeRequest { pub struct TicketCompletionEvent { pub operation_id: String, pub ticket_id: String, - /// Frozen historical payload only; not a content attestation or live API. - pub item_revision: String, + /// Frozen historical marker, read only to preserve archived completion payloads. + /// It is never checked for readiness, content attestation, or live completion. + #[serde(rename = "item_revision")] + pub legacy_item_marker: String, pub merge_request_ids: Vec, pub requirement_approval_event_id: String, pub completed_by: WorkerIdentity, diff --git a/crates/merge-request/tests/store.rs b/crates/merge-request/tests/store.rs index 52b364b69..7ee3ee4e9 100644 --- a/crates/merge-request/tests/store.rs +++ b/crates/merge-request/tests/store.rs @@ -164,11 +164,11 @@ fn v12_migration_revokes_old_grants_preserves_integration_and_requires_fresh_con assert_eq!(grants[1].0, "old-issued"); assert_eq!(grants[1].1, "revoked"); assert!(grants[1].2.is_some()); - let legacy_terms: i64 = connection.query_row( - "SELECT COUNT(*) FROM merge_request_thread_events WHERE payload_json LIKE '%ticket_item_revision%'", + let invalid_content_attestations: i64 = connection.query_row( + "SELECT COUNT(*) FROM merge_request_thread_events WHERE kind IN ('review_requested','review') AND json_extract(payload_json, '$.ticket_content_digest') IS NOT ''", [], |row| row.get(0), ).unwrap(); - assert_eq!(legacy_terms, 0); + assert_eq!(invalid_content_attestations, 0); let saved = store.get_by_id("W", "MR").unwrap(); assert_eq!(saved.merged_result().unwrap(), &merged); let integration = saved.integration_approval().unwrap(); @@ -1314,7 +1314,7 @@ fn historical_ticket_completion_payload_remains_readable_and_unchanged_on_migrat let payload = r#"{ "operation_id": "historical-operation", "ticket_id": "T", - "item_revision": "historical-revision", + "item_revision": "historical-item-marker", "merge_request_ids": ["MR-2", "MR"], "requirement_approval_event_id": "historical-review", "completed_by": {"runtime_id": "runtime", "worker_id": "coder"}, @@ -1350,6 +1350,7 @@ fn historical_ticket_completion_payload_remains_readable_and_unchanged_on_migrat assert_eq!(body, "historical body"); assert_eq!(state, "done"); let historical: TicketCompletionEvent = serde_json::from_str(&saved_payload).unwrap(); + assert_eq!(historical.legacy_item_marker, "historical-item-marker"); assert_eq!( serde_json::to_value(&historical).unwrap(), serde_json::from_str::(payload).unwrap() diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 3bedc21bb..adb511167 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1142,7 +1142,8 @@ pub enum Event { notification_request_id: String, message: String, }, - /// Revisioned FIFO replacement following enqueue, activation, cancel, or clear. + /// Complete FIFO replacement following enqueue, activation, cancel, or clear. + /// Clients replace their pending view in stream order; reconnect uses the snapshot. PendingSubmissionsChanged { pending: PendingSubmissionsSnapshot, }, @@ -2035,8 +2036,6 @@ mod tests { } }) ); - assert!(value.get("execution_generation").is_none()); - assert!(value.get("revision").is_none()); } #[test] @@ -2276,7 +2275,7 @@ mod tests { } #[test] - fn lifecycle_commands_reject_removed_nested_preconditions() { + fn lifecycle_commands_reject_unknown_nested_preconditions() { for method in ["pause", "cancel", "resume", "compact", "shutdown"] { let wire = serde_json::json!({ "method": method, @@ -2284,16 +2283,12 @@ mod tests { }); // Current commands are arrival-time operations, not Run-bound CAS. assert!(serde_json::from_value::(wire.clone()).is_ok()); - for field in [ - "expected_execution_generation", - "expected_worker_state_revision", - ] { - let mut old_command = wire.clone(); - old_command["params"]["command"][field] = serde_json::json!(1); - let error = serde_json::from_value::(old_command) - .expect_err("removed command preconditions must not be silently ignored"); - assert!(error.to_string().contains(field), "{method}: {error}"); - } + let field = "unexpected_state_guard"; + let mut unknown_command = wire.clone(); + unknown_command["params"]["command"][field] = serde_json::json!(1); + let error = serde_json::from_value::(unknown_command) + .expect_err("unknown command preconditions must not be silently ignored"); + assert!(error.to_string().contains(field), "{method}: {error}"); } } @@ -2308,8 +2303,6 @@ mod tests { }, ] { let json = serde_json::to_string(&method).unwrap(); - assert!(!json.contains("expected_execution_generation")); - assert!(!json.contains("expected_worker_state_revision")); let decoded: Method = serde_json::from_str(&json).unwrap(); match decoded { Method::Pause { command } | Method::Compact { command } => { @@ -2867,14 +2860,21 @@ mod tests { }; let json = serde_json::to_string(&event).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed["event"], "worker_state"); - assert!( - parsed["data"]["snapshot"] - .get("execution_generation") - .is_none() + assert_eq!( + parsed, + serde_json::json!({ + "event": "worker_state", + "data": { + "snapshot": { + "last_command_id": 9, + "state": { + "kind": "busy", + "state": { "kind": "run", "state": "running" } + } + } + } + }) ); - assert!(parsed["data"]["snapshot"].get("revision").is_none()); - assert_eq!(parsed["data"]["snapshot"]["state"]["kind"], "busy"); let decoded: Event = serde_json::from_str(&json).unwrap(); assert!(matches!( diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index fd9158b06..d5758ad75 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -6022,7 +6022,6 @@ pub enum WorkspaceDeletionState { #[serde(rename_all = "snake_case")] pub enum WorkspaceDeletionBlockerKind { LastAccessibleWorkspace, - RevisionConflict, DirtyWorkdir, WorkerRemovalBlocked, WorkdirRemovalBlocked, @@ -11845,7 +11844,6 @@ mod tests { let source: RepositorySource = serde_json::from_value(serde_json::json!({ "kind": "http", "uri": "http://git.example.test/team/project.git", - "revision": 1, })) .unwrap(); @@ -11925,7 +11923,10 @@ mod tests { let decoded: SkillCatalogResponse = serde_json::from_str(&json).expect("deserialize Skill catalog"); assert_eq!(decoded, response); - assert!(!json.contains("\"revision\"")); + assert_eq!( + serde_json::from_str::(&json).unwrap()["projection"], + serde_json::json!({"tree_digest": "tree-digest"}) + ); assert!(!json.contains("\"tree_digest\":null")); } @@ -12200,6 +12201,26 @@ mod tests { #[test] fn workspace_deletion_wire_contract_is_closed_and_typed() { + let categories = serde_json::to_value([ + WorkspaceDeletionBlockerKind::LastAccessibleWorkspace, + WorkspaceDeletionBlockerKind::DirtyWorkdir, + WorkspaceDeletionBlockerKind::WorkerRemovalBlocked, + WorkspaceDeletionBlockerKind::WorkdirRemovalBlocked, + WorkspaceDeletionBlockerKind::RetentionHold, + WorkspaceDeletionBlockerKind::CleanupUnavailable, + ]) + .unwrap(); + assert_eq!( + categories, + serde_json::json!([ + "last_accessible_workspace", + "dirty_workdir", + "worker_removal_blocked", + "workdir_removal_blocked", + "retention_hold", + "cleanup_unavailable" + ]) + ); let preflight = WorkspaceDeletionPreflightResponse { workspace_id: "workspace-test".to_string(), display_name: "Test".to_string(), @@ -12221,7 +12242,7 @@ mod tests { preflight ); let mut stale = value.as_object().unwrap().clone(); - stale.insert("revision".to_string(), serde_json::json!(7)); + stale.insert("unexpected".to_string(), serde_json::json!(7)); assert!( serde_json::from_value::(stale.into()).is_err() ); @@ -12516,16 +12537,14 @@ mod tests { } #[test] - fn repository_response_rejects_stale_field_aliases() { - let stale = serde_json::json!({ + fn repository_response_rejects_unknown_item_fields() { + let valid = serde_json::json!({ "workspace_id": "workspace-test", "items": [{ "repository_key": "main", - "display_name": "main", "kind": "git", "provider": "git", "source": {"kind": "local_path", "uri": "/srv/project"}, - "source_revision": 1, "source_fingerprint": "sha256:test", "observed_status": "ready", "record_authority": "workspace-control-plane" @@ -12533,8 +12552,10 @@ mod tests { "source": "workspace-control-plane", "diagnostics": [] }); - - assert!(serde_json::from_value::(stale).is_err()); + serde_json::from_value::(valid.clone()).unwrap(); + let mut unknown = valid; + unknown["items"][0]["unexpected_state_guard"] = serde_json::json!(1); + assert!(serde_json::from_value::(unknown).is_err()); } #[test] @@ -12657,21 +12678,23 @@ mod tests { }; round_trip(evidence.clone()); let mut stale = serde_json::to_value(evidence).unwrap(); - stale["workspace_identity_revision"] = serde_json::json!(1); + stale["unexpected_state_guard"] = serde_json::json!(1); assert!(serde_json::from_value::(stale).is_err()); round_trip(RevokeRuntimeTrustKeyRequest { expected_binding_id: "binding-observed".into(), }); assert!( serde_json::from_value::(serde_json::json!({ - "expected_revision": 1 + "expected_binding_id": "binding-observed", + "unexpected_state_guard": 1 })) .is_err() ); assert!( serde_json::from_value::(serde_json::json!({ "operation_id": "remove-binding", - "expected_binding_revision": 1 + "expected_binding_id": "binding-observed", + "unexpected_state_guard": 1 })) .is_err() ); @@ -12683,6 +12706,14 @@ mod tests { #[test] fn repository_key_mutations_require_observed_key_fingerprints() { + round_trip(DeleteRepositorySshCredentialRequest { + operation_id: "delete-key".into(), + expected_public_key_fingerprint: "sha256:observed-key".into(), + }); + round_trip(DeleteRepositorySshHostTrustRequest { + operation_id: "delete-trust".into(), + expected_fingerprint: "sha256:observed-host-key".into(), + }); round_trip(RotateRepositorySshCredentialRequest { operation_id: "rotate-key".into(), expected_public_key_fingerprint: "sha256:observed-key".into(), @@ -12700,14 +12731,16 @@ mod tests { assert!( serde_json::from_value::(serde_json::json!({ "operation_id": "delete-key", - "expected_revision": 1 + "expected_public_key_fingerprint": "sha256:observed-key", + "unexpected_state_guard": 1 })) .is_err() ); assert!( serde_json::from_value::(serde_json::json!({ "operation_id": "delete-trust", - "expected_revision": 1 + "expected_fingerprint": "sha256:observed-host-key", + "unexpected_state_guard": 1 })) .is_err() ); @@ -12763,7 +12796,7 @@ mod tests { assert!(output.contains("export type WorkspaceConfigTreeResponse =")); assert!(output.contains("export type ConfigCommitRequest =")); assert!(output.contains("export type ProfileSettingsResponse =")); - assert!(!output.contains("config_revision")); + assert!(output.contains("base_digest: string")); assert!(output.contains("provenance: WorkspaceProfileSourceProvenance")); assert!(output.contains( "export type WorkspaceProfileSourceProvenance = \"project_profile_source_tree\"" diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index c8acacd6c..b5ccb41e0 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -314,11 +314,13 @@ mod tests { let old = r#"{"kind":"subject_behavior_refresh","body":"exact archived body","behavior_revision":7}"#; let item: SystemItem = serde_json::from_str(old).unwrap(); assert_eq!(item.history_text(), "exact archived body"); - assert!( - serde_json::to_value(item) - .unwrap() - .get("behavior_revision") - .is_none() + assert_eq!( + serde_json::to_value(item).unwrap(), + serde_json::json!({ + "kind": "subject_behavior_refresh", + "body": "exact archived body", + "behavior_md": "" + }) ); } diff --git a/crates/standalone/src/job_store/legacy_migrations.rs b/crates/standalone/src/job_store/legacy_migrations.rs index 30c85827f..ec05286cf 100644 --- a/crates/standalone/src/job_store/legacy_migrations.rs +++ b/crates/standalone/src/job_store/legacy_migrations.rs @@ -103,7 +103,8 @@ mod tests { let mut original = serde_json::to_value(request("done")).unwrap(); original["input_revision"] = json!("old-token"); let original = serde_json::to_string_pretty(&original).unwrap(); - let receipt = "{\"accepted\":true,\"input_revision\":\"receipt-domain-data\"}".to_string(); + // A Job result is opaque domain data, not a legacy attempt binding. + let receipt = "{\"accepted\":true,\"receipt_tag\":\"receipt-domain-data\"}".to_string(); let digest = job::result_digest( &serde_json::from_str(&receipt).unwrap(), job::ABSOLUTE_MAX_RESULT_BYTES, diff --git a/crates/standalone/src/store.rs b/crates/standalone/src/store.rs index e7ba2984c..6b2137ee8 100644 --- a/crates/standalone/src/store.rs +++ b/crates/standalone/src/store.rs @@ -426,6 +426,10 @@ impl StandaloneWorkerStore { // The create-new commit marker excludes other writers across this // comparison and replacement. Compare the observed metadata itself, // including its manifest, pointer and lifecycle state, not a counter. + // Before replacing the file, reject an update based on metadata from + // before another writer stopped/resumed the Worker or changed its + // session pointer. Those changes may share a timestamp; arrival-order + // wins would silently restore an old lifecycle/pointer. if serde_json::to_value(¤t).map_err(StandaloneStoreError::Json)? != serde_json::to_value(expected).map_err(StandaloneStoreError::Json)? { @@ -961,7 +965,7 @@ permission = "write" write_worker_record(&mut encoded, &migrated).unwrap(); let saved: serde_json::Value = serde_json::from_slice(&encoded).unwrap(); assert!(saved.get("revision").is_none()); - assert_eq!(saved["schema_version"], 2); + assert_eq!(saved["schema_version"], SCHEMA_VERSION); let resumed = store .update_active_pointer( &migrated, @@ -974,7 +978,7 @@ permission = "write" ) .unwrap(); assert!(disk.get("revision").is_none()); - assert_eq!(disk["schema_version"], 2); + assert_eq!(disk["schema_version"], SCHEMA_VERSION); assert_eq!(resumed.subject, record.subject); assert_eq!(resumed.active_segment_id, record.active_segment_id); } diff --git a/crates/subjektiv/src/legacy_migrations.rs b/crates/subjektiv/src/legacy_migrations.rs index 783179bff..f20e21694 100644 --- a/crates/subjektiv/src/legacy_migrations.rs +++ b/crates/subjektiv/src/legacy_migrations.rs @@ -187,7 +187,7 @@ WHEN EXISTS ( AND memory_id = NEW.memory_id AND revision = NEW.revision ) BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is sealed'); + SELECT RAISE(ABORT, 'subjektiv memory history evidence is sealed'); END; CREATE TRIGGER memory_revision_derivations_no_late_insert BEFORE INSERT ON memory_revision_derivations @@ -220,11 +220,11 @@ END; CREATE TRIGGER memory_revision_seals_no_update BEFORE UPDATE ON memory_revision_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv revision seals are immutable'); + SELECT RAISE(ABORT, 'subjektiv memory history seals are immutable'); END; CREATE TRIGGER memory_revision_seals_no_delete BEFORE DELETE ON memory_revision_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv revision seals are retained'); + SELECT RAISE(ABORT, 'subjektiv memory history seals are retained'); END; CREATE TRIGGER staging_resolution_seals_no_update BEFORE UPDATE ON staging_resolution_seals BEGIN @@ -245,19 +245,19 @@ END; CREATE TRIGGER memory_revisions_no_update BEFORE UPDATE ON memory_revisions BEGIN - SELECT RAISE(ABORT, 'subjektiv memory revisions are immutable'); + SELECT RAISE(ABORT, 'subjektiv memory history entries are immutable'); END; CREATE TRIGGER memory_revisions_no_delete BEFORE DELETE ON memory_revisions BEGIN - SELECT RAISE(ABORT, 'subjektiv memory revisions are retained'); + SELECT RAISE(ABORT, 'subjektiv memory history entries are retained'); END; CREATE TRIGGER memory_revision_candidates_no_update BEFORE UPDATE ON memory_revision_candidates BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is immutable'); + SELECT RAISE(ABORT, 'subjektiv memory history evidence is immutable'); END; CREATE TRIGGER memory_revision_candidates_no_delete BEFORE DELETE ON memory_revision_candidates BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is retained'); + SELECT RAISE(ABORT, 'subjektiv memory history evidence is retained'); END; CREATE TRIGGER memory_revision_derivations_no_update BEFORE UPDATE ON memory_revision_derivations BEGIN @@ -858,7 +858,8 @@ mod tests { subject_id: String, body_md: String, memory_refs: Vec, - built_from_store_revision: u64, + #[serde(rename = "built_from_store_revision")] + legacy_memory_counter: u64, created_at: String, } static V1_MIGRATIONS: &[FeatureMigration] = &[FeatureMigration::new( @@ -983,7 +984,7 @@ mod tests { subject_id: subject_id.clone(), body_md: "Legacy summary without generation-policy evidence.".into(), memory_refs: Vec::new(), - built_from_store_revision: 0, + legacy_memory_counter: 0, created_at: now(), }; store @@ -1010,7 +1011,7 @@ mod tests { params![ subject_id, snapshot.id, - to_i64(snapshot.built_from_store_revision)?, + to_i64(snapshot.legacy_memory_counter)?, serde_json::to_string(&snapshot)?, snapshot.created_at ], diff --git a/crates/subjektiv/src/lib.rs b/crates/subjektiv/src/lib.rs index 136ef7266..b461e8d8a 100644 --- a/crates/subjektiv/src/lib.rs +++ b/crates/subjektiv/src/lib.rs @@ -1628,6 +1628,10 @@ impl SubjektivStore { "unknown generation `{generation_id}` for subject `{subject_id}`" )) })?; + // Before publication, compare the generation's frozen Memory input + // with live input in this same IMMEDIATE transaction. An older job + // can finish after Memory changes; first/last arrival and timestamps + // cannot make its summary describe the newer input. if current_fingerprint != memory_fingerprint { return Err(SubjektivError::SurfaceGenerationConflict(format!( "generation input {memory_fingerprint} is stale; current Memory input fingerprint is {}", @@ -2167,6 +2171,10 @@ fn write_memory_change( .optional()?; let current = current_raw.map(|raw| parse_memory(&raw)).transpose()?; + // Compare the observed Memory change before appending history or replacing + // current state, under this same IMMEDIATE transaction. A late decision based + // on old content must not overwrite a newer accepted change. Timestamps can + // tie, and arrival order does not prove which Memory the decision examined. let (change_id, created_at) = match (current.as_ref(), expected_change_id) { (None, None) => (issued_id("memory-change"), now()), (None, Some(_)) => return Err(SubjektivError::MemoryNotFound(memory_id.to_string())), diff --git a/crates/ticket/src/lib.rs b/crates/ticket/src/lib.rs index 5d0be3af2..ee50132b9 100644 --- a/crates/ticket/src/lib.rs +++ b/crates/ticket/src/lib.rs @@ -2523,6 +2523,10 @@ impl SqliteTicketBackend { // Exact recorded replay is independent of today's live role/assignment. // Parent transport authenticates identity before restoring receipt context. self.require_active_source_assignment(conn, ticket_id)?; + // Compare content and workflow state before any state/audit write, using + // the snapshot read in this same write transaction. Acceptance criteria + // may change without a state change, so a late completion must not win + // solely by arrival order. Equal timestamps do not prove equal content. let actual = ticket_content_digest(&previous); if actual != request.expected_content_digest { return Err(TicketError::StaleContent { diff --git a/crates/worker-runtime/src/fs_store.rs b/crates/worker-runtime/src/fs_store.rs index c0fcdec54..dba335e13 100644 --- a/crates/worker-runtime/src/fs_store.rs +++ b/crates/worker-runtime/src/fs_store.rs @@ -3125,11 +3125,9 @@ mod tests { }}); migrate_legacy_request_evidence(&mut document).unwrap(); let request = &document["request"]; - assert_eq!(request["memory_settings"]["language"], "English"); - assert!( - request["memory_settings"] - .get("settings_revision") - .is_none() + assert_eq!( + request["memory_settings"], + serde_json::json!({"workspace_id": "workspace-a", "language": "English"}) ); assert_eq!( request["repository"]["source_fingerprint"], diff --git a/crates/worker-runtime/src/main.rs b/crates/worker-runtime/src/main.rs index cad90cdbd..08cdb7e64 100644 --- a/crates/worker-runtime/src/main.rs +++ b/crates/worker-runtime/src/main.rs @@ -640,6 +640,9 @@ struct RuntimeAuthFile { identity: Option, #[serde(default)] workspace_issuers: Vec, + /// Frozen pre-ID issuer records, retained as TOML history only, never as authority. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + archived_legacy_issuers: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -938,6 +941,7 @@ fn read_runtime_auth_file(path: &Path) -> Result let mut document: toml::Value = toml::from_str(&contents) .map_err(|_| ProcessError::auth("runtime auth store is corrupt"))?; let mut migrated = false; + let mut archived_legacy_issuers = Vec::new(); if let Some(records) = document .get_mut("workspace_issuers") .and_then(toml::Value::as_array_mut) @@ -951,6 +955,9 @@ fn read_runtime_auth_file(path: &Path) -> Result legacy, ) .map_err(|_| ProcessError::auth("runtime legacy trust store is corrupt"))?; + archived_legacy_issuers.push(toml::to_string(&*value).map_err(|_| { + ProcessError::auth("runtime auth history serialization failed") + })?); *value = toml::Value::try_from(record).map_err(|_| { ProcessError::auth("runtime auth migration serialization failed") })?; @@ -958,9 +965,10 @@ fn read_runtime_auth_file(path: &Path) -> Result } } } - let auth: RuntimeAuthFile = document + let mut auth: RuntimeAuthFile = document .try_into() .map_err(|_| ProcessError::auth("runtime auth store is corrupt"))?; + auth.archived_legacy_issuers.extend(archived_legacy_issuers); validate_workspace_issuer_trust_records(&auth.workspace_issuers) .map_err(|_| ProcessError::auth("runtime Workspace issuer trust store is corrupt"))?; if migrated { @@ -1637,6 +1645,54 @@ mod tests { assert_eq!(error, "invalid trust-workspace list arguments"); } + #[test] + fn pre_id_issuer_migration_retains_history_without_restoring_authority() { + use sha2::{Digest as _, Sha256}; + + let identity = RuntimeIdentityMaterial::generate("WK-legacy").unwrap(); + let public_key = decode_public_key(&identity.public_key).unwrap(); + let fingerprint = format!( + "sha256:{}", + Sha256::digest(public_key) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + ); + for state in ["active", "revoked"] { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("runtime-auth.toml"); + // These frozen field spellings prove the persisted pre-ID boundary. + let legacy = serde_json::json!({ + "workspace_id": "workspace-1", + "backend_url": "https://backend.example.test", + "key_id": "WK-legacy", + "algorithm": "ed25519", + "public_key": identity.public_key, + "public_key_fingerprint": fingerprint, + "identity_revision": 7, + "trust_generation": 9, + "state": state, + "registered_at_unix": 10, + "updated_at_unix": 20, + }); + let original_record = toml::Value::try_from(&legacy).unwrap(); + let document = serde_json::json!({"workspace_issuers": [legacy]}); + std::fs::write(&path, toml::to_string(&document).unwrap()).unwrap(); + + let migrated = read_runtime_auth_file(&path).unwrap(); + assert_eq!( + migrated.workspace_issuers[0].state, + worker_runtime::workspace_issuer::WorkspaceIssuerTrustState::Revoked + ); + assert_eq!(migrated.archived_legacy_issuers.len(), 1); + let archived: toml::Value = + toml::from_str(&migrated.archived_legacy_issuers[0]).unwrap(); + assert_eq!(archived, original_record); + write_runtime_auth_file(&path, &migrated).unwrap(); + assert_eq!(read_runtime_auth_file(&path).unwrap(), migrated); + } + } + #[test] fn oversized_runtime_auth_store_fails_closed_before_parsing() { let temp = tempfile::tempdir().unwrap(); @@ -1691,6 +1747,7 @@ mod tests { &RuntimeAuthFile { identity: Some(identity), workspace_issuers: Vec::new(), + archived_legacy_issuers: Vec::new(), }, ) .unwrap(); diff --git a/crates/worker-runtime/src/worker_source.rs b/crates/worker-runtime/src/worker_source.rs index d847f2103..e18692d89 100644 --- a/crates/worker-runtime/src/worker_source.rs +++ b/crates/worker-runtime/src/worker_source.rs @@ -1480,7 +1480,6 @@ mod tests { assert!(request.starts_with("POST /api/w/workspace-a/workers/remove HTTP/1.1")); assert!(request.contains("\"target_runtime_id\":\"runtime-target\"")); assert!(request.contains("\"target_worker_id\":\"worker-target\"")); - assert!(!request.contains("expected_worker_revision")); assert!(request.contains("\"reason\":\"retire obsolete Worker\"")); let request_source_token = request .lines() diff --git a/crates/worker-runtime/src/working_directory.rs b/crates/worker-runtime/src/working_directory.rs index 3e7781d3c..956d1c181 100644 --- a/crates/worker-runtime/src/working_directory.rs +++ b/crates/worker-runtime/src/working_directory.rs @@ -62,8 +62,9 @@ pub struct WorkingDirectoryEvidence { pub host_key_fingerprint: Option, } -// Frozen persisted evidence reader. Legacy counters only mark that SSH -// access is required; they are never converted to key identity or grants. +// Frozen persisted evidence reader. At decode time, reject zero legacy counters +// and use their presence only to require fresh SSH access before session opening. +// They never identify keys or grant access; live authority checks fingerprints. #[derive(Deserialize)] struct FrozenEvidenceRead { repository_id: String, @@ -76,19 +77,21 @@ struct FrozenEvidenceRead { public_key_fingerprint: Option, host_key_fingerprint: Option, repository_access_required: Option, - credential_revision: Option, - host_trust_revision: Option, + #[serde(rename = "credential_revision")] + legacy_credential_counter: Option, + #[serde(rename = "host_trust_revision")] + legacy_host_trust_counter: Option, } impl TryFrom for WorkingDirectoryEvidence { type Error = &'static str; fn try_from(wire: FrozenEvidenceRead) -> Result { - if wire.credential_revision == Some(0) || wire.host_trust_revision == Some(0) { + if wire.legacy_credential_counter == Some(0) || wire.legacy_host_trust_counter == Some(0) { return Err("legacy SSH evidence contains an invalid authority marker"); } let repository_access_required = wire.repository_access_required.unwrap_or(false) - || wire.credential_revision.is_some() - || wire.host_trust_revision.is_some() + || wire.legacy_credential_counter.is_some() + || wire.legacy_host_trust_counter.is_some() || wire.public_key_fingerprint.is_some() || wire.host_key_fingerprint.is_some(); Ok(Self { @@ -3284,6 +3287,8 @@ mod tests { let evidence = record["working_directory"]["evidence"] .as_object_mut() .unwrap(); + let mut expected_evidence = evidence.clone(); + expected_evidence.insert("repository_access_required".into(), serde_json::json!(true)); evidence.remove("repository_access_required"); evidence.insert("credential_revision".into(), serde_json::json!(3)); evidence.insert("host_trust_revision".into(), serde_json::json!(2)); @@ -3299,10 +3304,7 @@ mod tests { let migrated: serde_json::Value = serde_json::from_slice(&fs::read(&record_path).unwrap()).unwrap(); let evidence = &migrated["working_directory"]["evidence"]; - assert_eq!(evidence["repository_access_required"], true); - assert!(evidence.get("credential_revision").is_none()); - assert!(evidence.get("host_trust_revision").is_none()); - assert!(evidence.get("public_key_fingerprint").is_none()); + assert_eq!(evidence, &serde_json::json!(expected_evidence)); drop(materializer); assert_eq!( RuntimeGitMaterializer::new(runtime.path()) @@ -4771,8 +4773,8 @@ mod tests { .join(MATERIALIZATION_RECORD); let mut legacy: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); - legacy["working_directory"]["evidence"]["repository_source_revision"] = - serde_json::json!(7); + let expected_evidence = legacy["working_directory"]["evidence"].clone(); + legacy["working_directory"]["evidence"]["obsolete_test_attribute"] = serde_json::json!(7); let original = serde_json::to_vec_pretty(&legacy).unwrap(); fs::write(&path, &original).unwrap(); @@ -4788,10 +4790,9 @@ mod tests { materializer.read_binding(id).unwrap(); let canonical: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); - assert!( - canonical["working_directory"]["evidence"] - .get("repository_source_revision") - .is_none() + assert_eq!( + canonical["working_directory"]["evidence"], + expected_evidence ); materializer.cleanup_working_directory(id).unwrap(); assert!(!binding.working_directory_root().exists()); diff --git a/crates/worker-runtime/src/workspace_issuer.rs b/crates/worker-runtime/src/workspace_issuer.rs index f0204dc49..53d706680 100644 --- a/crates/worker-runtime/src/workspace_issuer.rs +++ b/crates/worker-runtime/src/workspace_issuer.rs @@ -92,21 +92,25 @@ struct LegacyWorkspaceIssuerTrustRecord { algorithm: String, public_key: String, public_key_fingerprint: String, - identity_revision: u64, - trust_generation: u64, + #[serde(rename = "identity_revision")] + legacy_identity_counter: u64, + #[serde(rename = "trust_generation")] + legacy_trust_counter: u64, state: WorkspaceIssuerTrustState, registered_at_unix: i64, updated_at_unix: i64, } -/// Migrate a pre-ID enrollment as revoked. Only an explicit operator replacement -/// can accept new trust; no legacy capability can authorize its own migration. +/// Migrate a pre-ID enrollment as revoked. At persisted-data decode, legacy counters +/// must be positive but are never compared with current identity or trust. +/// Only an explicit operator replacement can accept new trust; no legacy +/// capability can authorize its own migration. pub fn migrate_legacy_workspace_issuer_trust_record( value: serde_json::Value, ) -> Result { let legacy: LegacyWorkspaceIssuerTrustRecord = serde_json::from_value(value).map_err(|_| WorkspaceIssuerTrustError::InvalidIdentifier)?; - if legacy.identity_revision == 0 || legacy.trust_generation == 0 { + if legacy.legacy_identity_counter == 0 || legacy.legacy_trust_counter == 0 { return Err(WorkspaceIssuerTrustError::InvalidIdentifier); } let mut record = WorkspaceIssuerTrustRecord::from_bundle( @@ -474,6 +478,9 @@ struct WorkspaceRuntimeVerificationDocument { records: Vec, #[serde(default)] pending: Vec, + /// Exact pre-ID document retained only as history; never used to authorize requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + archived_legacy_document: Option, } // Frozen verification evidence from before trust/binding IDs. Never promote @@ -490,12 +497,16 @@ struct LegacyVerificationDocument { struct LegacyVerificationRecord { workspace_id: String, runtime_id: String, - binding_revision: u64, + #[serde(rename = "binding_revision")] + legacy_binding_counter: u64, workspace_key_id: String, - workspace_identity_revision: u64, - workspace_trust_generation: u64, + #[serde(rename = "workspace_identity_revision")] + legacy_workspace_identity_counter: u64, + #[serde(rename = "workspace_trust_generation")] + legacy_workspace_trust_counter: u64, runtime_public_key_fingerprint: String, - runtime_identity_revision: u64, + #[serde(rename = "runtime_identity_revision")] + legacy_runtime_identity_counter: u64, verified_at: i64, } @@ -507,6 +518,9 @@ impl FileWorkspaceRuntimeVerificationAuthority { } } + // No pre-ID acknowledgement is live authority. Keep its exact source bytes + // in the persisted archive while new challenges populate only current records. + // Neither read(), get(), nor capability verification consults that archive. fn read( &self, ) -> Result { @@ -531,6 +545,9 @@ impl FileWorkspaceRuntimeVerificationAuthority { version: 2, records: Vec::new(), pending: Vec::new(), + archived_legacy_document: Some(String::from_utf8(bytes).map_err(|_| { + WorkspaceCapabilityVerificationError::VerificationAuthorityUnavailable + })?), }; self.write(&migrated)?; return Ok(migrated); @@ -575,6 +592,7 @@ impl FileWorkspaceRuntimeVerificationAuthority { version: 2, records: Vec::new(), pending: Vec::new(), + archived_legacy_document: None, }) } Err(_) => Err(WorkspaceCapabilityVerificationError::VerificationAuthorityUnavailable), @@ -1993,7 +2011,7 @@ mod tests { } #[test] - fn frozen_verification_file_is_invalidated_durably() { + fn frozen_verification_file_is_invalidated_durably_without_losing_archived_evidence() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("verification.json"); fs::write(&path, serde_json::to_vec(&serde_json::json!({"version":1,"records":[{ @@ -2001,18 +2019,50 @@ mod tests { "workspace_key_id":"WK-1","workspace_identity_revision":1,"workspace_trust_generation":1, "runtime_public_key_fingerprint":"sha256:old","runtime_identity_revision":1,"verified_at":10 }]})).unwrap()).unwrap(); + let original_bytes = fs::read(&path).unwrap(); let authority = FileWorkspaceRuntimeVerificationAuthority::new(&path); assert!(authority.get("workspace-1", "runtime-1").unwrap().is_none()); let migrated: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); assert_eq!(migrated["version"], 2); assert_eq!(migrated["records"], serde_json::json!([])); + assert_eq!( + migrated["archived_legacy_document"] + .as_str() + .unwrap() + .as_bytes(), + original_bytes + ); assert!( FileWorkspaceRuntimeVerificationAuthority::new(&path) .get("workspace-1", "runtime-1") .unwrap() .is_none() ); + let current = WorkspaceRuntimeVerificationRecord { + workspace_id: "workspace-1".into(), + runtime_id: "runtime-1".into(), + binding_id: "accepted-binding".into(), + workspace_key_id: "WK-1".into(), + workspace_public_key_fingerprint: format!("sha256:{}", "a".repeat(64)), + workspace_trust_id: "accepted-trust".into(), + runtime_public_key_fingerprint: format!("sha256:{}", "b".repeat(64)), + verified_at: 20, + }; + authority.record(current.clone()).unwrap(); + let restored = FileWorkspaceRuntimeVerificationAuthority::new(&path); + assert_eq!( + restored.get("workspace-1", "runtime-1").unwrap(), + Some(current) + ); + let updated: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + assert_eq!( + updated["archived_legacy_document"] + .as_str() + .unwrap() + .as_bytes(), + original_bytes + ); } #[test] diff --git a/crates/worker/src/feature/builtin/manage_worker.rs b/crates/worker/src/feature/builtin/manage_worker.rs index 7fd67933a..96dab0b7e 100644 --- a/crates/worker/src/feature/builtin/manage_worker.rs +++ b/crates/worker/src/feature/builtin/manage_worker.rs @@ -2111,15 +2111,7 @@ mod tests { for field in ["runtime_id", "worker_id", "reason"] { assert!(schema.contains(field)); } - for forbidden in [ - "expected_worker_revision", - "proof", - "actor", - "workspace_id", - "policy", - "plan", - "stage", - ] { + for forbidden in ["proof", "actor", "workspace_id", "policy", "plan", "stage"] { assert!(!schema.contains(forbidden), "schema leaked {forbidden}"); } } @@ -2149,23 +2141,28 @@ mod tests { .await .unwrap_err(); } - let _error = tool - .execute( - &serde_json::json!({ - "subject": { - "kind": "runtime_worker", - "runtime_id": "runtime-1", - "worker_id": "worker-7", - }, - "expected_worker_revision": "revision-1", - "reason": "retire", - "source_proof": "caller-controlled" - }) - .to_string(), - ToolExecutionContext::new("call-spoof", "batch-remove", 0), - ) - .await - .unwrap_err(); + // Unknown authority fields must be rejected individually, before mutation. + for (field, value) in [ + ("unexpected_state_guard", "caller-controlled"), + ("source_proof", "caller-controlled"), + ] { + let mut input = serde_json::json!({ + "subject": { + "kind": "runtime_worker", + "runtime_id": "runtime-1", + "worker_id": "worker-7", + }, + "reason": "retire", + }); + input[field] = serde_json::json!(value); + let _error = tool + .execute( + &input.to_string(), + ToolExecutionContext::new("call-spoof", "batch-remove", 0), + ) + .await + .unwrap_err(); + } assert!(client.removals.lock().unwrap().is_empty()); } diff --git a/crates/worker/src/feature/builtin/resource_projection.rs b/crates/worker/src/feature/builtin/resource_projection.rs index d7feb5043..e249066b4 100644 --- a/crates/worker/src/feature/builtin/resource_projection.rs +++ b/crates/worker/src/feature/builtin/resource_projection.rs @@ -841,7 +841,7 @@ mod tests { let projected = project_ticket_detail(json!({ "id": "internal-ticket", "resource_key": "T-588", - "content_digest": "revision-1", + "content_digest": "ticket-content-digest", "title": "Queued Submit", "body": "Body", "state": "planning", @@ -892,7 +892,7 @@ mod tests { .expect("current Ticket blocker shape must project"); let projected = serde_json::to_value(projected).expect("serialize Ticket detail"); - assert_eq!(projected["content_digest"], "revision-1"); + assert_eq!(projected["content_digest"], "ticket-content-digest"); assert_eq!( projected["relations"]["blockers"], json!([{ @@ -944,7 +944,7 @@ mod tests { }); let projected = project_ticket_detail(json!({ "resource_key": "T-716", - "content_digest": "current-ticket-revision", + "content_digest": "current-ticket-content-digest", "title": "Source evidence", "body": "Body", "state": if merged { "done" } else { "inprogress" }, @@ -982,7 +982,7 @@ mod tests { Some(&json!(integration_error)) ); } - assert_eq!(projected["content_digest"], "current-ticket-revision"); + assert_eq!(projected["content_digest"], "current-ticket-content-digest"); assert_eq!(projected["evidence"]["review_after_rescope"], attested); assert_eq!(projected["evidence"]["complete_for_integration"], merged); } diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index 73beccb1c..a1901a27d 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -806,7 +806,6 @@ mod tests { "MergeRequestReview", "MergeRequestReadinessCheck", "MergeRequestComplete", - "MergeRequestAddRevision", ] { assert!( !combined.contains(stale_operation), diff --git a/crates/worker/src/shared_state.rs b/crates/worker/src/shared_state.rs index a9106e7e7..3f19aca1a 100644 --- a/crates/worker/src/shared_state.rs +++ b/crates/worker/src/shared_state.rs @@ -393,9 +393,16 @@ mod tests { ))); let parsed: serde_json::Value = serde_json::from_str(&state.status_json()).unwrap(); assert_eq!(parsed["state"], "running"); - assert!(parsed["worker_state"].get("execution_generation").is_none()); - assert!(parsed["worker_state"].get("revision").is_none()); - assert_eq!(parsed["worker_state"]["state"]["kind"], "busy"); + assert_eq!( + parsed["worker_state"], + json!({ + "last_command_id": 0, + "state": { + "kind": "busy", + "state": { "kind": "maintenance", "state": "compacting" } + } + }) + ); assert_eq!(parsed["worker_name"], "test-worker"); assert!(parsed["segment_id"].is_string()); } diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 7218c3d76..051b1e64a 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -10793,7 +10793,7 @@ mod build_summary_prompt_tests { let binding = crate::BackendJobExecutionBinding { job_id: "job-1".into(), attempt_id: "attempt-1".into(), - input_digest: Some("revision-1".into()), + input_digest: Some("input-content-digest".into()), subjektiv_consolidation: true, }; worker.bind_backend_job(binding.clone()).unwrap(); @@ -14244,7 +14244,6 @@ permission = "write" _ => None, }) .unwrap(); - assert!(payload.get("revision").is_none()); let restored: PendingActivationState = serde_json::from_value(payload.clone()).unwrap(); assert_eq!(restored.snapshot(), cleared); } diff --git a/crates/workspace-drive/src/lib.rs b/crates/workspace-drive/src/lib.rs index 4be8aa41e..c1551aa1f 100644 --- a/crates/workspace-drive/src/lib.rs +++ b/crates/workspace-drive/src/lib.rs @@ -690,6 +690,9 @@ fn directory(c: &Connection, id: NodeId) -> Result<()> { // Called only inside the same IMMEDIATE transaction that publishes the mutation. // Compare the last successful request, not time: even identical bytes or a move // away and back cannot revive an old request. Receipts prevent request ID reuse. +// A queued update/delete based on an older observation must not overwrite/delete +// a newer commit merely because it arrives last; same-ms commits also rule out +// timestamp comparison. The check precedes blob writes and node publication. fn changeable(c: &Transaction<'_>, id: NodeId, expected_mutation_id: &str) -> Result { let n = node(c, id)?; if n.parent_id.is_none() { diff --git a/crates/workspace-drive/src/migrations.rs b/crates/workspace-drive/src/migrations.rs index 3d57d73f8..cf28d1c27 100644 --- a/crates/workspace-drive/src/migrations.rs +++ b/crates/workspace-drive/src/migrations.rs @@ -12,6 +12,7 @@ pub(super) static MIGRATIONS: &[FeatureMigration] = &[ "bind_nodes_to_committed_requests", bind_committed_requests, ), + FeatureMigration::new(3, "name_legacy_replay_counter", name_legacy_replay_counter), ]; fn create_original_schema(tx: &Transaction<'_>) -> feature_storage::Result<()> { tx.execute_batch("\ @@ -61,7 +62,7 @@ fn bind_committed_requests(tx: &Transaction<'_>) -> feature_storage::Result<()> WHERE json_extract(result_json, '$.deleted') = 0 GROUP BY 1,2; CREATE TABLE drive_legacy_replays ( request_id TEXT PRIMARY KEY REFERENCES drive_receipts(request_id) ON DELETE CASCADE, - expected_mutation_id TEXT NOT NULL, expected_revision TEXT NOT NULL); + expected_mutation_id TEXT NOT NULL, legacy_expected_counter TEXT NOT NULL); INSERT INTO drive_legacy_replays SELECT receipt.request_id, observed.request_id, CAST(observed.previous_counter AS TEXT) FROM drive_receipts AS receipt JOIN drive_observed_requests AS observed @@ -85,6 +86,23 @@ fn bind_committed_requests(tx: &Transaction<'_>) -> feature_storage::Result<()> Ok(()) } +fn name_legacy_replay_counter(tx: &Transaction<'_>) -> feature_storage::Result<()> { + // Already-deployed v2 databases used this column name. New v2 applications + // create the concrete name directly; both paths retain the same replay-only + // decimal strings and leave the original request digests untouched. + let old_column: bool = tx.query_row( + "SELECT EXISTS(SELECT 1 FROM pragma_table_info('drive_legacy_replays') WHERE name='expected_revision')", + [], + |row| row.get(0), + )?; + if old_column { + tx.execute_batch( + "ALTER TABLE drive_legacy_replays RENAME COLUMN expected_revision TO legacy_expected_counter;", + )?; + } + Ok(()) +} + // Frozen pre-cutover typed payload encoding, including field order and decimal // strings. Never reconstruct a fingerprint from the result: compare the caller's // complete actor/payload against the original digest. Historical counters below @@ -94,19 +112,22 @@ fn bind_committed_requests(tx: &Transaction<'_>) -> feature_storage::Result<()> enum LegacyMutation<'a> { Update { id: NodeId, - expected_revision: &'a str, + #[serde(rename = "expected_revision")] + legacy_expected_counter: &'a str, content_type: &'a str, bytes: &'a [u8], }, Relocate { id: NodeId, - expected_revision: &'a str, + #[serde(rename = "expected_revision")] + legacy_expected_counter: &'a str, parent: NodeId, name: &'a str, }, Delete { id: NodeId, - expected_revision: &'a str, + #[serde(rename = "expected_revision")] + legacy_expected_counter: &'a str, }, } @@ -119,7 +140,7 @@ pub(super) fn matches_legacy_replay( ) -> crate::Result { let evidence = tx .query_row( - "SELECT expected_mutation_id,expected_revision FROM drive_legacy_replays WHERE request_id=?1", + "SELECT expected_mutation_id,legacy_expected_counter FROM drive_legacy_replays WHERE request_id=?1", [request_id], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)), ) @@ -139,7 +160,7 @@ pub(super) fn matches_legacy_replay( expected_mutation_id, LegacyMutation::Update { id: *id, - expected_revision: &counter, + legacy_expected_counter: &counter, content_type, bytes, }, @@ -153,7 +174,7 @@ pub(super) fn matches_legacy_replay( expected_mutation_id, LegacyMutation::Relocate { id: *id, - expected_revision: &counter, + legacy_expected_counter: &counter, parent: *parent, name, }, @@ -165,7 +186,7 @@ pub(super) fn matches_legacy_replay( expected_mutation_id, LegacyMutation::Delete { id: *id, - expected_revision: &counter, + legacy_expected_counter: &counter, }, ), Mutation::CreateFolder { .. } | Mutation::CreateFile { .. } => return Ok(false), @@ -254,7 +275,10 @@ mod tests { let storage = FeatureStorage::new(&path).workspace("ws").unwrap(); let registration = Drive::register(&storage).unwrap(); let drive = Drive::open(&storage, ®istration, &temp.path().join("blobs")).unwrap(); - assert_eq!(drive.database.schema_version().unwrap(), 2); + assert_eq!( + drive.database.schema_version().unwrap(), + MIGRATIONS.last().unwrap().version() + ); assert_eq!(drive.root().unwrap().last_mutation_id, ""); let file = drive.metadata(NodeId(4)).unwrap(); assert_eq!(file.last_mutation_id, "file-create"); @@ -394,6 +418,119 @@ mod tests { .unwrap() } + #[test] + fn fresh_schema_uses_only_the_concrete_replay_counter_column() { + let temp = tempfile::tempdir().unwrap(); + let storage = FeatureStorage::new(temp.path().join("metadata")) + .workspace("ws") + .unwrap(); + let registration = Drive::register(&storage).unwrap(); + let drive = Drive::open(&storage, ®istration, &temp.path().join("blobs")).unwrap(); + assert_current_replay_schema(&drive.database); + } + + fn assert_current_replay_schema(database: &feature_storage::FeatureDatabase) { + assert_eq!( + database.schema_version().unwrap(), + MIGRATIONS.last().unwrap().version() + ); + database + .try_with_connection::<_, feature_storage::FeatureStorageError>(|c| { + let mut columns = c.prepare( + "SELECT name FROM pragma_table_info('drive_legacy_replays') ORDER BY cid", + )?; + let names = columns + .query_map([], |row| row.get::<_, String>(0))? + .collect::>>()?; + assert_eq!( + names, + [ + "request_id", + "expected_mutation_id", + "legacy_expected_counter" + ] + ); + Ok(()) + }) + .unwrap(); + } + + #[test] + fn v2_replay_column_upgrade_preserves_evidence_digests_and_replay_after_reopen() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("metadata"); + seed_update(&path, false); + let original = { + let storage = FeatureStorage::new(&path).workspace("ws").unwrap(); + let registration = storage + .register(FeatureRegistration::new( + "workspace-drive", + &MIGRATIONS[..2], + )) + .unwrap(); + let database = storage.open(®istration).unwrap(); + // Actual deployed v2 schema: the old name is a frozen DB boundary, + // not the DDL used when applying v2 to a newly created database. + database + .transaction(|tx| { + tx.execute_batch( + "ALTER TABLE drive_legacy_replays RENAME COLUMN legacy_expected_counter TO expected_revision;", + )?; + Ok(()) + }) + .unwrap(); + fingerprints(&database) + }; + let storage = FeatureStorage::new(&path).workspace("ws").unwrap(); + let registration = Drive::register(&storage).unwrap(); + let drive = Drive::open(&storage, ®istration, &temp.path().join("blobs")).unwrap(); + assert_current_replay_schema(&drive.database); + assert_eq!(fingerprints(&drive.database), original); + drive + .database + .try_with_connection::<_, feature_storage::FeatureStorageError>(|c| { + let mut rows = c.prepare( + "SELECT request_id,expected_mutation_id,legacy_expected_counter FROM drive_legacy_replays ORDER BY request_id", + )?; + let evidence = rows + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + })? + .collect::>>()?; + assert_eq!( + evidence, + vec![ + ("delete".into(), "create-deleted".into(), "1".into()), + ("file-update".into(), "file-create".into(), "1".into()), + ("move-0".into(), "create".into(), "1".into()), + ] + ); + Ok(()) + }) + .unwrap(); + let nodes = drive.list(NodeId(1), None, 100).unwrap().nodes; + for (request, mutation) in changed_replays() { + let RequestStatus::Committed { result } = drive.request_status(request).unwrap() else { + panic!("lost {request}") + }; + assert_eq!(drive.mutate(request, "actor", mutation).unwrap(), result); + } + assert_eq!(drive.list(NodeId(1), None, 100).unwrap().nodes, nodes); + assert_eq!(fingerprints(&drive.database), original); + drop(drive); + let drive = Drive::open(&storage, ®istration, &temp.path().join("blobs")).unwrap(); + assert_current_replay_schema(&drive.database); + assert_eq!(fingerprints(&drive.database), original); + assert_eq!(drive.list(NodeId(1), None, 100).unwrap().nodes, nodes); + for (request, mutation) in changed_replays() { + assert!(drive.mutate(request, "actor", mutation).is_ok()); + } + } + #[test] fn upgrade_preserves_original_digests_and_never_uses_old_counters_for_new_mutations() { let temp = tempfile::tempdir().unwrap(); @@ -445,7 +582,10 @@ mod tests { let storage = FeatureStorage::new(&path).workspace("ws").unwrap(); let registration = Drive::register(&storage).unwrap(); let drive = Drive::open(&storage, ®istration, &temp.path().join("blobs")).unwrap(); - assert_eq!(drive.database.schema_version().unwrap(), 2); + assert_eq!( + drive.database.schema_version().unwrap(), + MIGRATIONS.last().unwrap().version() + ); // Historical replay must not overwrite a newer committed mutation. for (id, expected) in [(NodeId(2), "move-0"), (NodeId(4), "file-update")] { drive diff --git a/crates/workspace-drive/src/tests.rs b/crates/workspace-drive/src/tests.rs index e0a506f72..13ff26672 100644 --- a/crates/workspace-drive/src/tests.rs +++ b/crates/workspace-drive/src/tests.rs @@ -20,7 +20,10 @@ fn create(parent: NodeId, name: &str) -> Mutation { fn migration_enforces_workspace_parent_and_unique_names() { let t = tempfile::tempdir().unwrap(); let d = fixture(&t); - assert_eq!(d.database.schema_version().unwrap(), 2); + assert_eq!( + d.database.schema_version().unwrap(), + MIGRATIONS.last().unwrap().version() + ); let root = d.root().unwrap(); d.mutate("a", "actor", create(root.id, "a")).unwrap(); assert!(matches!( diff --git a/crates/workspace-server/src/authority.rs b/crates/workspace-server/src/authority.rs index d3241b528..8f8001af2 100644 --- a/crates/workspace-server/src/authority.rs +++ b/crates/workspace-server/src/authority.rs @@ -174,8 +174,11 @@ impl merge_request::RepositorySource for AuthorityMergeRequestSource { } } -pub trait TicketMergeRevisionSource: Send + Sync { - fn resolve_subject_ref( +/// Resolve an open Merge Request source selector to its provider-published Git commit. +/// Ticket detail/query calls this for open requests only; merged requests use their +/// immutable recorded source/result commits and need no mutable branch observation. +pub trait TicketSourceCommitResolver: Send + Sync { + fn resolve_source_commit( &self, ticket_id: &str, repository_id: &str, @@ -183,10 +186,10 @@ pub trait TicketMergeRevisionSource: Send + Sync { ) -> std::result::Result; } -struct UnresolvedTicketMergeRevisionSource; +struct UnresolvedTicketSourceCommitResolver; -impl TicketMergeRevisionSource for UnresolvedTicketMergeRevisionSource { - fn resolve_subject_ref( +impl TicketSourceCommitResolver for UnresolvedTicketSourceCommitResolver { + fn resolve_source_commit( &self, _ticket_id: &str, _repository_id: &str, @@ -205,7 +208,7 @@ pub struct SqliteWorkspaceAuthority { store: SqliteWorkspaceStore, ticket_backend: SqliteTicketBackend, merge_request_store: Arc, - merge_revision_source: Arc, + source_commit_resolver: Arc, } impl SqliteWorkspaceAuthority { @@ -231,15 +234,15 @@ impl SqliteWorkspaceAuthority { ) .map_err(|error| Error::Store(error.to_string()))?, ), - merge_revision_source: Arc::new(UnresolvedTicketMergeRevisionSource), + source_commit_resolver: Arc::new(UnresolvedTicketSourceCommitResolver), }) } - pub fn with_merge_revision_source( + pub fn with_source_commit_resolver( mut self, - merge_revision_source: Arc, + source_commit_resolver: Arc, ) -> Self { - self.merge_revision_source = merge_revision_source; + self.source_commit_resolver = source_commit_resolver; self } @@ -843,7 +846,7 @@ impl SqliteWorkspaceAuthority { }) }, |selector| { - self.merge_revision_source.resolve_subject_ref( + self.source_commit_resolver.resolve_source_commit( id, &request.repository_id, selector, @@ -1883,7 +1886,9 @@ fn ticket_evidence_summary( server_api::TicketSourceRefObservation::Unavailable { .. } ) }); - // Exact revision/snapshot attestation, not a timestamp heuristic. + // Evaluated on every Ticket detail/query: approval must bind the current Ticket + // content digest and every relevant MR source/result commit. Later timestamps + // or last-arriving approvals cannot attest to different content. let review_after_rescope = approved_current_subject && requirement_approved; let review_status = if !has_merge_request { None @@ -2778,7 +2783,7 @@ mod tests { event_id: "request-1".to_string(), sequence: 1, subject_ref: "commit-1".to_string(), - ticket_content_digest: "revision-1".to_string(), + ticket_content_digest: "ticket-content-1".to_string(), ticket_merge_request_subjects: vec![merge_request::MergeRequestReviewSubject { merge_request_id: "mr-1".to_string(), subject_ref: "commit-1".to_string(), @@ -2792,7 +2797,7 @@ mod tests { sequence: 2, request_event_id: "request-1".to_string(), subject_ref: "commit-1".to_string(), - ticket_content_digest: "revision-1".to_string(), + ticket_content_digest: "ticket-content-1".to_string(), ticket_merge_request_subjects: vec![merge_request::MergeRequestReviewSubject { merge_request_id: "mr-1".to_string(), subject_ref: "commit-1".to_string(), @@ -2884,7 +2889,7 @@ mod tests { fn project_evidence( requests: &[MergeRequest], - revision: &str, + content_digest: &str, observations: &[std::result::Result], ) -> (Vec, TicketEvidenceSummary) { let summaries = requests @@ -2898,7 +2903,7 @@ mod tests { ) }) .collect::>(); - let approved = ticket_requirement_approved(requests, &summaries, revision); + let approved = ticket_requirement_approved(requests, &summaries, content_digest); let evidence = ticket_evidence_summary(&["main", "other"], &summaries, approved); (summaries, evidence) } @@ -2919,7 +2924,7 @@ mod tests { ] { let (summaries, evidence) = project_evidence( &[reviewed_merge_request(decision, revoked)], - "revision-1", + "ticket-content-1", &[Ok(source.into())], ); assert_eq!(evidence.complete_for_integration, approved); @@ -2941,7 +2946,7 @@ mod tests { )]; let (summaries, evidence) = project_evidence( &[reviewed_merge_request(ReviewDecision::Approve, false)], - "revision-2", + "ticket-content-2", &[unavailable_source("source_ref_provider_timeout")], ); assert!( @@ -2976,7 +2981,7 @@ mod tests { unavailable_source("source_ref_runtime_unavailable"), ] { let (summaries, evidence) = - project_evidence(&[merged_request()], "revision-1", &[observation]); + project_evidence(&[merged_request()], "ticket-content-1", &[observation]); assert_eq!( summaries[0].current_subject_ref.as_deref(), Some("commit-1") @@ -3032,7 +3037,7 @@ mod tests { } let (summaries, evidence) = project_evidence( &[request], - "revision-1", + "ticket-content-1", &[unavailable_source("source_ref_not_found")], ); assert!(summaries[0].integration_evidence_error.is_some(), "{case}"); @@ -3047,7 +3052,7 @@ mod tests { fn approve_snapshot( request: &mut MergeRequest, - revision: &str, + content_digest: &str, subjects: Vec, ) { let sequence = request @@ -3063,14 +3068,14 @@ mod tests { MergeRequestThreadEvent::ReviewRequested(review) => { review.event_id = format!("request-{sequence}"); review.sequence = sequence; - review.ticket_content_digest = revision.into(); + review.ticket_content_digest = content_digest.into(); review.ticket_merge_request_subjects = subjects.clone(); } MergeRequestThreadEvent::Review(review) => { review.event_id = format!("review-{sequence}"); review.request_event_id = format!("request-{sequence}"); review.sequence = sequence + 1; - review.ticket_content_digest = revision.into(); + review.ticket_content_digest = content_digest.into(); review.ticket_merge_request_subjects = subjects.clone(); } _ => unreachable!(), @@ -3080,7 +3085,7 @@ mod tests { } #[test] - fn exact_revision_and_multi_result_snapshot_require_fresh_attestation_not_later_dates() { + fn exact_ticket_content_and_multi_result_snapshot_require_fresh_attestation_not_later_dates() { let mut requests = vec![ merged_request(), reviewed_merge_request(ReviewDecision::Approve, false), @@ -3091,7 +3096,7 @@ mod tests { unavailable_source("source_ref_not_found"), Ok("commit-1".into()), ]; - let (_, stale) = project_evidence(&requests, "revision-1", &observations); + let (_, stale) = project_evidence(&requests, "ticket-content-1", &observations); assert!(stale.approved_current_subject); assert!(!stale.review_after_rescope); // linked set grew, even though dates did not change assert!(!ticket_evidence_matches(&stale, "approved_review")); @@ -3105,12 +3110,12 @@ mod tests { subject_ref: "commit-1".into(), }, ]; - approve_snapshot(&mut requests[0], "revision-2", snapshot); - let (summaries, fresh) = project_evidence(&requests, "revision-2", &observations); + approve_snapshot(&mut requests[0], "ticket-content-2", snapshot); + let (summaries, fresh) = project_evidence(&requests, "ticket-content-2", &observations); assert!(fresh.complete_for_integration); assert_eq!(summaries[0].review_excerpt.as_deref(), Some("review body")); assert!( - !project_evidence(&requests, "revision-3", &observations) + !project_evidence(&requests, "ticket-content-3", &observations) .1 .complete_for_integration ); @@ -3123,12 +3128,12 @@ mod tests { } requests[1].state = MergeRequestState::Merged; assert!( - project_evidence(&requests, "revision-2", &observations) + project_evidence(&requests, "ticket-content-2", &observations) .1 .complete_for_integration ); assert!( - !project_evidence(&requests[..1], "revision-2", &observations[..1]) + !project_evidence(&requests[..1], "ticket-content-2", &observations[..1]) .1 .complete_for_integration ); @@ -3137,7 +3142,7 @@ mod tests { "2026-01-01T00:00:00Z", Some("body"), )]; - let stale = project_evidence(&requests, "revision-3", &observations).1; + let stale = project_evidence(&requests, "ticket-content-3", &observations).1; assert!(ticket_attention_matches( "done", &stale, @@ -3163,7 +3168,7 @@ mod tests { ); let (summaries, _) = project_evidence( &[merged_request()], - "revision-1", + "ticket-content-1", &[unavailable_source("unused")], ); assert!( @@ -3173,7 +3178,7 @@ mod tests { let mut closed = reviewed_merge_request(ReviewDecision::Approve, false); closed.state = MergeRequestState::Closed; assert!( - !project_evidence(&[closed], "revision-1", &[Ok("commit-1".into())]) + !project_evidence(&[closed], "ticket-content-1", &[Ok("commit-1".into())]) .1 .complete_for_integration ); diff --git a/crates/workspace-server/src/backend_job.rs b/crates/workspace-server/src/backend_job.rs index e7d2189bc..5ec55f31b 100644 --- a/crates/workspace-server/src/backend_job.rs +++ b/crates/workspace-server/src/backend_job.rs @@ -564,9 +564,9 @@ mod tests { fn request() -> BackendJobRequest { BackendJobRequest { - job_id: "ticket-check:T-1:r7".to_string(), + job_id: "ticket-check:T-1:content-7".to_string(), purpose: "ticket_item_check".to_string(), - input_ref: "ticket://T-1/revisions/7".to_string(), + input_ref: "ticket://T-1/contents/content-7".to_string(), input: serde_json::json!({"title": "Check me"}), instruction: "Check this immutable Ticket snapshot.".to_string(), profile: "builtin:backend-job".to_string(), @@ -591,8 +591,8 @@ mod tests { }, "attempt": { "workspace_id": "workspace-a", - "job_id": "ticket-check:T-1:r7", - "attempt_id": "ticket-check:T-1:r7:attempt:2", + "job_id": "ticket-check:T-1:content-7", + "attempt_id": "ticket-check:T-1:content-7:attempt:2", "attempt": 2, "input_digest": "7", "state": "reserved", @@ -687,7 +687,7 @@ mod tests { job::JobOutcome { job_id: reservation.job.request.job_id.clone(), input_digest: "7".into(), - attempt_id: "ticket-check:T-1:r7:attempt:2".into(), + attempt_id: "ticket-check:T-1:content-7:attempt:2".into(), attempt: 2, state: job::JobState::Pending, attempt_state: job::JobAttemptState::Reserved, @@ -766,11 +766,11 @@ mod tests { let encoded = serde_json::to_string(&request).unwrap(); assert_eq!( encoded, - r#"{"job_id":"ticket-check:T-1:r7","purpose":"ticket_item_check","input_ref":"ticket://T-1/revisions/7","input":{"title":"Check me"},"instruction":"Check this immutable Ticket snapshot.","profile":"builtin:backend-job","source_worker":{"runtime_id":"runtime-a","worker_id":"worker-a"},"notification_target":{"runtime_id":"runtime-a","worker_id":"worker-a"},"limits":{"max_concurrent_jobs":8,"timeout_seconds":120,"max_result_bytes":16384,"max_attempts":2}}"# + r#"{"job_id":"ticket-check:T-1:content-7","purpose":"ticket_item_check","input_ref":"ticket://T-1/contents/content-7","input":{"title":"Check me"},"instruction":"Check this immutable Ticket snapshot.","profile":"builtin:backend-job","source_worker":{"runtime_id":"runtime-a","worker_id":"worker-a"},"notification_target":{"runtime_id":"runtime-a","worker_id":"worker-a"},"limits":{"max_concurrent_jobs":8,"timeout_seconds":120,"max_result_bytes":16384,"max_attempts":2}}"# ); assert_eq!( request.fingerprint().unwrap(), - "sha256:250b626572e954d2aa2228364274c946d9f6b176eb0f0a205157e55031581147" + "sha256:d7a3eb420eb1404e1a0aacd198726ce7f23b84a00b5d81a1b15d002b3075b8b0" ); let neutral = request.to_job_request(); neutral.validate().unwrap(); @@ -784,7 +784,7 @@ mod tests { ); assert_eq!( request.worker_input("attempt-1").unwrap(), - "Check this immutable Ticket snapshot.\n\nBackend Job envelope (immutable):\njob_id: ticket-check:T-1:r7\nattempt_id: attempt-1\ninput_digest: sha256:7d0f26b7fc612e982fd7acf2b64f29d2e8f65c7e1292683e36d7dcac38905c50\ninput_ref: ticket://T-1/revisions/7\ninput_json: {\"title\":\"Check me\"}\n\nReturn success only through the structured Backend Job result capability. Final prose and Worker Idle/Stopped state are not result authority." + "Check this immutable Ticket snapshot.\n\nBackend Job envelope (immutable):\njob_id: ticket-check:T-1:content-7\nattempt_id: attempt-1\ninput_digest: sha256:7d0f26b7fc612e982fd7acf2b64f29d2e8f65c7e1292683e36d7dcac38905c50\ninput_ref: ticket://T-1/contents/content-7\ninput_json: {\"title\":\"Check me\"}\n\nReturn success only through the structured Backend Job result capability. Final prose and Worker Idle/Stopped state are not result authority." ); } diff --git a/crates/workspace-server/src/frozen_content_state_migration.rs b/crates/workspace-server/src/frozen_content_state_migration.rs index 947c270f1..9fb7ae16e 100644 --- a/crates/workspace-server/src/frozen_content_state_migration.rs +++ b/crates/workspace-server/src/frozen_content_state_migration.rs @@ -89,7 +89,7 @@ fn migrate_content_state_v85_to_v86(conn: &Connection) -> Result<()> { "worker_create_reservations", "memory_settings_revision", )? { - remove_legacy_reservation_memory_revision(&tx)?; + remove_legacy_reservation_memory_counter(&tx)?; } // Older retained chains rebuilt this table without the lookup index. // Converge on the canonical schema for both legacy creation paths. @@ -169,7 +169,7 @@ fn migrate_content_state_v85_to_v86(conn: &Connection) -> Result<()> { result } -fn remove_legacy_reservation_memory_revision(conn: &Connection) -> Result<()> { +fn remove_legacy_reservation_memory_counter(conn: &Connection) -> Result<()> { // This is the exact table CHECK introduced by the frozen v52 -> v53 // migration. Fresh v85 databases do not have it. Do not rewrite arbitrary // constraints: SQLite's DROP COLUMN must still reject unknown dependencies. diff --git a/crates/workspace-server/src/frozen_content_state_tests.rs b/crates/workspace-server/src/frozen_content_state_tests.rs index 5b77ad7ee..138b83683 100644 --- a/crates/workspace-server/src/frozen_content_state_tests.rs +++ b/crates/workspace-server/src/frozen_content_state_tests.rs @@ -371,7 +371,7 @@ fn content_cutover_keeps_each_evaluation_provenance_and_deduplicates_only_identi let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("content.db"); let (conn, _) = legacy_content_database(&path); - for (revision, toolchain, projection, schema) in [ + for (legacy_counter, toolchain, projection, schema) in [ (4, "new-toolchain", "projection", "schema-2"), (5, "toolchain", "new-projection", "schema-1"), ] { @@ -379,7 +379,7 @@ fn content_cutover_keeps_each_evaluation_provenance_and_deduplicates_only_identi "INSERT INTO workspace_config_tree_revisions SELECT workspace_id,?1,tree_digest,?2,?3,manifest_json,'later',?4 FROM workspace_config_tree_revisions WHERE revision=1", - params![revision, toolchain, projection, schema], + params![legacy_counter, toolchain, projection, schema], ) .unwrap(); } diff --git a/crates/workspace-server/src/frozen_flow_content_migration.rs b/crates/workspace-server/src/frozen_flow_content_migration.rs index 42ec85ab8..ac0275c8d 100644 --- a/crates/workspace-server/src/frozen_flow_content_migration.rs +++ b/crates/workspace-server/src/frozen_flow_content_migration.rs @@ -18,11 +18,11 @@ fn migrate_flow_content_v84_to_v85(conn: &Connection) -> Result<()> { while let Some(row) = rows.next()? { let workspace_id: String = row.get(0)?; let flow_id: String = row.get(1)?; - let revision: i64 = row.get(2)?; + let legacy_counter: i64 = row.get(2)?; let content: String = row.get(3)?; let stored_digest: String = row.get(4)?; let definition_json: String = row.get(5)?; - let identity = format!("{workspace_id}/{flow_id} revision {revision}"); + let identity = format!("{workspace_id}/{flow_id} legacy counter {legacy_counter}"); let source_digest = format!( "sha256:{}", Sha256::digest(content.as_bytes()) diff --git a/crates/workspace-server/src/frozen_flow_content_tests.rs b/crates/workspace-server/src/frozen_flow_content_tests.rs index 372da09a0..8b8ecfe6d 100644 --- a/crates/workspace-server/src/frozen_flow_content_tests.rs +++ b/crates/workspace-server/src/frozen_flow_content_tests.rs @@ -56,7 +56,7 @@ fn legacy_flow_database(path: &Path) -> Connection { params![alpha, alpha_definition.content_digest], ) .unwrap(); - for (revision, content, definition, created_at) in [ + for (legacy_counter, content, definition, created_at) in [ (1, &alpha, &alpha_definition, "first"), (2, &beta, &beta_definition, "second"), (3, &alpha, &alpha_definition, "third"), @@ -64,7 +64,7 @@ fn legacy_flow_database(path: &Path) -> Connection { conn.execute( "INSERT INTO flow_source_revisions VALUES('space','flow',?1,?2,?3,?4,?5)", params![ - revision, + legacy_counter, content, definition.content_digest, serde_json::to_string_pretty(definition).unwrap(), diff --git a/crates/workspace-server/src/frozen_repository_key_migration.rs b/crates/workspace-server/src/frozen_repository_key_migration.rs index 5d64a99fe..fe6e2ce8a 100644 --- a/crates/workspace-server/src/frozen_repository_key_migration.rs +++ b/crates/workspace-server/src/frozen_repository_key_migration.rs @@ -32,8 +32,8 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( tx.execute_batch( "CREATE TEMP TABLE repository_key_cutover ( workspace_id TEXT NOT NULL, kind TEXT NOT NULL, resource_id TEXT NOT NULL, - legacy_revision INTEGER NOT NULL, operation_id TEXT NOT NULL, fingerprint TEXT NOT NULL, created_at TEXT NOT NULL, - PRIMARY KEY(workspace_id,kind,resource_id,legacy_revision));", + legacy_counter INTEGER NOT NULL, operation_id TEXT NOT NULL, fingerprint TEXT NOT NULL, created_at TEXT NOT NULL, + PRIMARY KEY(workspace_id,kind,resource_id,legacy_counter));", )?; for (table, kind, id, fingerprint) in [ ( @@ -65,10 +65,10 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( })? .collect::>>()? }; - for (workspace, resource, revision, fingerprint, created) in rows { + for (workspace, resource, legacy_counter, fingerprint, created) in rows { let mut statement = tx.prepare("SELECT operation_id FROM repository_secret_operations WHERE workspace_id=?1 AND resource_kind=?2 AND resource_id=?3 AND result_revision=?4 AND created_at=?5")?; let matches = statement - .query_map(params![workspace, kind, resource, revision, created], |r| { + .query_map(params![workspace, kind, resource, legacy_counter, created], |r| { r.get::<_, String>(0) })? .collect::>>()?; @@ -81,7 +81,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( workspace, kind, resource, - revision, + legacy_counter, matches[0], fingerprint, created @@ -126,12 +126,12 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( tx.execute_batch("DROP INDEX idx_workdir_create_credential_candidates_revision;")?; tx.execute_batch(include_str!("frozen_repository_key_schema.sql"))?; tx.execute_batch("INSERT INTO repository_secret_operations SELECT workspace_id,operation_id,request_fingerprint,resource_kind,resource_id,operation_id,created_at FROM repository_secret_operations_v86; - INSERT INTO repository_ssh_credentials SELECT c.workspace_id,c.credential_id,c.name,c.public_key_algorithm,c.public_key_fingerprint,m.operation_id,c.status,c.created_at,c.rotated_at FROM repository_ssh_credentials_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_revision=c.current_revision; - INSERT INTO repository_ssh_credential_keys SELECT c.workspace_id,c.credential_id,m.operation_id,c.public_key_algorithm,c.public_key_fingerprint,c.created_at FROM repository_ssh_credential_revisions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_revision=c.revision; - INSERT INTO repository_ssh_host_trusts SELECT c.workspace_id,c.host_trust_id,c.hostname,c.port,c.key_algorithm,c.host_key,c.fingerprint,m.operation_id,c.created_at,c.updated_at FROM repository_ssh_host_trusts_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='host_trust' AND m.resource_id=c.host_trust_id AND m.legacy_revision=c.current_revision; - INSERT INTO repository_ssh_host_trust_keys SELECT c.workspace_id,c.host_trust_id,m.operation_id,c.hostname,c.port,c.key_algorithm,c.host_key,c.fingerprint,c.created_at FROM repository_ssh_host_trust_revisions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='host_trust' AND m.resource_id=c.host_trust_id AND m.legacy_revision=c.revision;")?; + INSERT INTO repository_ssh_credentials SELECT c.workspace_id,c.credential_id,c.name,c.public_key_algorithm,c.public_key_fingerprint,m.operation_id,c.status,c.created_at,c.rotated_at FROM repository_ssh_credentials_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_counter=c.current_revision; + INSERT INTO repository_ssh_credential_keys SELECT c.workspace_id,c.credential_id,m.operation_id,c.public_key_algorithm,c.public_key_fingerprint,c.created_at FROM repository_ssh_credential_revisions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_counter=c.revision; + INSERT INTO repository_ssh_host_trusts SELECT c.workspace_id,c.host_trust_id,c.hostname,c.port,c.key_algorithm,c.host_key,c.fingerprint,m.operation_id,c.created_at,c.updated_at FROM repository_ssh_host_trusts_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='host_trust' AND m.resource_id=c.host_trust_id AND m.legacy_counter=c.current_revision; + INSERT INTO repository_ssh_host_trust_keys SELECT c.workspace_id,c.host_trust_id,m.operation_id,c.hostname,c.port,c.key_algorithm,c.host_key,c.fingerprint,c.created_at FROM repository_ssh_host_trust_revisions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='host_trust' AND m.resource_id=c.host_trust_id AND m.legacy_counter=c.revision;")?; let secrets = { - let mut statement = tx.prepare("SELECT s.workspace_id,s.secret_id,s.revision,s.purpose,s.encryption_algorithm,s.nonce,s.ciphertext,s.created_at,m.operation_id FROM server_secret_versions_v86 s LEFT JOIN repository_key_cutover m ON m.workspace_id=s.workspace_id AND m.kind='credential' AND m.resource_id=s.secret_id AND m.legacy_revision=s.revision")?; + let mut statement = tx.prepare("SELECT s.workspace_id,s.secret_id,s.revision,s.purpose,s.encryption_algorithm,s.nonce,s.ciphertext,s.created_at,m.operation_id FROM server_secret_versions_v86 s LEFT JOIN repository_key_cutover m ON m.workspace_id=s.workspace_id AND m.kind='credential' AND m.resource_id=s.secret_id AND m.legacy_counter=s.revision")?; statement .query_map([], |r| { Ok(( @@ -148,7 +148,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( })? .collect::>>()? }; - for (workspace, id, revision, purpose, algorithm, nonce, ciphertext, created, operation) in + for (workspace, id, legacy_counter, purpose, algorithm, nonce, ciphertext, created, operation) in secrets { if algorithm != "aes-256-gcm-v1" { @@ -164,7 +164,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( database, &workspace, &id, - revision, + legacy_counter, &operation, &purpose, &nonce, @@ -201,7 +201,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( })? .collect::>>()? }; - for (workspace, event, kind, resource, revision, actor, created) in audits { + for (workspace, event, kind, resource, legacy_counter, actor, created) in audits { let resource_kind = if kind.starts_with("credential_") { "credential" } else if kind.starts_with("host_trust_") { @@ -212,7 +212,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( let mut statement=tx.prepare("SELECT operation_id FROM repository_secret_operations_v86 WHERE workspace_id=?1 AND resource_kind=?2 AND resource_id=?3 AND result_revision=?4 AND created_at=?5")?; let matches = statement .query_map( - params![workspace, resource_kind, resource, revision, created], + params![workspace, resource_kind, resource, legacy_counter, created], |r| r.get::<_, String>(0), )? .collect::>>()?; @@ -246,7 +246,7 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( HAVING count(*)=1) AS mutation_kind FROM repository_secret_operations_v86 o) e LEFT JOIN repository_key_cutover m ON m.workspace_id=e.workspace_id AND m.kind=e.resource_kind AND m.resource_id=e.resource_id AND m.created_at<=e.created_at - AND m.legacy_revision=CASE WHEN e.mutation_kind IN ('credential_rotated','host_trust_rotated') THEN e.result_revision-1 + AND m.legacy_counter=CASE WHEN e.mutation_kind IN ('credential_rotated','host_trust_rotated') THEN e.result_revision-1 WHEN e.mutation_kind IN ('credential_deleted','host_trust_deleted') THEN e.result_revision END; INSERT INTO workdir_create_operations SELECT o.workspace_id,o.operation_id,'workdir-create-v86:' || COALESCE(CAST(o.source_revision AS TEXT),'unknown') || ':' || o.request_fingerprint, @@ -257,14 +257,14 @@ fn migrate_repository_keys_v86_to_v87_with_secret_source( CASE WHEN a.operation_id IS NULL THEN o.repository_access_mode END FROM workdir_create_operations_v86 o LEFT JOIN workdir_terminal_archive_cutover a ON a.workspace_id=o.workspace_id AND a.operation_id=o.operation_id - LEFT JOIN repository_key_cutover c ON c.workspace_id=o.workspace_id AND c.kind='credential' AND c.resource_id=o.credential_id AND c.legacy_revision=o.credential_revision AND c.created_at<=o.updated_at - LEFT JOIN repository_key_cutover h ON h.workspace_id=o.workspace_id AND h.kind='host_trust' AND h.resource_id=o.host_trust_id AND h.legacy_revision=o.host_trust_revision AND h.created_at<=o.updated_at; + LEFT JOIN repository_key_cutover c ON c.workspace_id=o.workspace_id AND c.kind='credential' AND c.resource_id=o.credential_id AND c.legacy_counter=o.credential_revision AND c.created_at<=o.updated_at + LEFT JOIN repository_key_cutover h ON h.workspace_id=o.workspace_id AND h.kind='host_trust' AND h.resource_id=o.host_trust_id AND h.legacy_counter=o.host_trust_revision AND h.created_at<=o.updated_at; INSERT INTO workdir_create_credential_candidates SELECT c.workspace_id,c.operation_id,c.ordinal,c.role,c.credential_id,m.fingerprint FROM workdir_create_credential_candidates_v86 c JOIN workdir_create_operations_v86 o ON o.workspace_id=c.workspace_id AND o.operation_id=c.operation_id LEFT JOIN repository_key_cutover m - ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_revision=c.credential_revision AND m.created_at<=o.updated_at + ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_counter=c.credential_revision AND m.created_at<=o.updated_at WHERE NOT EXISTS(SELECT 1 FROM workdir_terminal_archive_cutover a WHERE a.workspace_id=c.workspace_id AND a.operation_id=c.operation_id); - INSERT INTO workdir_create_credential_retentions SELECT c.workspace_id,c.operation_id,c.ordinal,c.credential_id,m.fingerprint,m.operation_id FROM workdir_create_credential_revision_retentions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_revision=c.credential_revision;")?; + INSERT INTO workdir_create_credential_retentions SELECT c.workspace_id,c.operation_id,c.ordinal,c.credential_id,m.fingerprint,m.operation_id FROM workdir_create_credential_revision_retentions_v86 c LEFT JOIN repository_key_cutover m ON m.workspace_id=c.workspace_id AND m.kind='credential' AND m.resource_id=c.credential_id AND m.legacy_counter=c.credential_revision;")?; let missing:i64=tx.query_row("SELECT count(*) FROM workdir_create_operations WHERE (credential_id IS NOT NULL AND credential_fingerprint IS NULL) OR (host_trust_id IS NOT NULL AND host_trust_fingerprint IS NULL)",[],|r|r.get(0))?; if missing != 0 { return Err(Error::Store( diff --git a/crates/workspace-server/src/frozen_repository_key_tests.rs b/crates/workspace-server/src/frozen_repository_key_tests.rs index 37762b339..d8599dcdf 100644 --- a/crates/workspace-server/src/frozen_repository_key_tests.rs +++ b/crates/workspace-server/src/frozen_repository_key_tests.rs @@ -177,6 +177,8 @@ fn migrated_workdir_request_replays_and_retries_without_accepting_changed_displa ) .unwrap(); migrate_repository_keys_v86_to_v87(&conn).unwrap(); + // The service consumes current receipt columns; unrelated schemas are not this fixture. + rename_saved_secret_receipt_counters(&conn).unwrap(); let store = SqliteWorkspaceStore { conn: Arc::new(Mutex::new(conn)), }; @@ -358,6 +360,8 @@ fn deleted_successful_workdir_archives_verbatim_history_without_authorizing_retr .unwrap(), 0 ); + // The service consumes current receipt columns; unrelated schemas are not this fixture. + rename_saved_secret_receipt_counters(&conn).unwrap(); let store = Arc::new(SqliteWorkspaceStore { conn: Arc::new(Mutex::new(conn)), }); @@ -655,7 +659,7 @@ fn migrated_ssh_mutations_replay_complete_old_input_without_reexecution_or_recei let rotated_digest = frozen_credential_request_digest("rotate", "", 1, &next_private); let host_created_digest = frozen_host_request_digest("old.example.test", 0, &old_fp); let host_rotated_digest = frozen_host_request_digest("new.example.test", 1, &old_fp); - for (operation, digest, kind, resource, revision, timestamp, audit) in [ + for (operation, digest, kind, resource, legacy_counter, timestamp, audit) in [ ( "create-key", &created_digest, @@ -695,25 +699,25 @@ fn migrated_ssh_mutations_replay_complete_old_input_without_reexecution_or_recei ] { conn.execute( "INSERT INTO repository_secret_operations VALUES('space',?1,?2,?3,?4,?5,?6)", - params![operation, digest, kind, resource, revision, timestamp], + params![operation, digest, kind, resource, legacy_counter, timestamp], ) .unwrap(); conn.execute( "INSERT INTO repository_secret_audit_events VALUES('space',?1,?2,?3,?4,'actor',?5)", - params![operation, audit, resource, revision, timestamp], + params![operation, audit, resource, legacy_counter, timestamp], ) .unwrap(); } conn.execute("INSERT INTO repository_ssh_credentials VALUES('space','key','Key','ssh-ed25519',?1,2,'active','2026-01-01','2026-01-02')",[&next_fp]).unwrap(); let encryption_key = LessSafeKey::new(UnboundKey::new(&AES_256_GCM, &[42; 32]).unwrap()); - for (revision, fingerprint, private, timestamp) in [ + for (legacy_counter, fingerprint, private, timestamp) in [ (1u64, &old_fp, &old_private, "2026-01-01"), (2, &next_fp, &next_private, "2026-01-02"), ] { - conn.execute("INSERT INTO repository_ssh_credential_revisions VALUES('space','key',?1,'ssh-ed25519',?2,?3)",params![revision,fingerprint,timestamp]).unwrap(); - let nonce = [revision as u8; 12]; + conn.execute("INSERT INTO repository_ssh_credential_revisions VALUES('space','key',?1,'ssh-ed25519',?2,?3)",params![legacy_counter,fingerprint,timestamp]).unwrap(); + let nonce = [legacy_counter as u8; 12]; let mut ciphertext = private.as_bytes().to_vec(); - let aad = format!("yoi/repository-secret/v1/space/key/{revision}/private_key"); + let aad = format!("yoi/repository-secret/v1/space/key/{legacy_counter}/private_key"); encryption_key .seal_in_place_append_tag( Nonce::assume_unique_for_key(nonce), @@ -721,16 +725,18 @@ fn migrated_ssh_mutations_replay_complete_old_input_without_reexecution_or_recei &mut ciphertext, ) .unwrap(); - conn.execute("INSERT INTO server_secret_versions VALUES('space','key',?1,'private_key','aes-256-gcm-v1',?2,?3,?4)",params![revision,nonce.as_slice(),ciphertext,timestamp]).unwrap(); + conn.execute("INSERT INTO server_secret_versions VALUES('space','key',?1,'private_key','aes-256-gcm-v1',?2,?3,?4)",params![legacy_counter,nonce.as_slice(),ciphertext,timestamp]).unwrap(); } conn.execute("INSERT INTO repository_ssh_host_trusts VALUES('space','host','new.example.test',22,'ssh-ed25519',?1,?2,2,'2026-01-01','2026-01-02')",params![public,old_fp]).unwrap(); - for (revision, hostname, timestamp) in [ + for (legacy_counter, hostname, timestamp) in [ (1, "old.example.test", "2026-01-01"), (2, "new.example.test", "2026-01-02"), ] { - conn.execute("INSERT INTO repository_ssh_host_trust_revisions VALUES('space','host',?1,?2,22,'ssh-ed25519',?3,?4,?5)",params![revision,hostname,public,old_fp,timestamp]).unwrap(); + conn.execute("INSERT INTO repository_ssh_host_trust_revisions VALUES('space','host',?1,?2,22,'ssh-ed25519',?3,?4,?5)",params![legacy_counter,hostname,public,old_fp,timestamp]).unwrap(); } migrate_repository_keys_v86_to_v87(&conn).unwrap(); + // The service consumes current receipt columns; unrelated schemas are not this fixture. + rename_saved_secret_receipt_counters(&conn).unwrap(); let store = Arc::new(SqliteWorkspaceStore { conn: Arc::new(Mutex::new(conn)), }); @@ -881,6 +887,8 @@ fn deleted_legacy_receipt_does_not_bind_to_later_reused_key_ordinal() { INSERT INTO repository_secret_operations VALUES('space','old-delete','intent','credential','key',7,'2025-01-01'); INSERT INTO repository_secret_audit_events VALUES('space','old-delete-audit','credential_deleted','key',7,'actor','2025-01-01');").unwrap(); migrate_repository_keys_v86_to_v87(&conn).unwrap(); + // The service consumes current receipt columns; unrelated schemas are not this fixture. + rename_saved_secret_receipt_counters(&conn).unwrap(); let store = Arc::new(SqliteWorkspaceStore { conn: Arc::new(Mutex::new(conn)), }); diff --git a/crates/workspace-server/src/frozen_runtime_binding_migration.rs b/crates/workspace-server/src/frozen_runtime_binding_migration.rs index cfc107f42..2f4a81ca6 100644 --- a/crates/workspace-server/src/frozen_runtime_binding_migration.rs +++ b/crates/workspace-server/src/frozen_runtime_binding_migration.rs @@ -62,7 +62,7 @@ fn migrate_runtime_bindings_v87_to_v88(conn: &Connection) -> Result<()> { )?; if unsupported { return Err(Error::Store( - "unsupported legacy Workspace signing identity revision".into(), + "legacy Workspace signing identity counter must equal one".into(), )); } } diff --git a/crates/workspace-server/src/latest_schema.sql b/crates/workspace-server/src/latest_schema.sql index bea40661c..341645d6d 100644 --- a/crates/workspace-server/src/latest_schema.sql +++ b/crates/workspace-server/src/latest_schema.sql @@ -54,7 +54,7 @@ CREATE TABLE "artifacts" ( worker_display TEXT, repository_id TEXT, source_kind TEXT, - source_revision TEXT, + source_reference TEXT, PRIMARY KEY (workspace_id, artifact_id), CHECK ((worker_ref_kind IS NULL) = (worker_ref_key IS NULL)), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, @@ -1679,10 +1679,10 @@ CREATE TABLE repository_secret_legacy_receipts ( request_fingerprint TEXT NOT NULL, resource_kind TEXT NOT NULL, resource_id TEXT NOT NULL, - result_revision INTEGER NOT NULL, + legacy_result_counter INTEGER NOT NULL, created_at TEXT NOT NULL, mutation_kind TEXT, - expected_revision INTEGER, + legacy_expected_counter INTEGER, expected_operation_id TEXT, expected_key_fingerprint TEXT, PRIMARY KEY (workspace_id, operation_id), diff --git a/crates/workspace-server/src/legacy_evidence_column_migration.rs b/crates/workspace-server/src/legacy_evidence_column_migration.rs new file mode 100644 index 000000000..32329d082 --- /dev/null +++ b/crates/workspace-server/src/legacy_evidence_column_migration.rs @@ -0,0 +1,24 @@ +// These are provenance and frozen replay evidence, not live update guards. +// Keep the original data while removing the misleading names from current DDL. +fn migrate_evidence_column_names_v88_to_v89(conn: &Connection) -> Result<()> { + if current_schema_version(conn)? != 88 { + return Err(Error::Store( + "Evidence column migration requires schema 88".into(), + )); + } + let tx = conn.unchecked_transaction()?; + tx.execute_batch("ALTER TABLE artifacts RENAME COLUMN source_revision TO source_reference;")?; + rename_saved_secret_receipt_counters(&tx)?; + tx.execute("INSERT INTO __yoi_schema_migrations(version,name) VALUES(89,'descriptive legacy evidence columns')", [])?; + tx.commit()?; + Ok(()) +} + +// Also used by focused legacy SSH fixtures that do not contain unrelated tables. +fn rename_saved_secret_receipt_counters(conn: &Connection) -> Result<()> { + conn.execute_batch( + "ALTER TABLE repository_secret_legacy_receipts RENAME COLUMN result_revision TO legacy_result_counter; + ALTER TABLE repository_secret_legacy_receipts RENAME COLUMN expected_revision TO legacy_expected_counter;", + )?; + Ok(()) +} diff --git a/crates/workspace-server/src/legacy_evidence_column_tests.rs b/crates/workspace-server/src/legacy_evidence_column_tests.rs new file mode 100644 index 000000000..0aeefd5ef --- /dev/null +++ b/crates/workspace-server/src/legacy_evidence_column_tests.rs @@ -0,0 +1,124 @@ +// These fixtures use actual schema-88 column names as input, not as current APIs. +fn old_evidence_database(path: &Path) -> Connection { + prepare_retained_schema(path, 88); + let conn = Connection::open(path).unwrap(); + configure_sqlite(&conn).unwrap(); + seed_migration_workspace(&conn); + for (index, source) in [Some("immutable-source-commit"), None] + .into_iter() + .enumerate() + { + conn.execute("INSERT INTO artifacts(workspace_id,artifact_id,kind,uri,created_at,created_by_kind,created_by_key,created_by_display,source_revision) + VALUES('space',?1,'file','artifact://saved','created','user','owner','Owner',?2)",params![format!("artifact-{index}"),source]).unwrap(); + } + for (operation, result, expected) in [ + ("old-update", 7, Some(6)), + ("old-create", 1, None), + ("old-zero", 0, Some(0)), + ] { + conn.execute("INSERT INTO repository_secret_operations VALUES('space',?1,'saved-request-digest','credential','deleted-key',?1,'created')",[operation]).unwrap(); + conn.execute("INSERT INTO repository_secret_legacy_receipts VALUES('space',?1,'saved-request-digest','credential','deleted-key',?2,'created','credential_deleted',?3,'prior-operation','key-fingerprint')",params![operation,result,expected]).unwrap(); + } + conn +} + +fn evidence_rows(conn: &Connection, table: &str) -> Vec> { + let mut stmt = conn + .prepare(&format!("SELECT * FROM {table} ORDER BY 1,2")) + .unwrap(); + let count = stmt.column_count(); + stmt.query_map([], |row| { + (0..count) + .map(|i| row.get(i)) + .collect::>>() + }) + .unwrap() + .collect::>>() + .unwrap() +} + +#[test] +fn evidence_column_cutover_preserves_all_values_and_current_schema_names() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("server.db"); + let conn = old_evidence_database(&path); + let artifacts = evidence_rows(&conn, "artifacts"); + let receipts = evidence_rows(&conn, "repository_secret_legacy_receipts"); + drop(conn); + let upgraded = SqliteWorkspaceStore::open(&path).unwrap(); + upgraded + .with_conn(|conn| { + assert_eq!(current_schema_version(conn)?, 89); + assert_eq!(evidence_rows(conn, "artifacts"), artifacts); + assert_eq!( + evidence_rows(conn, "repository_secret_legacy_receipts"), + receipts + ); + assert_eq!( + conn.query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| { + row.get::<_, i64>(0) + })?, + 0 + ); + for (table, column) in [ + ("artifacts", "source_reference"), + ("repository_secret_legacy_receipts", "legacy_result_counter"), + ( + "repository_secret_legacy_receipts", + "legacy_expected_counter", + ), + ] { + assert!(column_exists(conn, table, column)?); + } + Ok(()) + }) + .unwrap(); + for store in [upgraded, SqliteWorkspaceStore::in_memory().unwrap()] { + store.with_conn(|conn| { + let mut stmt = conn.prepare("SELECT name,sql FROM sqlite_schema WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'")?; + let objects = stmt.query_map([],|row|Ok((row.get::<_,String>(0)?,row.get::<_,String>(1)?)))?.collect::>>()?; + for (name,sql) in objects { + assert!(!name.to_ascii_lowercase().contains("revision") && !sql.to_ascii_lowercase().contains("revision"),"old name in current schema: {name}"); + } + Ok(()) + }).unwrap(); + } +} + +#[test] +fn evidence_column_cutover_rolls_back_every_rename_before_retry() { + let dir = tempfile::tempdir().unwrap(); + let conn = old_evidence_database(&dir.path().join("server.db")); + conn.execute_batch("CREATE TRIGGER reject_schema_89 BEFORE INSERT ON __yoi_schema_migrations WHEN NEW.version=89 BEGIN SELECT RAISE(ABORT,'reject schema 89'); END;").unwrap(); + let schema = evidence_rows(&conn, "sqlite_schema"); + let receipts = evidence_rows(&conn, "repository_secret_legacy_receipts"); + let artifacts = evidence_rows(&conn, "artifacts"); + let history = evidence_rows(&conn, "__yoi_schema_migrations"); + let pragmas = |conn: &Connection| { + ( + conn.pragma_query_value(None, "foreign_keys", |row| row.get::<_, i64>(0)) + .unwrap(), + conn.pragma_query_value(None, "legacy_alter_table", |row| row.get::<_, i64>(0)) + .unwrap(), + ) + }; + let before = pragmas(&conn); + assert!( + migrate_evidence_column_names_v88_to_v89(&conn) + .unwrap_err() + .to_string() + .contains("reject schema 89") + ); + assert_eq!(current_schema_version(&conn).unwrap(), 88); + assert_eq!(evidence_rows(&conn, "sqlite_schema"), schema); + assert_eq!( + evidence_rows(&conn, "repository_secret_legacy_receipts"), + receipts + ); + assert_eq!(evidence_rows(&conn, "artifacts"), artifacts); + assert_eq!(evidence_rows(&conn, "__yoi_schema_migrations"), history); + assert_eq!(pragmas(&conn), before); + conn.execute_batch("DROP TRIGGER reject_schema_89").unwrap(); + migrate_evidence_column_names_v88_to_v89(&conn).unwrap(); + assert_eq!(current_schema_version(&conn).unwrap(), 89); +} diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index bd46bb686..fefe24d92 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -42,7 +42,7 @@ mod workspace_subscription; pub use authority::{ InternalMemoryStagingEntry, MemoryAuthority, MemoryDocument, MemoryStagingResolution, - ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource, + ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, TicketSourceCommitResolver, WorkspaceAuthority, }; pub use config::{ResolvedWorkspaceBackendConfig, ServerHostConfigFile}; diff --git a/crates/workspace-server/src/main.rs b/crates/workspace-server/src/main.rs index b7ae4a9a9..8858a8cec 100644 --- a/crates/workspace-server/src/main.rs +++ b/crates/workspace-server/src/main.rs @@ -160,7 +160,7 @@ fn load_skill_workspace_config( let store = SqliteWorkspaceStore::open(ServerConfig::default_server_database_path())?; store.load_workspace_config(workspace_id)?.ok_or_else(|| { Box::new(CliError(format!( - "Workspace `{workspace_id}` has no active config revision" + "Workspace `{workspace_id}` has no active config snapshot" ))) as Box }) } @@ -655,7 +655,7 @@ fn print_migrate_help() { fn print_skills_help() { println!( - "yoi-server skills\n\nUsage:\n yoi-server skills list --workspace \n yoi-server skills lint --workspace \n yoi-server skills show --workspace \n\nDescription:\n Reads the active Server DB virtual-config revision. Catalog output is lightweight and omits imported Markdown content; detail output includes that content. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace Workspace id in the Server DB (required)\n -h, --help Print help" + "yoi-server skills\n\nUsage:\n yoi-server skills list --workspace \n yoi-server skills lint --workspace \n yoi-server skills show --workspace \n\nDescription:\n Reads the active Server DB virtual-config snapshot. Catalog output is lightweight and omits imported Markdown content; detail output includes that content. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace Workspace id in the Server DB (required)\n -h, --help Print help" ); } diff --git a/crates/workspace-server/src/repository_access.rs b/crates/workspace-server/src/repository_access.rs index b272913aa..304909848 100644 --- a/crates/workspace-server/src/repository_access.rs +++ b/crates/workspace-server/src/repository_access.rs @@ -308,7 +308,7 @@ impl RepositorySecretService { let Some((_,resource_kind,resource_id,result_operation_id))=read_operation(conn,workspace_id,&operation_id)? else { return Ok(None); }; let created_at=conn.query_row("SELECT created_at FROM repository_secret_operations WHERE workspace_id=?1 AND operation_id=?2",params![workspace_id,operation_id],|r|r.get(0))?; let legacy: Option<(Option,Option,Option,Option)>=conn.query_row( - "SELECT mutation_kind,expected_revision,expected_operation_id,expected_key_fingerprint FROM repository_secret_legacy_receipts WHERE workspace_id=?1 AND operation_id=?2", + "SELECT mutation_kind,legacy_expected_counter,expected_operation_id,expected_key_fingerprint FROM repository_secret_legacy_receipts WHERE workspace_id=?1 AND operation_id=?2", params![workspace_id,operation_id],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?)), ).optional()?; let (mutation_kind,legacy_precondition_proven)=if let Some((kind,counter,prior,key))=legacy { @@ -1626,7 +1626,7 @@ fn matches_legacy_secret_replay( request: LegacySecretRequest<'_>, ) -> Result { let evidence: Option<(String,Option,Option,Option,Option)> = conn.query_row( - "SELECT request_fingerprint,mutation_kind,expected_revision,expected_operation_id,expected_key_fingerprint FROM repository_secret_legacy_receipts WHERE workspace_id=?1 AND operation_id=?2 AND resource_id=?3", + "SELECT request_fingerprint,mutation_kind,legacy_expected_counter,expected_operation_id,expected_key_fingerprint FROM repository_secret_legacy_receipts WHERE workspace_id=?1 AND operation_id=?2 AND resource_id=?3", params![workspace_id,operation_id,resource_id], |r| Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?)), ).optional()?; let Some((original, Some(mutation_kind), Some(counter), prior_operation, prior_key)) = evidence @@ -2074,7 +2074,7 @@ pub(crate) fn migrate_legacy_repository_secret_envelope( database_path: &Path, workspace_id: &str, credential_id: &str, - legacy_revision: u64, + legacy_counter: u64, operation_id: &str, purpose: &str, nonce: &[u8], @@ -2092,7 +2092,7 @@ pub(crate) fn migrate_legacy_repository_secret_envelope( Error::RegistryInconsistency("Repository secret migration nonce is invalid".to_string()) })?; let old_aad = format!( - "yoi/repository-secret/v1/{workspace_id}/{credential_id}/{legacy_revision}/{purpose}" + "yoi/repository-secret/v1/{workspace_id}/{credential_id}/{legacy_counter}/{purpose}" ); let mut plaintext = zeroize::Zeroizing::new(ciphertext.to_vec()); let plaintext_len = key diff --git a/crates/workspace-server/src/schema_equivalence_tests.rs b/crates/workspace-server/src/schema_equivalence_tests.rs index 39a74ebed..5e9618f19 100644 --- a/crates/workspace-server/src/schema_equivalence_tests.rs +++ b/crates/workspace-server/src/schema_equivalence_tests.rs @@ -42,6 +42,7 @@ fn fresh_and_upgraded_state_contract_schemas_enforce_the_same_constraints() { let fresh = SqliteWorkspaceStore::in_memory().unwrap(); let mut mismatches = Vec::new(); for table in [ + "artifacts", "workspace_config_trees", "workspace_config_tree_history", "workspace_memory_settings", diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 4f359f61c..339facd0e 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -155,7 +155,7 @@ use crate::auth::{ }; use crate::authority::{ MemoryAuthority, ObjectiveAuthority, ObjectiveCreateInput, ObjectiveEditInput, - SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource, merge_request_summary, + SqliteWorkspaceAuthority, TicketAuthority, TicketSourceCommitResolver, merge_request_summary, }; use crate::backend_job::{ ABSOLUTE_MAX_CONCURRENT_JOBS, BackendJobAttemptRecord, BackendJobAttemptState, @@ -3607,11 +3607,9 @@ impl WorkspaceApi { // The observer snapshot retains the unconfigured authority, not itself: // source observation uses Repository access + Runtime authority without a cycle. let observer_api = api.clone(); - api.authority = - api.authority - .with_merge_revision_source(Arc::new(RuntimeTicketMergeRevisionSource { - api: observer_api, - })); + api.authority = api.authority.with_source_commit_resolver(Arc::new( + RuntimeTicketSourceCommitResolver { api: observer_api }, + )); if let Some(dispatcher) = worker_remove_dispatcher { dispatcher .install_executor(Arc::new( @@ -4635,7 +4633,7 @@ impl WorkspaceApi { &self.config.workspace_id, &delivery_id, BackendJobDeliveryState::Failed, - Some("current_revision_unavailable"), + Some("current_ticket_content_unavailable"), Some(&error.to_string()), &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), ); @@ -4657,7 +4655,7 @@ impl WorkspaceApi { } Ok(ticket_item_checker::TicketItemCheckNotification::Stale) => { let detail = format!( - "checked revision {} is no longer current ({})", + "checked Ticket content digest {} is no longer current ({})", checker_input.content_digest, ticket_item_checker::content_digest(¤t_ticket) ); @@ -4665,7 +4663,7 @@ impl WorkspaceApi { &self.config.workspace_id, &delivery_id, BackendJobDeliveryState::Completed, - Some("notification_suppressed_stale_revision"), + Some("notification_suppressed_stale_ticket_content"), Some(&detail), &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), ); @@ -15190,12 +15188,12 @@ struct MergeRequestRepositorySource { } #[derive(Clone)] -struct RuntimeTicketMergeRevisionSource { +struct RuntimeTicketSourceCommitResolver { api: WorkspaceApi, } -impl TicketMergeRevisionSource for RuntimeTicketMergeRevisionSource { - fn resolve_subject_ref( +impl TicketSourceCommitResolver for RuntimeTicketSourceCommitResolver { + fn resolve_source_commit( &self, ticket_id: &str, repository_id: &str, @@ -15357,6 +15355,9 @@ fn remap_source_ref_error(error: ApiError) -> ApiError { } } +/// Observe the provider-published source commit through the assigned Runtime, not +/// a Server-local checkout. Open/list/show/review/complete paths call this at the +/// point they need current source evidence; provider failure remains unavailable. fn observe_published_source_ref( api: &WorkspaceApi, workspace_id: &str, @@ -15368,6 +15369,10 @@ fn observe_published_source_ref( .map_err(remap_source_ref_error) } +/// Before opening an MR or granting review of an open MR, re-read the current +/// Coder attachments and require one clean checkout of the same Repository, +/// normalized branch selector and provider-published HEAD commit. A newer +/// observation time alone says nothing about which code is being reviewed. fn require_assigned_workdir_source( api: &WorkspaceApi, assignment: &crate::store::TicketWorkerAssignmentRecord, @@ -15457,7 +15462,7 @@ fn validate_assigned_workdir_source( if !workdir_selector_matches || workdir.current_ref.as_deref() != Some(resolved_ref) { return Err( worker_runtime::working_directory::WorkingDirectoryDiagnostic { - code: "source_ref_revision_mismatch".to_string(), + code: "source_workdir_ref_mismatch".to_string(), message: "Current Coder Workdir selector and HEAD do not match the provider-published source ref".to_string(), }, ); @@ -18017,7 +18022,7 @@ fn list_current_worker_workdir_catalog_for_worker( workspace_id: api.config.workspace_id.clone(), items, next_cursor: page.next_cursor, - digest: page.revision, + digest: page.catalog_digest, }) } @@ -18033,7 +18038,7 @@ fn list_current_worker_workdir_attachments( Error::InvalidInput("attachment page limit must be 1..=100".to_string()).into(), ); } - let (mut links, digest) = api.store.list_worker_workdir_links_page_with_revision( + let (mut links, digest) = api.store.list_worker_workdir_links_page_with_digest( &api.config.workspace_id, worker, limit + 1, @@ -20839,7 +20844,7 @@ async fn scoped_subjektiv_memory_backend( ), Op::ValidateProposal(_) => ( Some(SubjektivWorkerAuthority::Subject), - "revision proposal validation", + "Memory proposal validation", ), Op::ReceiptStatus(_) => ( Some(SubjektivWorkerAuthority::Subject), @@ -21289,7 +21294,7 @@ async fn scoped_list_skills( .load_workspace_config(&path.workspace_id)? .ok_or_else(|| { Error::RegistryInconsistency(format!( - "Workspace {} has no active config revision", + "Workspace {} has no active config snapshot", path.workspace_id )) })?; @@ -21306,7 +21311,7 @@ async fn scoped_lint_skills( .load_workspace_config(&path.workspace_id)? .ok_or_else(|| { Error::RegistryInconsistency(format!( - "Workspace {} has no active config revision", + "Workspace {} has no active config snapshot", path.workspace_id )) })?; @@ -21323,7 +21328,7 @@ async fn scoped_get_skill( .load_workspace_config(&path.workspace_id)? .ok_or_else(|| { Error::RegistryInconsistency(format!( - "Workspace {} has no active config revision", + "Workspace {} has no active config snapshot", path.workspace_id )) })?; @@ -21342,7 +21347,7 @@ async fn scoped_activate_skill( .load_workspace_config(&path.workspace_id)? .ok_or_else(|| { Error::RegistryInconsistency(format!( - "Workspace {} has no active config revision", + "Workspace {} has no active config snapshot", path.workspace_id )) })?; @@ -21611,7 +21616,7 @@ async fn scoped_confirm_repository_ssh_host_trust( })?; if request.expected_host_key_fingerprint != probe.expected_host_key_fingerprint { return Err(ApiError::from(Error::WorkspaceConfigConflict( - "SSH host trust revision changed after the connection test".to_string(), + "SSH host trust fingerprint changed after the connection test".to_string(), ))); } let host_trust = api.repository_secrets.put_host_trust( @@ -32899,9 +32904,7 @@ fn api_error_status(error: &Error) -> StatusCode { StatusCode::NOT_IMPLEMENTED } Error::RuntimeOperationFailed { code, .. } - if code == "profile_registry_revision_conflict" - || code == "profile_source_revision_conflict" - || code == "workspace_metadata_updated_at_conflict" + if code == "workspace_metadata_updated_at_conflict" || code == "workspace_cleanup_plan_stale" || code == "workspace_cleanup_worker_blocked" || code == "workspace_cleanup_workdir_blocked" @@ -35929,6 +35932,10 @@ mod tests { ); } + // Architectural source check, run by the ordinary Server unit suite: route + // wiring must use Runtime/provider authority and capability validation must + // precede provider access. This is not a freshness proof; the boundary tests + // below and ticket_evidence_tests exercise commit/content equality directly. #[test] fn merge_request_http_paths_observe_refs_through_runtime_provider_authority() { let source = include_str!("server.rs"); @@ -36034,8 +36041,57 @@ mod tests { "work/T-549", "abc123" ), - Err(diagnostic) if diagnostic.code == "source_ref_revision_mismatch" + Err(diagnostic) if diagnostic.code == "source_workdir_ref_mismatch" )); + + // A recent observation cannot excuse the wrong branch, a missing HEAD, + // another Repository or unobserved cleanliness. Each input isolates one + // externally observed source requirement. + workdir.current_ref = Some("abc123".into()); + workdir.observed_at_epoch_seconds = Some(u64::MAX); + for (repository_id, selector, head, cleanliness, expected_code) in [ + ( + "other-repository", + "work/T-549", + Some("abc123"), + Some("clean"), + "source_workdir_repository_mismatch", + ), + ( + "repository-1", + "other-branch", + Some("abc123"), + Some("clean"), + "source_workdir_ref_mismatch", + ), + ( + "repository-1", + "work/T-549", + None, + Some("clean"), + "source_workdir_ref_mismatch", + ), + ( + "repository-1", + "work/T-549", + Some("abc123"), + None, + "source_workdir_dirty", + ), + ] { + workdir.repository_id = repository_id.into(); + workdir.current_selector = Some(selector.into()); + workdir.current_ref = head.map(str::to_owned); + workdir.cleanliness = cleanliness.map(str::to_owned); + let diagnostic = validate_assigned_workdir_source( + &workdir, + "repository-1", + "refs/heads/work/T-549", + "abc123", + ) + .unwrap_err(); + assert_eq!(diagnostic.code, expected_code); + } } fn completed_upload_file(sha256: &str) -> protocol::UploadedFileRef { @@ -40003,9 +40059,7 @@ mod tests { created["behavior_md"], "Prefer explicit evidence.\nAsk when uncertain." ); - assert!(created.get("behavior_revision").is_none()); assert_eq!(created["state"], "active"); - assert!(created.get("store_revision").is_none()); assert!(created.get("current_worker").is_none()); let created_id = created["id"].as_str().unwrap(); let persisted = store.subject(created_id).unwrap().unwrap(); @@ -40040,7 +40094,6 @@ mod tests { ) .await; assert_eq!(updated["behavior_md"], "Updated without rewriting history."); - assert!(updated.get("behavior_revision").is_none()); request_json_authenticated( app.clone(), "PATCH", @@ -40066,7 +40119,6 @@ mod tests { ) .await; assert_eq!(cleared["behavior_md"], ""); - assert!(cleared.get("behavior_revision").is_none()); assert!( api.store .current_worker_singleton_owner( @@ -42897,7 +42949,7 @@ mod tests { ); let unknown_identity = RuntimeIdentityMaterial::generate("unknown-runtime").unwrap(); - let unknown_revision = create_remote_runtime( + let unknown_key_generation = create_remote_runtime( State(api.clone()), Extension(test_owner_actor()), Json(CreateRemoteRuntimeRequest { @@ -42914,7 +42966,7 @@ mod tests { .await .unwrap_err(); assert_eq!( - unknown_revision.into_response().status(), + unknown_key_generation.into_response().status(), StatusCode::CONFLICT ); @@ -44249,7 +44301,7 @@ mod tests { } #[tokio::test] - async fn ticket_checker_burst_keeps_excess_revisions_durable_and_drains_them() { + async fn ticket_checker_burst_keeps_excess_content_checks_durable_and_drains_them() { let temp = tempfile::tempdir().unwrap(); let (api, _execution) = test_api_with_recording_backend(temp.path()).await; let source = spawn_ticket_check_source(&api, "ticket-check-burst-author"); @@ -44318,7 +44370,7 @@ mod tests { ); // Restart reconciliation is idempotent for running attempts and retains - // the queued revisions until a slot becomes available. + // the queued content checks until a slot becomes available. api.recover_backend_jobs().unwrap(); let mut completed = std::collections::BTreeSet::new(); for _ in 0..300 { @@ -44421,7 +44473,7 @@ mod tests { ]; let mut checked_inputs = Vec::new(); for (ticket, source, expected_body) in snapshots { - let revision = ticket_item_checker::content_digest(&ticket); + let content_digest = ticket_item_checker::content_digest(&ticket); let mut jobs = ticket_check_jobs_for_snapshot(&api, &ticket); assert_eq!( jobs.len(), @@ -44430,7 +44482,7 @@ mod tests { ); let job = jobs.pop().unwrap(); let input = ticket_item_checker::parse_input(&job.request.input).unwrap(); - assert_eq!(input.content_digest, revision); + assert_eq!(input.content_digest, content_digest); assert_eq!(input.body, expected_body); assert_eq!(job.request.source_worker.as_ref(), Some(&source)); assert_eq!(job.request.notification_target.as_ref(), Some(&source)); @@ -44468,7 +44520,7 @@ mod tests { let source = spawn_ticket_check_source(&api, "ticket-check-editor"); let backend = browser_ticket_backend(&api).unwrap(); let ticket_ref = backend - .create(ticket::NewTicket::new("Checker revision fencing")) + .create(ticket::NewTicket::new("Checker content freshness")) .unwrap(); let edit = |body: &str| TicketBackendOperation::EditItem { @@ -44563,7 +44615,7 @@ mod tests { assert_eq!(stale_delivery.state, BackendJobDeliveryState::Completed); assert_eq!( stale_delivery.failure_category.as_deref(), - Some("notification_suppressed_stale_revision") + Some("notification_suppressed_stale_ticket_content") ); api.accept_backend_job_result( @@ -48559,11 +48611,11 @@ mod tests { ); let Json(replayed) = call(request.clone()).await.unwrap(); assert_eq!(replayed.events, completed.events); - for field in ["reason", "revision", "state", "references"] { + for field in ["reason", "content_digest", "state", "references"] { let mut changed = request.clone(); match field { "reason" => changed.reason = "different judgment".into(), - "revision" => changed.expected_content_digest = "stale".into(), + "content_digest" => changed.expected_content_digest = "stale".into(), "state" => changed.expected_state = TicketWorkflowState::Planning, "references" => changed.references.push(ticket::TicketReference { kind: "document".into(), @@ -49989,7 +50041,7 @@ mod tests { Json(server_api::BrowserTransitionTicketStateRequest { state: server_api::BrowserTicketWorkflowState::Done, operation_key: "stale".into(), - expected_content_digest: "stale-revision".into(), + expected_content_digest: "stale-content-digest".into(), expected_state: server_api::BrowserTicketWorkflowState::Planning, reason: "Stale decision".into(), body: None, @@ -52440,7 +52492,7 @@ mod tests { .uri(format!("/api/w/{TEST_WORKSPACE_ID}/workers/remove")) .header(CONTENT_TYPE, "application/json") .body(Body::from( - r#"{"target_runtime_id":"runtime-target","target_worker_id":"target-worker","expected_worker_revision":"revision-1","reason":"retire target Worker","source_proof":"browser-controlled","actor":"orchestrator","policy":"purge"}"#, + r#"{"target_runtime_id":"runtime-target","target_worker_id":"target-worker","reason":"retire target Worker","source_proof":"browser-controlled","actor":"orchestrator","policy":"purge"}"#, )) .unwrap(), ) @@ -54116,7 +54168,7 @@ mod tests { restarted .workdir_catalog_page(TEST_WORKSPACE_ID, 100, None) .unwrap() - .revision + .catalog_digest ); let no_worker = runtime_source_request(&identity, None, "GET", &base, Vec::new()); assert_eq!( @@ -54559,7 +54611,7 @@ mod tests { assert_eq!(page.next_offset, None); assert_eq!( page.digest, first.digest, - "lookup does not narrow the collection revision" + "lookup does not narrow the collection digest" ); let foreign_worker_id = seed_cleanup_worker(api, 902, "normal"); @@ -54621,7 +54673,7 @@ mod tests { .unwrap(); assert_eq!( unchanged, first, - "another caller's connection cannot change this revision" + "another caller's connection cannot change this digest" ); api.store .detach_worker_workdir_connection( @@ -54657,8 +54709,8 @@ mod tests { assert_eq!(capability_changed.items, reattached.items); assert_ne!(capability_changed.digest, reattached.digest); let restarted = SqliteWorkspaceStore::open(&api.config.database_path).unwrap(); - let (restarted_page, restarted_revision) = restarted - .list_worker_workdir_links_page_with_revision( + let (restarted_page, restarted_digest) = restarted + .list_worker_workdir_links_page_with_digest( TEST_WORKSPACE_ID, &fixture.worker, 51, @@ -54667,8 +54719,8 @@ mod tests { ) .unwrap(); assert_eq!(restarted_page.len(), 51); - assert_eq!(restarted_revision, capability_changed.digest); - // A SQLite backup restores the same authoritative content/revision, without a sync store. + assert_eq!(restarted_digest, capability_changed.digest); + // A SQLite backup restores the same authoritative content/digest, without a sync store. let source = rusqlite::Connection::open(&api.config.database_path).unwrap(); let mut snapshot = rusqlite::Connection::open_in_memory().unwrap(); rusqlite::backup::Backup::new(&source, &mut snapshot) @@ -54678,7 +54730,7 @@ mod tests { let restored = SqliteWorkspaceStore::from_connection(snapshot).unwrap(); assert_eq!( restored - .list_worker_workdir_links_page_with_revision( + .list_worker_workdir_links_page_with_digest( TEST_WORKSPACE_ID, &fixture.worker, 51, @@ -54686,10 +54738,10 @@ mod tests { None ) .unwrap(), - (restarted_page, restarted_revision) + (restarted_page, restarted_digest) ); // Independent SQLite connections race a last-row capability update against exact lookup. - // The page and complete revision must always describe the same snapshot, not two reads. + // The page and complete collection digest must always describe the same snapshot, not two reads. let mut read_write_links = api .store .list_worker_workdir_links(TEST_WORKSPACE_ID, &fixture.worker) @@ -54710,8 +54762,8 @@ mod tests { &read_write_links, ) .unwrap(); - let (_, read_write_revision) = restarted - .list_worker_workdir_links_page_with_revision( + let (_, read_write_digest) = restarted + .list_worker_workdir_links_page_with_digest( TEST_WORKSPACE_ID, &fixture.worker, 1, @@ -54719,7 +54771,7 @@ mod tests { Some(&replacement.connection_id), ) .unwrap(); - assert_ne!(read_write_revision, capability_changed.digest); + assert_ne!(read_write_digest, capability_changed.digest); let reader = SqliteWorkspaceStore::open(&api.config.database_path).unwrap(); let barrier = Arc::new(std::sync::Barrier::new(2)); std::thread::scope(|scope| { @@ -54743,8 +54795,8 @@ mod tests { }); barrier.wait(); for _ in 0..100 { - let (page, revision) = reader - .list_worker_workdir_links_page_with_revision( + let (page, digest) = reader + .list_worker_workdir_links_page_with_digest( TEST_WORKSPACE_ID, worker, 1, @@ -54753,13 +54805,13 @@ mod tests { ) .unwrap(); assert_eq!(page.len(), 1); - if revision == read_write_revision { + if digest == read_write_digest { assert_eq!( page[0].capabilities, workdir::WorkdirSessionCapabilities::READ_WRITE ); } else { - assert_eq!(revision, capability_changed.digest); + assert_eq!(digest, capability_changed.digest); assert_eq!( page[0].capabilities, workdir::WorkdirSessionCapabilities::READ_ONLY @@ -57895,7 +57947,7 @@ mod tests { } #[tokio::test] - async fn runtime_removal_config_and_revision_guards_preserve_active_trust() { + async fn runtime_removal_config_and_binding_identity_guards_preserve_active_trust() { let root = tempfile::tempdir().unwrap(); let api = test_api(root.path()).await; let original = register_test_runtime(&api, "guarded-runtime").await; @@ -61450,14 +61502,14 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); .await; let committed_digest = committed["snapshot"]["digest"].as_str().unwrap(); - let revision_snapshot = get_json( + let config_snapshot = get_json( app.clone(), &format!("{source_tree_path}/history/{committed_digest}"), ) .await; - assert_eq!(revision_snapshot["digest"], committed_digest); + assert_eq!(config_snapshot["digest"], committed_digest); assert_eq!( - revision_snapshot["entries"]["notes/readme.txt"]["content"], + config_snapshot["entries"]["notes/readme.txt"]["content"], "nested entry" ); @@ -61484,7 +61536,6 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); let path = format!("/api/w/{TEST_WORKSPACE_ID}/settings/profiles"); let settings = get_json(app.clone(), &path).await; assert_eq!(settings["default_profile"], "builtin:companion"); - assert!(settings.get("config_revision").is_none()); assert!(settings["tree_digest"].as_str().is_some()); assert!(settings["projection_digest"].as_str().is_some()); assert!( diff --git a/crates/workspace-server/src/server/subjektiv_job.rs b/crates/workspace-server/src/server/subjektiv_job.rs index 7be86dc41..0fcb072af 100644 --- a/crates/workspace-server/src/server/subjektiv_job.rs +++ b/crates/workspace-server/src/server/subjektiv_job.rs @@ -282,7 +282,7 @@ mod result_budget_tests { let result = serde_json::json!({ "subject_id":"\\\"".repeat(128),"candidate_ids":batch, "candidate_dispositions":dispositions, - "surface":{"availability":"failed","generation_id":"g".repeat(100),"store_revision":u64::MAX,"reason_code":"\\\"".repeat(128)}, + "surface":{"availability":"failed","generation_id":"g".repeat(100),"reason_code":"\\\"".repeat(128)}, }); assert!( serde_json::to_vec(&result).unwrap().len() diff --git a/crates/workspace-server/src/server/tests/ticket_evidence_tests.rs b/crates/workspace-server/src/server/tests/ticket_evidence_tests.rs index 0993681e0..113d6d056 100644 --- a/crates/workspace-server/src/server/tests/ticket_evidence_tests.rs +++ b/crates/workspace-server/src/server/tests/ticket_evidence_tests.rs @@ -19,7 +19,7 @@ const EVIDENCE_SOURCE: &str = "immutable-approved-source"; const EVIDENCE_TARGET: &str = "immutable-target-result"; // Deliberately freeze review dates: freshness must follow the exact item -// revision and source/result snapshot, not a comparison of wall-clock dates. +// content digest and source/result snapshot, not a comparison of wall-clock dates. fn evidence_time() -> chrono::DateTime { "2000-01-01T00:00:00Z".parse().unwrap() } @@ -121,7 +121,7 @@ impl EvidenceApiFixture { } } - fn revision(&self) -> String { + fn content_digest(&self) -> String { ticket_item_checker::content_digest( &self.backend.show(self.ticket_id.clone().into()).unwrap(), ) @@ -150,7 +150,7 @@ impl EvidenceApiFixture { .request_review(RequestMergeRequestReview { merge_request_id: EVIDENCE_MR.into(), ticket_id: self.ticket_id.clone(), - ticket_content_digest: self.revision(), + ticket_content_digest: self.content_digest(), ticket_merge_request_subjects: self.subjects(), subject_ref: EVIDENCE_SOURCE.into(), child_session_id, @@ -194,7 +194,7 @@ impl EvidenceApiFixture { fn completion(&self, _approval: &ReviewEvent) -> ticket::TicketCompletion { ticket::TicketCompletion { operation_key: "evidence-ticket-completion".into(), - expected_content_digest: self.revision(), + expected_content_digest: self.content_digest(), expected_state: TicketWorkflowState::InProgress, reason: "Implementation judged complete independently of MR attestation".into(), references: Vec::new(), @@ -287,7 +287,7 @@ impl EvidenceApiFixture { self.ticket_id.clone().into(), TicketItemEdit { body: Some(MarkdownText::new( - "Revised requirements require a fresh exact-revision approval.", + "Revised requirements require a fresh exact-content approval.", )), ..Default::default() }, @@ -301,12 +301,12 @@ async fn done_merged_remote_ticket_uses_immutable_result_in_show_query_list_and_ let fixture = EvidenceApiFixture::new().await; let approval = fixture.approve("integration"); let merge = fixture.integrate(&approval); - let revision = fixture.revision(); + let content_digest = fixture.content_digest(); let completion = fixture .backend .complete(&fixture.ticket_id, fixture.completion(&approval)) .unwrap(); - assert_eq!(ticket::ticket_content_digest(&completion), revision); + assert_eq!(ticket::ticket_content_digest(&completion), content_digest); assert!( fixture .api @@ -326,7 +326,7 @@ async fn done_merged_remote_ticket_uses_immutable_result_in_show_query_list_and_ let shown = fixture.show().await; assert_eq!(shown.state, "done"); - assert_eq!(shown.content_digest, revision); + assert_eq!(shown.content_digest, content_digest); assert!( shown.evidence.complete_for_integration, "{:?}", @@ -376,13 +376,13 @@ async fn done_merged_remote_ticket_uses_immutable_result_in_show_query_list_and_ } #[tokio::test] -async fn latest_postmerge_approval_requires_exact_revision_and_stored_source_result_snapshot() { +async fn latest_postmerge_approval_requires_exact_content_and_stored_source_result_snapshot() { let fixture = EvidenceApiFixture::new().await; let integration = fixture.approve("integration"); let merge = fixture.integrate(&integration); fixture.rescope(); - let revision = fixture.revision(); - assert_ne!(integration.ticket_content_digest, revision); + let content_digest = fixture.content_digest(); + assert_ne!(integration.ticket_content_digest, content_digest); let stale = fixture.show().await; assert!(stale.evidence.approved_current_subject); // integration proof survives @@ -405,9 +405,9 @@ async fn latest_postmerge_approval_requires_exact_revision_and_stored_source_res ); assert_eq!(fixture.show().await.state, "inprogress"); - let latest = fixture.approve("latest-revision"); + let latest = fixture.approve("latest-content"); assert_eq!(latest.created_at, integration.created_at); - assert_eq!(latest.ticket_content_digest, revision); + assert_eq!(latest.ticket_content_digest, content_digest); assert_eq!(latest.ticket_merge_request_subjects, fixture.subjects()); let fresh = fixture.show().await; assert!( @@ -420,7 +420,7 @@ async fn latest_postmerge_approval_requires_exact_revision_and_stored_source_res Some("Requirement approval integration") ); fixture.assert_filters(true, false, false).await; - // A current revision alone is insufficient: simulate a stored attestation + // A current Ticket content digest alone is insufficient: simulate a stored attestation // against a different result set using typed serialized payloads in this // temporary fixture database, never a live Workspace database. let mut wrong_snapshot = latest.clone(); @@ -490,7 +490,7 @@ async fn latest_postmerge_approval_requires_exact_revision_and_stored_source_res .backend .complete(&fixture.ticket_id, fixture.completion(&repaired)) .unwrap(); - assert_eq!(ticket::ticket_content_digest(&completion), revision); + assert_eq!(ticket::ticket_content_digest(&completion), content_digest); assert_eq!(fixture.show().await.state, "done"); fixture.assert_filters(true, false, false).await; } @@ -649,7 +649,7 @@ async fn closing_ticket_with_open_unreviewed_mr_does_not_change_its_proof_or_sta &fixture.ticket_id, ticket::TicketStateUpdate { operation_key: "close-with-open-mr".into(), - expected_content_digest: fixture.revision(), + expected_content_digest: fixture.content_digest(), expected_state: TicketWorkflowState::InProgress, state: TicketWorkflowState::Closed, reason: "The request was withdrawn; no integration or approval is asserted".into(), diff --git a/crates/workspace-server/src/server/tests/ticket_worker_claim_tests.rs b/crates/workspace-server/src/server/tests/ticket_worker_claim_tests.rs index 26bff24e5..3e8a47a1b 100644 --- a/crates/workspace-server/src/server/tests/ticket_worker_claim_tests.rs +++ b/crates/workspace-server/src/server/tests/ticket_worker_claim_tests.rs @@ -458,7 +458,7 @@ async fn pending_manual_worker_claim_reconciles_same_runtime_restore_and_commits ), ) .unwrap(); - let admitted_revision = api.authority.ticket(&ticket_id).unwrap().content_digest; + let admitted_content_digest = api.authority.ticket(&ticket_id).unwrap().content_digest; let request = server_api::SetTicketRoleAssignmentRequest { operation_id: "claim-runtime-reconciliation".into(), principal: server_api::TicketAssignmentPrincipal::Worker { @@ -515,7 +515,7 @@ async fn pending_manual_worker_claim_reconciles_same_runtime_restore_and_commits .get_ticket_claim_recovery(TEST_WORKSPACE_ID, &request.operation_id) .unwrap() .unwrap(); - assert_eq!(recovery.content_digest, admitted_revision); + assert_eq!(recovery.content_digest, admitted_content_digest); assert!(recovery.selected.is_empty()); assert!(recovery.original_links.is_empty()); assert!(recovery.effective_links.is_empty()); @@ -572,7 +572,7 @@ async fn pending_manual_worker_claim_reconciles_same_runtime_restore_and_commits ); assert_eq!( api.authority.ticket(&ticket_id).unwrap().content_digest, - admitted_revision + admitted_content_digest ); assert_eq!( api.config_store diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs index df5489aba..d6414a8f1 100644 --- a/crates/workspace-server/src/server/tests/value_profiles_tests.rs +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -438,7 +438,7 @@ async fn value_profiles_save_project_and_runtime_consume_the_same_content() { "omission must remain omission in the Profile value" ); let mut snapshot = serde_json::to_value(&resolved).unwrap(); - snapshot.as_object_mut().unwrap().remove("profile"); // revision-specific provenance only + snapshot.as_object_mut().unwrap().remove("profile"); // snapshot-specific provenance only manifests.push(snapshot); if index == 0 { saved_manifest = @@ -785,7 +785,7 @@ async fn value_profiles_legacy_source_config_requires_explicit_lossless_migratio old_schema, ) .unwrap(); - // Fixture simulates a previously persisted v1 schema/revision. + // Fixture simulates a previously persisted v1 schema/config snapshot. let legacy = api .config_store .commit_evaluated_workspace_config(TEST_WORKSPACE_ID, &candidate) diff --git a/crates/workspace-server/src/server/worker_operations.rs b/crates/workspace-server/src/server/worker_operations.rs index 3f8419430..fe84fb7f2 100644 --- a/crates/workspace-server/src/server/worker_operations.rs +++ b/crates/workspace-server/src/server/worker_operations.rs @@ -434,7 +434,7 @@ impl WorkspaceWorker { }) } - /// The caller owns the full cleanup-plan revision/digest check. The removal service + /// The caller owns the full cleanup-plan digest check. The removal service /// rechecks live assignment/retention/use at its existing commit boundary. pub(super) async fn delete_from_plan( &self, diff --git a/crates/workspace-server/src/skills.rs b/crates/workspace-server/src/skills.rs index ebfe890b6..f5cee29a1 100644 --- a/crates/workspace-server/src/skills.rs +++ b/crates/workspace-server/src/skills.rs @@ -217,7 +217,7 @@ fn merged_skills( source_diagnostic = Some(SkillDiagnostic::error( "skill_source_mismatch", format!( - "Skill `{name}` must be the imported value of `{canonical_path}` in the active config revision" + "Skill `{name}` must be the imported value of `{canonical_path}` in the active config snapshot" ), Some(format!("workspace:{name}")), )); @@ -233,7 +233,7 @@ fn merged_skills( } else { source_diagnostic = Some(SkillDiagnostic::error( "missing_skill_source", - format!("Skill `{name}` requires `{canonical_path}` in the active config revision"), + format!("Skill `{name}` requires `{canonical_path}` in the active config snapshot"), Some(format!("workspace:{name}")), )); } diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index 6c9460485..3db49cc5d 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -28,7 +28,7 @@ use crate::workspace_deletion::WorkspaceDeletionStore; use crate::{Error, Result}; const OLDEST_SCHEMA_VERSION: i64 = 50; -const LATEST_SCHEMA_VERSION: i64 = 88; +const LATEST_SCHEMA_VERSION: i64 = 89; const WORKSPACE_DRIVE_GRANTS_MIGRATION_NAME: &str = "durable Workspace Drive grants"; const WORKSPACE_CONFIG_GRANTS_MIGRATION_NAME: &str = "Workspace config grants and logical Workdirs"; const SCHEMA_BASELINE_NAME: &str = "workspace schema baseline"; @@ -276,6 +276,11 @@ const MIGRATIONS: &[Migration] = &[ name: "Runtime binding identities and retention content contracts", apply: migrate_runtime_bindings_v87_to_v88, }, + Migration { + version: 89, + name: "descriptive legacy evidence columns", + apply: migrate_evidence_column_names_v88_to_v89, + }, ]; #[derive(Clone, Copy)] @@ -956,7 +961,7 @@ pub struct WorkdirRegistryRecord { pub updated_at: String, } -/// Projection inputs captured together with the catalog revision in one SQLite snapshot. +/// Projection inputs captured together with the catalog digest in one SQLite snapshot. #[derive(Debug, Clone)] pub struct WorkdirCatalogEntry { pub record: WorkdirRegistryRecord, @@ -970,7 +975,9 @@ pub struct WorkdirCatalogEntry { pub struct WorkdirCatalogPage { pub entries: Vec, pub next_cursor: Option, - pub revision: String, + /// SHA-256 of the full safe public catalog projection, independent of this page. + /// The entries and digest are read in one SQLite transaction. + pub catalog_digest: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -2126,7 +2133,7 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore { offset: u32, connection_id: Option<&str>, ) -> Result>; - fn list_worker_workdir_links_page_with_revision( + fn list_worker_workdir_links_page_with_digest( &self, workspace_id: &str, worker: &RuntimeWorkerRef, @@ -6760,7 +6767,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job attempt `{attempt_id}`")))?; if attempt.attempt != job.current_attempt || attempt.input_digest != job.request.input_digest()? { return Err(Error::InvalidInput( - "Backend Job attempt is stale for the current input revision".to_string(), + "Backend Job attempt is stale for the current input digest".to_string(), )); } if let Some(bound) = &attempt.worker { @@ -6875,7 +6882,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { || attempt.attempt != job.current_attempt { return Err(Error::InvalidInput( - "Backend Job result is stale for the current attempt or input revision".to_string(), + "Backend Job result is stale for the current attempt or input digest".to_string(), )); } if attempt.worker.as_ref() != Some(source_worker) { @@ -9369,7 +9376,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { Ok(WorkdirCatalogPage { entries, next_cursor, - revision: hasher.finalize().iter().map(|byte| format!("{byte:02x}")).collect::(), + catalog_digest: hasher.finalize().iter().map(|byte| format!("{byte:02x}")).collect::(), }) }) } @@ -10421,7 +10428,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { offset: u32, connection_id: Option<&str>, ) -> Result> { - self.list_worker_workdir_links_page_with_revision( + self.list_worker_workdir_links_page_with_digest( workspace_id, worker, limit, @@ -10431,7 +10438,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { .map(|(links, _)| links) } - fn list_worker_workdir_links_page_with_revision( + fn list_worker_workdir_links_page_with_digest( &self, workspace_id: &str, worker: &RuntimeWorkerRef, @@ -12636,7 +12643,7 @@ struct FrozenWorkspaceRuntimeBinding { base_url: String, public_key: String, public_key_fingerprint: String, - binding_revision: u64, + legacy_counter: u64, state: WorkspaceRuntimeBindingState, authentication_mode: WorkspaceRuntimeAuthenticationMode, workspace_key_id: Option, @@ -12666,7 +12673,7 @@ fn read_frozen_workspace_runtime_binding( base_url: row.get(3)?, public_key: row.get(4)?, public_key_fingerprint: row.get(5)?, - binding_revision: row.get(6)?, + legacy_counter: row.get(6)?, state, authentication_mode, workspace_key_id: row.get(9)?, @@ -12681,6 +12688,7 @@ include!("frozen_flow_content_migration.rs"); include!("frozen_content_state_migration.rs"); include!("frozen_repository_key_migration.rs"); include!("frozen_runtime_binding_migration.rs"); +include!("legacy_evidence_column_migration.rs"); fn prepare_connection(conn: &Connection) -> Result<()> { prepare_connection_with_secret_source( @@ -12971,15 +12979,14 @@ fn verify_workspace_runtime_binding_schema(conn: &Connection) -> Result<()> { )); } } - let revision_default = conn.query_row( + let legacy_counter_default = conn.query_row( "SELECT dflt_value FROM pragma_table_info('workspace_runtime_bindings') WHERE name = 'binding_revision'", [], |row| row.get::<_, Option>(0), )?; - if revision_default.as_deref() != Some("1") { + if legacy_counter_default.as_deref() != Some("1") { return Err(Error::Store( - "workspace_runtime_bindings binding_revision default does not match schema-52" - .to_string(), + "legacy Runtime binding counter default must equal one for schema-52".to_string(), )); } let sql = conn.query_row( @@ -13072,7 +13079,7 @@ fn verify_workspace_runtime_binding_schema(conn: &Connection) -> Result<()> { base_url: row.get(3)?, public_key: row.get(4)?, public_key_fingerprint: row.get(5)?, - binding_revision: row.get(6)?, + legacy_counter: row.get(6)?, state: if row.get::<_, Option>(9)?.is_some() { WorkspaceRuntimeBindingState::Revoked } else { @@ -14647,7 +14654,7 @@ mod tests { use super::*; #[test] - fn schema_v74_migrates_historical_worker_projection_rows_without_revision_state() { + fn schema_v74_migrates_historical_worker_projection_rows_without_counter_state() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("workspace.db"); drop(SqliteWorkspaceStore::open(&path).unwrap()); @@ -16326,6 +16333,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx include!("schema_equivalence_tests.rs"); include!("migration_entrypoint_tests.rs"); + include!("legacy_evidence_column_tests.rs"); fn prepare_retained_schema(path: &Path, version: i64) { prepare_schema_v52(path); @@ -16638,6 +16646,10 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx version: 88, name: "Runtime binding identities and retention content contracts".to_string() }, + WorkspaceSchemaMigrationStep { + version: 89, + name: "descriptive legacy evidence columns".to_string() + }, ] ); @@ -16736,6 +16748,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx (86, "content-fenced config, Memory settings and Jobs".to_string()), (87, "Repository SSH key identities and encrypted operation objects".to_string()), (88, "Runtime binding identities and retention content contracts".to_string()), +(89, "descriptive legacy evidence columns".to_string()), ] ); assert!(!table_exists(conn, "trusted_runtime_records")?); @@ -16954,14 +16967,14 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx workspace_id, job_id, purpose, input_revision, input_ref, request_json, intent_fingerprint, state, current_attempt, created_at, updated_at ) VALUES ( - 'workspace-a', 'job-a', 'check', 'revision-a', 'test://job-a', '{}', + 'workspace-a', 'job-a', 'check', 'legacy-input-token', 'test://job-a', '{}', 'sha256:test', 'pending', 1, '1', '1' ); INSERT INTO backend_job_attempts ( workspace_id, job_id, attempt_id, attempt, input_revision, state, deadline_at, created_at, updated_at ) VALUES ( - 'workspace-a', 'job-a', 'job-a:attempt:1', 1, 'revision-a', 'reserved', + 'workspace-a', 'job-a', 'job-a:attempt:1', 1, 'legacy-input-token', 'reserved', '2', '1', '1' ); INSERT INTO backend_job_deliveries ( @@ -17659,7 +17672,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx } #[test] - fn workspace_runtime_verification_is_revision_bound_and_restart_safe() { + fn workspace_runtime_verification_is_key_bound_and_restart_safe() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); let store = SqliteWorkspaceStore::open(&path).unwrap(); @@ -18063,7 +18076,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx } #[test] - fn runtime_binding_key_mutations_are_revisioned_idempotent_and_audited() { + fn runtime_binding_key_mutations_are_identity_guarded_idempotent_and_audited() { let store = SqliteWorkspaceStore::in_memory().unwrap(); store .with_conn(|conn| { @@ -18444,7 +18457,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx } #[tokio::test] - async fn workspace_display_name_update_is_revision_guarded_and_preserves_identity() { + async fn workspace_display_name_update_is_timestamp_guarded_and_preserves_identity() { let dir = tempfile::tempdir().unwrap(); let database_path = dir.path().join("server.db"); let store = SqliteWorkspaceStore::open(&database_path).unwrap(); @@ -23188,7 +23201,7 @@ INSERT INTO worker_registry ( let mut refresh = resource_job_request( &format!("surface-{surface_state}"), &subject, - "surface-revision", + "surface-generation", ); refresh .grants @@ -23435,7 +23448,7 @@ INSERT INTO worker_registry ( let request = BackendJobRequest { job_id: "check:T-1:r1".to_string(), purpose: "ticket_item_check".to_string(), - input_ref: "ticket://T-1/revisions/1".to_string(), + input_ref: "ticket://T-1/contents/content-1".to_string(), input: serde_json::json!({"title": "check"}), instruction: "Check the immutable input.".to_string(), profile: "builtin:backend-job".to_string(), diff --git a/crates/workspace-server/src/workdir_create_operations.rs b/crates/workspace-server/src/workdir_create_operations.rs index b30fe51fe..a41fc8ca7 100644 --- a/crates/workspace-server/src/workdir_create_operations.rs +++ b/crates/workspace-server/src/workdir_create_operations.rs @@ -99,8 +99,8 @@ pub fn request_fingerprint_for_replay( return Ok(current); } if let Some(legacy) = persisted.strip_prefix("workdir-create-v86:") { - if let Some((revision, digest)) = legacy.split_once(':') { - if let Ok(revision) = revision.parse::() { + if let Some((legacy_counter, digest)) = legacy.split_once(':') { + if let Ok(legacy_counter) = legacy_counter.parse::() { let mut hasher = request_hasher( repository_id, selector, @@ -108,7 +108,7 @@ pub fn request_fingerprint_for_replay( display_name, repository_source_fingerprint, ); - hasher.update(revision.to_be_bytes()); + hasher.update(legacy_counter.to_be_bytes()); if encode_request_digest(hasher) == digest { return Ok(persisted.to_string()); } diff --git a/crates/workspace-server/src/workspace_signing_identity.rs b/crates/workspace-server/src/workspace_signing_identity.rs index b09fdec84..bf58d407d 100644 --- a/crates/workspace-server/src/workspace_signing_identity.rs +++ b/crates/workspace-server/src/workspace_signing_identity.rs @@ -357,7 +357,8 @@ mod frozen_material_v1 { version: u32, workspace_id: String, key_id: String, - revision: u64, + #[serde(rename = "revision")] + legacy_counter: u64, private_key: String, } @@ -369,7 +370,7 @@ mod frozen_material_v1 { pub(super) fn decode(bytes: &[u8]) -> Option { let old: Material = serde_json::from_slice(bytes).ok()?; - if old.version != 1 || old.revision != 1 { + if old.version != 1 || old.legacy_counter != 1 { return None; } Some(WorkspaceSigningPrivateMaterial { @@ -415,8 +416,8 @@ mod frozen_material_v1 { let material = WorkspaceSigningPrivateMaterial::generate("ws-1", "WK-1").unwrap(); let mut old = serde_json::to_value(material).unwrap(); old["version"] = 1.into(); - for revision in [0, 2, u64::MAX] { - old["revision"] = revision.into(); + for legacy_counter in [0, 2, u64::MAX] { + old["revision"] = legacy_counter.into(); assert!(decode_private_material(&serde_json::to_vec(&old).unwrap()).is_err()); } old["revision"] = 1.into(); diff --git a/docs/design/standalone-subjektiv.md b/docs/design/standalone-subjektiv.md index a23fed143..a98dec422 100644 --- a/docs/design/standalone-subjektiv.md +++ b/docs/design/standalone-subjektiv.md @@ -24,8 +24,8 @@ The validation counts and former API terminology below record T-709 at the cited - `worker::subjektiv` exposes a nonserializable live Host capability plus a separate attenuated consolidation capability. Backend creation still requires the actual authenticated client and bound snapshot. Local creation is explicit Host injection; unavailable Backend never falls back locally. - The local catalog keeps private scope/language settings and common Feature storage under `subjektiv/`, separately from generic Subject Job ledgers. The common attribution table's existing `runtime_id` lookup key is the local storage namespace for local rows, not a running Runtime identity; public local evidence leaves `runtime_id`, Workspace, and Account fields absent. Worker IDs, Subject binding, and operator-managed scope must all match before Session content is read. - The shared strict FsStore public index is also used for committed evidence admission. Stable references are intersected with visible committed entries; numeric hints accept the two existing capture producers. Open-Run evidence is deferred only before a request, never exposed by Session tools or recorded before commit. Persisted local human provenance is distinct from Backend Account provenance and maps to the existing public `HumanInput` class. -- Standalone integration tests use real Hosts/SQLite, scripted extraction/consolidation/surface models, and test-owned second processes. Nine cases cover inert/disabled policy, reopen, saved-binding restore, cross-Worker recall/source discovery, exact explicit commit staging, normal extract/pointer → generic Job → CAS apply → resident surface, same-Subject cross-process exclusion/abnormal-exit recovery, and transport loss after apply followed by surface-only repair without duplicate Memory revision. -- Generic Job tests additionally cover immutable grant persistence, missing/foreign/revision/consumer grants, per-attempt/deadline revocation, cancellation, timeout, cleanup failure, and stale capabilities. Session adapter tests cover no-candidate discovery, foreign Subject/scope/Worker denial, bounded UTF-8 and JSON output, filter/generation-bound cursors, missing/corrupt storage, and hidden/uncommitted/partial-tail exclusion without observation writes. +- Standalone integration tests use real Hosts/SQLite, scripted extraction/consolidation/surface models, and test-owned second processes. Nine cases cover inert/disabled policy, reopen, saved-binding restore, cross-Worker recall/source discovery, exact explicit commit staging, normal extract/pointer → generic Job → CAS apply → resident surface, same-Subject cross-process exclusion/abnormal-exit recovery, and transport loss after apply followed by surface-only repair without duplicate immutable Memory change. +- Generic Job tests additionally cover immutable grant persistence, missing grants, foreign grants, mismatched input bindings and wrong-consumer grants, per-attempt/deadline revocation, cancellation, timeout, cleanup failure, and stale capabilities. Session adapter tests cover no-candidate discovery, foreign Subject/scope/Worker denial, bounded UTF-8 and JSON output, filter/generation-bound cursors, missing/corrupt storage, and hidden/uncommitted/partial-tail exclusion without observation writes. Verified commands: diff --git a/docs/development/ui-ux/subject-memory-information.md b/docs/development/ui-ux/subject-memory-information.md index 40184fe80..9040405df 100644 --- a/docs/development/ui-ux/subject-memory-information.md +++ b/docs/development/ui-ux/subject-memory-information.md @@ -1,10 +1,10 @@ # Subject / Memory information audit (T-692) -This document records T-692's UI decisions and validation at the commits cited below. Its counter labels and fixture values describe that historical implementation, not the current API. Current Memory uses opaque `change_id` references, `memory_fingerprint` and `built_from_memory_fingerprint`; see [the current store contract](../../design/subjektiv-store.md). Historical observations and test results below are retained unchanged. +This document records T-692's UI decisions and validation at the commits cited below. Its counter values describe that historical implementation, not the current API. Obsolete UI labels are paraphrased by purpose below, not quoted as current copy. Current Memory uses immutable `change_id` references, `memory_fingerprint` and `built_from_memory_fingerprint`; see [the current store contract](../../design/subjektiv-store.md). Historical validation results are not new executions. ## Decision and scope -The previous Subjects index exposed `Store revision` alongside identity, state and +The previous Subjects index exposed a committed-Memory update counter alongside identity, state and update time; Subject detail led with technical metadata and raw surface states. These numbers require implementation knowledge before they help a user choose a Subject or understand what is available. The revised hierarchy starts with @@ -23,22 +23,22 @@ surface does not attest what is already in any running Worker's history. | --- | --- | --- | | Subject role and ID | Normal identification. Similar or identical roles need a stable identifier for selection and comparison. | Keep role as heading and the complete wrapping ID visible in index and detail. | | Subject active/retired state | Normal operation decision: whether a new Worker connection can use the Subject. | Replace raw `active` with `Available`; keep `Retired`. Detail explains connection eligibility, not an assertion that a Worker is running. | -| Current Worker | Normal connection decision, derived from the existing Backend singleton projection. | Promote `Connected` / `Not connected` and display name. Do not infer ownership from store revision or claim all Workers received current Memory. Worker start remains the ordinary launch form. | -| Committed-Memory presence | Normal reading decision, independent from surface generation or candidates. | Show `Available` / `None` only on the successful first bounded page. On later pages say `N on this page`; errors say `Unavailable`. Never infer counts from revision or scan all pages. | +| Current Worker | Normal connection decision, derived from the existing Backend singleton projection. | Promote `Connected` / `Not connected` and display name. Do not infer ownership from historical store counter or claim all Workers received current Memory. Worker start remains the ordinary launch form. | +| Committed-Memory presence | Normal reading decision, independent from surface generation or candidates. | Show `Available` / `None` only on the successful first bounded page. On later pages say `N on this page`; errors say `Unavailable`. Never infer counts from an update counter or scan all pages. | | List counts | Normal navigation context, not a store-wide statistic. | Label `N shown` and `more available`, preserving existing bounded cursor pagination. | | Resident availability | Normal context decision. A missing surface is not missing committed Memory. | `Ready to use`, `Ready, no context`, `Not generated`, `Needs refresh`, `Generation failed`, and request `Unavailable` are distinct. These describe the current generated store artifact, not content truth or Worker-history propagation. | | Resident body and generated time | Normal content/read freshness context. | Keep the shared Markdown renderer, generated date and exact snapshot reference count. Ready-empty explains that a valid snapshot has no body; stale/failed do not display a partial or old snapshot. | -| Subject store revision | Diagnostic only: internal committed-Memory change number, not record count, schema version or quality score. | Remove it from the index's normal columns. Retain the exact value under detail's `Technical details`, with its meaning explained. Backend comparisons are untouched. | +| Historical Subject store counter | Diagnostic only: internal committed-Memory change number, not record count, schema version or quality score. | Remove it from the index's normal columns. Retain the exact value under detail's `Technical details`, with its meaning explained. Backend comparisons are untouched. | | Subject update time | Secondary metadata; not a context-injection or generation completion timestamp. | Label `Last changed`; on detail put it with Subject diagnostics rather than competing with the availability summary. | -| Snapshot ID and built-from revision | Diagnostic correlation, distinct from per-Memory revisions. | Retain under `Surface sources and diagnostics`, labelled `Generated from Subject revision` and explained. | -| Surface Memory references | Detailed audit/reachability. | Preserve reference IDs, immutable revision values and Memory links in the disclosure. | +| Snapshot ID and historical built-from counter | Diagnostic correlation, distinct from individual Memory history entries. | Retain under `Surface sources and diagnostics`, labelled the historical generated-from counter label and explained. | +| Surface Memory references | Detailed audit/reachability. | Preserve reference IDs, immutable history references and Memory links in the disclosure. | | Memory claim, kind, lifecycle state, excerpt and body | Normal content decision. Active, resolved and retracted are not interchangeable. | Retain list scanning and body reading. Keep the Markdown renderer and current/historical distinction. | -| Individual Memory revision and history | Detailed content comparison, also useful for identifying a selected version. | Explicit `Memory revision` labels, separate from Subject counters. Preserve immutable history, selected revision links and revision pagination. | -| Candidate provenance, evidence, origin, source refs and derivations | Detailed audit, required to understand why a record exists. | Retain IDs, ranges, summaries, excerpts, origins and derived-revision links. Counts say `shown` except where the API provides exact per-candidate totals. Preserve body and evidence continuation links. | -| Raw field names / duplicated general revision terminology | Unnecessary in the normal hierarchy; multiple unrelated counters are misleading. | Replace bare revision labels and raw surface enums with purpose-specific labels. Do not delete API fields, stored information, diagnostics or their detailed access. | +| Individual Memory history entries | Detailed content comparison, also useful for identifying a selected version. | The historical UI labelled each Memory history entry separately from Subject counters. Preserve immutable history, selected history-entry links and history pagination. | +| Candidate provenance, evidence, origin, source refs and derivations | Detailed audit, required to understand why a record exists. | Retain IDs, ranges, summaries, excerpts, origins and derived-history-entry links. Counts say `shown` except where the API provides exact per-candidate totals. Preserve body and evidence continuation links. | +| Raw field names / duplicated general update-counter terminology | Unnecessary in the normal hierarchy; multiple unrelated counters are misleading. | Replace bare counter labels and raw surface enums with purpose-specific labels. Do not delete API fields, stored information, diagnostics or their detailed access. | No diagnostic datum was removed from Subject detail, surface detail or Memory -provenance/history. The index no longer shows a store revision, but the exact +provenance/history. The index no longer shows a historical store counter, but the exact value remains one click away in Subject detail. No invented regeneration, editing or recovery action was added; `Needs refresh` does not mean an update job is running. Creation still creates only a Subject, and selection still uses the @@ -55,7 +55,7 @@ The existing real-store regression stages a candidate and attempts an invalid apply. It asserts the candidate has no resolution and the committed Memory list remains empty. The component regression `candidate-only Subjects show no committed Memories, not an inferred candidate count` -uses that read projection (store revision 0, ungenerated surface, empty successful +uses that read projection (historical store counter 0, ungenerated surface, empty successful committed list) and checks `None`, `No committed Memories yet.`, `Not connected` and `Not generated`, with no resident body. Thus a staged candidate does not turn into a committed-Memory count or a claim that context has been generated. @@ -99,7 +99,7 @@ supplement does not change rendered markup, styling, copy or data contracts. - The focused production-shell browser test proves main-owned vertical scrolling with wheel/keyboard/touch, 320px/mobile shell behavior, no page-wide overflow, local table/code scroll, Markdown safety, creation/launch separation, bounded - pagination and Memory/source/revision reachability. + pagination and Memory/source/history-entry reachability. Independent review at source `4cf6c130` ran root `cargo check`, `cargo fmt --all -- --check`, diff hygiene, Web check (0 errors/warnings), 12 diff --git a/docs/development/web-state-preconditions.md b/docs/development/web-state-preconditions.md new file mode 100644 index 000000000..2a678ea2e --- /dev/null +++ b/docs/development/web-state-preconditions.md @@ -0,0 +1,20 @@ +# Web state checks and retained identities + +The Web client uses purpose-specific content and operation identities, not an independent update counter. The checks below describe current boundaries, not a claim that the full cleanup or final generated-contract validation is complete. + +| Boundary | What is compared, how, and when | Why order or time alone is insufficient | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Config source commit | The editor sends its saved `base_digest` and staged changes; update/rename/delete carry each observed entry's `expected_digest`. Local WASM checks entry conditions before replacing local content. The Server owns persistence admission. Success installs the returned snapshot as the new base; rejection retains the draft until explicit reload/reapply. | A later request can be authored from old content. Digests identify the actual source tree/entry, including a return to identical content; wall-clock age is not the condition. | +| Config diagnostics | Before publication the editor requires a non-aborted request and filters diagnostics by the analyzed tree digest. | A late result is not evidence about currently displayed source. Content identity and asynchronous invalidation are different checks. | +| Drive update/relocate/delete | The controller pins `expectedMutationId` from the user's observed node. The adapter sends it unchanged. `workspace-drive::mutate` checks current `last_mutation_id` via `changeable` in the same SQLite transaction before mutation. Publication stores the actual successful request ID and a durable receipt. | This identifies a real mutation, not a separately allocated state number. Content-only equality misses move/rename or ABA return to identical bytes. Last-arriving writes can overwrite another operation's result; timestamps do not prove operation identity. | +| Drive read/preview | Each chunk checks the current node's `last_mutation_id` against the observed operation inside the store transaction. Conflict does not silently switch to latest bytes. | Chunks from different writes must not be combined, regardless of arrival order. | +| Drive response-loss recovery | Before sending, the controller records one pending request ID. Pending/unknown receipts block another write for that selection. Recovery queries the same request. The store checks actor and typed payload against the existing receipt fingerprint before returning its result. | Unknown outcome does not mean the write failed. A fresh ID can duplicate a committed side effect; ordering cannot identify which payload committed. | +| UI asynchronous publication | Selection and write/page invalidation fences are checked before publishing returned data. These are local request-lifetime fences, not persisted resource-state conditions. | The projection follows the latest selected request, not the last response to arrive. No server-visible opaque state number is needed here. | +| Subject behavior save | The client sends exact `expected_behavior_md`, including empty text, with the replacement. A successful response must describe the requested text; mismatch is an unknown outcome and is not automatically retried. | Observed text already supplies the content condition; no extra counter is needed. Application to Worker history is a separate operation. | +| Memory history display | `change_id` and `current_change_id` identify immutable entries. The parser preserves Server order; current/historical is exact identity equality. | Stable links need immutable identities, but IDs do not establish chronology and must not be parsed or sorted as numbers. | +| Runtime trust display | The parser requires verification `binding_id` to equal the displayed binding. Mutations use the observed enrollment identity; revoke confirmation also compares the exact displayed key fingerprint before sending. | Re-enrolling the same public key creates a different binding lifetime. Matching key bytes or a recent response cannot make old verification current. | +| Merge Request review freshness | Review/request `subject_ref` must match current Git source ref. Revoked verdicts are excluded; effective events follow supplied thread order. Merged results use validated immutable integration evidence, not a new live observation. | Source equality identifies what was reviewed. Arrival order or target-only movement cannot confer approval on another source commit. | + +`ConfigProjectionValidator` is a typed static-template-catalog validation policy (`kind`, namespace, key aliases), not an opaque freshness token. Source tree, projection and toolchain digests describe different inputs/results. Key fingerprints identify key bytes; binding IDs identify enrollment lifetimes; mutation IDs identify actual committed requests. These are not interchangeable and are not increasing numbers. + +WIP remains `0.2.0`; naming cleanup alone does not justify a new package/protocol version or generic validator number. Final API/OpenAPI/protocol freshness must be checked after authoritative Rust changes are integrated. Earlier Web build or test success does not prove that final generation check. diff --git a/docs/development/workdir-refusal-diagnostics.md b/docs/development/workdir-refusal-diagnostics.md index 1fe2d7575..b3faf706f 100644 --- a/docs/development/workdir-refusal-diagnostics.md +++ b/docs/development/workdir-refusal-diagnostics.md @@ -3,7 +3,7 @@ This change improves the internal `api_error` record for `POST /api/w/{workspace_id}/workers/self/workdir-session/operations`. It does **not** identify or resolve the reported 2026-10-09 403: the running -binary revision and actual request are still unverified. Fixture success is +binary source commit and actual request are still unverified. Fixture success is not evidence about that incident. No deployment, live asset cleanup, privilege change, retry policy, or WIP dependency/version change is included (WIP remains 0.2.0). diff --git a/docs/development/workspace-request-admission.md b/docs/development/workspace-request-admission.md index cbbcb7cfa..de48b6f92 100644 --- a/docs/development/workspace-request-admission.md +++ b/docs/development/workspace-request-admission.md @@ -7,7 +7,7 @@ Workspace HTTP dispatch uses `WorkspaceAdmission` for **lifecycle admission**, n 1. Acquire an immediate lifecycle lease. While deletion drains, new ordinary requests fail closed instead of queueing behind the deletion operation. 2. Validate the bounded request body, Runtime signature, audience/Workspace/permission/method/target/body binding, expiry, transactional JTI consumption, current Runtime trust, Worker membership/reservation and singleton ownership. Browser actor/origin checks are unchanged. No TTL extension or 401 retry policy is introduced. 3. Check the persisted Workspace state. Scoped requests, including GETs, reject a deleting Workspace, including after Server reconstruction. An absent Workspace remains 404. Server-owned deletion status/polling is outside scoped dispatch. -4. Execute the inner router. Resource owners retain their existing exclusion: per-Worker session/control/removal locks; transactional Workdir attachment exclusivity and removal fences; SQLite domain transactions and revision/CAS checks; the resource broker's one-shot handle consumption. Legacy memory document read/replace/write uses a short, resource-specific mutex because it spans multiple store calls. +4. Execute the inner router. Resource owners retain their existing exclusion: per-Worker session/control/removal locks; transactional Workdir attachment exclusivity and removal fences; SQLite domain transactions and content/identity precondition checks; the resource broker's one-shot handle consumption. Legacy memory document read/replace/write uses a short, resource-specific mutex because it spans multiple store calls. 5. Drop the lifecycle lease on response, error, or cancellation. An admitted long operation is not rechecked for proof expiry merely because its business execution lasts past the proof TTL. This does not grant subsequent requests or disable provider/attachment authority checks. The proof verifier's clock is injected into the Server instance for deterministic route tests, not obtained from test environment variables. Production uses the same wall-clock seconds and existing proof rules. Clock-skew policy remains a separate scope (T-728). @@ -31,4 +31,4 @@ They do not bypass authentication or authorization. Callback admission closes at Timeout uses Tokio's test clock; proof tests advance an independent injected clock by 120 seconds. Bounds detect hangs, not guessed sleeps. Dispatch task cancellation represents cancellation of the HTTP handler future; provider disconnect is a dropped transport response. These are in-process boundary tests, not a real-socket disconnect or live browser/Runtime E2E test. Background command and websocket lifetimes retain their existing domain-specific cleanup; request admission covers the handler until its response, not the entire upgraded connection. -Past production `401 expired` incidents have **not** been correlated with live revision/request traces. The structural failure path and deterministic regression evidence do not establish that all prior 401s had this cause. T-731 and its retained session/MR/source/Workdir are unrelated and untouched. WIP remains 0.2.0; no generated DTO/schema/route artifacts change. +Past production `401 expired` incidents have **not** been correlated with live binary-source/request traces. The structural failure path and deterministic regression evidence do not establish that all prior 401s had this cause. T-731 and its retained session/MR/source/Workdir are unrelated and untouched. WIP remains 0.2.0; no generated DTO/schema/route artifacts change. diff --git a/docs/development/workspace-schema-migrations.md b/docs/development/workspace-schema-migrations.md index fb8d15521..294919b37 100644 --- a/docs/development/workspace-schema-migrations.md +++ b/docs/development/workspace-schema-migrations.md @@ -15,7 +15,7 @@ The current retained Workspace chain is: 1. schema 50: `workspace schema baseline` 2. schema 51: `workspace runtime bindings` -3. schema 52: `workspace Runtime binding revision and audit` +3. schema 52: Runtime-binding counter and audit (historical migration purpose; the persisted migration name remains frozen). 4. schema 53: `durable Workspace deletion operations` A database may begin at any retained baseline. Its following history rows must be the exact prefix diff --git a/docs/report/2026-08-06-ticket-search-audit-visibility.md b/docs/report/2026-08-06-ticket-search-audit-visibility.md index 1cd5fba9d..5aa8d7ed5 100644 --- a/docs/report/2026-08-06-ticket-search-audit-visibility.md +++ b/docs/report/2026-08-06-ticket-search-audit-visibility.md @@ -2,6 +2,8 @@ Tracking Ticket: `00001KZRNHB35` +2026-10-10 編集注: 以下は当時の調査と提案の要旨であり、現行 API の仕様ではない。旧状態番号を含む仮の型名や逐語 schema は、対象 snapshot / Git commit / 履歴参照という目的別の説明へ整理した。新しい API 名が当時存在したという記録にはしない。 + ## 発生日 2026-08-06 @@ -64,7 +66,7 @@ commit `e6a2da54` - test evidence - dirty state - assignment id -- report対象revision +- report対象の item snapshot Markdown headingや文言に依存した監査になる。 @@ -76,7 +78,7 @@ threadの`item_edit` eventは、変更fieldとreplacement countだけを返す Ticket item updated: title, body. Body replacement applied to 10 occurrence(s). ``` -編集前title/body、編集後snapshot、item revision idを取得できない。Implementation reportがどのitem revisionを対象にしたかも記録されない。 +編集前title/body、編集後snapshot、item 履歴参照を取得できない。Implementation reportがどのitem snapshotを対象にしたかも記録されない。 このため、「実装完了後にTicketが別scopeへrescopeされた」のか、「Implementation reportの読み違い」なのかをthreadだけから確実に判定できない。 @@ -89,7 +91,7 @@ Ticket item updated: title, body. Body replacement applied to 10 occurrence(s). - close後にunresolved request_changesが追加された - Implementation report後のmaterial rescope - report commitがcurrent repositoryで解決不能 -- MR current revisionとreview対象revisionが不一致 +- MR current commit と review 対象 commitが不一致 ### relation削除operationがない @@ -167,29 +169,20 @@ Markdown bodyに加えて、最低限次をtyped attributesとして保存する - `repository_id` - `base_commit` - `head_commit` -- `merge_request_id` / `revision_id` +- MR ID / 対象 Git commit の参照 - validation evidence refs - dirty/untracked state - source Runtime/Worker identity -reportは作成時のTicket item revisionを参照する。`QueryTicket.evidence`と`attention`はMarkdown headingをparseせず、このtyped authorityをqueryする。 +reportは作成時のTicket item snapshotを参照する。`QueryTicket.evidence`と`attention`はMarkdown headingをparseせず、このtyped authorityをqueryする。 -### retrievable item revisions +### retrievable item snapshots -`ShowTicket`のoptional revision selectorまたはbounded version projectionでitem historyを取得できるようにする。専用tool追加は、同じprojectionではsize/authorityを分離できない場合だけ検討する。 +`ShowTicket`のoptional history selectorまたはbounded version projectionでitem historyを取得できるようにする。専用tool追加は、同じprojectionではsize/authorityを分離できない場合だけ検討する。 -```text -TicketItemRevision { - revision_id, - title, - body_digest, - body or bounded diff, - edited_at, - source, -} -``` +当時の提案は、編集前後の title、body digest、本文または bounded diff、編集時刻、source と、その内容を再取得できる履歴参照を一組として保存するものだった。ここでは当時の仮の型/schema 名は再掲せず、現行型名としても提示しない。 -Implementation report/review/close resolutionから対象item revisionを参照する。 +Implementation report/review/close resolutionから対象item snapshotを参照する。 ### stable tool projection @@ -199,7 +192,7 @@ Implementation report/review/close resolutionから対象item revisionを参照 relationのadd/remove、Queue、review、Close、item editなどはauthorization、precondition、idempotency、notification、compensation、audit/result型が異なるため、generic `MutateTicket`や`MutateObjective`へ統合しない。明示commandを維持し、model-facing tool数はprofile・role・Flow別catalog projectionで抑える。 -Implementation reportはtyped Ticket thread eventとして扱い、item revision参照とbounded query結果から監査可能にする。 +Implementation reportはtyped Ticket thread eventとして扱い、item snapshot参照とbounded query結果から監査可能にする。 ## 推奨順序 @@ -213,7 +206,7 @@ Implementation reportはtyped Ticket thread eventとして扱い、item revision ## 期待する監査手順 1. `QueryTicket(states=active, attention=implementation_report_not_closed)`で候補抽出。 -2. candidateごとに`ShowTicket`でcurrent item revision、report対象revision、latest reviewを取得。 +2. candidateごとに`ShowTicket`でcurrent item snapshot、report対象の item snapshot、latest reviewを取得。 3. typed commit/MR evidenceをrepository authorityで検証。 4. unresolved `request_changes`とdependencyを確認。 5. User/Orchestratorが明示的にcloseする。 diff --git a/docs/report/2026-08-11-flow-verifier-stale-session-capture.md b/docs/report/2026-08-11-flow-verifier-stale-session-capture.md index e5a891db0..e28b65339 100644 --- a/docs/report/2026-08-11-flow-verifier-stale-session-capture.md +++ b/docs/report/2026-08-11-flow-verifier-stale-session-capture.md @@ -1,5 +1,7 @@ # Flow verifier uses a stale committed-session capture +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + Date: 2026-08-11 Ticket: `00001KZPQW4GJ` Flow instance: `019ff243-d4df-71f2-beb3-cbc360f58c34` @@ -33,7 +35,7 @@ Independent read-only Reviewer verdict: > APPROVE — I found no blocker/high security or correctness issue in commits `8ae930c5` + `f60c2d58`. -The Reviewer explicitly confirmed the constrained four-field tool input, proof-only destructive boundary, exact Runtime-result Worker revision binding, successful recovery after registry purge, and failed-operation re-entry through the authoritative prepare/executing fence. +The Reviewer explicitly confirmed the constrained four-field tool input, proof-only destructive boundary, exact Runtime-result Worker-state binding used at the time, successful recovery after registry purge, and failed-operation re-entry through the authoritative prepare/executing fence. ## Impact @@ -41,7 +43,7 @@ A correct, tested, independently approved implementation cannot advance from `im ## Suggested fix -Before evaluating a transition, refresh the verifier's session capture from the latest committed Worker history revision and include stable references to: +Before evaluating a transition, refresh the verifier's session capture from the latest committed Worker history snapshot and include stable references to: - the current branch and commit, - bounded validation command results, diff --git a/docs/report/2026-09-23-remote-worker-repository-access-restore.md b/docs/report/2026-09-23-remote-worker-repository-access-restore.md index 6db5b4246..a3383674a 100644 --- a/docs/report/2026-09-23-remote-worker-repository-access-restore.md +++ b/docs/report/2026-09-23-remote-worker-repository-access-restore.md @@ -1,5 +1,7 @@ # Repository secret delivery expiry deletes a live Workdir helper and blocks restore +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## Symptom The `workspace:wip` T-3 Coder completed a clean local commit but could not publish it: @@ -112,7 +114,7 @@ Server-issued operation: runtime:read Runtime expectation: workers:input ``` -The exact 401 text is `WorkspaceCapabilityVerificationError::WrongOperation`. Verification had already accepted token shape, Workspace trust/signature, Workspace, Runtime, binding revision, and Worker identity. Method, path, body-digest, and expiry checks occur later and were not reached. +The exact 401 text is `WorkspaceCapabilityVerificationError::WrongOperation`. Verification had already accepted token shape, Workspace trust/signature, Workspace, Runtime, the historical binding counter, and Worker identity. Method, path, body-digest, and expiry checks occur later and were not reached. Retrying the same restore without correcting the Server classifier deterministically returns the same 401. diff --git a/docs/report/2026-09-24-worker-lifecycle-restore-atomicity.md b/docs/report/2026-09-24-worker-lifecycle-restore-atomicity.md index 2100ee089..ffef7e8f9 100644 --- a/docs/report/2026-09-24-worker-lifecycle-restore-atomicity.md +++ b/docs/report/2026-09-24-worker-lifecycle-restore-atomicity.md @@ -1,5 +1,7 @@ # Worker lifecycle, restore atomicity, and observation freshness +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## Scope This report records the state-model and crash-boundary problems found while investigating why one restored Worker appeared as `idle` in the Web client and `unknown` in the Runtime CLI. The display mismatch exposed a broader problem: lifecycle, controller state, observation freshness, restart intent, and recovery evidence are represented by overlapping fields without one enforced authority. @@ -57,7 +59,7 @@ A concrete counterexample is: The persisted aggregate was not rolled back: its identity file has already changed while its execution file is old. This outcome must not be classified as a proven rollback. Identity, restore specification, execution binding, and recovery evidence need one atomic aggregate commit. -A revision manifest is not preferable here. The store is single-writer, Worker records are bounded, and the existing same-directory temporary-file replacement already supports one aggregate `worker.json`. A manifest would add a second commit pointer and orphan cleanup without resolving Backend reconciliation. +A manifest pointing to separate immutable aggregate snapshots is not preferable here. The store is single-writer, Worker records are bounded, and the existing same-directory temporary-file replacement already supports one aggregate `worker.json`. A manifest would add a second commit pointer and orphan cleanup without resolving Backend reconciliation. Even a single-file replacement has a commit-ambiguity boundary. `atomic_write_json` renames the temporary file and then syncs the parent directory. If rename succeeds but directory sync fails, the new record is already visible while crash durability is unknown. Persistence errors therefore need a phase-aware classification: failure before rename is definitely not committed; failure after rename is `CommitOutcomeUnknown` and must never be reported as a proven rollback. @@ -187,7 +189,7 @@ Fault injection must target each persistence boundary. Same-process assertions a ## Implementation order 1. Stop publishing stale unavailable snapshots as current REST `worker_state` and add projection regression coverage. -2. Replace split identity/execution authority with an atomic aggregate record or revisioned aggregate commit while preserving validated migration input. +2. Replace split identity/execution authority with an atomic aggregate record or immutable aggregate snapshot commit while preserving validated migration input. 3. Introduce explicit durable reconciliation evidence and stable operation identity for Restore and Stop. 4. Quarantine candidate execution observation until aggregate commit and make retry/restart converge on the recorded operation. 5. Remove catalog foreground-state duplication and update clients to use current observed snapshots. diff --git a/docs/report/2026-10-05-review-verdict-validation.md b/docs/report/2026-10-05-review-verdict-validation.md index d3cad9724..2d83efa4f 100644 --- a/docs/report/2026-10-05-review-verdict-validation.md +++ b/docs/report/2026-10-05-review-verdict-validation.md @@ -1,6 +1,8 @@ # Review verdict validation and single-call policy -During T-696's independent review, `ReviewRequested` captured MR `01a10bc8-59f4-7630-8166-d8cd71943e15`, source `9812e68b4d4021ea778a40d9ead4fe545d09e8bf`, and Ticket revision `00001M4517HE0:5`. The Reviewer reproduced a pending-upload deletion defect and attempted a structured `request_changes` verdict. The request used finding severity `medium`; the Backend returned HTTP 422, listing the accepted values as `blocker`, `major`, `minor`, and `note`. No review ID or verdict event was returned. +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + +During T-696's independent review, `ReviewRequested` captured MR `01a10bc8-59f4-7630-8166-d8cd71943e15`, source `9812e68b4d4021ea778a40d9ead4fe545d09e8bf`, and the then-current Ticket item reference `00001M4517HE0:5`. The Reviewer reproduced a pending-upload deletion defect and attempted a structured `request_changes` verdict. The request used finding severity `medium`; the Backend returned HTTP 422, listing the accepted values as `blocker`, `major`, `minor`, and `note`. No review ID or verdict event was returned. The Reviewer obeyed the role prompt's instruction to call `ReviewMergeRequest` exactly once and did not retry. Its prose recommendation and artifacts are evidence of investigation, not recorded review authority. The parent preserves the finding, fixes the defect, and requests a new independent review bound to the updated source; it does not invent a verdict or bypass the rejected operation. diff --git a/docs/report/2026-10-05-reviewer-subworker-stopped-before-run.md b/docs/report/2026-10-05-reviewer-subworker-stopped-before-run.md index 38f6fd395..cbfd0eb99 100644 --- a/docs/report/2026-10-05-reviewer-subworker-stopped-before-run.md +++ b/docs/report/2026-10-05-reviewer-subworker-stopped-before-run.md @@ -1,10 +1,12 @@ # Reviewer SubWorkerがreview開始前に停止する +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## 状況 T-698の公開済みMerge Requestに対して、`builtin:reviewer`を指定し、対象Ticket/Merge Request、writable delegated scope、command grantを明示してReviewer SubWorkerを3回起動した。 -いずれも`review_requested` eventは正しいsource refとTicket revisionを記録したが、SubWorkerは直後に`Stopped`となった。session observationには最初のuser taskだけが存在し、assistant output、tool call、typed review verdictは記録されなかった。Merge Request threadにもapprove/request_changesは追加されなかった。 +いずれも`review_requested` eventは正しいsource refと当時の Ticket item referenceを記録したが、SubWorkerは直後に`Stopped`となった。session observationには最初のuser taskだけが存在し、assistant output、tool call、typed review verdictは記録されなかった。Merge Request threadにもapprove/request_changesは追加されなかった。 ## 障壁 diff --git a/docs/report/2026-10-05-subworker-ticket-context.md b/docs/report/2026-10-05-subworker-ticket-context.md index e05394172..127fdc28c 100644 --- a/docs/report/2026-10-05-subworker-ticket-context.md +++ b/docs/report/2026-10-05-subworker-ticket-context.md @@ -1,8 +1,10 @@ # Delegated coding context and artifact access -During T-696 recovery, the parent Coder could read the authoritative Ticket through `ShowTicket`, but default coding SubWorkers did not receive that tool. A delegated task that simply asks a child to reread the Ticket therefore cannot be fulfilled. One child attempted a CLI fallback; this is not an authorized substitute for typed Backend tools. The parent supplied the authoritative revision and relevant requirements instead and retained lifecycle/publish authority. +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. -Improvement: make the child tool/capability projection explicit in the spawn result, and support passing a bounded authoritative Ticket snapshot with revision into a child without implying mutation authority. Delegation prompts should include the necessary contract rather than assuming the child has the parent's Backend tools. +During T-696 recovery, the parent Coder could read the authoritative Ticket through `ShowTicket`, but default coding SubWorkers did not receive that tool. A delegated task that simply asks a child to reread the Ticket therefore cannot be fulfilled. One child attempted a CLI fallback; this is not an authorized substitute for typed Backend tools. The parent supplied the authoritative Ticket snapshot and relevant requirements instead and retained lifecycle/publish authority. + +Improvement: make the child tool/capability projection explicit in the spawn result, and support passing a bounded authoritative Ticket snapshot with its immutable reference into a child without implying mutation authority. Delegation prompts should include the necessary contract rather than assuming the child has the parent's Backend tools. The Bash provider also advertised a full-output spill under the parent's declared readable `bash-output` directory, but `Read` rejected the returned absolute path as outside the attachment scope. The parent did not bypass that rejection; it used smaller bounded queries. Improvement: return a typed read capability/artifact reference for spill output, or ensure the advertised spill path is actually readable through the selected attachment. diff --git a/docs/report/2026-10-06-t704-validation.md b/docs/report/2026-10-06-t704-validation.md index 7ba86a81a..48b61f39a 100644 --- a/docs/report/2026-10-06-t704-validation.md +++ b/docs/report/2026-10-06-t704-validation.md @@ -1,5 +1,7 @@ # T-704 implementation and validation ledger +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + Base: `6f9621ca` (clean detached assigned checkout). Parent created `work/t-704-backend-subjektiv-jobs` without replacing a branch or discarding changes. Parent owns Backend/Profile/Subject authorization, cutover, documentation and @@ -24,7 +26,7 @@ Independent MR review is separate from implementation delegation. - Synchronous bounded clean-context surface generation before result submission. Preparation reports an already-current snapshot to avoid needless conflicting regeneration. Job failure acknowledgements are `failed_confirmed` only after - checking the actual failed marker/reason/revision and generation provenance. + checking the actual failed marker/reason/then-current store condition and generation provenance. Subject feature migration 6 adds immutable generation Job/attempt provenance, without changing Subject/Memory/candidate content. - Durable results contain actual candidate dispositions and a separate verified @@ -99,7 +101,7 @@ resolution records for a 100-ID batch could exceed the common 16 KiB result boun even when candidate decisions and surface generation had actually completed. That pending old-source review was stopped rather than treated as approval. -The follow-up keeps full immutable receipts/reasons/revision/source edges in their +The follow-up keeps full immutable receipts/reasons/immutable history references/source edges in their existing store and persists only verified `{candidate_id, action}` summaries in the Job result. Batch selection conservatively budgets the complete result, including JSON-escaped IDs in both claim and outcome, plus a reserved bounded diff --git a/docs/report/T-711-import-cycle.md b/docs/report/T-711-import-cycle.md index 37dbaf422..83dda0fa3 100644 --- a/docs/report/T-711-import-cycle.md +++ b/docs/report/T-711-import-cycle.md @@ -5,7 +5,7 @@ Status: fixed at the snapshot import-loader boundary; regression coverage retain ## Dogfooding problem -During T-711 builtin-import validation, a nested import cycle in a lazy object caused a stack overflow instead of a recoverable configuration diagnostic. A configuration author could therefore terminate evaluation while writing an invalid recipe, rather than receive an error and keep the active revision. This report records the implementation-session failure and the guard added by the parent config-source implementation; it is not a recommendation to rerun the crashing version inside a live Yoi Worker. +During T-711 builtin-import validation, a nested import cycle in a lazy object caused a stack overflow instead of a recoverable configuration diagnostic. A configuration author could therefore terminate evaluation while writing an invalid recipe, rather than receive an error and keep the active configuration snapshot. This report records the implementation-session failure and the guard added by the parent config-source implementation; it is not a recommendation to rerun the crashing version inside a live Yoi Worker. ## Minimal reproducer @@ -59,7 +59,7 @@ cargo test -p config-source --lib builtin_values_do_not_bypass_cycle_or_path_lim cargo test -p config-source --lib snapshot_import_depth_is_checked_before_evaluation_recurses ``` -The server follow-up test `value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back` separately exercises authenticated HTTP commits: reserved builtin mutations and unknown imports are rejected without changing the active revision, even when a Workspace file exists at the unknown builtin's suffix. Editor import completions remain covered by the WASM editor tests; no server completion endpoint was introduced. +The server follow-up test `value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back` separately exercises authenticated HTTP commits: reserved builtin mutations and unknown imports are rejected without changing the active configuration snapshot, even when a Workspace file exists at the unknown builtin's suffix. Editor import completions remain covered by the WASM editor tests; no server completion endpoint was introduced. ## Operational lesson diff --git a/docs/report/state-contract-cleanup-recovery-checkpoint.md b/docs/report/state-contract-cleanup-recovery-checkpoint.md index e3efdd146..fb0663260 100644 --- a/docs/report/state-contract-cleanup-recovery-checkpoint.md +++ b/docs/report/state-contract-cleanup-recovery-checkpoint.md @@ -1,109 +1,67 @@ -# State-contract cleanup completion and recovery record +# State-contract cleanup historical checkpoint and follow-up -The interrupted cleanup has been restored, integrated with `develop` at -`72ce817de4592e8c9703e0725dd705f46cba63aa`, completed, and validated. This record -supersedes the earlier incomplete restart checkpoint. No live Server, Runtime, -or user database was stopped or migrated as part of this work. +This record describes the earlier recovery and validation after integration with `develop` at `72ce817de4592e8c9703e0725dd705f46cba63aa`. Its former assertion of complete cleanup was incorrect: the subsequent case-insensitive audit at `c78f7f7d` found 875 matching lines in 96 files, including current names, comments, documents and fixtures. The earlier results below remain historical evidence, not proof that the original full-codebase removal request was satisfied. No live Server, Runtime, or user database was stopped or migrated during that earlier work. ## Recovery and integration - Workdir attachment: `yoi-origin`; branch: `hare/develop`. -- Preserved the Workdir delegation fix (`7321d6c7`), asynchronous cleanup fix - (`dc8e586a`), and the T-731/T-732 changes integrated by `72ce817d`. -- Applied stash `dd31c0ecd41d2aca1ef29889a5194fb4499cb149` once after merging - develop. It remains as recovery evidence; do not apply it again over this work. +- Preserved the Workdir delegation fix (`7321d6c7`), asynchronous cleanup fix (`dc8e586a`), and the T-731/T-732 changes integrated by `72ce817d`. +- Applied stash `dd31c0ecd41d2aca1ef29889a5194fb4499cb149` once after merging develop. It remains as recovery evidence; do not apply it again over this work. - Older stash `a386a9e2943d53f331a555a8c255741bc004c419` also remains untouched. -- Direct implementation/review SubWorkers were collected after their committed - sessions and concrete changes/test evidence were inspected. +- Direct implementation/review SubWorkers were collected after their committed sessions and concrete changes/test evidence were inspected. -## Completed contracts +## Contracts recorded by the earlier implementation -State/update ordinals have been replaced by content digests, key fingerprints, -binding/trust/connection IDs, immutable mutation IDs, and observation tokens. -Schema/protocol versions, ordered events, command IDs, and local asynchronous -invalidation fences remain distinct contracts and were not indiscriminately removed. +State/update ordinals have been replaced by content digests, key fingerprints, binding/trust/connection IDs, immutable mutation IDs, and observation tokens. Schema/protocol versions, ordered events, command IDs, and local asynchronous invalidation fences remain distinct contracts and were not indiscriminately removed. -- Config source/history preserves separate source/toolchain/projection evaluations. - Cache identities occupy a namespace forbidden to real virtual paths, including - JSON-deserialized paths. Native and generated WASM regressions cover the boundary. -- Job/content migration retains completed results and cleanup/replay evidence. - Proven reserved-only work can fail definitively; uncertain execution is not - silently admitted again. Historical Flow definitions are validated, not recompiled. -- Drive preserves old receipt fingerprints and validates old Update/Relocate/Delete - replays against frozen evidence rather than granting authority from old ordinals. -- SSH migration reseals envelopes under actual mutation receipts. Terminal Workdir - history whose credentials were deleted remains non-authorizing archival evidence. - INSERT/UPDATE guards prevent that archive from acquiring candidate/lease authority. -- Runtime binding verification ties together binding/trust identities and actual - public-key fingerprints. Worker command-ID replay is separate from admission-time - state. Ticket checker intent IDs include the complete immutable request. +- Config source/history preserves separate source/toolchain/projection evaluations. Cache identities occupy a namespace forbidden to real virtual paths, including JSON-deserialized paths. Native and generated WASM regressions cover the boundary. +- Job/content migration retains completed results and cleanup/replay evidence. Proven reserved-only work can fail definitively; uncertain execution is not silently admitted again. Historical Flow definitions are validated, not recompiled. +- Drive preserves old receipt fingerprints and validates old Update/Relocate/Delete replays against frozen evidence rather than granting authority from old ordinals. +- SSH migration reseals envelopes under actual mutation receipts. Terminal Workdir history whose credentials were deleted remains non-authorizing archival evidence. INSERT/UPDATE guards prevent that archive from acquiring candidate/lease authority. +- Runtime binding verification ties together binding/trust identities and actual public-key fingerprints. Worker command-ID replay is separate from admission-time state. Ticket checker intent IDs include the complete immutable request. ## Migration safety and review corrections -- Uncertain Worker removals (`executing`/`failed`) block **before any retained - migration runs**, including ordinary Server open and migration CLI entrypoints. - Tests start at schema84, verify unchanged schema/receipt after rejection, then - reconcile with the old contract and upgrade successfully. -- SSH dry-run uses the original database's secret context while validating an - in-memory backup. Actual encrypted-envelope tests prove dry-run non-mutation, - formal migration, new-AAD decryption, and unchanged master-key material. -- Interrupted signing identity provisioning resumes the same reserved key/material. - Frozen old receipt validation precedes operation-key/fingerprint conversion; - activation and migration failures roll back. Completed and create receipts are - also covered. -- The old reservation CHECK constraint is removed through a transactional rebuild - retaining other columns, constraints, indexes, triggers, and incoming/outgoing - foreign keys. Unknown dependencies are not broadly rewritten. -- Fresh/upgraded metadata comparison covers 36 state-contract tables: columns, - defaults, PK/FK structure, index columns, and triggers. It caught and repaired a - missing history DEFAULT and retained-chain reservation lookup index. This is not - a general SQL-equivalence proof of all CHECK expressions or index predicates; - separate behavior/rollback tests cover the relevant authority constraints. -- Independent data and Server reviews found the above defects; follow-up reviews - confirmed their resolution without additional blockers in the reviewed scope. +- Uncertain Worker removals (`executing`/`failed`) block **before any retained migration runs**, including ordinary Server open and migration CLI entrypoints. Tests start at schema84, verify unchanged schema/receipt after rejection, then reconcile with the old contract and upgrade successfully. +- SSH dry-run uses the original database's secret context while validating an in-memory backup. Actual encrypted-envelope tests prove dry-run non-mutation, formal migration, new-AAD decryption, and unchanged master-key material. +- Interrupted signing identity provisioning resumes the same reserved key/material. Frozen old receipt validation precedes operation-key/fingerprint conversion; activation and migration failures roll back. Completed and create receipts are also covered. +- The old reservation CHECK constraint is removed through a transactional rebuild retaining other columns, constraints, indexes, triggers, and incoming/outgoing foreign keys. Unknown dependencies are not broadly rewritten. +- Fresh/upgraded metadata comparison covers 36 state-contract tables: columns, defaults, PK/FK structure, index columns, and triggers. It caught and repaired a missing history DEFAULT and retained-chain reservation lookup index. This is not a general SQL-equivalence proof of all CHECK expressions or index predicates; separate behavior/rollback tests cover the relevant authority constraints. +- Independent data and Server reviews found the above defects; follow-up reviews confirmed their resolution without additional blockers in the reviewed scope. -## Final validation +## Earlier validation (not rerun evidence) - Root `cargo check`: PASS. -- All 21 changed Rust crates tested together: **3,795 passed**, **3 ignored**, - zero failures, including doctests. The Server contributes 809 library tests and - 11 CLI tests (one library test ignored). +- All 21 changed Rust crates tested together: **3,795 passed**, **3 ignored**, zero failures, including doctests. The Server contributes 809 library tests and 11 CLI tests (one library test ignored). - `server-api --features typescript`: **96 passed**. -- Additional T-731/T-732 integration coverage: yoi, workdir, tools, fs-operation - tests passed separately. +- Additional T-731/T-732 integration coverage: yoi, workdir, tools, fs-operation tests passed separately. - Web standard suite: **589 passed**; component suite: **315 passed / 33 files**. - Web type check: zero errors/warnings; production build passed (chunk-size warning). -- Config WASM parity includes 18 passing cases, including literal-cache-key collision - and JSON URI-path refusal. Generated JS/types/WASM were compared with the pinned - wasm-bindgen 0.2.117 toolchain. OpenAPI/API/protocol generated checks passed. -- `cargo fmt --all -- --check` and `git diff --check HEAD`: PASS. Included frozen - Rust migration/test files were formatted explicitly as well. +- Config WASM parity includes 18 passing cases, including literal-cache-key collision and JSON URI-path refusal. Generated JS/types/WASM were compared with the pinned wasm-bindgen 0.2.117 toolchain. OpenAPI/API/protocol generated checks passed. +- `cargo fmt --all -- --check` and `git diff --check HEAD`: PASS. Included frozen Rust migration/test files were formatted explicitly as well. -Local evidence logs (ignored build artifacts, not part of stash/history): -`target/revision-final-combined-rust-tests.log`, -`target/revision-final-typescript-tests.log`, -`target/revision-final-root-check.log`, -`target/revision-final-web-{tests,component,check,build}.log`, -`target/revision-virtual-path-web-{config-tests,final-check}.log`, -`target/revision-web-final-{artifacts,normalized-artifacts}.log`, and -`target/revision-schema-equivalence.log`. +The earlier session recorded local ignored evidence logs for the combined Rust crate tests, TypeScript-feature tests, root check, Web standard/component/check/build, virtual-path WASM parity, generated-artifact comparison and schema equivalence. The old basename list is summarized here rather than renamed to invented paths; these local artifacts were not part of stash/history. ## Deployment and separate findings -Deployment/restart remains user-operated. Take the normal database/key-material -backup before migration. Resolve uncertain old Worker removals with the old Server -before attempting cutover. Existing migrated Runtime bindings become configured; -re-enrollment/verification requires the actual Runtime-issued trust ID from -`trust-workspace add/show`, not an invented value or public-key fingerprint. +Deployment/restart remains user-operated. Take the normal database/key-material backup before migration. Resolve uncertain old Worker removals with the old Server before attempting cutover. Existing migrated Runtime bindings become configured; re-enrollment/verification requires the actual Runtime-issued trust ID from `trust-workspace add/show`, not an invented value or public-key fingerprint. -Previously identified Server-only capability revocation and missing enrollment -identity in Runtime-to-Server source proofs predate this cleanup. They are not -claimed fixed here. The separately recorded Runtime HTTP-only feature dependency -issue also remains outside this cleanup. +Previously identified Server-only capability revocation and missing enrollment identity in Runtime-to-Server source proofs predate this cleanup. They are not claimed fixed here. The separately recorded Runtime HTTP-only feature dependency issue also remains outside this cleanup. -Workdir operations intermittently returned HTTP 401 during the work. Failed calls -were not treated as successful edits/tests; subsequent reads and final combined -validation established the recorded result. No authorization or live-process -workaround was used. The default wasm-bindgen CLI had a schema-version mismatch; -a matching CLI was installed only under ignored `target/revision-wasm-tools`, with -no global CLI change or additional change to the restored Cargo.lock. +Workdir operations intermittently returned HTTP 401 during the work. Failed calls were not treated as successful edits/tests; subsequent reads and final combined validation established the recorded result. No authorization or live-process workaround was used. The default wasm-bindgen CLI had a schema-version mismatch; a matching CLI was installed only under an ignored target-local tools directory, with no global CLI change or additional change to the restored Cargo.lock. + +## Original-request follow-up (2026-10-10; implementation and audit recorded) + +The complete follow-up result is in `docs/report/state-contract-name-audit.md`. The original request was applied to current identifiers, API/type/field names, comments, documentation, fixtures and generated contracts, not merely counters. The follow-up was implemented and validated on `hare/develop` with base `3263d501`, which added the separate original-directive evidence document after `c78f7f7d`. + +- The case-insensitive audit started at 96 tracked text files / 875 matching lines. Final source inventory has 32 matching files / 546 lines: 522 saved-format and migration-fixture lines, 14 removal/rejection checks, and 10 naming-policy or original-directive evidence lines. Current identifiers, API/generated contracts, current schema names and file names have no remaining prohibited name. +- This is **not** a claim of zero literal occurrences. Exact per-file, per-line exceptions and data-loss consequences are recorded in `tools/state-contract-name-exceptions.json`; `--report` emits every literal. The guard passes with zero unapproved occurrences and zero stale exceptions. +- The final audit rejected 14 unnecessary diagnostic/local-variable/fixture lines; those were fixed rather than added to the exceptions. Existing migration labels remain because `verify_schema_history` compares their saved names exactly. +- Server schema89 and Drive schema3 remove the old names from current evidence columns while retaining original values, receipts and fingerprints. Migration tests cover populated old inputs, replay, rollback and fresh/upgraded schema. +- The detailed report records what is compared, to which authority, and when, and where FIFO/content/timestamps replace generic update counters. +- Regular generators and freshness checks synchronized OpenAPI, API and protocol. The combined 14-crate run passed 3,209 tests with 3 ignored. After the last 14-line cleanup, the changed three crates passed again. Additional protocol TypeScript, api-macros OpenAPI and E2E compile checks passed; no product E2E was run. +- Generated Web validation passed: standard 591, component 315, check with zero errors/warnings, production build with the existing chunk-size warning. An extra nonstandard test has one pre-existing Ticket route source assertion failure, explicitly retained in the detailed report; this is not an all-tests-green claim. +- Root compile, format/diff checks and the naming guard's five tests passed. Independent read-only review found no blocking defect in the migration/guard scope; exception review separately refused unnecessary uses before final cleanup. +- Ignored own Web outputs and local documentation were separately audited after rebuild: 2,135 regular files, zero matches. Old build binaries/logs, dependencies, Git history/stashes and external symlink targets are not claimed erased. + +No live environment/database was changed, no push was performed, and the two recovery stashes remain preserved. Deployment/restart remains user-operated. diff --git a/docs/report/state-contract-name-audit.md b/docs/report/state-contract-name-audit.md new file mode 100644 index 000000000..38c115203 --- /dev/null +++ b/docs/report/state-contract-name-audit.md @@ -0,0 +1,126 @@ +# 状態契約の命名整理と全件監査 + +## 判定 + +現行の識別子・API・生成契約・最新DDLから禁止名称を除去し、具体的な状態契約と再導入検査を実装・検証した。ただし、コードベースの文字列を完全ゼロにはしていない。大小文字無視の最終結果は **32ファイル・546該当行**。内訳は旧保存形式とそのfixture **522行**、移行後の消失などを確かめる検査 **14行**、命名禁止規則とユーザー原文証拠 **10行**。必要性と削除時の影響を以下と限定例外一覧に記録した。 + +guardは未承認0・stale例外0で成功。これは旧語の完全ゼロを意味せず、残す全行が分類・固定され、それ以外の再導入が拒否されることを意味する。新しい識別子・ファイル名の残存は0。今回の変更は `hare/develop` の `3263d501` を基準に実装・検証した。 + +元依頼は数値カウンタの廃止だけではなく、コードベース全体から禁止された名称を除去し、何を・どう・いつ確認するのかを明示するもの。先行実装 `c78f7f7d` だけで完了とした判断は誤りだった。今回の開始時監査は追跡テキスト96ファイル・875該当行。途中のHEAD `3263d501` は別作業によるユーザー原文証拠の文書追加のみで、その証拠は改変していない。 + +## 実装の範囲 + +現行Rust/API/Web/文書/無関係fixtureと生成物の名前を用途に合わせて整理した。未使用conflict enumは別名で温存せず削除。Workdir一覧の値は実際の公開projectionの `catalog_digest`、MR source解決はGit commit、歴史的な完了記録のRust側フィールドは `legacy_item_marker` として現行判断から分離した。 + +Server schema89はartifact provenanceと旧secret receiptの列名を変更する。Drive schema3は既存schema2の旧replay列を変更する。保存された値、原request fingerprint、receiptは変更しない。現在のDDLには旧綴りを作らない。旧SQL/JSONの綴りは移行・原文保持・旧fingerprintの再現に限って残す。 + +## 何を、どう、いつ確認するか + +| 境界 | 対象・比較・時点 | 順序や時刻だけでは足りない理由 | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Ticket状態変更 | 書込みtransaction内で、要求時の内容digestとworkflow stateを現在値と比較してからstate/auditを書込む。同一operationの再送は別途receipt照合。 | stateが同じまま受入条件を編集した場合、古い条件による完了判断を通してはいけない。 | +| MR review/integration | 公開Git refをcommitへ解決し、review対象のcommit・現在のTicket内容など、承認が実際に対象としたものを判定時に照合。 | 到着順の後勝ちは、未承認commitを承認済みとして扱ってしまう。 | +| Drive | 同一IMMEDIATE transaction内でnodeの最後のmutation request IDを比較してから更新・移動・削除。chunk readも同じIDに固定。 | 同一時刻の更新、同内容書戻し、移動の往復、複数chunkの新旧混在を時刻では区別できない。 | +| Config | tree全体または変更entryの実内容digestをcommit前に照合。解決結果cacheは入力とtoolchain/projection条件で識別。 | 古い編集結果で他者の内容を消さない。単なる新旧順では内容一致を証明できない。 | +| Subject/Memory | Memory変更の履歴追加前に観測したchange IDを比較。surface公開前には生成入力のfingerprintと現在値を照合。behavior編集は観測した本文そのものを照合。 | 新しいMemoryの後に到着した古い生成Jobを公開してはいけない。 | +| Runtime/鍵 | enrollment lifetimeをbinding/trust IDで区別し、鍵の実体をfingerprintで比較して検証・利用する。旧文書は現行認可に復活させない。 | 同じ鍵の再登録や失効・再登録を時刻や鍵だけでは区別できない。 | +| standalone保存 | create-new commit marker保持中に観測metadata全体を比較し、確認後にファイル置換。 | 停止・再開・session pointer変更を古いrecordで戻さない。別の更新番号は不要。 | +| pending FIFO/表示 | stream順の完全置換、再接続時snapshot。Memory historyはServerの順序を保持。 | この用途では追加の汎用更新番号は不要。IDから順序も推測しない。 | + +実装境界のコメントと `docs/development/web-state-preconditions.md` に対象・比較・時点と理由を記録した。schema/protocol versionは保存形式の選択であり、状態の新旧を任せる番号とは区別する。今回の名称修正でWIPの新番号は追加していない。 + +## 残す綴りと削除時の影響 + +各行の固定根拠は `tools/state-contract-name-exceptions.json`、生の全該当行は命名guardの `--report` で再現できる。以下はファイル別の全残存。`crates/` 配下は同prefixを省略した。 + +| ファイル | 該当行数 | 必要性・対象 | +| ---------------------------------------------------------- | -------: | ------------------------------------------------------------------- | +| `manifest/src/lib.rs` | 8 | 旧Memory settings snapshotの読取り・復元fixture | +| `merge-request/src/lib.rs` | 2 | 保存済みcompletion属性のroundtrip、旧review archive移行 | +| `merge-request/tests/store.rs` | 2 | 旧review/completion原文保持のfixture | +| `session-store/src/system_item.rs` | 1 | 旧Session履歴の読取りfixture | +| `standalone/src/job_store/legacy_migrations.rs` | 14 | schema1/2 Job request/attempt/receipt/再送の移行・rollback | +| `standalone/src/store.rs` | 4 | schema1 Worker metadata復元と旧key除去 | +| `subjektiv/src/legacy_migrations.rs` | 113 | 旧Memory履歴・参照edges・decision receipt・surface provenanceの移行 | +| `ticket/src/lib.rs` | 1 | 旧receiptを現行判断の証拠に昇格させないfixture | +| `worker-runtime/src/fs_store.rs` | 14 | 旧Job/request/config/SSH evidenceの変換・fixture | +| `worker-runtime/src/main.rs` | 2 | 旧enrollmentの検出・原文保持・失効 | +| `worker-runtime/src/working_directory.rs` | 4 | 旧SSH evidenceの読取りとfresh access必須化 | +| `worker-runtime/src/workspace_issuer.rs` | 8 | 旧trust/verificationの読取りと旧authority失効 | +| `workspace-drive/src/migrations.rs` | 25 | 旧node/receipt移行、旧typed payloadのfingerprint再現、schema2列改名 | +| `workspace-server/src/frozen_content_state_migration.rs` | 14 | 旧Config/Memory/reservation/Job保存形式の移行 | +| `workspace-server/src/frozen_content_state_tests.rs` | 31 | 同移行の実形式fixture・消失・rollback | +| `workspace-server/src/frozen_flow_content_migration.rs` | 7 | 旧Flow source/compiled definitionを再compileせず移行 | +| `workspace-server/src/frozen_flow_content_tests.rs` | 13 | 旧Flow履歴保持と不整合rollback | +| `workspace-server/src/frozen_legacy_migrations.rs` | 49 | retained migration chainの旧列・表・FK・index | +| `workspace-server/src/frozen_repository_key_migration.rs` | 39 | 旧secret復号/reseal、operation fingerprint、SSH retention/audit | +| `workspace-server/src/frozen_repository_key_schema.sql` | 2 | schema87/88の旧receipt列、schema89への移行入力 | +| `workspace-server/src/frozen_repository_key_tests.rs` | 22 | 旧暗号secret/SSH fixture、再送・archive・rollback | +| `workspace-server/src/frozen_runtime_binding_migration.rs` | 25 | 旧binding/policy/署名鍵/provisioning receiptの移行 | +| `workspace-server/src/frozen_runtime_binding_tests.rs` | 17 | 同移行の実形式fixtureと失敗時保持 | +| `workspace-server/src/frozen_schema_v85.sql` | 60 | 旧DB移行を検証する凍結DDL | +| `workspace-server/src/legacy_evidence_column_migration.rs` | 3 | 旧artifact/SSH receipt列を値保持して改名する入力 | +| `workspace-server/src/legacy_evidence_column_tests.rs` | 2 | 旧artifact値保持・現行DDLの旧名消失 | +| `workspace-server/src/migration_entrypoint_tests.rs` | 5 | 公開migration入口の旧policy/removal/SSH fixture | +| `workspace-server/src/store.rs` | 44 | 旧schema reader/validation/fixture、保存済みmigration名 | +| `workspace-server/src/workspace_signing_identity.rs` | 5 | version1のprivate materialを同じ鍵のまま復元 | +| `AGENTS.md` | 1 | 禁止対象の明示 | +| `tools/check-state-contract-names.ts` | 1 | 検出対象の定義 | +| `docs/report/2026-10-10-compaction-user-directive-loss.md` | 8 | ユーザー指示と指示脱落の原文証拠 | +| **合計** | **546** | **32ファイル、全行に限定例外の根拠あり** | + +特に `store.rs` のmigration名2行は `verify_schema_history()` が保存済み `__yoi_schema_migrations.name` と完全一致を要求する。これは単なる表示文ではなく、改名すると既存DBが非canonicalとして拒否される保存形式境界。一方、診断文・triggerエラー文・ローカル変数・汎用unknown-field fixtureの14行は監査で不要と判定し、例外へ追加せず修正した。 + +- 旧DBの列・表・制約名:既存DBを特定して移行するため。削除すると移行SQLが旧データを読めない。 +- 旧保存JSON/TOMLのキー:履歴・snapshot・receiptを読み、元記録を保持するため。現行Rustフィールドや新規入力契約とは分離する。削除すると旧データが読めなくなる、または原文を欠落させる。 +- 旧fingerprint/AADのbyte encoding:キー名と順序を含む旧payloadを正しく照合・復号するため。変更すると同一の正当な再送や暗号化保存データを読めなくなる。 +- 旧形式fixtureと消失検査:上記移行とrollbackを実際の旧入力で証明するため。無関係な旧名fixtureや重複した不存在assertは除去した。 +- 禁止規則自身とユーザー原文証拠:検出対象の定義と、実際の依頼を狭めないための証拠。現行状態モデルの名前ではない。 + +これは旧設計を現在の操作へ延命する許可ではない。既存保存形式の移行・再送・履歴保持サポートを廃止すれば互換literalは削除できるが、その場合に読めなくなるデータがある。文字列分割・符号化で検索を逃れる処置はしていない。 + +## 再導入防止 + +`AGENTS.md` に命名禁止と具体的な状態契約の規則を置いた。`tools/check-state-contract-names.ts` はGit管理ファイルと非ignored未追跡ファイルのパス・本文を大小文字無視で検査し、binaryも対象とする。例外はファイルごとの理由、該当行のdigest、出現数で固定し、追加・変更・別ファイルへのコピー・削除による古い例外を拒否する。directory単位の免除はない。`--report` は該当箇所の実際の文字列を出力する。 + +```sh +env -u LD_LIBRARY_PATH deno test tools/check-state-contract-names.test.ts +env -u LD_LIBRARY_PATH deno run --allow-read --allow-run=git tools/check-state-contract-names.ts --report +``` + +Git履歴・stash・他作業のWorkdir・外部依存・旧実行ログは現在のコードとは分けて扱い、検索をゼロに見せる目的で消していない。ignored inventoryはtarget配下162,446件、node_modules 13,975件、Web生成領域2,336件、その他55件(検査時点)。targetの旧バイナリ/証拠ログ、外部依存、symlink先、zipバックアップ、別Workdirの内容までゼロ化したという主張はしない。 + +ignored内の自作Web build/生成server・client出力/ローカル文書については、最終再生成後の通常ファイル2,135件を別監査し一致0。旧検証ログの名前/内容とローカルwasm-bindgen-test-runnerには残存があるが、履歴/外部toolを削除して検索結果を隠してはいない。Gitの非ignored監査とは別に `target/state-name-ignored-inventory.json`、`target/state-name-ignored-owned-audit.json`、`target/state-name-ignored-current-generated-final.json` に対象と結果を記録した。 + +## 生成の再現 + +正規generatorを使用した。主な再生成コマンドは次のとおり(rootで実行)。`generate_legacy_typescript` のstdoutは `web/workspace/src/lib/generated/legacy-server-api.ts` と比較・同期する。 + +```sh +cargo run -p server-api --example export_openapi -- openapi/server-api.json +for generator in generate_repository_openapi_types generate_runtime_api_types generate_ticket_api_types generate_workdir_api_types generate_worker_launch_api_types generate_companion_api_types; do + cargo run -p server-api --features typescript --example "$generator" +done +cargo run -p server-api --features typescript --example generate_legacy_typescript +cargo run -p protocol --features typescript --example generate_typescript +``` + +6つのAPI generatorとprotocol generatorには `-- --check` を付けてfreshnessも確認した。`cargo test -p server-api --features typescript` の96 testsと、protocol TypeScript featureの85 testsも成功した。 + +## 検証 + +- 変更14 Rust crateをまとめて検証(server-apiのTypeScript featureを含む):3,209 passed、3 ignored、0 failed。 +- Server schema89:保存値/NULL/FK/current DDLを検査、最後のhistory INSERT失敗時の全rename rollbackと再試行も成功。 +- Drive schema3:既存schema2のevidence/digest/replay結果/live nodeと再open後の動作を保持。 +- protocolのTypeScript feature、api-macrosのOpenAPI feature、E2E fixtureのcompileを追加検証。E2Eの実プロセスは起動しない。 +- 正規generatorによるOpenAPI/API/protocol同期とfreshness check成功。途中の未同期による失敗は生成後の統合テストで解消。 +- 生成後Web標準591 passed、component315 passed/33 files、checkは0 errors/0 warnings、build成功(chunk-size warning)。 +- root cargo check、workspace format、included migration/test filesのformat、diff check成功。最終命名guardは546該当行・未承認0・stale例外0。 +- 最後の不要14行修正後、対象25 testsと変更3crate全体(Subjektiv/Runtime/Server)を再実行して成功。例外一覧を増やさずguardを通した。 +- 命名guardの5 tests成功(大小文字・複合語・ファイル名・binary・限定例外の改変/増殖/残骸を検査)。 +- 独立read-only reviewでは今回のmigrationとguard本体/直接testsにblocking指摘なし。schema同等性検査はCHECK式やpartial-index述語の完全な同等性証明ではない。 +- 追加実行した非標準Webテストには既存のsource-text assertion失敗が1件ある。`Current requirement evidence` を変更していないTicket routeへ要求するもので、HEADでも同文字列が無いことを確認。無関係な修正はしていない。`target/state-name-nonstandard-existing-failure.log` に記録。 + +今回の主証拠ログは `target/state-name-rust-combined-final.log`、`target/state-name-root-check-final.log`、`target/state-name-api-freshness.log`、`target/state-name-generated-web-{standard,component,check,build}.log`。前回の検証件数を今回の結果として流用していない。 + +live Server/Runtime/DBは変更せず、push・restart・rolloutは実施していない。既存stash2件は保持している。 diff --git a/docs/report/t718-subworker-path-context.md b/docs/report/t718-subworker-path-context.md index fb70a6329..d4ff76fca 100644 --- a/docs/report/t718-subworker-path-context.md +++ b/docs/report/t718-subworker-path-context.md @@ -1,5 +1,7 @@ # T-718: SubWorker の cwd・参照コンテキストに関する障壁 +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## 観測 T-718 の実装を `crates/ticket` / `crates/worker` / `crates/merge-request` に分担した。SubWorkerSpawn の cwd を各 crate に設定すると、子のファイル tool は `src/lib.rs` 等で成功する一方、親で使える repository-root-relative な `crates//src/...` は crate prefix が二重になって失敗した。cwd 未指定の Glob は一部の子で transient bash-output 側のパスを logical path として扱い失敗した。 @@ -10,6 +12,6 @@ T-718 の実装を `crates/ticket` / `crates/worker` / `crates/merge-request` - 子の startup context に実際の Workdir attachment alias・logical root・tool cwd を明示する。 - cwd と path root の関係を tool schema / Spawn 説明と一致させ、cwd 外に委譲した read-only scope の参照方法も示す。 -- Ticket implementation の子へ typed Ticket read を委譲できない場合は、親に authoritative item revision と本文を渡す責任があることを明示する。 +- Ticket implementation の子へ typed Ticket read を委譲できない場合は、親に authoritative item snapshot reference と本文を渡す責任があることを明示する。 本件では委譲 scope を広げたり Backend storage を直接操作する回避はしていない。製品要件の追加ではなく、分担時の tooling/context で発生した調整コストの記録である。 diff --git a/docs/report/t724-validation.md b/docs/report/t724-validation.md index 37a575935..f7dd954c8 100644 --- a/docs/report/t724-validation.md +++ b/docs/report/t724-validation.md @@ -1,5 +1,7 @@ # T-724 validation evidence +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + Scope: `T-724`, authoritative item `00001M4D5ZN6P:4`, resume instruction thread sequence 7. Baseline: `c79506b60f1abcb3d8698d01491f2b6e644170d0` (clean detached checkout). Source branch: `work/T-724-workspace-drive`. T-717/T-722 are integrated; @@ -12,9 +14,9 @@ T-723 Tools/WIP and T-714 implementation are not modified. | Shared Workspace asset, no Repository/Git prerequisite | Workspace sidebar Drive; optional-node route; real `/api/workspaces` repositoryless bootstrap, zero repository records/config, folder/Markdown/PNG/empty-file and Worker artifact roundtrip. | | DB hierarchy, stable string node URL | `driveHref`, strict decimal/ref/metadata parsers; root/list/parent-chain APIs only; rename/move/delete/recreate lifecycle in real adapter and production browser. No blob path or second persisted tree. | | Paged folder list, metadata and mutations | Service cursors, bounded pages, breadcrumb, search, creation, native binary download, replace, relocate, empty-folder delete; real and fixture roundtrips. | -| CAS and draft retention | Per-Workspace/node draft with read revision; 2-account real concurrent CAS (one success/one conflict), 2-tab browser drafts, explicit refresh vs discard; component regression proves open rename form does not silently rebase on refresh. | -| Identity/revision fences | Controller selection/write/page epochs; saved tuple, scoped receipts and separate cancel generation; delayed old read/save/status/workspace browser gates; keyed image decode events and URL revocation component regression. | -| Safe Markdown/raster/binary | Existing raw-HTML-rejecting renderer; same-Workspace explicit resource navigation and credential-free external HTTP(S), no automatic inline image requests; real revision-bound PNG transfer, 256 KiB raster bound, HTML/SVG download-only, failed image/truncated/empty browser states. | +| CAS and draft retention | Per-Workspace/node draft with observed node state at read time; 2-account real concurrent CAS (one success/one conflict), 2-tab browser drafts, explicit refresh vs discard; component regression proves open rename form does not silently rebase on refresh. | +| Identity/content-condition fences | Controller selection/write/page epochs; saved tuple, scoped receipts and separate cancel generation; delayed old read/save/status/workspace browser gates; keyed image decode events and URL revocation component regression. | +| Safe Markdown/raster/binary | Existing raw-HTML-rejecting renderer; same-Workspace explicit resource navigation and credential-free external HTTP(S), no automatic inline image requests; real node-content-bound PNG transfer, 256 KiB raster bound, HTML/SVG download-only, failed image/truncated/empty browser states. | | Upload publication / errors / OutcomeUnknown | Raw Blob ≤16 MiB + SHA-256; transfer receipt vs DB-published acknowledgment; cancellation/loss preserve request ID and query current-authorized result, no unconditional reupload; typed name/CAS/limit/denied/storage failures. | | Backend current permission and Worker grants | Existing authoritative Worker catalog; owner setting flag; explicit read_only/read_write, separate Profile enablement, complete paged grants, revoke; real signed read-only Worker denial/revoke and exact Web grant-adapter owner/member HTTP boundaries; component and browser grant interactions. | | Latest only / no added workflow | Explicit replacement/history warning; no versions, approval/publication workflow, public share links or recursive folder delete. | @@ -61,7 +63,7 @@ Reproduce using `web/workspace/test/drive-real-api/README.md` or - Web `deno task test`: **566 passed**, including 36 content/controller boundary tests and 2 preview/link-policy tests. - Web `deno task test:component`: **302 passed / 32 files**, including actual - Markdown rendering/alt labels, late decode fences, pinned form revision, grant + Markdown rendering/alt labels, late decode fences, pinned observed form condition, grant administration, and dependent existing Worker/Console/Markdown surfaces. - Separate fixture-client compatibility tests: **4 passed**. These prove mock wire compatibility, not real storage or authorization. @@ -95,7 +97,7 @@ Production rebuild and implementer-owned visual acceptance completed on runner correctly reports `completed-with-errors` for injected failures; visual acceptance is the implementer's review, not that exit/status label. - Visual result: **pass**, no unresolved new visual regression. Artifacts use - baseline source revision plus dirty context; production source is frozen by + baseline source commit plus dirty context; production source is frozen by the subsequent commit (only test formatting/documentation changed afterward). A final grant regression also proves catalog readiness toggles cannot abandon an @@ -124,7 +126,7 @@ Intermediate visual review found and fixed: actual `text` prop; retain no automatic image loading. Component and production browser regressions protect this. - Parent semantic review additionally fixed late image-decode event identity and - open rename/replacement revision pinning, with focused component tests. + open rename/replacement observed-condition pinning, with focused component tests. Injected permission/storage/list errors and superseded aborted breadcrumb reads remain in official context with explicit dispositions. A successful capture exit diff --git a/docs/report/t731-review-verdict-registration-401.md b/docs/report/t731-review-verdict-registration-401.md index fded7f508..4f2bf301e 100644 --- a/docs/report/t731-review-verdict-registration-401.md +++ b/docs/report/t731-review-verdict-registration-401.md @@ -1,8 +1,10 @@ # T-731 independent review verdict registration rejected +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## Confirmed observation -- Ticket: T-731, item revision `00001M4G6PHJQ:0`. +- Ticket: T-731, historical item reference `00001M4G6PHJQ:0`. - Selected MR: `01a120d6-36fb-7673-92ce-a4cd50ba0d7d`, repository `main`. - Selectors: `work/T-731-workdir-denial-diagnostics` → `develop`. - First reviewed source: `f9d70e2ae2c1b226fd5832dfa13102b9f1b6ac4c`. @@ -45,7 +47,7 @@ in this Ticket. - Reviewed source after the first forward fix: `10c55874e3cc4c3562f608e10c56ff1c4c2a4d3c`. - Fresh request: MR seq3 event `01a120f4-6c90-7791-a36f-69d52e2cbd4b`, - `2026-10-09T13:57:31.897275884+00:00`, same Ticket revision and sole MR. + `2026-10-09T13:57:31.897275884+00:00`, same Ticket item snapshot and sole MR. - Independent review found a second operational regression: normal Write's bare NotFound match failed to create files for typed/nested diagnostic context. A public-tool harness reproduced it despite 59 focused tests passing. diff --git a/docs/report/test-code-audit-20260806.md b/docs/report/test-code-audit-20260806.md index cd9951dc4..a6807f5e5 100644 --- a/docs/report/test-code-audit-20260806.md +++ b/docs/report/test-code-audit-20260806.md @@ -1,5 +1,7 @@ # Test code audit — 2026-08-06 +Editorial note (2026-10-10): obsolete state terminology is summarized by purpose below; cited IDs, commits and validation results still describe the original investigation, not new executions. + ## Conclusion The repository has a large test corpus and many high-value authority/state tests, but it is not currently a reliable repository-wide quality gate. Test quantity is not outgrowing production code, yet test bodies are becoming larger and are concentrated in a few inline modules. The Rust workspace suite does not currently compile, the tracked repository has no CI workflow that runs it, and part of the Web UI suite checks source text instead of behavior. @@ -127,7 +129,7 @@ The repository documentation describes tests as required, but the repository its The strongest tests use real SQLite stores, in-process routers, Runtime brokers, WASM artifacts, or lifecycle state and verify externally meaningful authority boundaries. Representative examples include: - `workspace-server/config_source.rs::invalid_candidate_is_never_persisted`; -- `workspace-server/config_source.rs::stale_expected_revision_is_rejected`; +- the then-existing config-source test rejecting a stale expected state (historical test purpose, not a current test identifier); - `workspace-server/store.rs::schema_v27_rebuild_rejects_cross_workspace_assignment_repository_drift`; - `workspace-server/store.rs::worker_spawn_operation_retry_allows_same_reserved_workdir`; - `workspace-server/server.rs::ticket_assignment_spawn_requires_inprogress_before_runtime_side_effects`; @@ -138,7 +140,7 @@ The strongest tests use real SQLite stores, in-process routers, Runtime brokers, - `web/workspace/test/config-source/wasm-parity.test.ts`; - `web/workspace/src/lib/workspace/sidebar/worker-subscription.test.ts`. -These tests protect state ordering, idempotency, workspace isolation, migration atomicity, stale-revision rejection, and information disclosure. They make a real contribution to quality. +These tests protect state ordering, idempotency, workspace isolation, migration atomicity, stale-state rejection, and information disclosure. They make a real contribution to quality. ### Low-value and brittle tests diff --git a/openapi/server-api.json b/openapi/server-api.json index d6791beae..53424c5a8 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -14714,7 +14714,6 @@ "description": "Stable category explaining why Workspace deletion cannot currently advance.", "enum": [ "last_accessible_workspace", - "revision_conflict", "dirty_workdir", "worker_removal_blocked", "workdir_removal_blocked", @@ -16136,7 +16135,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4" + "x-yoi-source-digest": "sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3" }, "openapi": "3.1.0", "paths": { diff --git a/tests/e2e/src/worker_environment.rs b/tests/e2e/src/worker_environment.rs index ec60d7078..5ab78e053 100644 --- a/tests/e2e/src/worker_environment.rs +++ b/tests/e2e/src/worker_environment.rs @@ -422,6 +422,21 @@ impl WorkerE2eEnvironment { false, )?; + let enrollment = self.run_command( + &runtime_binary, + &[ + "trust-workspace", + "show", + "--workspace-id", + &workspace_id, + "--fs-root", + self.runtime_data_dir.to_string_lossy().as_ref(), + ], + false, + )?; + let enrollment: Value = serde_json::from_slice(&enrollment.stdout)?; + let workspace_trust_id = json_string(&enrollment, "/trust_id")?; + let mut command = self.isolated_command(&runtime_binary); command.args([ "--bind", @@ -449,9 +464,9 @@ impl WorkerE2eEnvironment { .post(format!("{server_url}/api/w/{workspace_id}/runtimes")) .json(&serde_json::json!({ "public_bundle": runtime_identity, + "workspace_trust_id": workspace_trust_id, "display_name": format!("E2E {}", self.runtime_id), - "endpoint": runtime_url, - "expected_revision": null + "endpoint": runtime_url })), "register Runtime", )?; diff --git a/tools/check-state-contract-names.test.ts b/tools/check-state-contract-names.test.ts new file mode 100644 index 000000000..de01364e3 --- /dev/null +++ b/tools/check-state-contract-names.test.ts @@ -0,0 +1,90 @@ +import { + checkFindings, + digest, + type ExceptionRule, + findNames, + forbiddenWord, +} from "./check-state-contract-names.ts"; +function assert(condition: boolean, message: string) { + if (!condition) throw new Error(message); +} +const spelling = `${forbiddenWord.toUpperCase()}Conflict`; + +Deno.test("case and compound names are rejected in content and file names", async () => { + const found = await findNames( + `src/${spelling}.rs`, + `enum Error { ${spelling} }`, + ); + assert(found.length === 2, "file name and content must both be inspected"); + assert( + checkFindings(found, []).unexpected.length === 2, + "both uses need review", + ); +}); +Deno.test("saved-data exceptions match an exact line in exactly one file", async () => { + const line = `SELECT old_${forbiddenWord} FROM archived_rows;`; + const rules: ExceptionRule[] = [{ + path: "migration.sql", + kind: "saved_data", + reason: "Read the persisted old column before dropping it", + lines: [{ sha256: await digest(line), count: 1 }], + }]; + assert( + checkFindings(await findNames("migration.sql", line), rules).unexpected + .length === 0, + "reviewed boundary should pass", + ); + const copied = checkFindings(await findNames("current.sql", line), rules); + assert( + copied.unexpected.length === 1 && copied.stale.length === 1, + "copying an allowed line elsewhere must fail", + ); + const changed = checkFindings( + await findNames("migration.sql", `${line} -- changed`), + rules, + ); + assert( + changed.unexpected.length === 1 && changed.stale.length === 1, + "changed boundary requires another review", + ); +}); +Deno.test("duplicate uses cannot borrow the same exception and deleted uses leave stale exceptions", async () => { + const line = `old_${forbiddenWord}`; + const rules: ExceptionRule[] = [{ + path: "wire.rs", + kind: "saved_data", + reason: "Deserialize a saved key", + lines: [{ sha256: await digest(line), count: 1 }], + }]; + assert( + checkFindings(await findNames("wire.rs", `${line}\n${line}`), rules) + .unexpected.length === 1, + "extra copy must fail", + ); + assert( + checkFindings([], rules).stale.length === 1, + "unused exceptions must be removed", + ); +}); +Deno.test("exceptions require a classified rationale", () => { + let rejected = false; + try { + checkFindings([], [{ + path: "x", + kind: "saved_data", + reason: "", + lines: [], + }]); + } catch { + rejected = true; + } + assert(rejected, "a blank waiver cannot be accepted"); +}); +Deno.test("generated binary bytes are not excluded from name detection", async () => { + const binary = new TextEncoder().encode(`\0asm\0${spelling}\0`); + assert( + (await findNames("module.wasm", new TextDecoder().decode(binary))) + .length === 1, + "binary names must be audited too", + ); +}); diff --git a/tools/check-state-contract-names.ts b/tools/check-state-contract-names.ts new file mode 100644 index 000000000..d575e4293 --- /dev/null +++ b/tools/check-state-contract-names.ts @@ -0,0 +1,132 @@ +// The sole spelling here defines the prohibited vocabulary, not a state model. +export const forbiddenWord = "revision"; + +export type ExceptionKind = "saved_data" | "rejection_test" | "naming_policy"; +export interface ExceptionRule { + path: string; + kind: ExceptionKind; + reason: string; + lines: { sha256: string; count: number }[]; +} +export interface Finding { + path: string; + line: number; + text: string; + sha256: string; +} + +export async function digest(text: string): Promise { + const bytes = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(text), + ); + return Array.from( + new Uint8Array(bytes), + (byte) => byte.toString(16).padStart(2, "0"), + ).join(""); +} + +export async function findNames( + path: string, + content: string, +): Promise { + const pattern = new RegExp(forbiddenWord, "i"); + const findings: Finding[] = []; + if (pattern.test(path)) { + findings.push({ path, line: 0, text: path, sha256: await digest(path) }); + } + const lines = content.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + if (pattern.test(lines[i])) { + findings.push({ + path, + line: i + 1, + text: lines[i], + sha256: await digest(lines[i].trim()), + }); + } + } + return findings; +} + +export function checkFindings(findings: Finding[], rules: ExceptionRule[]) { + const remaining = new Map(); + for (const rule of rules) { + if ( + !rule.reason.trim() || + !["saved_data", "rejection_test", "naming_policy"].includes(rule.kind) + ) { + throw new Error(`Missing classified rationale for ${rule.path}`); + } + for (const line of rule.lines) { + const key = `${rule.path}\0${line.sha256}`; + if ( + remaining.has(key) || !Number.isSafeInteger(line.count) || + line.count < 1 + ) { + throw new Error(`Invalid or duplicate exception in ${rule.path}`); + } + remaining.set(key, line.count); + } + } + const unexpected: Finding[] = []; + for (const finding of findings) { + const key = `${finding.path}\0${finding.sha256}`; + const count = remaining.get(key) ?? 0; + if (count === 0) unexpected.push(finding); + else remaining.set(key, count - 1); + } + const stale = [...remaining].filter(([, count]) => count !== 0).map(([key]) => + key.replace("\0", ":") + ); + return { unexpected, stale }; +} + +if (import.meta.main) { + const result = await new Deno.Command("git", { + args: ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], + stdout: "piped", + stderr: "piped", + }).output(); + if (!result.success) throw new Error(new TextDecoder().decode(result.stderr)); + const paths = [ + ...new Set( + new TextDecoder().decode(result.stdout).split("\0").filter(Boolean), + ), + ].sort(); + const findings: Finding[] = []; + for (const path of paths) { + let bytes: Uint8Array; + try { + bytes = await Deno.readFile(path); + } catch (error) { + if (error instanceof Deno.errors.NotFound) continue; + throw error; + } + // Include generated binaries in the audit; decoding is only for finding ASCII + // names, never for rewriting data. No directory or extension is exempted. + findings.push(...await findNames(path, new TextDecoder().decode(bytes))); + } + const rules: ExceptionRule[] = JSON.parse( + await Deno.readTextFile("tools/state-contract-name-exceptions.json"), + ); + const { unexpected, stale } = checkFindings(findings, rules); + if (Deno.args.includes("--report")) { + for (const finding of findings) { + console.log(`${finding.path}:${finding.line}: ${finding.text}`); + } + for (const rule of rules) { + console.log(`[${rule.kind}] ${rule.path}: ${rule.reason}`); + } + } + for (const finding of unexpected) { + console.error( + `UNAPPROVED ${finding.path}:${finding.line}: ${finding.text}`, + ); + } + for (const key of stale) console.error(`STALE EXCEPTION ${key}`); + console.log( + `${findings.length} matching lines; ${unexpected.length} unapproved; ${stale.length} stale exceptions`, + ); + Deno.exit(unexpected.length || stale.length ? 1 : 0); +} diff --git a/tools/state-contract-name-exceptions.json b/tools/state-contract-name-exceptions.json new file mode 100644 index 000000000..53ae6cd1d --- /dev/null +++ b/tools/state-contract-name-exceptions.json @@ -0,0 +1,2186 @@ +[ + { + "path": "AGENTS.md", + "kind": "naming_policy", + "reason": "命名禁止の原語を明示する規則そのもの。消すと大小文字・複合語を含む禁止対象が曖昧になる。現行状態モデルの名称ではない。", + "lines": [ + { + "sha256": "b24c8ab3aabb18044f1011eda6cab552a47c735e6d6bc9ebf52df32a39eb6d2c", + "count": 1 + } + ] + }, + { + "path": "crates/manifest/src/lib.rs", + "kind": "saved_data", + "reason": "read_persisted_worker_manifest_snapshot の無version・schema 2/3 readerが旧Memory設定keyを検証してschema 4へ変換するための綴りとそのfixture。削除するとWorkspace identity・language・実行設定を持つ既存Worker manifestを復元できず、部分設定やmixed authorityの拒否検証も失う。現行manifestのwriterは旧keyを出さない。", + "lines": [ + { + "sha256": "895bf9a35a75911f2ee00873cee8216858b19d28c331be7363d5d92905e20463", + "count": 1 + }, + { + "sha256": "44642e3fb83c8cf5e7c24f3b9d7af519667c9d27cbb0bae27fadd91411f8aa42", + "count": 1 + }, + { + "sha256": "9da6d5e4d2c8a31faa9f8acb6c9ce0295532fa0822446f630d25d30d6777e8f2", + "count": 1 + }, + { + "sha256": "a553b91a997ed3b135abf3baef1ae2a4362320f813c231ca115b94e4fb8377f6", + "count": 1 + }, + { + "sha256": "d7699cd6473a4a80e8826b302e5906fdf9b29ca8ae9d3c66d736747551496752", + "count": 2 + }, + { + "sha256": "aff9330758edaa811259d84e5ebea60cdb7bbdfbd318acfa478af3e395b8fabb", + "count": 2 + } + ] + }, + { + "path": "crates/merge-request/src/lib.rs", + "kind": "saved_data", + "reason": "TicketCompletionEventの歴史的payload reader/writerは保存済みTicket completion属性を同じJSONで保持する。v12移行は旧review keyを取り除く前に原文をimmutable archiveへ保存し旧grantをrevokeする。綴りを変えると古い完了payloadの読取/roundtripか旧reviewの移行を失う。live completion/readinessの根拠には使わない。", + "lines": [ + { + "sha256": "0ad13adedc96d68be7f53aee2486a27f67a4f46a6b611c23d8e8a07a0385b808", + "count": 1 + }, + { + "sha256": "c4e2e113ea3005dda9175c98951118186742b9aa6235c9c592b791c19bcc7b1c", + "count": 1 + } + ] + }, + { + "path": "crates/merge-request/tests/store.rs", + "kind": "saved_data", + "reason": "freeze_v12_review_payloads とhistorical completion fixtureは旧reviewの原文archive・grant失効・統合結果保持、および保存済みTicket completion属性のbyte/JSON roundtripを検証する。旧keyを任意語に変えると実在した保存形式を通る保証を失う。", + "lines": [ + { + "sha256": "c9691d0873568a9b67b760d8f3d28c108ae2529cb56a19155627a3d83f1d390c", + "count": 1 + }, + { + "sha256": "29b401ff2dd21a5cbaa1803e7159b18bdfe02d4efe2d63aac01852e5cc9778c6", + "count": 1 + } + ] + }, + { + "path": "crates/session-store/src/system_item.rs", + "kind": "saved_data", + "reason": "frozen_subject_behavior_log_reads_old_counter_without_republishing_itの保存済みSystemItem fixture。旧JSONを読んでもexact archived bodyを維持することを証明する。削除すると過去Session log形式の読取回帰を検出できない。", + "lines": [ + { + "sha256": "5b5998029cfed30634e8b063b759aefd70e30dab3d02ce9defe9086977fc9811", + "count": 1 + } + ] + }, + { + "path": "crates/standalone/src/job_store/legacy_migrations.rs", + "kind": "saved_data", + "reason": "JobStore::openがschema 1/2 DBを移行する際の旧attempt列・request key、およびその実データfixture。原request/fingerprint/attemptをimmutable archiveへ退避して内容digestを再計算し、receipt・failure・grant・replayを保持する。削除すると既存Jobsを開けず、rollback時の旧binding保持検証も失う。", + "lines": [ + { + "sha256": "91b31d4f17e2c3b826887aa989655998930699db07190ae182cab69479b4157b", + "count": 1 + }, + { + "sha256": "95afb0c8b156e3d2bed11671f1c35f9f74d1589fc20d540f4b16c8d7c887f2ec", + "count": 1 + }, + { + "sha256": "869701cd7b0975f2837f6d9e61fddd0c9b6da36b0250cd77653aee9ca6343c01", + "count": 1 + }, + { + "sha256": "16cb51c05f2f830669af2440ba1e92b1d10f1aff6fa2933cebb36f055a28f926", + "count": 1 + }, + { + "sha256": "0a183581dadfa685bb2b3a58bd300883f2649816e1a63b4e90f903b6494036e0", + "count": 1 + }, + { + "sha256": "af70f24d3b73b08f64576a21ecfe31b605cbf9ba4bd3b3c446fb0d921e949bd9", + "count": 1 + }, + { + "sha256": "a37ba64e23c033ccac9057cec0c881c4826616e154d5a60dbeeae3c6a97f0dbd", + "count": 1 + }, + { + "sha256": "f555731b0a9a79e6be6307d196f1eea1247ecb0f24c8c49c26492cf64d1a0f78", + "count": 1 + }, + { + "sha256": "7aa68b52c228a072d05fa14a37d238736291b76177ad9a2e0c38cfe0d6d7f426", + "count": 1 + }, + { + "sha256": "4768381600ae71cb90f054f8cba748a4b3273194250bba05f3acedab56b480e2", + "count": 1 + }, + { + "sha256": "0e576b0e9502be2d0c26d4707d2703bc660513a97b6a2e7782de67731fae0823", + "count": 1 + }, + { + "sha256": "06ece4e9cdf7ff9d0db0982d037ec3f9a97490a879b51b847ef03289e039e5d4", + "count": 1 + }, + { + "sha256": "3579e071aaf8beea6580b3701356b31277181ea5bf1e76c017170b3643951672", + "count": 1 + }, + { + "sha256": "58e1d6ca58f896a644dd2b95be1e7a2afde9f62ede07fea2fff6ced2c145d68b", + "count": 1 + } + ] + }, + { + "path": "crates/standalone/src/store.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: decode_worker_recordのschema-1保存JSON adapterとそのfixture。旧counterだけ除き、manifest・Subject・Session pointer・停止metadataを保持して現行保存形式へ変換する。削除すると既存Standalone Workerを復元する互換境界とその検証を失う。", + "lines": [ + { + "sha256": "0072170c5ddd7379585f851b2ee9929420877246a82d1a6b99c351dd0653ebfa", + "count": 1 + }, + { + "sha256": "3e802d52e870906c99c486b5e83d7bb6e37b80b6599f2fea10322488d90a1742", + "count": 1 + } + ] + }, + { + "path": "crates/standalone/src/store.rs", + "kind": "saved_data", + "reason": "decode_worker_recordのschema-1保存JSON adapterとそのfixture。旧counterだけ除き、manifest・Subject・Session pointer・停止metadataを保持して現行保存形式へ変換する。削除すると既存Standalone Workerを復元する互換境界とその検証を失う。", + "lines": [ + { + "sha256": "4bcd12d150cb01ab0f50e7dfa96e8ca3f0128e975ae63e0f4bc431490369d32b", + "count": 1 + }, + { + "sha256": "c66ee75622bc5ecda6ef14c07e9cd693cd1ab8335bb13bb9f5735275f2ac8f34", + "count": 1 + } + ] + }, + { + "path": "crates/subjektiv/src/legacy_migrations.rs", + "kind": "saved_data", + "reason": "FeatureStorageのschema 1..6移行とremove_state_numbers/convert_jsonが既存DB列・履歴表・JSON参照を識別する綴り、および投入fixture。全recordの原文をarchiveし、Memory change/predecessor・candidate/derivation edges・decision receipt・surface provenanceを新identityへ対応付ける。削除するとSubject/Memory履歴・参照・replayの復元が壊れる。旧surfaceをfresh扱いしない検証も必要。単なる旧名のエラーメッセージは承認対象外。", + "lines": [ + { + "sha256": "5ec82b793d313dc5f6df05ee3a05146536e81235a44869a5254e7a7ba1665ead", + "count": 3 + }, + { + "sha256": "e2cc80185594b9f8849ebb75fdf74c159ef3df1300320cfef3324c48440a365e", + "count": 1 + }, + { + "sha256": "db61f8d62fc843a87e0f023e1018ce12d1d4779e7e9cf5cde4d2582572002901", + "count": 1 + }, + { + "sha256": "4a671ca3a159258ef8c9014a26be1907094f205a7f8d31cb4619d90236f53a89", + "count": 1 + }, + { + "sha256": "83244d33dce1d9862b866b86c8ad0be4756576aef94157acd79ba506df040159", + "count": 1 + }, + { + "sha256": "ff90c702561070763ed3cab8223f1e8af35943f32bac77ede934b04c4621e89c", + "count": 1 + }, + { + "sha256": "1498bf243a4fd6bde42a72947b7c188d10e2ca06dbed5bfe858f1d043a7e9d36", + "count": 2 + }, + { + "sha256": "f1b1b26fd35f6522181d6bef45cee09256a01595cfe77af82acdd248eee2c9b0", + "count": 1 + }, + { + "sha256": "d961bead14c98f33705dd7fd6056d68d1d701865c921f881a0080f3c08ea3f6a", + "count": 5 + }, + { + "sha256": "a16ee03e21ad95fdd0b5c54a2382b19d4aa61fa6c319311ee68d214f8f3d52a2", + "count": 1 + }, + { + "sha256": "1bf54107893da3f0cbb7da1cf7a994d1c691df30daa71312a153258890f5589a", + "count": 5 + }, + { + "sha256": "ff84e5199326de2ecac61d68f1debb2b46f8cbecafcf97f8bed49402ce5bb692", + "count": 2 + }, + { + "sha256": "a3b5561da66361e3111077a84e3b015384d1b33217401f70e80863857e5875fa", + "count": 1 + }, + { + "sha256": "77a625492e8aa959ed6e106e0e918a49be174e26e4e65e253fac32389fe86f9c", + "count": 1 + }, + { + "sha256": "7221ecd8ecee22e3bdd1d9cdc3337664f88618d7448060c7cce576d68c4d833d", + "count": 1 + }, + { + "sha256": "f5deffa9d43630fd36569961435eba8ca78530033f13b528deac641069cb3110", + "count": 1 + }, + { + "sha256": "3b696d655272df49121d747a2fb5228fb9e0eca110ce7761b2927608fd863a08", + "count": 4 + }, + { + "sha256": "0441cd4a2a1b4cad1be421d24cdfd2342a4ad815dadc5f73973d9a12bcc769d0", + "count": 1 + }, + { + "sha256": "cdc551145ca509666559a29495293dbe2a02adfb881c50e0f35063e5191254fc", + "count": 1 + }, + { + "sha256": "51c74d51580634a5e3f985284702fc4dbe0a00cd9636c4f4d6f8fa3ca01ff9b7", + "count": 1 + }, + { + "sha256": "6219b56470370ff004103d291bd512fc2a94410fcbaf842dbba85ca3bad5ec92", + "count": 1 + }, + { + "sha256": "c50ff3a5c9b1038553a84cf40949813af13bdc18ec7d0ae71913f08e6692d6c7", + "count": 1 + }, + { + "sha256": "8be535a7848b73a6cef1ca78b08b5734626f8db9fdc94376ed315a31fc652404", + "count": 2 + }, + { + "sha256": "fd6c91c450ae7876e4b82945061570730da6f12caf4aba911e36cf29702b8608", + "count": 2 + }, + { + "sha256": "d2c56adcd7aea8968a38f16d9069391edbc22522cea857ef8d64c6164ea55798", + "count": 1 + }, + { + "sha256": "fbd43f49ec5e665fa54de1f29015861c9d7a113edccbeaf5b48917098901c9a6", + "count": 1 + }, + { + "sha256": "a7ff0a428fd72fdaaa5b8b1b107926c4236c031517ad130c5fba95e3e0303d17", + "count": 1 + }, + { + "sha256": "44015a4c6260db6199a010e43d728e74fb6170ef53a51c0e3816bbfa0d503fd8", + "count": 1 + }, + { + "sha256": "d9f75cbe1fc0df0412a505d2d5f2eac0906cee7dfc66c24cabbf57ca40fbc50c", + "count": 1 + }, + { + "sha256": "e4047ff3d831bbfdb41a1e8285cd7a41ff4f70cf34ef5f09f45cd493c040eaa2", + "count": 1 + }, + { + "sha256": "6104685e6f2217d328db51daf9dca18576068264c84ac41c2ef101cd8509de7c", + "count": 1 + }, + { + "sha256": "ee9d0ba29789b6292853bb4fbbfb5795aefc42418a041bf79cc77eefa37beaaa", + "count": 1 + }, + { + "sha256": "596aaeaf61e6d59520839a6aabfb394e3069d8e6a141b9cb6fe1268c6ef178b9", + "count": 1 + }, + { + "sha256": "c9495213b8554fa87f9583d0731f4cbe5166225746b1a60033dfecadd7620b56", + "count": 1 + }, + { + "sha256": "59604dc68d1fc436ab381abe2dbb32fe95f8755a18201082a111125d80cc9ee6", + "count": 1 + }, + { + "sha256": "94cddbb48c5aab0172bbe96f2907dcde70dabecfc4ead49795d94e649c98f58c", + "count": 1 + }, + { + "sha256": "fe40dee1b513165d59faa31e462dcf81b93c4bdab57214bf8185d1a3c65c0e9b", + "count": 1 + }, + { + "sha256": "bc56670cbdc6c51e23e635a5d7ce8dd22e602cc907bb02603a61be87f54eb819", + "count": 1 + }, + { + "sha256": "23155664866e894f478307c2e52e64818d06380248577a89a3c0baef24721437", + "count": 1 + }, + { + "sha256": "eafc5bbeace185e30564c38b60f7c7d70a000316b827cec737d8ed0fc8ed738f", + "count": 1 + }, + { + "sha256": "e8b7d218732c804d6d4a421e681c32c1af1236b25bfb84a2dbc8846ecd90a6f9", + "count": 1 + }, + { + "sha256": "bec0a876835f1139ebf4814a7f363d04515af77faba760bb6d5e9b934ee941ae", + "count": 1 + }, + { + "sha256": "fdf6a8ef67154c0bf56cbf0e2bca59abfe6323c0e694d6465179c5af67492bc2", + "count": 1 + }, + { + "sha256": "7cbca1c52c2956e02b71f96279bf07141edf0c3ecd2837a6697a336b4b2d415c", + "count": 1 + }, + { + "sha256": "03631aeb6c631d791dac4907f68fe31c1043294a75add1175462784ad17b24ab", + "count": 1 + }, + { + "sha256": "724e77ccadc159936eb409b9559e965a3e743d863d653c9f8fce7f7884e1d955", + "count": 1 + }, + { + "sha256": "e63ba0c9adb8b4630daee8aba359385667d0f874ba38de9869b9fe03e93eb627", + "count": 1 + }, + { + "sha256": "2fc45636fc8893eb77ac3508b202aecf399458de74ff2c690ca00f1641c28293", + "count": 1 + }, + { + "sha256": "2fa3b93393abaa48a5e5fe036407416c9eb3ba942638d3c2d23e504a3495f6e1", + "count": 1 + }, + { + "sha256": "b89d417690410f74dd2acb0dd68e28407b4bda70879cc965b8a5ff646287f27f", + "count": 1 + }, + { + "sha256": "bf227ba847c24ab8cb591d4d8c43e184c53a824f316863aa47f9486afa62b4b3", + "count": 1 + }, + { + "sha256": "eef72e24ea058464497f595bd5498b15eafa12bf4a3701d861ee4d77b72011ef", + "count": 1 + }, + { + "sha256": "12467910594102acfe56e4a422775ae6cf88ca43ae4876bc450fd7296fad88b4", + "count": 1 + }, + { + "sha256": "c32f259beb2a52f17437edc1b353b0f3e476e6a5073919eb58a034a55c829b02", + "count": 1 + }, + { + "sha256": "e61edb1e37216b0841487519efaad5322af689e886369a4a8169d352f181b633", + "count": 1 + }, + { + "sha256": "4483536f462c4f3b7c8bf91c1ce2f4fa20a10883dad53e51e37978c1de79fb25", + "count": 1 + }, + { + "sha256": "b21b714ee01a74237762019ec70a26b0854525ceace84cca239e2601d7b46fca", + "count": 1 + }, + { + "sha256": "99078511a45c6998fecbdbea406ec44503ee54ff36abed9b203e5e7e4617570b", + "count": 1 + }, + { + "sha256": "e0a2560e419f20a299596463282c49235bec92e7f349865b80df901adec31e84", + "count": 1 + }, + { + "sha256": "c44589f8227e67ba87754f3b7b2600a2b0d0019fca8df61a5d0ebd481229624d", + "count": 1 + }, + { + "sha256": "529bfcfc490276654971aedcbd96f8ea211a8af6e13a8f36683a275d6e006d7e", + "count": 1 + }, + { + "sha256": "a3577068d4351c5f6b5a5b6256a28db9226a57ce55a26febe90e74a306912110", + "count": 1 + }, + { + "sha256": "d4a057bb0019067f3ffab88f1b835c949df570e7f75e717c784bc3f1d4766895", + "count": 1 + }, + { + "sha256": "2c25b36a6762710da11d032ebb0b998200b088567dbcb101c772ede488c08ce3", + "count": 1 + }, + { + "sha256": "7b417bb0f62c7b7a1f674e219630a9b58790b9fd401ade40ea8c3133c19f0110", + "count": 1 + }, + { + "sha256": "de8301e068fdb96e622eb2ec75bf7ff01351c778d655ab73a2909da0d737a62d", + "count": 1 + }, + { + "sha256": "4fb088b34b28c71a5b9566043e13da7622fefbf24311300f295c7c75ed2bbab0", + "count": 1 + }, + { + "sha256": "e27eba1ed5ece48cd7493eec033de58616351e06eeec711fbd5cbf9ac9482ec9", + "count": 1 + }, + { + "sha256": "92727f0405d30eaf54b9432a0434af03bc1f8bf73e92c467e43ad4ae89da6ba1", + "count": 1 + }, + { + "sha256": "672920d9c1f6a6eda286cab0c39ede05c27732a99c9319b8731acba62dd1948b", + "count": 1 + }, + { + "sha256": "f2ee44ddbaa8e5a4a48d90cd33f3b35edd4069fee81b16c6548f4526edbc71c9", + "count": 1 + }, + { + "sha256": "0935123d8eca4b70c3a3b1b19a552f750a414cc53687655cc0598958a83a36ab", + "count": 1 + }, + { + "sha256": "e75b06f355d5c0261e0e5e60a3cb8e37725582575e38ac4bc840842d9a47e364", + "count": 1 + }, + { + "sha256": "f3ac630723e44932a71b356ae9c3c805638e6d6ddf2ce77fb66587e05abb2b40", + "count": 1 + }, + { + "sha256": "30f0d94a494f8a6d91d43eb2710fe5b523f7cdfdac6c29e6032aa13c587a9c5a", + "count": 1 + }, + { + "sha256": "d1927a3057e5bad0d1c1ddc5945faf84563f6e608ea47167511cc9b8c4b10aca", + "count": 1 + }, + { + "sha256": "6dbb16341df981e8df275f81267a134e6c15d2f4a9d8ab76100754da10428806", + "count": 1 + }, + { + "sha256": "f6d4cfe9a245d9ca77eea50a2a57c6fa9561fadccca4dfa4279bc100ae7d9333", + "count": 2 + }, + { + "sha256": "c0a80b23acc183e12503f829d88dae1808e48e5fb406aff975c292c9dc21d35a", + "count": 2 + }, + { + "sha256": "64e26d2fda77af14128a81886b0808616de520797c356e1b5ca8cba69392421b", + "count": 1 + }, + { + "sha256": "e248729ce44c98f37f3f4396c6be12c4c72424058fcd7c28bc60ae69340ca82e", + "count": 1 + }, + { + "sha256": "b241f3eb4890ac180cb0ab471d37e6dd073edb6065e13171f6446c8bede470f5", + "count": 1 + }, + { + "sha256": "c82e01bcd5d7a8113d8697e1a539d2fc52456a14ad96953e4d2cbe7dd58fa96b", + "count": 1 + }, + { + "sha256": "ec2dc651a1ca77077d879fee5b0f26b97fcbe4e8831c611284e6ffa55c30973c", + "count": 1 + }, + { + "sha256": "b8f5bfcac251b3cde53a617dfba362b61829c72bffb7d0e5f76c135d9e3d8c81", + "count": 1 + }, + { + "sha256": "a65cb819ddb1b9c447b937ee88a49f35c171103c248ce607f1115a53c8a4b582", + "count": 1 + }, + { + "sha256": "7676b529d2ff24d90c3a6a784f33e3d67caf81af2f3530a4808a5515480278df", + "count": 1 + }, + { + "sha256": "c981a8f6d160ec986f3e63665b9dddb83761ca2f2672a504030de10e23dde7aa", + "count": 1 + }, + { + "sha256": "bc34dccc0a3048e62ab5553ac86ba041409b60d8a8259e8e7225bf2818fc04e7", + "count": 1 + }, + { + "sha256": "b2b0a489ea02acc1e4b17066c6e0503ee184ccdb2adb6eed4bf04e221d6bbebd", + "count": 1 + }, + { + "sha256": "209cc6be2f272bfcebbba5d5aa3962462dc8b5ebf9a602c0dfb67317c5aadae9", + "count": 1 + }, + { + "sha256": "6f5c7c6d382c941151d22c190303a7f6859fe67efb99b345ef1eace54973adce", + "count": 1 + }, + { + "sha256": "20be9a312619e1049b019f01a8a89adb4b1efbe8a6b014026460a97eafa64539", + "count": 1 + }, + { + "sha256": "1f0cfa646323098172225890c40729af0d7f56d79123699fe1dfef6f6c03d1cd", + "count": 1 + } + ] + }, + { + "path": "crates/ticket/src/lib.rs", + "kind": "saved_data", + "reason": "frozen_counter_receipts_remain_immutable_and_cannot_be_reinterpreted_as_content_requestsの旧completion receipt属性fixture。保存済み旧fingerprintを新content requestの証拠に昇格せず、移行後も履歴を不変に保つ保証。keyを任意語に変えると実在したreceipt形式での回帰検出を失う。", + "lines": [ + { + "sha256": "060994ddcd0e463815b01cd11b19723fd3652bd4c741fccdb33628e97691f092", + "count": 1 + } + ] + }, + { + "path": "crates/worker-runtime/src/fs_store.rs", + "kind": "saved_data", + "reason": "Runtime store schema 6..9 request migrationの旧Job/Memory/source/config/SSH key綴りと実形式fixture。Job/attempt identity、操作owner、source/config digest、language、保存済みRestore evidenceを保持し、未知のSSH fingerprintを捏造しない。削除すると既存Runtime storeの起動移行またはfail-closed evidence変換が壊れる。", + "lines": [ + { + "sha256": "2b4cc5ad537c224f32ac1a46acaf0f5d29fc9389277b94b942c909b3f2f063dc", + "count": 1 + }, + { + "sha256": "3040b1409539446054d5469283fdf59e658ada23c7aafd032787624a7a9f4e48", + "count": 1 + }, + { + "sha256": "7b7eb0bd2fca79a47895167ff8d639781f1db38c8641cdad70802e5ecccdec56", + "count": 1 + }, + { + "sha256": "a6ce0f1cc7c588908d25c6592049e5a27fde5287f90d6ee5eb2ffd08dccccc82", + "count": 1 + }, + { + "sha256": "eb760e10f595fef48a66369d13f6c347012860c7cbc0de654c6178ec31cc367f", + "count": 1 + }, + { + "sha256": "b4085b2b779ea47ca6be1cd9fb314a90893c61804263abb6c46ad5060b5b6ef6", + "count": 1 + }, + { + "sha256": "2f0d053e0f4bdea85dfdc476022acb82312a860635e68b6a3a1827fdc5d43829", + "count": 1 + }, + { + "sha256": "ee8ae2910e73987003bf663a5278a2f8ef09884d671020b574c8723c0691323d", + "count": 1 + }, + { + "sha256": "4bcd12d150cb01ab0f50e7dfa96e8ca3f0128e975ae63e0f4bc431490369d32b", + "count": 1 + }, + { + "sha256": "70a6066e5e8a244bb7f0002a658ab1b61e4a6cca0e3bd118a171830b9feb943c", + "count": 1 + }, + { + "sha256": "55c515a690366b81e4312edbeb41b6bc3bb95c3ff2c7c5f90f7ee88708ef6b81", + "count": 1 + }, + { + "sha256": "a5a73937b3d995980680274358e53d18507b726ef7bf76381c4f7248f194081a", + "count": 1 + }, + { + "sha256": "d7cdf8e8fa660126e12e1faab46c47158110333aa1021e5276dbc39cd390b603", + "count": 1 + }, + { + "sha256": "8a22728573a86efaa96d522309789a0e2a85f2d59b71c2aa91da6c9fc08f85b7", + "count": 1 + } + ] + }, + { + "path": "crates/worker-runtime/src/main.rs", + "kind": "saved_data", + "reason": "read_runtime_auth_fileが旧issuer enrollment TOMLを識別し、原文をarchived_legacy_issuersへ保存してrevokedなtrustへdurably変換するkeyとfixture。削除すると既存auth storeを読むか、旧issuerの原文保持・自動再承認禁止の検証が壊れる。", + "lines": [ + { + "sha256": "9709bec9500027b885d3d43edf626f896c2d1623b36bc851cc5df309473082ef", + "count": 1 + }, + { + "sha256": "1da94bf8dcdd38aea77361276331f795c820dd8bcaee492410ba92d6023c05a5", + "count": 1 + } + ] + }, + { + "path": "crates/worker-runtime/src/working_directory.rs", + "kind": "saved_data", + "reason": "FrozenEvidenceReadが旧SSH evidenceのkey存在を使ってrepository_access_requiredを必ず立てるためのrenameとfixture。削除すると既存materializationがSSH evidenceを無視してfresh access無しでsessionを開く回帰を検出できない。旧counterはkey identityでもlive authorityでもない。任意unknown-field normalizationの旧名fixtureは承認対象外。", + "lines": [ + { + "sha256": "fd6f43942d27a6fa2c7c26002d723e1c27da23d0d25c78fa9e13b500bc4f0118", + "count": 1 + }, + { + "sha256": "4fe12d081ab4c6aeed50a51aa90b543f7d5ba72f15f64126f6aa2367c77d9993", + "count": 1 + }, + { + "sha256": "4fa68e5b0d78bdf4e7514850d7d0f4ccf12c6f8a59aeff360b092f176f7ef526", + "count": 1 + }, + { + "sha256": "f5ef6820fbb7fc871b1a9fc9f532ef2ad314decb35938fe39baa35e07369d175", + "count": 1 + } + ] + }, + { + "path": "crates/worker-runtime/src/workspace_issuer.rs", + "kind": "saved_data", + "reason": "LegacyWorkspaceIssuerTrustRecord/LegacyVerificationDocumentの保存済みenrollment・verification keyとfixture。旧trustをrevokedへ移行し、旧verification bytesをarchiveするがlive recordsを空にするconsumerがある。削除すると既存記録を読めないか、旧capabilityを再承認なしに復活させない移行検証を失う。", + "lines": [ + { + "sha256": "56e399dc4e828e66677d4ad09b4c66a9c09658322b3886a06b4ae688c63dfc84", + "count": 1 + }, + { + "sha256": "ea16c387d0048571bd0a5576f1358af32acf63f8ee5ebe08c262da83c5a04d88", + "count": 1 + }, + { + "sha256": "bfd48831eb694b879a2859d42dba00fe74a4a817177acc61a44795cbd19b1056", + "count": 1 + }, + { + "sha256": "94af0abdfb45c712e9cd1949ac6a15d1a21c3b271f004c9282f81b5fd5ae4f5d", + "count": 1 + }, + { + "sha256": "d099e3a7216349c0ef30026967b8fe344e3b08b58a77bad6437468502b56bf71", + "count": 1 + }, + { + "sha256": "bed791d41faa85b38602c238d30460681e6d9ebbe56c4c08b843dd1971a589b2", + "count": 1 + }, + { + "sha256": "ce36822c65f785f330e42b7d37241062d994dc4e1c18abaa327b7996da5b6521", + "count": 1 + }, + { + "sha256": "5f4aed5ed50f2bbf54bbb9293948c3294eb2d30086d0fbe7cb07899efb5c96ba", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-drive/src/migrations.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: Drive::registerのschema 1..3旧node列・receipt JSONと、matches_legacy_replayのpre-cutover typed serialization key。actor/payload全体の元fingerprintはfield綴り・順序・decimal string込みなので変更不能。保存済みblob/deletion result・request_status・同一request replayを失わず、live node CASには旧counterを使わない移行/fixtureに限定する。", + "lines": [ + { + "sha256": "280c5eb3d4fcec6360653c3347f14d7a5753ade824ebf3f687c26f7c0fbb403b", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-drive/src/migrations.rs", + "kind": "saved_data", + "reason": "Drive::registerのschema 1..3旧node列・receipt JSONと、matches_legacy_replayのpre-cutover typed serialization key。actor/payload全体の元fingerprintはfield綴り・順序・decimal string込みなので変更不能。保存済みblob/deletion result・request_status・同一request replayを失わず、live node CASには旧counterを使わない移行/fixtureに限定する。", + "lines": [ + { + "sha256": "7aecfb51a37d21c8203ebd0d672839bf2e13b4a303f47bb6b7267794dfc886d3", + "count": 1 + }, + { + "sha256": "2be5a8b7dad838014cf460f9b1d9183a5cde7bdcf90217f2e36fb8d4406cf808", + "count": 1 + }, + { + "sha256": "9dc8e4e9a87ab0e3079f181d4384ed13a1ebe47f1c4b426a7bbbac118dcc4020", + "count": 1 + }, + { + "sha256": "4d7b2f7184c36b2ae51f9bb6bc2b43067cccb93df2b0a3494e92aa18dbe1cf86", + "count": 1 + }, + { + "sha256": "97be6f18534df4cd8b59acccc051ba461245deeab4e80c601d75ced613587830", + "count": 1 + }, + { + "sha256": "799ae6c3205366b580a6df511657bb0c5dfbd85515a77aa12585b007419e7d21", + "count": 1 + }, + { + "sha256": "6fa43834f63b7af3157ff18eb4ded16c8fc5970dc46db30d2585c4b227b9f8bd", + "count": 1 + }, + { + "sha256": "487b08b4e378baffe0a20946c59fc1b4c66d79e85720809b7bde737d9d43e0f8", + "count": 1 + }, + { + "sha256": "f49d7aaa1f8ab6e1b2177b0ca5714049124b20f026cb28d902e3b03b3cb3b192", + "count": 1 + }, + { + "sha256": "c01d2954d66dc3772e7099f43ed53ebb4a5e4c687744358bf7a23f5fc4409475", + "count": 3 + }, + { + "sha256": "7beca5582e5896612b580e094e966410086eef5c5b21c06393deb80ee5e01c4b", + "count": 1 + }, + { + "sha256": "98bebc15fa006d467ff78761d113c60a9c6b45e3cdfbc23ad9c3cef783ddf88e", + "count": 1 + }, + { + "sha256": "da9f42a8350fbdda2397135e280c172b1795eadb93559b2e4341f40c3b41e64d", + "count": 1 + }, + { + "sha256": "8d97c8eb4b9b254e2dff76e77129a11e5032970e4084a6ade391c7c785171ce8", + "count": 1 + }, + { + "sha256": "bb71b541ab42da958c1f1e4a1eecffb3bd2804d56431722f9051f494d60f43e5", + "count": 1 + }, + { + "sha256": "fb11ae85237de2308262a9e2008022c98d35662a61d1350e1aaaa0d984e107b1", + "count": 1 + }, + { + "sha256": "d7f2368d7fff552b539242e53e0a5fb76511ea8613a10b357527df43a58b803c", + "count": 1 + }, + { + "sha256": "ddd3eed2cb34bf550b0583ed1cbf321afa026665fe73fb3af9a18dc0264f421b", + "count": 1 + }, + { + "sha256": "843b0f4eeb69d6b16b92701f6927d3fba831343314341b3c5570f6466bb8266a", + "count": 1 + }, + { + "sha256": "9a4c2e2ddbfced4fc95c88a1b5e3f46a46ab96441a0b94909c3783f519b8aa99", + "count": 1 + }, + { + "sha256": "748d815cbb47ed37dea132e7657fcb9a8f3002dd5f050261578354acfe1d262b", + "count": 1 + }, + { + "sha256": "5fd04443f2b03ba3826735bef99cafc7be7624e1b78043d8d9fcd4949902c916", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_content_state_migration.rs", + "kind": "saved_data", + "reason": "schema 85から86への旧config history/Memory/reservation/Job列とJSON key、および実在したCHECK句。既存config評価provenanceとFK/constraint、language、Job結果/cleanupを保持しながら新content契約へ移すconsumer。旧DDLを任意置換すると既存DBを開けず、追加constraintや未確定Jobの安全な失効を損なう。", + "lines": [ + { + "sha256": "0a419ed7e40d38ac8cf7c49ee2238fc92a469499aacec6e4b26045e5a2eb9f84", + "count": 1 + }, + { + "sha256": "6d41393ee6a57abaf20a861ab83832ae0fc1d3b1119f499c557ee7d1795df655", + "count": 1 + }, + { + "sha256": "52c965431aaa4499e603bffbac286747d8ad6b88db596494b3bbd40fee00c33d", + "count": 1 + }, + { + "sha256": "b230ab364be3cd794f1a84188e54a20aa1a1b2be70dde948971e327dc4af3de5", + "count": 1 + }, + { + "sha256": "8c9d03c26d103840737d723f7be92eecb02a04eb868bc7c7882684f2ae21e8f5", + "count": 1 + }, + { + "sha256": "784b16a1adf7a2304e91d0adc9338f9145241775aa6787ac8719a2869574ece0", + "count": 1 + }, + { + "sha256": "70ffe9e4da6367bce90f54fbb2c9444298cf25187a2ebdacb103fb08a6e111ac", + "count": 1 + }, + { + "sha256": "b494acebce40eacc29412386dc5263861f9e5557d98fbcc1b6d7dc91b9b06e30", + "count": 1 + }, + { + "sha256": "7fd1848c94fb89b6137de5c3256c9c0c32f4e36f7bbd641bb23becdc2fb983e0", + "count": 1 + }, + { + "sha256": "b0fe075b83cf002cd547b7420c45f0aa92ee7b60252468448ccca647852d395c", + "count": 1 + }, + { + "sha256": "869701cd7b0975f2837f6d9e61fddd0c9b6da36b0250cd77653aee9ca6343c01", + "count": 1 + }, + { + "sha256": "c1d44bc9022b1cf7a03f750fec939849f07184d6d695257d6b78876b19fe3807", + "count": 1 + }, + { + "sha256": "48359a7475d203d6bbc5964d765baae50fe0475b4cd22735f9e87fc83bfe418f", + "count": 1 + }, + { + "sha256": "1f0a5b4cf38de0c02a057437d710904ba1a1295c03c93575e33e55e2e971dcb7", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_content_state_tests.rs", + "kind": "rejection_test", + "reason": "移行成功後の旧config history table・Memory/reservation列の消失を検証する。共通のtrim行は成功後の旧reservation列消失3箇所と移行失敗rollback後の旧列保持1箇所で合計4回使用するため、一つのhash/countに束ねる。削除すると旧名の残存か失敗時のデータ保持違反を検出できなくなる。fixture投入SQLそのものはsaved_dataの別rule。", + "lines": [ + { + "sha256": "78ed8457dcb80799f2b147acb7c28f3418e9b004ccd1fdeaba5a4f8a0d75ae79", + "count": 4 + }, + { + "sha256": "9ca76066ca5c617b36529a3e49a03b8e5642913548519c9f367d477200149e71", + "count": 1 + }, + { + "sha256": "7cd1fb7e3506840602f1e8bac503429a8d278fd3847ef2e8ce52721c872a058b", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_content_state_tests.rs", + "kind": "saved_data", + "reason": "legacy_content_databaseのschema-85/歴史的reservation CHECK/Job fixtureと成功・rollback検証。実在した旧列・history tableへのSQLで、評価provenance、言語、FK/独自制約、Job receipt/cleanupの保持を証明する。旧SQLを現行名に置換すると本物の移行を検証しなくなる。ローカル変数名は承認しない。", + "lines": [ + { + "sha256": "84a2b10f5704cacc2ba8f66d5c19bc06de4be928cf3fadb07a267121b07ae1b4", + "count": 1 + }, + { + "sha256": "b3872190a49a0abe3c5e6775ead9ef3e49a6e27f3878c4322c63024a6666311b", + "count": 1 + }, + { + "sha256": "517220f3c4b1fb8cc19a5f2f7e2999397f8a64c76cb7ec5c1c05db0e9ffcde91", + "count": 1 + }, + { + "sha256": "349c4083e977573516cb47bbd28da863c543c747c2f98459f77138f6bebb1404", + "count": 1 + }, + { + "sha256": "c98c143de4c6a1aaf685aba1cf0b76e6149c835b8bc7558119842ae0b2bc319d", + "count": 1 + }, + { + "sha256": "c451b6dc8faf17f023ae3377ecd22f572cc455c4c038f94bde77eee55c7a20b8", + "count": 1 + }, + { + "sha256": "b8a27e915c1b047ad48a9a52b3ad151861cb5e60abbad3ce20911d78bd784be0", + "count": 1 + }, + { + "sha256": "b31c9b0b8005479afe039cb3848bfafe7154c02b25112e573fdc32ed056be9b4", + "count": 1 + }, + { + "sha256": "c992210b40f6b0880dcd7c81221d31f38e2a0d478676a4e871887d421ad293ca", + "count": 1 + }, + { + "sha256": "48359a7475d203d6bbc5964d765baae50fe0475b4cd22735f9e87fc83bfe418f", + "count": 1 + }, + { + "sha256": "cf0038fa42b01492373824c612bf36d0ee853dadff7ad6739639d1badcde00cc", + "count": 1 + }, + { + "sha256": "5b26c004c724dbfe2ed4bb02ac31ebfdd802d8fafd5609d57bb49430c6838e2d", + "count": 1 + }, + { + "sha256": "cf7c43c092d1999ec7bada7d525309f3bedc883bed60fc36b07bd81411744294", + "count": 1 + }, + { + "sha256": "374e26146eb7dddcd4f174aaa27d19f274aeedc3b4cc0ba020f395c0d30dc0b4", + "count": 1 + }, + { + "sha256": "8ac4fd694ab95064ecff9d611d9cf8a5086691e5c8c3699ddd88ac0f57042414", + "count": 1 + }, + { + "sha256": "8c7451c92bc8b816e56633614bc046e7ffe9d5f28736ab1f7f4f50c5f1189661", + "count": 1 + }, + { + "sha256": "fbd5cc28857a27f4d8adc7ecdabdf51174067cb1ed675fb363e2121b22cbe326", + "count": 1 + }, + { + "sha256": "0c08edc6ed33a35ecfe27252f0f2380f97c0f912cdada13c7abd85bc27319f9d", + "count": 2 + }, + { + "sha256": "f2530e2a4d831c23f9804044553770cb6532a066b225c026cd4c1f6c8dba1810", + "count": 1 + }, + { + "sha256": "b3990dea33d6c0deb4dd2c98a53074c4d8ff739d86645cb5b903c0a7544070bc", + "count": 1 + }, + { + "sha256": "697bbac33d1cce1a0684f4ffac80f178e2c555d89dcb9083a0df344ba7c87990", + "count": 2 + }, + { + "sha256": "c350fcc80c30db06ae8f8da64fb6062ff6e19e3f3462b1ba72a9e28554c0a33f", + "count": 1 + }, + { + "sha256": "7192c80c7b19816ba6c22356ffc6865938bccb5f676507cd159bcf151a11f9bd", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_flow_content_migration.rs", + "kind": "saved_data", + "reason": "schema 84から85への旧Flow source/history列・table。元source digestと保存済みcompiled definitionを検証して内容identityへ変換し、再compileせず履歴を保持する。綴りを消すと既存Flow DBが読めず、過去definitionの意味を失う。", + "lines": [ + { + "sha256": "37a0315ef387422ee62e1b2fa5f1a1fbd0e7e42014bb5ae7e81995dcb35a530b", + "count": 1 + }, + { + "sha256": "65e322294908a20e2fb2647a1c014f0cb6c736f66076642982f80314df58d6af", + "count": 1 + }, + { + "sha256": "8cfe9a56a054ab9e4e339ad274e61bf1faf5327695cb14ec53e6fecdd4e483c9", + "count": 1 + }, + { + "sha256": "a690d894a299fd5a79c074361f2ecd7c013e36f4ce8a263099266ea0b2720450", + "count": 1 + }, + { + "sha256": "9c24460ba6885edbc9896326dcac39f064e0768e10980db8f2b03798ece9d642", + "count": 1 + }, + { + "sha256": "156637b0842538569f031ed7c476a23585458ae7e7edd3da40b4f6eadb816832", + "count": 1 + }, + { + "sha256": "282bf6b865f78d5ae98b57326a05b5ef9cce3b5c33ef44fa2251d6af278a6afb", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_flow_content_tests.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: legacy_flow_databaseが旧source/historyを作るSQLと、保存済みdefinition・rollback確認SQL。compiler変更後も実在した旧履歴を再compileせず保持し、不整合時に旧DBを原状保持する検証。ローカル変数名は保存データと無関係なので承認しない。", + "lines": [ + { + "sha256": "f9b267228d51e8314a384d05d28eb27574b8b62ef86f6539d77453692a4ceb93", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_flow_content_tests.rs", + "kind": "saved_data", + "reason": "legacy_flow_databaseが旧source/historyを作るSQLと、保存済みdefinition・rollback確認SQL。compiler変更後も実在した旧履歴を再compileせず保持し、不整合時に旧DBを原状保持する検証。ローカル変数名は保存データと無関係なので承認しない。", + "lines": [ + { + "sha256": "d4267c846f643567c9817616d85ec0aa52e2c82fdc779fa8afaa9d261ed1e388", + "count": 1 + }, + { + "sha256": "8b529cbf9e2e58d4693f556910e2470969b0d5d3891c938f55b4972843330803", + "count": 1 + }, + { + "sha256": "eb701565f75aff75fac860b7481148c364446cdec4e1d97930ea74dbfd7b89d6", + "count": 1 + }, + { + "sha256": "26bcf7da66fec7cf066c6379e406aed25bb8eae32d01dc54d2456b7300cd7be9", + "count": 1 + }, + { + "sha256": "50660f1b721341901121f0bdf70c05fe454592429d9a691d6954acc83d8dd5e6", + "count": 1 + }, + { + "sha256": "b10583baa394ca694e5c4c2c51d79c58ee8461aa9907b3054afef2f2716f7027", + "count": 1 + }, + { + "sha256": "efd0cf6947c01ccea8323edb4c37483f1729a73d88462a13a7356d4cda228d07", + "count": 1 + }, + { + "sha256": "4e276b26f542cb0f2871638b15e756d570bdfe5ae1998b5aa3c62dbffa418735", + "count": 1 + }, + { + "sha256": "f0fcfbeb48ac3747428e3d37940c523fda70ee61f645b23de7a47d7ed0a65859", + "count": 1 + }, + { + "sha256": "903d8aeedf333fb3544708c9ca845b72a3c52292d1a3d4092c3bbc766ba26f03", + "count": 2 + }, + { + "sha256": "28d6d00549f2296aa4a37da1d03677f53f34a6e608d602cff1745117a243986d", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_legacy_migrations.rs", + "kind": "saved_data", + "reason": "store.rsのMIGRATIONSが既存schema 50..84 DBを順に移行するfrozen SQL。旧column/table/index/FK名は投入済み旧schemaに一致し、後続content・SSH key・binding cutoverの入力になる。消すとretained versionからのupgrade chain、Job/reservation、SSH retention、binding/audit履歴の保持が壊れる。現行latest_schemaの定義ではない。", + "lines": [ + { + "sha256": "1d670129ea61b5ea4ef0e4da9ae7be90ddcbc98415847d4e54c071d8f57a660a", + "count": 1 + }, + { + "sha256": "5bdf53adf2183def9a44f31ff09eac335e3bdb9644c352e96d8721b9196923f6", + "count": 2 + }, + { + "sha256": "13305075edd2c979164ef976902d1ccecb7a8f4c55e32f9edea53387799f76d0", + "count": 2 + }, + { + "sha256": "4c0efa2442009257a9ba47581868d57d59fafa68d16121c83a9f4ea53b49a2df", + "count": 1 + }, + { + "sha256": "cbce4bab74e1125a910764375a27983814d0bb1d4c066835a546b82c2fce318c", + "count": 1 + }, + { + "sha256": "83de28233408d4dcaa91ceab082e2db7b857a6b29bfe86e2d0126461657c846c", + "count": 1 + }, + { + "sha256": "d5c05fb43b325cd6602344ae13be1c6fc208841ec92e982c88cadf7dbe1c7aee", + "count": 1 + }, + { + "sha256": "6af31648cd0b249d74d5dfb143f62fb55c5a6790022c455706a0e03128c0c8a5", + "count": 1 + }, + { + "sha256": "3387c61794eafb0666b3d635b1e51562cf43e048b2a3c2ab551c3f60c3ae9687", + "count": 1 + }, + { + "sha256": "b6cf61c11bf278355e0dfc66219abdefd64c29621883836875221b8cd9ede2ef", + "count": 1 + }, + { + "sha256": "4c0ac49313881803b75dd94d847b41845da3637d37245becbd4e7f205b8a9255", + "count": 1 + }, + { + "sha256": "7fafaedcd53827ec04b1d5b2c8be7415d91e84549af55dbdef264ec76f528cab", + "count": 1 + }, + { + "sha256": "14167f46caa13763193ee2e17a07d48d61035b1f8567bd300818dc810eefdf48", + "count": 1 + }, + { + "sha256": "1e4e225a217adf9e3fff1451a11fcfa83ebbee4f6a1d9641378dbe2eb5c0eeb3", + "count": 3 + }, + { + "sha256": "5b18fe230a31a55b5ac1053f9056a8cc6812f918e10d4baa86a719f344dc618c", + "count": 3 + }, + { + "sha256": "95f172e8946c24dfbf4670006f56aed62ece902e3177d7f4dd602bb2949625e7", + "count": 1 + }, + { + "sha256": "d6285c57719b6a136aa393a4742918bbef97b2d19c7ba9218f33122e5f5aa85f", + "count": 1 + }, + { + "sha256": "09961c87f0c14a0f21486ab86ae17d7067ac2308b99c61d2ef9084bb8af200b4", + "count": 3 + }, + { + "sha256": "c22bd9dd606eff6de7bb8b4ceb3891a45295e57e0f8437448b65abc691b08837", + "count": 1 + }, + { + "sha256": "2ea5dbdeea039958b7048994a21f4f981eb0d1c5bd6085fe2310639ddca79bd6", + "count": 1 + }, + { + "sha256": "22c91e2bbd5adca75ae55d3f204e6724d6573610aaf0e181295310aa082bd384", + "count": 1 + }, + { + "sha256": "699132813295c9855958ed2dccceafab661a2a948e8dacd50d9837daf2d7e983", + "count": 1 + }, + { + "sha256": "1a52753288a74f60309d6e31fd22298547961c7d90c10f8e18f83f3a77b50aa8", + "count": 1 + }, + { + "sha256": "9315bb8b7f04e016a55a6e66aa45b64531130e8450cd2b4d342218aa87936176", + "count": 1 + }, + { + "sha256": "762a7ad6693e20ce7649d8c713d118539bcdc080d49909cb813fb12b724695ff", + "count": 1 + }, + { + "sha256": "04b8eefb961ceca200de40764f913dfaac4f3c751b439b8309d282c7a87890e1", + "count": 1 + }, + { + "sha256": "76d6931874a606a551bf1ea071cd61fbe72f6fcc0b4b83a68645cff6e2ae84e1", + "count": 1 + }, + { + "sha256": "21fc7b45772630b55972294bcae08c63fad1723e6413fe9b09d95ca9216b9baf", + "count": 1 + }, + { + "sha256": "b80515c50a007075bcde4268cd5362d4f03e8e1b768b3f5d7d9400b408252de1", + "count": 3 + }, + { + "sha256": "9873ec338873990d85f0c69285e626113a550cc55b6f4a8ddf753fb7185261b0", + "count": 1 + }, + { + "sha256": "427469b8b3e7840cadfb1a97f539b2c75660f131375cedeed00ac42ef071a7f1", + "count": 1 + }, + { + "sha256": "afd15214fdd8b86d5c0f6feabf6a4f6a890afcf04cf3f03bbdac32dadfbac749", + "count": 1 + }, + { + "sha256": "fa4806ac18201dc5ab2f257c4083f208c07c2cc519041a1313afce3f97d1ae5f", + "count": 1 + }, + { + "sha256": "68e09fe0ac794e1a4c0566c2d5271b01a1bb914ed5a458c84039b9fda190f7bf", + "count": 1 + }, + { + "sha256": "8e4824ebd2f8e0191ac757aad0ec483fd100c9b14b9c98a02003f4a73f034df2", + "count": 1 + }, + { + "sha256": "56a8591983c44127575b19cf5036a0fc41d375a9013351e7baa8f185b36ff9bd", + "count": 1 + }, + { + "sha256": "4b6cf0ac319dac976c1e101486315b1176ca2de0189941c2f01636651fad0c55", + "count": 1 + }, + { + "sha256": "e11c9337adf209a59a7de01e6fe48a4c2103dcb190235fe20547f02dc41de94f", + "count": 1 + }, + { + "sha256": "da34d8146a8213980633d0faafcc73b6ba6d934e614f7c475db45445b0b122d1", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_repository_key_migration.rs", + "kind": "saved_data", + "reason": "schema 86から87の旧SSH key/secret/operation/retention/source列名。旧secret AADと暗号化payloadを開き新key identityへresealし、元operation fingerprint・retention・auditを対応付けるconsumerがある。削除すると既存秘密鍵を復号できず、SSH retry/retentionやauditを失う。曖昧な対応はrollbackし原文archiveを保持する。", + "lines": [ + { + "sha256": "95284617f203ea22df2ba1fe49903db531a0dd68dbf8654981505516a07dfcce", + "count": 3 + }, + { + "sha256": "62ef848caa92101e5d51b885c78edd1449617a6d2a2765bc25baf9a34475002b", + "count": 3 + }, + { + "sha256": "196005be206e6f21f3ab1c19a187d85d82de45f0e45710c4e3894ffaa5dfb4ce", + "count": 1 + }, + { + "sha256": "87a8b267ec5d0d1f0bda8a5c6fc8b78808dabdcc10444b947a5e811a9388c37b", + "count": 1 + }, + { + "sha256": "a73c9b9f90f3bf0b514d3c19ef5aedfd1c232fd20db16b7132738a20d6004939", + "count": 1 + }, + { + "sha256": "052ece41bdf100b1949c629b51c8ace6d0fae53df33ad48bf5c930df5cb6cc9d", + "count": 1 + }, + { + "sha256": "e095d3724c151ba99e860558dd834f9b3e1aea400882670240997b49141473a9", + "count": 1 + }, + { + "sha256": "82f0618b3d0faf95927ea8f0b1e8ca4ea81c8638bbea6f3ca06dc836b3582018", + "count": 1 + }, + { + "sha256": "5d8ac6dffcd0146f53adbc3b4cf20c198dbdbab8e2ce4d381400085cf36d44ed", + "count": 1 + }, + { + "sha256": "da0e88b3a157465b924076143bc4b924324d8e4ebb84fe8408efbca868724b5d", + "count": 1 + }, + { + "sha256": "b24b0439e7d718b772cc11c46570ae4dd3f67aa878119ddfe7d8e79dc8239a20", + "count": 1 + }, + { + "sha256": "0bb8732a39df8acb7f63a8be5e4abbaf4f40b18297afecf02021377d058082ac", + "count": 1 + }, + { + "sha256": "b36de8d8455b3774fd2cf0730f06732075ec9732761c1a8792d5e4d066606a0d", + "count": 2 + }, + { + "sha256": "900d76b744bb316196326261bd42f9c80593035d52b423be51e3d68cc4cf2cff", + "count": 1 + }, + { + "sha256": "338850a9bfecff4b022425c9ae72825ab783a5c2ea413a655cf889018069b19c", + "count": 1 + }, + { + "sha256": "36110ee72fd1410ee7f477ae5233e900dfdf85a826925ce2a6b0c2975161967c", + "count": 1 + }, + { + "sha256": "786969a32f56a53d4af226b0b01be353bb55fedf0ce2a1a72633abfd23f10fe3", + "count": 1 + }, + { + "sha256": "5b34bd84600fed51846e6790eb84d50839c97e4a8b4db84750a952e7866b4d30", + "count": 1 + }, + { + "sha256": "dd7d9fcaf04ee29e42d3f5f8688ac9368a02756a9f1e78df3b16a73ab2e5e8f1", + "count": 1 + }, + { + "sha256": "35b50395095c8fe03e85ca1f26e6ece7aa8ef9015f76686d187d7338d44539f5", + "count": 1 + }, + { + "sha256": "5fdf5a0ea447257be21c71420dd1168303a377afc59418cca7c063125b1403ea", + "count": 1 + }, + { + "sha256": "4f5206406bd3263d41f5e16a914d8c9ff467c45bb7eb900089f955da01109316", + "count": 1 + }, + { + "sha256": "09c3565cb5d873b3e0ef80a354835665618aab8e1342656f8528d3364d2c0556", + "count": 1 + }, + { + "sha256": "7799fb1e28c774e846e4e830bb31df56f600d150d68d86b3e8f14a2ed8463aad", + "count": 1 + }, + { + "sha256": "d1829663286f049ee67715a1649f4af0205ce1a16486a68603febd144b1b7d3e", + "count": 1 + }, + { + "sha256": "302cbd2e9dbe3871a663a7aaeec1139d977108c5cc684353ec0888ecf74a044f", + "count": 1 + }, + { + "sha256": "8cf8692688205f6b268bccc193f2d8329565c67b8189a96a0d36400ea601cd78", + "count": 1 + }, + { + "sha256": "6f4968cf8c4e4da551b6fefb7c7fcf6048e867684189af3f9854e652d4331f3e", + "count": 1 + }, + { + "sha256": "1d9c89812a12ce4e7d3d9bc87231c76c077e38f69a9a8e22f11608494d0eb97c", + "count": 1 + }, + { + "sha256": "8948263f582419232838423d99d269c423dee4b21c8189d97fde3de97afc2104", + "count": 1 + }, + { + "sha256": "9b2d38cea48eaf100fdcd2444036da17a0a8104090b429e6c64e56255d45d9c0", + "count": 1 + }, + { + "sha256": "f8333763f67f3a40ae3e245f523e2b0dca6a7b6c1b42d5aa61273bcbf649c5f2", + "count": 1 + }, + { + "sha256": "26631275a8d296889f3ef0dd43f6038b23bbaf5e65886f28b3815854be9890b1", + "count": 1 + }, + { + "sha256": "f59825e59e5e4b4c9857915c0551b61428073e2024edd6e27cb109e10cd97b97", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_repository_key_schema.sql", + "kind": "saved_data", + "reason": "schema-87 cutoverが作成済みの旧legacy receipt counter列2行。schema-89 legacy_evidence_column_migrationが実在したこの保存列を改名する入力DDL。消すとschema 87/88既存DBとretained upgrade chainの整合性・元SSH操作fingerprintのreplay evidenceを失う。最終current schemaは具体的なlegacy counter名を使用する。", + "lines": [ + { + "sha256": "4126d49032f3c39aa5f9965b585797efa1cc98d3839df08a16bbcfc232e0dc5b", + "count": 1 + }, + { + "sha256": "3dc5216d749d382221ef335bfe012b1bd1a2c58b36be8c60e7583671381a5bf1", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_repository_key_tests.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: legacy_repository_keysの旧SSH key/暗号secret/operation/retention/Workdir fixtureとrollback SQL。旧AADを本当に開いてresealし、元fingerprintでのreplay、retention、audit、不可逆な旧Workdirのarchiveを保持することを検証する。綴りを変えると既存secretの復号可能性と不整合rollbackの検証を失う。", + "lines": [ + { + "sha256": "844ccca7b5306efc14510327930a38a22e957f043ca5a58bfcb1211b3c7819d0", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_repository_key_tests.rs", + "kind": "saved_data", + "reason": "legacy_repository_keysの旧SSH key/暗号secret/operation/retention/Workdir fixtureとrollback SQL。旧AADを本当に開いてresealし、元fingerprintでのreplay、retention、audit、不可逆な旧Workdirのarchiveを保持することを検証する。綴りを変えると既存secretの復号可能性と不整合rollbackの検証を失う。", + "lines": [ + { + "sha256": "a62f153e1748caa20f7cb81656b8c6b29d32b01bf533a3ffaf36fb5be9a5ae3b", + "count": 1 + }, + { + "sha256": "de6b3a38afad3d08bc4bcb534c1374446ae59b28ed08235643fd61c3b42992fd", + "count": 1 + }, + { + "sha256": "7b82d25a130f540b608ce5f2e18d7e0a5ebba80486a8a9b701f45b19348acfc8", + "count": 1 + }, + { + "sha256": "e40c8ebda44bef3b5285be6937bb1d3264907044e8cbdb000557de1c5033cbf8", + "count": 1 + }, + { + "sha256": "78b1ca52067ac1cd03627a7efde38da5d89c463cd12a5118c27404500b28b0d2", + "count": 1 + }, + { + "sha256": "af41b9c5edf66189130020a79568cad6b86b56835935e06969d34a4cc0569e93", + "count": 1 + }, + { + "sha256": "9ec9ccf2b8e304c1cb1ef9b755a5cfe4d203879a9787394a9c6edca59c6fddca", + "count": 1 + }, + { + "sha256": "2e4c58e55d4b3043c9b48762ca6d58123cebc985736af8b518560833b618b675", + "count": 1 + }, + { + "sha256": "ca209d9c4fa92f98d6bf531ae2f83309c6916cee50b81dce2f8e73a60746b61a", + "count": 1 + }, + { + "sha256": "cbdf372850ccc0ec2e3e967a2fa9985f8dba62c711fd7a86aca9b8243bd166cc", + "count": 1 + }, + { + "sha256": "e2a097bb7eb84411dbb616f2928039dfbc863228cbdf0d81f8d5cc5c0b274a39", + "count": 1 + }, + { + "sha256": "7a909fc5eaf8121eeeee22d55808f8a0a5a27f0d33c4b606f9d586cb777e40f7", + "count": 1 + }, + { + "sha256": "e374241ea87cad8b2776eabf0685f5ff3952f411b06be4fd401df884789f3997", + "count": 1 + }, + { + "sha256": "76b4ffcfe57946f1099948aecf1fe89d9be2be86edef929b1b4af80e342e23c2", + "count": 1 + }, + { + "sha256": "887a03dea6a8ffcf9898b3e2fd2d0e7a873baa9bdef4f4f5a7bd3794bdf70930", + "count": 1 + }, + { + "sha256": "5ff35122a026eeff7fbabd6f0baab871f353d71661bc6eb973e26103bcec375c", + "count": 1 + }, + { + "sha256": "4293204365d78e56ed8b75221aecca5860068739b254bc83da956f66c7de126f", + "count": 1 + }, + { + "sha256": "b2495afb6d5b32f13e77eb7a7fa4866f12531d3ed059aa44288ccb69d0fa4e26", + "count": 1 + }, + { + "sha256": "4492c6d6d4434fa4f866261f243faafb4a2c6e6d7c6943bc72d749ed2357d58e", + "count": 1 + }, + { + "sha256": "4a4ea05d99ba512fbe0ecb7c31d176a08c59bb6e8b1156624bee907ad4dfc103", + "count": 1 + }, + { + "sha256": "ff1f9c06c6d8764dc1e7e60e384d394d63208336e6094ec1dceb31037731b04e", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_runtime_binding_migration.rs", + "kind": "saved_data", + "reason": "schema 87から88の旧binding/signing/provisioning/policy/removal列、歴史的existing-workspace operation key、旧index変換と移行postcondition。元鍵material・予約owner・completed receipt・policy contentとbinding対応を保持し、旧provisioning fingerprintを検証する。削除すると既存署名鍵/予約receiptを再利用できず、retention/removal履歴かmixed旧schema拒否を失う。単なる診断文は承認しない。", + "lines": [ + { + "sha256": "47490ba8a96c01b6e1a316d667dcc5b504e11697770f55910ba478ae512e17a3", + "count": 1 + }, + { + "sha256": "e481815ee163b317b2ffb5a3f54cd26b6793c788dc9c250307e04737f14b0cde", + "count": 1 + }, + { + "sha256": "beadbcef395555b3c4ca3b73d53846d33de0a6fb3b6265f64df340bc4a69bd3a", + "count": 1 + }, + { + "sha256": "ae18c4b89c1aa214aff69647718e275fff4a4a1e3e268f9ab67b1e9784bd9f9f", + "count": 2 + }, + { + "sha256": "aba4dfa1f810920ee92a9024f21f6003f2ecd0c31f6f4ef89ade9e23f15cdac8", + "count": 1 + }, + { + "sha256": "22f7df3c22cf0e2c5419634532974abca088ab94c9f291f6369a2d0e0424709e", + "count": 2 + }, + { + "sha256": "5c02f21d72146fb9aaa7d3e710627f46cf1ebf70ab17ffc14c41e4e42119b6c4", + "count": 1 + }, + { + "sha256": "7eda139936b6f0dd04981ce1e7e49d508965a50744ec56942f296ed9eb5fe7b8", + "count": 4 + }, + { + "sha256": "26b8cf27100d8b07d45a13bda2d76e4da4167fa2e2928fc57704bf5ec1a7b1ce", + "count": 1 + }, + { + "sha256": "b40db8d9b2b98d628ff8080b3a52da09da725bdea95cf75f1de415483c843890", + "count": 1 + }, + { + "sha256": "246a632a6e1a981f7b20f3475ac8ada00b3b67c12219c746afe9af5e7291d664", + "count": 1 + }, + { + "sha256": "76a0ab871227a8c7f56806f764c58bbbbc105ffcaff8531fc060b61b9df1087b", + "count": 1 + }, + { + "sha256": "bc6acd2a01dfbd2a9ea0b678ca1828d7f247484d1b2b1ea34100e543ff359c22", + "count": 1 + }, + { + "sha256": "186f1c6eb7b2b87121c8988be4385a13b6dfa9863343668f4bf7ced7be85c659", + "count": 1 + }, + { + "sha256": "f329951dfca0cfd266b55dcd1790c97b5c4cdc6425862220812ba41d124b2cee", + "count": 1 + }, + { + "sha256": "4154334813ad850dcce125e5b7e41a27309bee5b4a2a440c4eed9bfc484cb534", + "count": 1 + }, + { + "sha256": "b99a045c5960a52a5e37880f04c26dfbd0dd48e231679906560c99d3d7656174", + "count": 1 + }, + { + "sha256": "c20d51c77aafd4deecf5f3f0d9282cd1ea22f0b9593c1a79f37652331723ffa8", + "count": 1 + }, + { + "sha256": "886f2dbc219514fc1a1e9144499003a61ba2bc0ff0359a987848935c60eb0ce4", + "count": 1 + }, + { + "sha256": "98ec9d7c4c7dc52fc6975df54883916091a0ca3f2e6843e74c143ed4455b0b8f", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_runtime_binding_tests.rs", + "kind": "saved_data", + "reason": "schema-87 binding/signing/retention/provisioning fixtureとrollback確認。元private material・historical provisioning operation key/fingerprint・完了receipt・binding/policyの対応を保持する検証で、旧列/keyを任意語に変えると旧DBの実データを通る保証とinvalid receipt時の原状保持を失う。", + "lines": [ + { + "sha256": "2778ef022a505aa159010d0aff7011daf7e74b9048435ef53b529bd472e9c5f2", + "count": 1 + }, + { + "sha256": "84cfa725c61bcb6a725632bb6d9832be9e822cd9f5a7622cd75365ff13df4e0d", + "count": 1 + }, + { + "sha256": "ae0ddbb9e0e640aad99ea7da7c71d98fa84dbef5c9ccf28079c5c1e53f91a24d", + "count": 1 + }, + { + "sha256": "de902791b0eff241b2856d511ad7aad3e3c4ccdfa220266c554351e9caa1c6de", + "count": 1 + }, + { + "sha256": "7ba61cebc84db2b9d735ad7abb93a9f9451d309f8be1f3a7817d849d5fbf51a6", + "count": 1 + }, + { + "sha256": "56ae4fd1efb8448d14a2dfcfbd16d876ca212dd5e8f80630e4a52c4725c68364", + "count": 1 + }, + { + "sha256": "20b7128d3726c445a5d03ef6902ee232643b7884316fdc399a7d8db3c03e57cc", + "count": 1 + }, + { + "sha256": "9a89c16bfd87af5b15605db20226de54b261482a73a7b0609efa5c3e95dbe39a", + "count": 1 + }, + { + "sha256": "5d0e90538d1694cea2d1dc01c9df632e457baa0398e3fc2360fd029b1ebdbcd7", + "count": 1 + }, + { + "sha256": "6696c19535c04ec87f1ca3648d9bbfc9c10f3f6bb113d3474938f445e6d0c087", + "count": 1 + }, + { + "sha256": "9055bdf1c693bc537b33fd10345acacdf1384eb27c7643c94d7bc612a053ce73", + "count": 1 + }, + { + "sha256": "3d8b103d690cfba64a5a79948a3048c3309cdd88c9279b54cbb9b8e43c8190b9", + "count": 1 + }, + { + "sha256": "86ef2bc8aa0637ce55e8a030d71833e79209b74dc89bbc0a9ec7144fb3140e9c", + "count": 1 + }, + { + "sha256": "97c89dacec64e0da68c3a8eaee10f8cb470694f831e570e8a4dac5eccd47c9e9", + "count": 1 + }, + { + "sha256": "d4a057bb0019067f3ffab88f1b835c949df570e7f75e717c784bc3f1d4766895", + "count": 2 + }, + { + "sha256": "d3910a305b4accb75029b08ae34f5691ec97c722c06267ac998994091b27fb7a", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/frozen_schema_v85.sql", + "kind": "saved_data", + "reason": "create_frozen_schema_v85とprepare_retained_schema等のmigration testsが読む凍結schema。実際の旧列/PK/FK/INDEX/default policyを持つDBを作り、旧versionからlatestへupgradesとcanonical schema equivalenceを検証する。消すとfixtureが現行schemaになり、実在した旧DBへの互換性・data preservationを証明できなくなる。", + "lines": [ + { + "sha256": "693d241ddfa319622435176fb7e90d1d75c5052ffb8f72fa81185e349b01499a", + "count": 1 + }, + { + "sha256": "36690eb724a0caee2f369ef4ea5807f6e2e54b826bf44e84599de14e2e20bef7", + "count": 1 + }, + { + "sha256": "09961c87f0c14a0f21486ab86ae17d7067ac2308b99c61d2ef9084bb8af200b4", + "count": 7 + }, + { + "sha256": "7bda5d7b0f84c834598135329e08ef42c7b14a2d85439580c64a7898bc05b5f4", + "count": 1 + }, + { + "sha256": "6a4ddc37708b139e19e303a6475c0567b4a3c29d2c2dcfa9e57dbd7ee7c9705c", + "count": 1 + }, + { + "sha256": "b66062c70153691dfb0732304d4470a04cf0cf966f5e93141626bd0b399816fc", + "count": 1 + }, + { + "sha256": "2dd882eccc49e69fd4b8fe007439e2484025b95cd0d21cefd4a4f26e97e635ab", + "count": 2 + }, + { + "sha256": "ce04bd741e26b92f959fd6e67cb7ad78bede071ce9d64a2c976eccad022178f5", + "count": 1 + }, + { + "sha256": "c32304e27f138a84e14154b735200d2e1b5e80a4135585ddd8179b6a843975d0", + "count": 1 + }, + { + "sha256": "576783f2d4c59046de213f2faae297e571270fbb055e5e9eb929dd8241dc23cf", + "count": 1 + }, + { + "sha256": "917d90ebdcbc335140511513da39dc4ad15a7b1a80c9a57013ab587d9444e7e9", + "count": 1 + }, + { + "sha256": "c8ca2642aee239c3b76301080498a8550da176ab6776d0cb3fd7923373bac64b", + "count": 1 + }, + { + "sha256": "2ea5dbdeea039958b7048994a21f4f981eb0d1c5bd6085fe2310639ddca79bd6", + "count": 1 + }, + { + "sha256": "1a52753288a74f60309d6e31fd22298547961c7d90c10f8e18f83f3a77b50aa8", + "count": 1 + }, + { + "sha256": "9315bb8b7f04e016a55a6e66aa45b64531130e8450cd2b4d342218aa87936176", + "count": 1 + }, + { + "sha256": "762a7ad6693e20ce7649d8c713d118539bcdc080d49909cb813fb12b724695ff", + "count": 1 + }, + { + "sha256": "d5c05fb43b325cd6602344ae13be1c6fc208841ec92e982c88cadf7dbe1c7aee", + "count": 1 + }, + { + "sha256": "6af31648cd0b249d74d5dfb143f62fb55c5a6790022c455706a0e03128c0c8a5", + "count": 1 + }, + { + "sha256": "3387c61794eafb0666b3d635b1e51562cf43e048b2a3c2ab551c3f60c3ae9687", + "count": 1 + }, + { + "sha256": "21fc7b45772630b55972294bcae08c63fad1723e6413fe9b09d95ca9216b9baf", + "count": 2 + }, + { + "sha256": "ce87e56cbe02ea1b7ca2194852488774f3fb08704b0241dbd9b871cae5072320", + "count": 1 + }, + { + "sha256": "afd15214fdd8b86d5c0f6feabf6a4f6a890afcf04cf3f03bbdac32dadfbac749", + "count": 1 + }, + { + "sha256": "fa4806ac18201dc5ab2f257c4083f208c07c2cc519041a1313afce3f97d1ae5f", + "count": 1 + }, + { + "sha256": "68e09fe0ac794e1a4c0566c2d5271b01a1bb914ed5a458c84039b9fda190f7bf", + "count": 1 + }, + { + "sha256": "8e4824ebd2f8e0191ac757aad0ec483fd100c9b14b9c98a02003f4a73f034df2", + "count": 1 + }, + { + "sha256": "56a8591983c44127575b19cf5036a0fc41d375a9013351e7baa8f185b36ff9bd", + "count": 1 + }, + { + "sha256": "4b6cf0ac319dac976c1e101486315b1176ca2de0189941c2f01636651fad0c55", + "count": 1 + }, + { + "sha256": "e11c9337adf209a59a7de01e6fe48a4c2103dcb190235fe20547f02dc41de94f", + "count": 1 + }, + { + "sha256": "da34d8146a8213980633d0faafcc73b6ba6d934e614f7c475db45445b0b122d1", + "count": 1 + }, + { + "sha256": "f105c6900b87409afce4d441d1fe31a54ccad660f0cabbdcf056e1b71dfba524", + "count": 1 + }, + { + "sha256": "a139b5f2bf73afd5afe83f52f7534ed66a95324bacff7baddd68bd336082354e", + "count": 1 + }, + { + "sha256": "b80515c50a007075bcde4268cd5362d4f03e8e1b768b3f5d7d9400b408252de1", + "count": 2 + }, + { + "sha256": "8803cc39ca81b9873833e1f9eb451b37fe2b4a9b48f1e47a68c317a87f013eaa", + "count": 1 + }, + { + "sha256": "935499fee2527ff3336dd4de5cc33a8b9f36c286f75fb96d94b9e7ed4050f4f4", + "count": 1 + }, + { + "sha256": "4f00de51edead8669dd93bd041570b208636a3d452f484846e825f1d9c1b39e8", + "count": 1 + }, + { + "sha256": "ca5bb19a46ab52ecb4a092584034818e7b78a35c42b6715bb0953911c9ba12be", + "count": 1 + }, + { + "sha256": "cd15c452b4911ea61fe955a03027a00c691cbabd052cb98eb23a453d16a68187", + "count": 1 + }, + { + "sha256": "d961bead14c98f33705dd7fd6056d68d1d701865c921f881a0080f3c08ea3f6a", + "count": 1 + }, + { + "sha256": "f0af9bf524d55a5fd4cba0a8562f3e4d5bd28f7357fc5595d6f2c0916c29da1a", + "count": 1 + }, + { + "sha256": "21e25b32009d462bf78cf5be6f45f5f640f192a28bd4d0dca5dab50f2f8044c7", + "count": 1 + }, + { + "sha256": "de515c1db929f95f66607e75ab41698f204391e9c9c988b133b10eda9d963763", + "count": 1 + }, + { + "sha256": "f14296b81ab632430d60237b9225297b7edd2b2a05b08a0e200c5860ce310c9c", + "count": 1 + }, + { + "sha256": "3a259298c89107cb34aee20ec478dff4e56de2bc878fca599eb146b0e1bb9247", + "count": 1 + }, + { + "sha256": "7b9a74c040d20c2f92113f5c08cf10d47b815fb2404e8f028ca6d0a7cf222c05", + "count": 1 + }, + { + "sha256": "f91720769b7c27ebfc1d034f99d0423ea3bbb54731cb228348b9c8f8533c1cf6", + "count": 1 + }, + { + "sha256": "2f902da45627710a200615c53dbbb51f77d21d37e0628154264079b3fd638df3", + "count": 1 + }, + { + "sha256": "76d6931874a606a551bf1ea071cd61fbe72f6fcc0b4b83a68645cff6e2ae84e1", + "count": 1 + }, + { + "sha256": "d6285c57719b6a136aa393a4742918bbef97b2d19c7ba9218f33122e5f5aa85f", + "count": 1 + }, + { + "sha256": "9f388464faf3a798adf48eeb14b991d16d40cc8ccdcefeaea8bbae8befadf128", + "count": 1 + }, + { + "sha256": "ecd0455df827ac00a55634644181273a1e487f2a0198908f6fb539198e698dcd", + "count": 1 + }, + { + "sha256": "dc1a01e48d7b6ff8b43c7d57fe935016541911736ca6cf36a9852d9d4e5674fb", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/legacy_evidence_column_migration.rs", + "kind": "saved_data", + "reason": "schema 88から89で旧artifact source列とSSH legacy receipt counter列をALTER RENAMEする綴り。既存artifact source value、元operation fingerprintと旧counter replay evidenceをそのまま保持する。消すと既存DBを移行できずartifact由来情報や同じSSH操作の再送判断を失う。", + "lines": [ + { + "sha256": "a9ea3b7d1f16ff91f84b1b7eaaaa32644a1debaedaaff6dc81898e07049cf166", + "count": 1 + }, + { + "sha256": "77abd49b48090370b92df090f8c38f4f5cc9c3b5a6943e34952fd757e94e6da7", + "count": 1 + }, + { + "sha256": "cf525bc1fa010f19227fc85c75c948f2364600c053b4fbdf5e1f629c835f7754", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/legacy_evidence_column_tests.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: schema-88 artifact fixtureは旧source列に元由来情報を保存してschema-89 upgrade後も値が保たれることを検証する。現行列に置換すると実在した旧列を改名する移行での値保持を証明できない。", + "lines": [ + { + "sha256": "564de34f3f1ffc45547581c8c6031e1408177ba2ca604022204778bc8d8a7b0c", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/legacy_evidence_column_tests.rs", + "kind": "saved_data", + "reason": "schema-88 artifact fixtureは旧source列に元由来情報を保存してschema-89 upgrade後も値が保たれることを検証する。現行列に置換すると実在した旧列を改名する移行での値保持を証明できない。", + "lines": [ + { + "sha256": "b9648425b02b56c6649eaaddd3a029ee4602ec8c48cf74de1d7b0d25860282ee", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/migration_entrypoint_tests.rs", + "kind": "saved_data", + "reason": "schema-87 retention/removal policyとschema-86 SSH key fixture。public migrate_database/open entrypointが旧DBのpolicy binding・terminal worker evidence・暗号鍵を移すことを検証する。旧SQLを現行名へ変えると公開入口を通る実データ保持/rollbackの証明を失う。", + "lines": [ + { + "sha256": "2a4cb9f8dacaf637103fe369c8ce5106bbb442097e644483c3900cdd98fade81", + "count": 1 + }, + { + "sha256": "507a9c7b394d2f53ec23cf7f61895882e6c36437f20984831d2d3e4de8082e61", + "count": 1 + }, + { + "sha256": "c62a95b51f929de93f0623206e571b88596f0cd6b2e8dffdbb62db6d7c6a3dd8", + "count": 1 + }, + { + "sha256": "eccb98cd2b351f242f42f7a06d3e527745e3fcb5c46ea005d7658c4e3b3b1479", + "count": 1 + }, + { + "sha256": "de6b3a38afad3d08bc4bcb534c1374446ae59b28ed08235643fd61c3b42992fd", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/store.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: 旧schema validation/readersとretained version fixtureの実在した列/表/PK/FK名。既存Runtime binding・Signing identity・reservation/Job/Memory・Worker projection/assignment/removal evidenceを順次移す入力を識別する。さらに既存migration名2行はverify_schema_historyがDBの__yoi_schema_migrations.nameと完全一致を要求するため変更すると既存DBを開けなくなる。削除検証は別kind、単なる診断文は承認対象外。", + "lines": [ + { + "sha256": "83028c3e174d647bc3ea5c0e2f7c3e97cca338285cbde0ea19718db6a9ad0755", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/store.rs", + "kind": "saved_data", + "reason": "旧schema validation/readersとretained version fixtureの実在した列/表/PK/FK名。既存Runtime binding・Signing identity・reservation/Job/Memory・Worker projection/assignment/removal evidenceを順次移す入力を識別する。さらに既存migration名2行はverify_schema_historyがDBの__yoi_schema_migrations.nameと完全一致を要求するため変更すると既存DBを開けなくなる。削除検証は別kind、単なる診断文は承認対象外。", + "lines": [ + { + "sha256": "878e5fd4a93e5163766b210e1e503103f68146da7c9dbf83eb89794e759c9239", + "count": 1 + }, + { + "sha256": "9570f606a1221dee4316dd6036d4dda2a4091e77a5ad33724bb278a48a35019f", + "count": 1 + }, + { + "sha256": "f329951dfca0cfd266b55dcd1790c97b5c4cdc6425862220812ba41d124b2cee", + "count": 3 + }, + { + "sha256": "aaa7464d13769b4d9b00eb2c53dcd70b691727ac45831da391fa1c3bf9a2aef0", + "count": 1 + }, + { + "sha256": "4154334813ad850dcce125e5b7e41a27309bee5b4a2a440c4eed9bfc484cb534", + "count": 1 + }, + { + "sha256": "7d80cfa16f8a18d2a6775f1fab976c9607f7135963dede5eab567cbea9f60135", + "count": 1 + }, + { + "sha256": "886f2dbc219514fc1a1e9144499003a61ba2bc0ff0359a987848935c60eb0ce4", + "count": 3 + }, + { + "sha256": "a0b32e9f70d84bcb5c9d052cec88bcd4243ec0e5fe6eee4cdb0b756b45ddef87", + "count": 1 + }, + { + "sha256": "f5645cbf1f50cac540864023ceec67965a38f5cdfba60df75b15877e21c1feb4", + "count": 1 + }, + { + "sha256": "22c91e2bbd5adca75ae55d3f204e6724d6573610aaf0e181295310aa082bd384", + "count": 1 + }, + { + "sha256": "d91d3d971f14e37f791ba52bedb3d149100a8f414f56c10af41f0fa10a1997b3", + "count": 1 + }, + { + "sha256": "1d670129ea61b5ea4ef0e4da9ae7be90ddcbc98415847d4e54c071d8f57a660a", + "count": 1 + }, + { + "sha256": "5bdf53adf2183def9a44f31ff09eac335e3bdb9644c352e96d8721b9196923f6", + "count": 2 + }, + { + "sha256": "13305075edd2c979164ef976902d1ccecb7a8f4c55e32f9edea53387799f76d0", + "count": 2 + }, + { + "sha256": "3f93039bbfa4c4ade8337810e904f82facbad95dfab83e74ac72be5eb76308e4", + "count": 1 + }, + { + "sha256": "cbce4bab74e1125a910764375a27983814d0bb1d4c066835a546b82c2fce318c", + "count": 1 + }, + { + "sha256": "6624a892d7c32e89297e67c66c708c6b41c8c13889ea13c2e9f7c118eef38cb0", + "count": 1 + }, + { + "sha256": "a62938ecd34f5323ac9bcbdc504bcc53f6d9592f112702e8b957b7f399e67664", + "count": 2 + }, + { + "sha256": "014fbe1da6d426eae007a7b979abf527e7e7649de6e42fc23a5105184c0d690f", + "count": 1 + }, + { + "sha256": "b6cf61c11bf278355e0dfc66219abdefd64c29621883836875221b8cd9ede2ef", + "count": 1 + }, + { + "sha256": "14167f46caa13763193ee2e17a07d48d61035b1f8567bd300818dc810eefdf48", + "count": 2 + }, + { + "sha256": "b5722ee6d03aadc482f0b29d00f386c1687c1cfb2b5a331b53bc9c1a05755724", + "count": 1 + }, + { + "sha256": "922889bab7a01f5d44ea5bb7a8abda99f6f5a60db23993d7df275be3f032b615", + "count": 1 + }, + { + "sha256": "8fa169a3a546ba7a010b17daeece10b00cdbb7f26f4c23db06bb790b9a9d6eb1", + "count": 1 + }, + { + "sha256": "7f7443ec5c59aade3fa0e02690c94fd6807abbcc5a56195ae53861dcb23ea2c0", + "count": 2 + }, + { + "sha256": "ae000893ab03e20304b5731ffa3c8051d8929f41c6833c67199b9a03f4a9742a", + "count": 2 + }, + { + "sha256": "5745fa9e0f217df83a2819b2447fde8cb3e7d090e292b0467aeab5a15378026b", + "count": 1 + }, + { + "sha256": "bb4ca69f4259cd5851b4e303ef27a51abfe4709e328f786539139b1399ee12f3", + "count": 1 + }, + { + "sha256": "d88c1648e2af05898f7db0a4bc2fc39a63798ea9a9521b26cb18a8d122995650", + "count": 1 + }, + { + "sha256": "f5c8c53aa4f3f4c109074f4ea70a4dfcb7ddff3bab486f6003c6d6cb613e89d7", + "count": 1 + }, + { + "sha256": "9b7c06dfe2c25363ca496902607ffa2232d31b4233cd41ce144c2e0a63c44bdb", + "count": 1 + }, + { + "sha256": "034d664553027aa4176f4ade827a14a1542758898b3c33da152c46b9e9673c0c", + "count": 1 + }, + { + "sha256": "cb940026cb3081247c9f8d488492032ee210242e891297b49b81590826eacde0", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/workspace_signing_identity.rs", + "kind": "rejection_test", + "reason": "移行後・現行出力から旧key/column/tableが消失したことを具体的な綴りで検証する負のassertion。削除すると旧名の再出力/残存を検出できなくなる。関連する保存境界: frozen_material_v1が既存version-1 private-material JSONをdecodeする旧keyとfixture。元private keyを同じWorkspace/key identityのまま現行materialへ復元する。綴りを消すと既存Workspace署名鍵をloadできず、新規鍵生成によるidentity変更か署名不能を招く。現行writerには旧keyが無い。", + "lines": [ + { + "sha256": "065b3bab89655a7c0f60973678da18e5768d04e5c572f9ae5eab6fdec3ffe09b", + "count": 1 + } + ] + }, + { + "path": "crates/workspace-server/src/workspace_signing_identity.rs", + "kind": "saved_data", + "reason": "frozen_material_v1が既存version-1 private-material JSONをdecodeする旧keyとfixture。元private keyを同じWorkspace/key identityのまま現行materialへ復元する。綴りを消すと既存Workspace署名鍵をloadできず、新規鍵生成によるidentity変更か署名不能を招く。現行writerには旧keyが無い。", + "lines": [ + { + "sha256": "39d1e108e0ca0565e38b4a9fbb59ed320bd70cf84f33883c479751694b6934c5", + "count": 1 + }, + { + "sha256": "97c89dacec64e0da68c3a8eaee10f8cb470694f831e570e8a4dac5eccd47c9e9", + "count": 2 + }, + { + "sha256": "e1c0aca0a4c23507d9f068607bef95f50a8907482db3a9435c5d01831b061e8d", + "count": 1 + } + ] + }, + { + "path": "docs/report/2026-10-10-compaction-user-directive-loss.md", + "kind": "naming_policy", + "reason": "別作業のユーザー命名禁止指示とcompact後の指示脱落を記録する原文証拠8行。ユーザーが改変を禁止している。語を置換すると元指示・縮小された完了判断の証拠を失う。現行状態契約の命名ではない。", + "lines": [ + { + "sha256": "be07543630f736159a176c85edf595169abed6a4398732b2954d9c07708bb3e3", + "count": 1 + }, + { + "sha256": "ee0add78ff03ede4f9dce20b12bf420bb4e3be56f490c9c0f0fc0f758961d44e", + "count": 1 + }, + { + "sha256": "58f8ba7f469176805b055b4b2f089f972f73e21775430ad1ac5ac44128a42e34", + "count": 1 + }, + { + "sha256": "e80d72f0aa5df11c66d3a2219c4c945e0f539bd2362b97c4c1258d029bd15697", + "count": 1 + }, + { + "sha256": "84ae8a6dcffd845305e4460c9aaa489d7df92a77ce0e692dcda81af3810ebdc4", + "count": 1 + }, + { + "sha256": "78b4c1ec00cbebfb5f1964b84adf73a41fd65a87d73a15c36286f2f9effeda56", + "count": 1 + }, + { + "sha256": "e1a923653f21a65378f5a9071167cea6f417f3446b1dfd400ddd020d31218141", + "count": 1 + }, + { + "sha256": "499677378465b65c47b836f4b9f4cb63adb28b898a269e76c2e05dad0bc07508", + "count": 1 + } + ] + }, + { + "path": "tools/check-state-contract-names.ts", + "kind": "naming_policy", + "reason": "findNames がcase-insensitiveに検出する禁止語の定義一行。削除すると新規導入・既存例外の追加/改変を検出できなくなる。SHA-256は検索回避でなくtrim行の変更証拠であり、pathと出現回数も固定する。", + "lines": [ + { + "sha256": "edad9af3a2d7cecad990c8908ea38a496fb7413a707c8b6acd72d1ab660e2806", + "count": 1 + } + ] + } +] diff --git a/tools/web-ux/README.md b/tools/web-ux/README.md index f98ea3272..8fe963bac 100644 --- a/tools/web-ux/README.md +++ b/tools/web-ux/README.md @@ -214,7 +214,7 @@ confirmation. Interaction screenshots are saved under `target/web-ux/settings-in `queue`, `notifications`, `legacy`, or `empty`. Each state includes a task for typography comparison. It captures 1440px, 768px, and 390px; `browser-tests/console_pending_shell_test.ts` additionally checks 320px and both themes, independent group visibility, full-width single groups, uniform 16px rows, -monospace inheritance, truncation, and retained-session cancellation gating. Live revision-fenced +monospace inheritance, truncation, and retained-session cancellation gating. Live ordered-FIFO-content-fenced cancellation and state transitions are covered by Console component tests. No real Worker is used. ## Console live Feature invocation fixture diff --git a/tools/web-ux/browser-tests/memory_document_shell_test.ts b/tools/web-ux/browser-tests/memory_document_shell_test.ts index 0a415ab5a..a5cb76b78 100644 --- a/tools/web-ux/browser-tests/memory_document_shell_test.ts +++ b/tools/web-ux/browser-tests/memory_document_shell_test.ts @@ -367,7 +367,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco ).count(), 1, ); - assertEquals(await paginationPage.getByText("Store revision", { exact: true }).count(), 0); + assertEquals(await paginationPage.getByText("Memory fingerprint", { exact: true }).count(), 0); assert((await paginationPage.getByText("Not connected", { exact: true }).count()) > 0); assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0); await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole( diff --git a/web/workspace/src/lib/generated/companion-api.ts b/web/workspace/src/lib/generated/companion-api.ts index 3f72d5bc9..a03341919 100644 --- a/web/workspace/src/lib/generated/companion-api.ts +++ b/web/workspace/src/lib/generated/companion-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-companion-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/companion-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check diff --git a/web/workspace/src/lib/generated/legacy-server-api.ts b/web/workspace/src/lib/generated/legacy-server-api.ts index 8a6f7fdee..ba2d538e1 100644 --- a/web/workspace/src/lib/generated/legacy-server-api.ts +++ b/web/workspace/src/lib/generated/legacy-server-api.ts @@ -63,7 +63,6 @@ export type WorkspaceDeletionState = export type WorkspaceDeletionBlockerKind = | "last_accessible_workspace" - | "revision_conflict" | "dirty_workdir" | "worker_removal_blocked" | "workdir_removal_blocked" diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index c9c810b09..47fdc9d9d 100644 --- a/web/workspace/src/lib/generated/protocol-validator.ts +++ b/web/workspace/src/lib/generated/protocol-validator.ts @@ -21,7 +21,7 @@ const MAX_CORRELATION_ID_BYTES = 128; const MAX_RESOURCE_ID_BYTES = 256; const MAX_WORKER_IDS_PER_SELECTOR = 256; const MAX_REJECTION_MESSAGE_BYTES = 1024; -const subscriptionFrameSchema: unknown = {"$defs":{"Alert":{"additionalProperties":false,"description":"User-facing alert emitted from the Worker layer.\n\nThis is a separate channel from `tracing` (developer logs): entries\nhere are assembled explicitly by the Worker when a condition should be\nsurfaced to the person driving the client. Keep messages short and\nhuman-readable.","properties":{"level":{"$ref":"#/$defs/AlertLevel"},"message":{"type":"string"},"source":{"$ref":"#/$defs/AlertSource"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"}},"required":["level","source","message","timestamp_ms"],"type":"object"},"AlertLevel":{"enum":["warn","error"],"type":"string"},"AlertSource":{"enum":["worker","engine","compactor","agents_md"],"type":"string"},"CommandEvent":{"oneOf":[{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"kind":{"const":"started","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_call_id":{"type":["string","null"]}},"required":["kind","command_id","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"output","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stream":{"$ref":"#/$defs/CommandStream"}},"required":["kind","command_id","stream","start_offset","end_offset","content","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"kind":{"const":"terminal","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stdout_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","command_id","status","stdout_end_offset","stderr_end_offset","observed_at_ms"],"type":"object"}]},"CommandSnapshot":{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"last_output_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr":{"$ref":"#/$defs/CommandStreamSlice"},"stdout":{"$ref":"#/$defs/CommandStreamSlice"},"tool_call_id":{"type":["string","null"]}},"required":["command_id","status","started_at_ms","observed_at_ms","stdout","stderr"],"type":"object"},"CommandStatus":{"description":"Live provider-owned command status. These values are operational Console\nstate only and are never appended to Worker history.","enum":["running","completed","failed","timed_out","cancelled"],"type":"string"},"CommandStream":{"enum":["stdout","stderr"],"type":"string"},"CommandStreamSlice":{"additionalProperties":false,"properties":{"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"truncated":{"type":"boolean"}},"required":["start_offset","end_offset","content","truncated"],"type":"object"},"CompactionLifecycle":{"additionalProperties":false,"description":"Stable parent-owned lifecycle for one compaction run.","properties":{"compaction_id":{"type":"string"},"ended_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"error":{"type":["string","null"]},"internal_worker":{"anyOf":[{"$ref":"#/$defs/InternalWorkerRef"},{"type":"null"}]},"new_segment_id":{"type":["string","null"]},"schema_version":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"description":"Milliseconds since the Unix epoch.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"state":{"$ref":"#/$defs/CompactionLifecycleState"},"summary":{"type":["string","null"]}},"required":["schema_version","compaction_id","state","started_at_ms"],"type":"object"},"CompactionLifecycleState":{"enum":["running","done","failed","interrupted"],"type":"string"},"CompactionPhase":{"enum":["preparing","summarizing","committing"],"type":"string"},"CompactionTrigger":{"enum":["manual","pre_run","request_threshold"],"type":"string"},"CompletionContext":{"additionalProperties":false,"properties":{"argument":{"type":["string","null"]},"invocation":{"$ref":"#/$defs/FeatureInvocationIdentity"}},"required":["invocation"],"type":"object"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.","properties":{"description":{"type":["string","null"]},"invocation":{"anyOf":[{"$ref":"#/$defs/FeatureInvocationDescriptor"},{"type":"null"}]},"is_dir":{"default":false,"type":"boolean"},"usage":{"type":["string","null"]},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\n`File` is the existing `@` path lane. `Feature` lists installed chat\ninvocations. `FeatureArgument` resolves a declared argument using the\nsource-qualified [`CompletionContext`].","enum":["file","feature","feature_argument"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Revisioned FIFO replacement following enqueue, activation, cancel, or clear.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"},"timestamp_ms":{"description":"Timestamp of the persisted Invoke, not the client's attach time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"required":["kind"],"type":"object"},"event":{"const":"invoke_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One AgentTurn boundary opened. An AgentTurn is a maximal run of\nLLM generation calls whose input messages are identical (i.e.\nretries from network errors / 5xx / stream disconnects collapse\ninto the same AgentTurn). When the input changes (a new tool\nresult lands, a user interrupts, etc.), the next LLM call belongs\nto a new AgentTurn.\n\n`turn` is the AgentTurn index from `Engine::turn_count`.\n\nCurrently retry is not yet implemented (`agen-stream-continuation`)\nso AgentTurn and `LlmCall` fire 1:1, but the contract here is\nthe AgentTurn boundary; consumers that want per-LLM-call signals\nmust subscribe to `LlmCallStart` / `LlmCallEnd` instead.","properties":{"data":{"additionalProperties":false,"properties":{"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn"],"type":"object"},"event":{"const":"turn_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"AgentTurn closed.","properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/TurnResult"},"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn","result"],"type":"object"},"event":{"const":"turn_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One LLM generation call started (1 request → 1 generation, retry\nincluded). Multiple `LlmCall*` pairs may fire inside a single\n`TurnStart` / `TurnEnd` pair when a request is retried; today\nthey fire 1:1 because retry is not implemented.\n\n`llm_call` is the worker-wide running counter from\n`Engine::llm_call_count`.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"LLM generation call ended.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A transport-level LLM request retry has been scheduled.\n\nThis is operational state for clients to render while the worker is\nwaiting in backoff. It is not part of conversation history.","properties":{"data":{"additionalProperties":false,"properties":{"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"error":{"type":"string"},"failed_attempt":{"description":"The attempt that just failed. 1 origin.","format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"status":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"wait_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call","failed_attempt","max_attempts","wait_ms","elapsed_ms","error"],"type":"object"},"event":{"const":"llm_retry","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Stream generation was interrupted after events had begun and the worker\nis continuing with a follow-up LLM request.","properties":{"data":{"additionalProperties":false,"properties":{"attempt":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"reason":{"type":"string"}},"required":["llm_call","attempt","max_attempts","reason"],"type":"object"},"event":{"const":"llm_continuation","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A reasoning / thinking block has started.\n\nAlways paired with a `ThinkingDone`. `ThinkingDelta` is optional:\nsome providers (or some configurations) emit thinking metadata\nwithout plaintext, in which case Start → Done arrive with no\ndeltas in between. Multiple thinking blocks per turn are allowed.","properties":{"event":{"const":"thinking_start","type":"string"}},"required":["event"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Thinking block completed. `text` is the full accumulated body\n(empty string when the provider didn't emit plaintext).","properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"event":{"const":"tool_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"json":{"type":"string"}},"required":["id","json"],"type":"object"},"event":{"const":"tool_call_args_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name","arguments"],"type":"object"},"event":{"const":"tool_call_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"anyOf":[{"$ref":"#/$defs/ToolResultDisposition"},{"type":"null"}]},"id":{"type":"string"},"is_error":{"default":false,"type":"boolean"},"output":{"description":"Full tool output. Absent when the tool chose to return\nsummary-only, or when the result was pruned.","type":["string","null"]},"summary":{"description":"Short human-readable summary. Always present; used by clients\nthat only want a 1-line rendering (e.g. collapsed views).","type":"string"}},"required":["id","summary","is_error"],"type":"object"},"event":{"const":"tool_result","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Token accounting for one LLM request.\n\n`input_tokens` is the prompt prefix occupancy (cache reads /\ncache writes included), as normalised by the worker layer.\n`cache_read_input_tokens` is the cache-hit subset of that\noccupancy; subtracting it yields the \"net upload\" the client\nactually paid full price to send on this request, which is what\nthe TUI status line accumulates per turn.","properties":{"data":{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"timestamp_ms":{"description":"Present for usage delivered from the committed session-log lane.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full live execution state. `Stopped` remains Runtime\ncatalog authority and is deliberately not represented here."}},"required":["session","greeting","state"],"type":"object"},"event":{"const":"snapshot","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A live event from a parent-owned Internal Worker. The payload reuses the\nnormal Worker event vocabulary while the wrapper carries stable origin\nidentity. Apply in arrival order on the parent's serialized stream; a\nreconnect snapshot replaces the projection before live events resume.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","event"],"type":"object"},"event":{"const":"internal_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Terminal removal fence for one parent-owned Internal Worker session.\n\nClients discard the matching child and descendants, then ignore later\nnested events for this identity until an authoritative snapshot replaces\nthe projection.","properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker"],"type":"object"},"event":{"const":"internal_worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Server-side segment log rotated to a fresh `SegmentStart`.\n\nFires on compaction and on auto-fork when the store head drifts\nA compaction/fork has replaced the authoritative segment. Clients drop\ntheir derived view and reseed from the canonical committed snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"session":{"$ref":"#/$defs/SessionSnapshot"}},"required":["session"],"type":"object"},"event":{"const":"segment_rotated","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Current Worker controller status. Broadcast on every controller-level\ntransition and included in `History` snapshots for late attach.","properties":{"data":{"additionalProperties":false,"properties":{"snapshot":{"$ref":"#/$defs/WorkerStateSnapshot"}},"required":["snapshot"],"type":"object"},"event":{"const":"worker_state","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"acknowledgement":{"$ref":"#/$defs/WorkerCommandAcknowledgement"}},"required":["acknowledgement"],"type":"object"},"event":{"const":"command_acknowledged","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Bounded, provider-owned command telemetry for the live Console. This is\nintentionally not a history entry and is reconstructed from\n`Snapshot.in_flight.commands` after reconnect.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/CommandEvent"}},"required":["event"],"type":"object"},"event":{"const":"command","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListCompletions`. Delivered only to the\nrequesting socket (not broadcast). `entries` is empty when no\ncandidates match or when the requested kind has no resolver\nwired up.","properties":{"data":{"additionalProperties":false,"properties":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"default":"","type":"string"},"request_id":{"type":["string","null"]}},"required":["kind","entries","prefix"],"type":"object"},"event":{"const":"completions","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListRewindTargets`. Clients should only open a picker\nin response to their own pending request; the event may be broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"targets":{"items":{"$ref":"#/$defs/RewindTarget"},"type":"array"}},"required":["head_entries","targets"],"type":"object"},"event":{"const":"rewind_targets","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A rewind has truncated the authoritative session. `session` is the\nretained canonical snapshot clients should use to reseed display state.","properties":{"data":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"summary":{"$ref":"#/$defs/RewindSummary"}},"required":["session","input","summary"],"type":"object"},"event":{"const":"rewind_applied","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListWorkers`. Payload is a stable JSON value so the Worker\ncrate can evolve discovery fields without introducing a protocol\ndependency on session-store.","properties":{"data":{"additionalProperties":false,"properties":{"workers":true},"required":["workers"],"type":"object"},"event":{"const":"workers_listed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RestoreWorker`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"worker_restored","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RegisterPeer`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"peer_registered","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"$ref":"#/$defs/Alert"},"event":{"const":"alert","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Latest memory extract/consolidation lifecycle event for UI observability.\n\nThis is not part of LLM history or prompt context; clients may display it\nbriefly as operational status.","properties":{"data":{"$ref":"#/$defs/MemoryWorkerEvent"},"event":{"const":"memory_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Runtime-only compaction progress. `None` clears the current status.\nThis never enters Session history and carries no operation or Segment identity.","properties":{"data":{"additionalProperties":false,"properties":{"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}]}},"type":"object"},"event":{"const":"compaction_progress","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Legacy compaction lifecycle event retained for wire read compatibility.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction completed and the session was rotated.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction failed or was cancelled. The session is unchanged.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_failed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"event":{"const":"shutdown","type":"string"}},"required":["event"],"type":"object"}]},"EventSubscriptionSelector":{"oneOf":[{"additionalProperties":false,"properties":{"topic":{"const":"runtime_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"properties":{"topic":{"const":"worker_lifecycle","type":"string"},"worker_ids":{"$ref":"#/$defs/SubscriptionWorkerIds"}},"required":["topic","worker_ids"],"type":"object"},{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"topic":{"const":"worker_protocol","type":"string"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["topic","worker_id"],"type":"object"},{"additionalProperties":false,"description":"Server-derived Workspace projection. Workspace identity comes from the\nauthenticated connection and is deliberately absent from this selector.","properties":{"topic":{"const":"workspace_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"description":"Typed extension point for the later Workdir subscription slice.","properties":{"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic"],"type":"object"}]},"FeatureInvocation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/InvocationArgumentValue"},"type":"array"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"invocation_id":{"description":"Stable client-generated id retained across queueing and transport retry.","type":"string"},"name":{"description":"Selected public name, retained for exact draft/history restoration. Host\nresolution is always by `identity`.","type":"string"}},"required":["invocation_id","identity","name","arguments"],"type":"object"},"FeatureInvocationDescriptor":{"additionalProperties":false,"properties":{"aliases":{"default":[],"items":{"type":"string"},"type":"array"},"arguments":{"default":[],"items":{"$ref":"#/$defs/InvocationArgumentDescriptor"},"type":"array"},"client_adapter":{"anyOf":[{"$ref":"#/$defs/InvocationClientAdapter"},{"type":"null"}]},"description":{"type":"string"},"display_name":{"type":"string"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"name":{"type":"string"},"syntax":{"$ref":"#/$defs/FeatureInvocationSyntax"}},"required":["identity","name","display_name","description","syntax","aliases","arguments"],"type":"object"},"FeatureInvocationIdentity":{"description":"Source-qualified identity of one invocation contribution, for example\n`builtin:attachments/attach`. Display names and aliases are never authority.","type":"string"},"FeatureInvocationSyntax":{"oneOf":[{"const":"parenthesized","description":"`/name(value, key=value)` with JSON-style quoted strings and `)` as the\nexplicit boundary before following natural language.","type":"string"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"blocks":{"items":{"$ref":"#/$defs/InFlightBlock"},"type":"array"},"commands":{"items":{"$ref":"#/$defs/CommandSnapshot"},"type":"array"},"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}],"description":"The currently running compaction, if any.\n\nThis is lifecycle progress only. Candidate history and the staged\nSegment remain private until the Segment is activated atomically."}},"type":"object"},"InFlightToolCallState":{"enum":["pending","streaming_args","done"],"type":"string"},"InternalWorkerKind":{"description":"Presentation category for an Internal Worker exposed through its parent's\nprotocol stream. Internal Workers never become independently addressable\nprotocol subjects.","oneOf":[{"enum":["sub_worker"],"type":"string"},{"additionalProperties":false,"properties":{"service":{"additionalProperties":false,"properties":{"kind":{"type":"string"}},"required":["kind"],"type":"object"}},"required":["service"],"type":"object"}]},"InternalWorkerRef":{"additionalProperties":false,"description":"Stable presentation identity for one parent-owned Internal Worker session.\n`name` is display-only; `session_id` is the identity used by clients.","properties":{"kind":{"$ref":"#/$defs/InternalWorkerKind"},"name":{"type":"string"},"parent_session_id":{"type":["string","null"]},"session_id":{"type":"string"}},"required":["session_id","name","kind"],"type":"object"},"InternalWorkerSnapshot":{"additionalProperties":false,"description":"Reconnect state for one visible Internal Worker. The parent serializes\nsnapshot publication and subsequent live events on the same ordered stream.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","session","status"],"type":"object"},"InvocationArgumentDescriptor":{"additionalProperties":false,"properties":{"completion":{"$ref":"#/$defs/InvocationCompletion","default":{"kind":"none"}},"description":{"type":["string","null"]},"name":{"type":"string"},"position":{"description":"Zero-based positional index. `None` means named-only. A positional\nargument may also be supplied by name; this makes chip editing stable.","format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"required":{"default":false,"type":"boolean"},"value_type":{"$ref":"#/$defs/InvocationArgumentType"}},"required":["name","value_type","completion","required"],"type":"object"},"InvocationArgumentType":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"enum","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"description":"A Worker-readable path. This does not grant filesystem authority.","properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"A resource selected and staged by the client. It must never be resolved\nas a path on the Worker host.","properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"}]},"InvocationArgumentValue":{"additionalProperties":false,"properties":{"name":{"type":"string"},"value":{"$ref":"#/$defs/InvocationValue"}},"required":["name","value"],"type":"object"},"InvocationClientAdapter":{"oneOf":[{"const":"attachment","description":"The client stages a local file and replaces the draft invocation with an\n`UploadedFile` chip. Local paths are not sent to or persisted by Worker.","type":"string"}]},"InvocationCompletion":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"static","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"Host-owned provider selected by source-qualified id. Metadata lookup is\nside-effect free; implementations never cross this wire boundary.","properties":{"kind":{"const":"provider","type":"string"},"provider":{"type":"string"}},"required":["kind","provider"],"type":"object"}]},"InvocationValue":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"type":"object"}]},"InvokeKind":{"description":"Kind of trigger that opened a new Invoke (IDLE → active) range.\n\nOne Invoke groups all entries from this trigger up to the next\n`Invoke` marker. The kind is the only payload — content (user text,\nnotify message, worker event body) is delivered by the immediately\nfollowing Turn entry, not by the marker itself.","oneOf":[{"const":"user_send","description":"`Method::Submit` — a user submission.","type":"string"},{"const":"notify","description":"`Method::Notify` — free-text notification injected into history.","type":"string"},{"const":"worker_event","description":"`Method::WorkerEvent` — typed lifecycle report from a child Worker.","type":"string"},{"const":"system_reminder","description":"A typed system reminder that crosses an IDLE boundary (mid-run\nreminders that don't break IDLE are SystemItem-only and do not\nopen a new Invoke).","type":"string"},{"const":"wakeup","description":"Cron / RemoteTrigger style scheduled wake-up. Reserved; no\nproducer is wired today but the variant is part of the initial\nset so future schedulers don't have to amend the wire enum.","type":"string"}]},"MemoryWorkerEvent":{"additionalProperties":false,"properties":{"message":{"description":"Human-readable compact form for actionbar rendering.","type":"string"},"reason":{"type":"string"},"run_id":{"type":"string"},"status":{"type":"string"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"},"trigger":{"type":"string"},"worker":{"type":"string"}},"required":["worker","status","run_id","trigger","reason","message","timestamp_ms"],"type":"object"},"Method":{"oneOf":[{"additionalProperties":false,"description":"Durably accept typed input for immediate activation or the session FIFO.\nRunning or Paused Workers queue Submit for a later turn; use Notify for\nadvisory information to incorporate into work already in progress.\n\n`submission_request_id` is generated by the authenticated caller and is\nused only for idempotent retry. Worker allocates the durable\n`submission_id` returned by [`Event::SubmissionAccepted`].","properties":{"method":{"const":"submit","type":"string"},"params":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","input"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Human-readable text injected into the target Worker's LLM context as a\nnon-blocking system message. Accepted notifications are delivered at the\nnext between-turn boundary, or start a coalesced notification run while\nthe Worker is idle. No side effects beyond LLM context; use\n`WorkerEvent` for typed lifecycle reports.","properties":{"method":{"const":"notify","type":"string"},"params":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Typed lifecycle report from a child Worker to its direct parent.","properties":{"method":{"const":"worker_event","type":"string"},"params":{"$ref":"#/$defs/WorkerEvent"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Return the authoritative FIFO summary without exposing queued payloads.","properties":{"method":{"const":"list_pending_submissions","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Remove one queued submission. Running or already activated submissions\nare immutable and therefore cannot be cancelled here.","properties":{"method":{"const":"cancel_pending_submission","type":"string"},"params":{"additionalProperties":false,"properties":{"submission_id":{"type":"string"}},"required":["submission_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove exactly the observed ordered set of submissions, preserving the\nactive run. Reject if the FIFO content changed before admission.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_submission_ids":{"items":{"type":"string"},"type":"array"}},"required":["expected_submission_ids"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Activate the next queued submission while the Worker is idle. This is an\nexplicit recovery operation and never resumes a paused run implicitly.","properties":{"method":{"const":"continue_pending","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_id":{"type":"string"}},"required":["expected_head_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"resume","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"cancel","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Stop the in-flight turn and transition to `Paused`.\n\nUnlike `Cancel` (which discards and returns to `Idle`), a paused\nWorker can resume the interrupted work via `Resume`, or accept a\nfresh `Submit` (orphan `tool_use` items are closed with a\nsynthetic tool result before the new user message is appended).","properties":{"method":{"const":"pause","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request an explicit compaction while the Worker is otherwise idle.\n\nThis is a typed control method: clients must not send `compact` as a\n`Method::Submit` user message.","properties":{"method":{"const":"compact","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Ask the Worker to list valid rewind targets from its authoritative session log.","properties":{"method":{"const":"list_rewind_targets","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Truncate the current session back to the selected rewind target and\nreturn the selected user input to the client composer.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"target":{"$ref":"#/$defs/RewindTargetId"}},"required":["target","expected_head_entries"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"shutdown","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request a list of completion candidates from the Worker.\n\nReply is sent on the same socket as `Event::Completions` (not\nbroadcast). The IPC server handles this directly and writes\nthe response straight back to the requesting socket.","properties":{"method":{"const":"list_completions","type":"string"},"params":{"additionalProperties":false,"properties":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"},"request_id":{"description":"Client query identity. Echoed verbatim so identical-prefix ABA\nresponses cannot cross edits, target switches, or permission changes.","type":["string","null"]}},"required":["kind","prefix"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"List Workers visible to this Worker from durable Worker state and the spawned-child\nregistry. This is not a host-wide Worker universe query.","properties":{"method":{"const":"list_workers","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Restore a visible stopped/restorable Worker, or report that it is already\nlive. Missing state and not-visible state are distinct errors.","properties":{"method":{"const":"restore_worker","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Register another existing Worker as a reciprocal peer of this Worker.\n\nThis is metadata/control state only: it does not ask the target's live\ncontroller for consent, and it must not grant delegated scope,\nspawned-child ownership, output cursors, or child lifecycle authority.","properties":{"method":{"const":"register_peer","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"}]},"PasteArtifactAvailability":{"enum":["available","unavailable","integrity_failed"],"type":"string"},"PasteArtifactMediaType":{"description":"One typed piece of a user submission.\n\n`Method::Submit` and `Event::UserMessage` carry `Vec`. Dumb\nclients (CLI piping, scripts) only need to produce a single\n`Segment::Text`; richer clients (TUI / GUI) construct typed atoms\n(paste chips, file refs) and\nsend them through directly so the Worker side never has to re-parse a\nflattened string.\n\nForward compat: payloads with unknown `kind` deserialize to\n`Segment::Unknown`. Worker treats this the same as known-but-unresolved\nvariants — emits an alert and inserts a `[unknown input segment]`\nplaceholder into the LLM context so neither user nor LLM is blind to\nthe dropped intent.","enum":["text_plain_utf8"],"type":"string"},"PasteArtifactRef":{"additionalProperties":false,"description":"Session-owned reference to a large pasted-input artifact.\n\nThe reference contains only bounded integrity and provenance metadata. The\nartifact body remains in session storage and is available to the model only\nthrough the scoped paste-artifact tools installed by Worker.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/PasteArtifactAvailability","description":"Availability observed when this immutable reference was committed.\nReads revalidate storage and integrity rather than trusting this field."},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"char_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"line_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"$ref":"#/$defs/PasteArtifactMediaType"},"sha256":{"type":"string"},"source_entry_id":{"type":"string"}},"required":["artifact_id","created_at_ms","media_type","availability","byte_len","char_count","line_count","sha256","source_entry_id"],"type":"object"},"PendingSubmissionSummary":{"additionalProperties":false,"description":"Bounded public projection of one pending submission. Only a short display\npreview is exposed; full payload segments and provenance remain in the log.","properties":{"accepted_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"preview":{"description":"Single-line display text, at most 240 Unicode scalars plus an ellipsis.\nAbsent on older runtimes. Never used to reconstruct the submitted input.","type":["string","null"]},"segment_count":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","accepted_at_ms","segment_count","byte_len"],"type":"object"},"PendingSubmissionsSnapshot":{"additionalProperties":false,"description":"Session-owned FIFO content projection used by snapshots and live events.","properties":{"head_id":{"default":null,"type":["string","null"]},"notification_count":{"default":0,"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"notification_previews":{"description":"Ordered display-only previews of waiting notifications, each bounded like\nPendingSubmissionSummary::preview. Older runtimes omit this field.","items":{"type":"string"},"type":"array"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RewindTarget":{"additionalProperties":false,"properties":{"disabled_reason":{"type":["string","null"]},"eligible":{"type":"boolean"},"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"id":{"$ref":"#/$defs/RewindTargetId"},"preview":{"type":"string"},"timestamp_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"truncate_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"turn_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"warning":{"type":["string","null"]}},"required":["id","expected_head_entries","truncate_entries","turn_index","preview","eligible"],"type":"object"},"RewindTargetId":{"additionalProperties":false,"properties":{"segment_id":{"type":"string"},"user_input_entry_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["segment_id","user_input_entry_index"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Explicitly selected, source-qualified Feature invocation. The host never\npromotes free text to this variant and validates it against the installed\ndeclaration before execution.","properties":{"invocation":{"$ref":"#/$defs/FeatureInvocation"},"kind":{"const":"feature_invoke","type":"string"}},"required":["kind","invocation"],"type":"object"},{"additionalProperties":false,"description":"Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.","properties":{"kind":{"const":"flow","type":"string"},"selector":{"type":"string"}},"required":["kind","selector"],"type":"object"},{"additionalProperties":false,"description":"Unknown variant from a newer client. Worker treats this as an\nunresolved input — surfaces an alert and inserts a placeholder.\nRound-trip is lossy: re-serializing yields `{\"kind\":\"unknown\"}`.","properties":{"kind":{"const":"unknown","type":"string"}},"required":["kind"],"type":"object"}]},"SessionContentPart":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"refusal","type":"string"},"refusal":{"type":"string"}},"required":["kind","refusal"],"type":"object"}]},"SessionEntryProvenance":{"enum":["human_input","worker_input","flow_instruction","backend_instruction","model_output","tool_output","derived_summary","legacy_unknown"],"type":"string"},"SessionMessageRole":{"enum":["user","assistant"],"type":"string"},"SessionSnapshot":{"additionalProperties":false,"description":"Canonical, storage-independent projection of committed session history.\n\nWorker protocols expose this DTO instead of append-log records. New\nstorage variants can therefore be added without teaching every client how\nto replay the durable log format.","properties":{"entries":{"items":{"$ref":"#/$defs/SessionSnapshotEntry"},"type":"array"},"pending_submissions":{"$ref":"#/$defs/PendingSubmissionsSnapshot","default":{"head_id":null,"notification_count":0,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Snapshot-only replay of persisted invocation/usage records. These are\naccounting metadata, never model context or conversation rows.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"invoke","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/InvokeKind"}},"required":["kind","trigger","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"usage","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","input_tokens","cache_read_input_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Display-only accounting reconstructed from persisted run/usage records.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_stats","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"requests":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"upload_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","elapsed_ms","requests","upload_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"attachment_id":{"description":"Stable, bounded identity derived from the owning Session entry, attachment\nposition, and immutable image bytes. It is a locator, not a bearer secret.","type":"string"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"type":"string"}},"required":["attachment_id","media_type","byte_len"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"restore_observation_token":{"description":"Opaque Runtime-owned Restore observation identity, never a display-state hash.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"type":"object"},"SubscriptionWorkerAvailability":{"description":"Whether the Workspace Server has a current Runtime observation for a Worker.\n\n`Unavailable` deliberately does not imply `Stopped`: it means the last observed\nlifecycle state is retained while the Runtime observation source is unavailable.","enum":["observed","unavailable"],"type":"string"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nCommands operate on the controller state at admission time, not a caller's\npreviously observed Run. A controller accepts command ids in strictly\nincreasing order; retrying an accepted id cannot apply it to a later Run.\nExact retries must retain the same command kind. Removed preconditions are\nrejected at the wire boundary rather than treated as arrival-time commands.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"last_finished_submission_request_id":{"description":"Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.","type":["string","null"]},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; +const subscriptionFrameSchema: unknown = {"$defs":{"Alert":{"additionalProperties":false,"description":"User-facing alert emitted from the Worker layer.\n\nThis is a separate channel from `tracing` (developer logs): entries\nhere are assembled explicitly by the Worker when a condition should be\nsurfaced to the person driving the client. Keep messages short and\nhuman-readable.","properties":{"level":{"$ref":"#/$defs/AlertLevel"},"message":{"type":"string"},"source":{"$ref":"#/$defs/AlertSource"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"}},"required":["level","source","message","timestamp_ms"],"type":"object"},"AlertLevel":{"enum":["warn","error"],"type":"string"},"AlertSource":{"enum":["worker","engine","compactor","agents_md"],"type":"string"},"CommandEvent":{"oneOf":[{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"kind":{"const":"started","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_call_id":{"type":["string","null"]}},"required":["kind","command_id","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"output","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stream":{"$ref":"#/$defs/CommandStream"}},"required":["kind","command_id","stream","start_offset","end_offset","content","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"kind":{"const":"terminal","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stdout_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","command_id","status","stdout_end_offset","stderr_end_offset","observed_at_ms"],"type":"object"}]},"CommandSnapshot":{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"last_output_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr":{"$ref":"#/$defs/CommandStreamSlice"},"stdout":{"$ref":"#/$defs/CommandStreamSlice"},"tool_call_id":{"type":["string","null"]}},"required":["command_id","status","started_at_ms","observed_at_ms","stdout","stderr"],"type":"object"},"CommandStatus":{"description":"Live provider-owned command status. These values are operational Console\nstate only and are never appended to Worker history.","enum":["running","completed","failed","timed_out","cancelled"],"type":"string"},"CommandStream":{"enum":["stdout","stderr"],"type":"string"},"CommandStreamSlice":{"additionalProperties":false,"properties":{"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"truncated":{"type":"boolean"}},"required":["start_offset","end_offset","content","truncated"],"type":"object"},"CompactionLifecycle":{"additionalProperties":false,"description":"Stable parent-owned lifecycle for one compaction run.","properties":{"compaction_id":{"type":"string"},"ended_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"error":{"type":["string","null"]},"internal_worker":{"anyOf":[{"$ref":"#/$defs/InternalWorkerRef"},{"type":"null"}]},"new_segment_id":{"type":["string","null"]},"schema_version":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"description":"Milliseconds since the Unix epoch.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"state":{"$ref":"#/$defs/CompactionLifecycleState"},"summary":{"type":["string","null"]}},"required":["schema_version","compaction_id","state","started_at_ms"],"type":"object"},"CompactionLifecycleState":{"enum":["running","done","failed","interrupted"],"type":"string"},"CompactionPhase":{"enum":["preparing","summarizing","committing"],"type":"string"},"CompactionTrigger":{"enum":["manual","pre_run","request_threshold"],"type":"string"},"CompletionContext":{"additionalProperties":false,"properties":{"argument":{"type":["string","null"]},"invocation":{"$ref":"#/$defs/FeatureInvocationIdentity"}},"required":["invocation"],"type":"object"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.","properties":{"description":{"type":["string","null"]},"invocation":{"anyOf":[{"$ref":"#/$defs/FeatureInvocationDescriptor"},{"type":"null"}]},"is_dir":{"default":false,"type":"boolean"},"usage":{"type":["string","null"]},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\n`File` is the existing `@` path lane. `Feature` lists installed chat\ninvocations. `FeatureArgument` resolves a declared argument using the\nsource-qualified [`CompletionContext`].","enum":["file","feature","feature_argument"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Complete FIFO replacement following enqueue, activation, cancel, or clear.\nClients replace their pending view in stream order; reconnect uses the snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"},"timestamp_ms":{"description":"Timestamp of the persisted Invoke, not the client's attach time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"required":["kind"],"type":"object"},"event":{"const":"invoke_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One AgentTurn boundary opened. An AgentTurn is a maximal run of\nLLM generation calls whose input messages are identical (i.e.\nretries from network errors / 5xx / stream disconnects collapse\ninto the same AgentTurn). When the input changes (a new tool\nresult lands, a user interrupts, etc.), the next LLM call belongs\nto a new AgentTurn.\n\n`turn` is the AgentTurn index from `Engine::turn_count`.\n\nCurrently retry is not yet implemented (`agen-stream-continuation`)\nso AgentTurn and `LlmCall` fire 1:1, but the contract here is\nthe AgentTurn boundary; consumers that want per-LLM-call signals\nmust subscribe to `LlmCallStart` / `LlmCallEnd` instead.","properties":{"data":{"additionalProperties":false,"properties":{"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn"],"type":"object"},"event":{"const":"turn_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"AgentTurn closed.","properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/TurnResult"},"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn","result"],"type":"object"},"event":{"const":"turn_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One LLM generation call started (1 request → 1 generation, retry\nincluded). Multiple `LlmCall*` pairs may fire inside a single\n`TurnStart` / `TurnEnd` pair when a request is retried; today\nthey fire 1:1 because retry is not implemented.\n\n`llm_call` is the worker-wide running counter from\n`Engine::llm_call_count`.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"LLM generation call ended.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A transport-level LLM request retry has been scheduled.\n\nThis is operational state for clients to render while the worker is\nwaiting in backoff. It is not part of conversation history.","properties":{"data":{"additionalProperties":false,"properties":{"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"error":{"type":"string"},"failed_attempt":{"description":"The attempt that just failed. 1 origin.","format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"status":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"wait_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call","failed_attempt","max_attempts","wait_ms","elapsed_ms","error"],"type":"object"},"event":{"const":"llm_retry","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Stream generation was interrupted after events had begun and the worker\nis continuing with a follow-up LLM request.","properties":{"data":{"additionalProperties":false,"properties":{"attempt":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"reason":{"type":"string"}},"required":["llm_call","attempt","max_attempts","reason"],"type":"object"},"event":{"const":"llm_continuation","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A reasoning / thinking block has started.\n\nAlways paired with a `ThinkingDone`. `ThinkingDelta` is optional:\nsome providers (or some configurations) emit thinking metadata\nwithout plaintext, in which case Start → Done arrive with no\ndeltas in between. Multiple thinking blocks per turn are allowed.","properties":{"event":{"const":"thinking_start","type":"string"}},"required":["event"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Thinking block completed. `text` is the full accumulated body\n(empty string when the provider didn't emit plaintext).","properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"event":{"const":"tool_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"json":{"type":"string"}},"required":["id","json"],"type":"object"},"event":{"const":"tool_call_args_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name","arguments"],"type":"object"},"event":{"const":"tool_call_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"anyOf":[{"$ref":"#/$defs/ToolResultDisposition"},{"type":"null"}]},"id":{"type":"string"},"is_error":{"default":false,"type":"boolean"},"output":{"description":"Full tool output. Absent when the tool chose to return\nsummary-only, or when the result was pruned.","type":["string","null"]},"summary":{"description":"Short human-readable summary. Always present; used by clients\nthat only want a 1-line rendering (e.g. collapsed views).","type":"string"}},"required":["id","summary","is_error"],"type":"object"},"event":{"const":"tool_result","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Token accounting for one LLM request.\n\n`input_tokens` is the prompt prefix occupancy (cache reads /\ncache writes included), as normalised by the worker layer.\n`cache_read_input_tokens` is the cache-hit subset of that\noccupancy; subtracting it yields the \"net upload\" the client\nactually paid full price to send on this request, which is what\nthe TUI status line accumulates per turn.","properties":{"data":{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"timestamp_ms":{"description":"Present for usage delivered from the committed session-log lane.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full live execution state. `Stopped` remains Runtime\ncatalog authority and is deliberately not represented here."}},"required":["session","greeting","state"],"type":"object"},"event":{"const":"snapshot","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A live event from a parent-owned Internal Worker. The payload reuses the\nnormal Worker event vocabulary while the wrapper carries stable origin\nidentity. Apply in arrival order on the parent's serialized stream; a\nreconnect snapshot replaces the projection before live events resume.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","event"],"type":"object"},"event":{"const":"internal_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Terminal removal fence for one parent-owned Internal Worker session.\n\nClients discard the matching child and descendants, then ignore later\nnested events for this identity until an authoritative snapshot replaces\nthe projection.","properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker"],"type":"object"},"event":{"const":"internal_worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Server-side segment log rotated to a fresh `SegmentStart`.\n\nFires on compaction and on auto-fork when the store head drifts\nA compaction/fork has replaced the authoritative segment. Clients drop\ntheir derived view and reseed from the canonical committed snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"session":{"$ref":"#/$defs/SessionSnapshot"}},"required":["session"],"type":"object"},"event":{"const":"segment_rotated","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Current Worker controller status. Broadcast on every controller-level\ntransition and included in `History` snapshots for late attach.","properties":{"data":{"additionalProperties":false,"properties":{"snapshot":{"$ref":"#/$defs/WorkerStateSnapshot"}},"required":["snapshot"],"type":"object"},"event":{"const":"worker_state","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"acknowledgement":{"$ref":"#/$defs/WorkerCommandAcknowledgement"}},"required":["acknowledgement"],"type":"object"},"event":{"const":"command_acknowledged","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Bounded, provider-owned command telemetry for the live Console. This is\nintentionally not a history entry and is reconstructed from\n`Snapshot.in_flight.commands` after reconnect.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/CommandEvent"}},"required":["event"],"type":"object"},"event":{"const":"command","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListCompletions`. Delivered only to the\nrequesting socket (not broadcast). `entries` is empty when no\ncandidates match or when the requested kind has no resolver\nwired up.","properties":{"data":{"additionalProperties":false,"properties":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"default":"","type":"string"},"request_id":{"type":["string","null"]}},"required":["kind","entries","prefix"],"type":"object"},"event":{"const":"completions","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListRewindTargets`. Clients should only open a picker\nin response to their own pending request; the event may be broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"targets":{"items":{"$ref":"#/$defs/RewindTarget"},"type":"array"}},"required":["head_entries","targets"],"type":"object"},"event":{"const":"rewind_targets","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A rewind has truncated the authoritative session. `session` is the\nretained canonical snapshot clients should use to reseed display state.","properties":{"data":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"summary":{"$ref":"#/$defs/RewindSummary"}},"required":["session","input","summary"],"type":"object"},"event":{"const":"rewind_applied","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListWorkers`. Payload is a stable JSON value so the Worker\ncrate can evolve discovery fields without introducing a protocol\ndependency on session-store.","properties":{"data":{"additionalProperties":false,"properties":{"workers":true},"required":["workers"],"type":"object"},"event":{"const":"workers_listed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RestoreWorker`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"worker_restored","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RegisterPeer`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"peer_registered","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"$ref":"#/$defs/Alert"},"event":{"const":"alert","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Latest memory extract/consolidation lifecycle event for UI observability.\n\nThis is not part of LLM history or prompt context; clients may display it\nbriefly as operational status.","properties":{"data":{"$ref":"#/$defs/MemoryWorkerEvent"},"event":{"const":"memory_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Runtime-only compaction progress. `None` clears the current status.\nThis never enters Session history and carries no operation or Segment identity.","properties":{"data":{"additionalProperties":false,"properties":{"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}]}},"type":"object"},"event":{"const":"compaction_progress","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Legacy compaction lifecycle event retained for wire read compatibility.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction completed and the session was rotated.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction failed or was cancelled. The session is unchanged.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_failed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"event":{"const":"shutdown","type":"string"}},"required":["event"],"type":"object"}]},"EventSubscriptionSelector":{"oneOf":[{"additionalProperties":false,"properties":{"topic":{"const":"runtime_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"properties":{"topic":{"const":"worker_lifecycle","type":"string"},"worker_ids":{"$ref":"#/$defs/SubscriptionWorkerIds"}},"required":["topic","worker_ids"],"type":"object"},{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"topic":{"const":"worker_protocol","type":"string"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["topic","worker_id"],"type":"object"},{"additionalProperties":false,"description":"Server-derived Workspace projection. Workspace identity comes from the\nauthenticated connection and is deliberately absent from this selector.","properties":{"topic":{"const":"workspace_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"description":"Typed extension point for the later Workdir subscription slice.","properties":{"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic"],"type":"object"}]},"FeatureInvocation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/InvocationArgumentValue"},"type":"array"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"invocation_id":{"description":"Stable client-generated id retained across queueing and transport retry.","type":"string"},"name":{"description":"Selected public name, retained for exact draft/history restoration. Host\nresolution is always by `identity`.","type":"string"}},"required":["invocation_id","identity","name","arguments"],"type":"object"},"FeatureInvocationDescriptor":{"additionalProperties":false,"properties":{"aliases":{"default":[],"items":{"type":"string"},"type":"array"},"arguments":{"default":[],"items":{"$ref":"#/$defs/InvocationArgumentDescriptor"},"type":"array"},"client_adapter":{"anyOf":[{"$ref":"#/$defs/InvocationClientAdapter"},{"type":"null"}]},"description":{"type":"string"},"display_name":{"type":"string"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"name":{"type":"string"},"syntax":{"$ref":"#/$defs/FeatureInvocationSyntax"}},"required":["identity","name","display_name","description","syntax","aliases","arguments"],"type":"object"},"FeatureInvocationIdentity":{"description":"Source-qualified identity of one invocation contribution, for example\n`builtin:attachments/attach`. Display names and aliases are never authority.","type":"string"},"FeatureInvocationSyntax":{"oneOf":[{"const":"parenthesized","description":"`/name(value, key=value)` with JSON-style quoted strings and `)` as the\nexplicit boundary before following natural language.","type":"string"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"blocks":{"items":{"$ref":"#/$defs/InFlightBlock"},"type":"array"},"commands":{"items":{"$ref":"#/$defs/CommandSnapshot"},"type":"array"},"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}],"description":"The currently running compaction, if any.\n\nThis is lifecycle progress only. Candidate history and the staged\nSegment remain private until the Segment is activated atomically."}},"type":"object"},"InFlightToolCallState":{"enum":["pending","streaming_args","done"],"type":"string"},"InternalWorkerKind":{"description":"Presentation category for an Internal Worker exposed through its parent's\nprotocol stream. Internal Workers never become independently addressable\nprotocol subjects.","oneOf":[{"enum":["sub_worker"],"type":"string"},{"additionalProperties":false,"properties":{"service":{"additionalProperties":false,"properties":{"kind":{"type":"string"}},"required":["kind"],"type":"object"}},"required":["service"],"type":"object"}]},"InternalWorkerRef":{"additionalProperties":false,"description":"Stable presentation identity for one parent-owned Internal Worker session.\n`name` is display-only; `session_id` is the identity used by clients.","properties":{"kind":{"$ref":"#/$defs/InternalWorkerKind"},"name":{"type":"string"},"parent_session_id":{"type":["string","null"]},"session_id":{"type":"string"}},"required":["session_id","name","kind"],"type":"object"},"InternalWorkerSnapshot":{"additionalProperties":false,"description":"Reconnect state for one visible Internal Worker. The parent serializes\nsnapshot publication and subsequent live events on the same ordered stream.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","session","status"],"type":"object"},"InvocationArgumentDescriptor":{"additionalProperties":false,"properties":{"completion":{"$ref":"#/$defs/InvocationCompletion","default":{"kind":"none"}},"description":{"type":["string","null"]},"name":{"type":"string"},"position":{"description":"Zero-based positional index. `None` means named-only. A positional\nargument may also be supplied by name; this makes chip editing stable.","format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"required":{"default":false,"type":"boolean"},"value_type":{"$ref":"#/$defs/InvocationArgumentType"}},"required":["name","value_type","completion","required"],"type":"object"},"InvocationArgumentType":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"enum","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"description":"A Worker-readable path. This does not grant filesystem authority.","properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"A resource selected and staged by the client. It must never be resolved\nas a path on the Worker host.","properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"}]},"InvocationArgumentValue":{"additionalProperties":false,"properties":{"name":{"type":"string"},"value":{"$ref":"#/$defs/InvocationValue"}},"required":["name","value"],"type":"object"},"InvocationClientAdapter":{"oneOf":[{"const":"attachment","description":"The client stages a local file and replaces the draft invocation with an\n`UploadedFile` chip. Local paths are not sent to or persisted by Worker.","type":"string"}]},"InvocationCompletion":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"static","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"Host-owned provider selected by source-qualified id. Metadata lookup is\nside-effect free; implementations never cross this wire boundary.","properties":{"kind":{"const":"provider","type":"string"},"provider":{"type":"string"}},"required":["kind","provider"],"type":"object"}]},"InvocationValue":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"type":"object"}]},"InvokeKind":{"description":"Kind of trigger that opened a new Invoke (IDLE → active) range.\n\nOne Invoke groups all entries from this trigger up to the next\n`Invoke` marker. The kind is the only payload — content (user text,\nnotify message, worker event body) is delivered by the immediately\nfollowing Turn entry, not by the marker itself.","oneOf":[{"const":"user_send","description":"`Method::Submit` — a user submission.","type":"string"},{"const":"notify","description":"`Method::Notify` — free-text notification injected into history.","type":"string"},{"const":"worker_event","description":"`Method::WorkerEvent` — typed lifecycle report from a child Worker.","type":"string"},{"const":"system_reminder","description":"A typed system reminder that crosses an IDLE boundary (mid-run\nreminders that don't break IDLE are SystemItem-only and do not\nopen a new Invoke).","type":"string"},{"const":"wakeup","description":"Cron / RemoteTrigger style scheduled wake-up. Reserved; no\nproducer is wired today but the variant is part of the initial\nset so future schedulers don't have to amend the wire enum.","type":"string"}]},"MemoryWorkerEvent":{"additionalProperties":false,"properties":{"message":{"description":"Human-readable compact form for actionbar rendering.","type":"string"},"reason":{"type":"string"},"run_id":{"type":"string"},"status":{"type":"string"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"},"trigger":{"type":"string"},"worker":{"type":"string"}},"required":["worker","status","run_id","trigger","reason","message","timestamp_ms"],"type":"object"},"Method":{"oneOf":[{"additionalProperties":false,"description":"Durably accept typed input for immediate activation or the session FIFO.\nRunning or Paused Workers queue Submit for a later turn; use Notify for\nadvisory information to incorporate into work already in progress.\n\n`submission_request_id` is generated by the authenticated caller and is\nused only for idempotent retry. Worker allocates the durable\n`submission_id` returned by [`Event::SubmissionAccepted`].","properties":{"method":{"const":"submit","type":"string"},"params":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","input"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Human-readable text injected into the target Worker's LLM context as a\nnon-blocking system message. Accepted notifications are delivered at the\nnext between-turn boundary, or start a coalesced notification run while\nthe Worker is idle. No side effects beyond LLM context; use\n`WorkerEvent` for typed lifecycle reports.","properties":{"method":{"const":"notify","type":"string"},"params":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Typed lifecycle report from a child Worker to its direct parent.","properties":{"method":{"const":"worker_event","type":"string"},"params":{"$ref":"#/$defs/WorkerEvent"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Return the authoritative FIFO summary without exposing queued payloads.","properties":{"method":{"const":"list_pending_submissions","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Remove one queued submission. Running or already activated submissions\nare immutable and therefore cannot be cancelled here.","properties":{"method":{"const":"cancel_pending_submission","type":"string"},"params":{"additionalProperties":false,"properties":{"submission_id":{"type":"string"}},"required":["submission_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove exactly the observed ordered set of submissions, preserving the\nactive run. Reject if the FIFO content changed before admission.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_submission_ids":{"items":{"type":"string"},"type":"array"}},"required":["expected_submission_ids"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Activate the next queued submission while the Worker is idle. This is an\nexplicit recovery operation and never resumes a paused run implicitly.","properties":{"method":{"const":"continue_pending","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_id":{"type":"string"}},"required":["expected_head_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"resume","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"cancel","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Stop the in-flight turn and transition to `Paused`.\n\nUnlike `Cancel` (which discards and returns to `Idle`), a paused\nWorker can resume the interrupted work via `Resume`, or accept a\nfresh `Submit` (orphan `tool_use` items are closed with a\nsynthetic tool result before the new user message is appended).","properties":{"method":{"const":"pause","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request an explicit compaction while the Worker is otherwise idle.\n\nThis is a typed control method: clients must not send `compact` as a\n`Method::Submit` user message.","properties":{"method":{"const":"compact","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Ask the Worker to list valid rewind targets from its authoritative session log.","properties":{"method":{"const":"list_rewind_targets","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Truncate the current session back to the selected rewind target and\nreturn the selected user input to the client composer.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"target":{"$ref":"#/$defs/RewindTargetId"}},"required":["target","expected_head_entries"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"shutdown","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request a list of completion candidates from the Worker.\n\nReply is sent on the same socket as `Event::Completions` (not\nbroadcast). The IPC server handles this directly and writes\nthe response straight back to the requesting socket.","properties":{"method":{"const":"list_completions","type":"string"},"params":{"additionalProperties":false,"properties":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"},"request_id":{"description":"Client query identity. Echoed verbatim so identical-prefix ABA\nresponses cannot cross edits, target switches, or permission changes.","type":["string","null"]}},"required":["kind","prefix"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"List Workers visible to this Worker from durable Worker state and the spawned-child\nregistry. This is not a host-wide Worker universe query.","properties":{"method":{"const":"list_workers","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Restore a visible stopped/restorable Worker, or report that it is already\nlive. Missing state and not-visible state are distinct errors.","properties":{"method":{"const":"restore_worker","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Register another existing Worker as a reciprocal peer of this Worker.\n\nThis is metadata/control state only: it does not ask the target's live\ncontroller for consent, and it must not grant delegated scope,\nspawned-child ownership, output cursors, or child lifecycle authority.","properties":{"method":{"const":"register_peer","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"}]},"PasteArtifactAvailability":{"enum":["available","unavailable","integrity_failed"],"type":"string"},"PasteArtifactMediaType":{"description":"One typed piece of a user submission.\n\n`Method::Submit` and `Event::UserMessage` carry `Vec`. Dumb\nclients (CLI piping, scripts) only need to produce a single\n`Segment::Text`; richer clients (TUI / GUI) construct typed atoms\n(paste chips, file refs) and\nsend them through directly so the Worker side never has to re-parse a\nflattened string.\n\nForward compat: payloads with unknown `kind` deserialize to\n`Segment::Unknown`. Worker treats this the same as known-but-unresolved\nvariants — emits an alert and inserts a `[unknown input segment]`\nplaceholder into the LLM context so neither user nor LLM is blind to\nthe dropped intent.","enum":["text_plain_utf8"],"type":"string"},"PasteArtifactRef":{"additionalProperties":false,"description":"Session-owned reference to a large pasted-input artifact.\n\nThe reference contains only bounded integrity and provenance metadata. The\nartifact body remains in session storage and is available to the model only\nthrough the scoped paste-artifact tools installed by Worker.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/PasteArtifactAvailability","description":"Availability observed when this immutable reference was committed.\nReads revalidate storage and integrity rather than trusting this field."},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"char_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"line_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"$ref":"#/$defs/PasteArtifactMediaType"},"sha256":{"type":"string"},"source_entry_id":{"type":"string"}},"required":["artifact_id","created_at_ms","media_type","availability","byte_len","char_count","line_count","sha256","source_entry_id"],"type":"object"},"PendingSubmissionSummary":{"additionalProperties":false,"description":"Bounded public projection of one pending submission. Only a short display\npreview is exposed; full payload segments and provenance remain in the log.","properties":{"accepted_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"preview":{"description":"Single-line display text, at most 240 Unicode scalars plus an ellipsis.\nAbsent on older runtimes. Never used to reconstruct the submitted input.","type":["string","null"]},"segment_count":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","accepted_at_ms","segment_count","byte_len"],"type":"object"},"PendingSubmissionsSnapshot":{"additionalProperties":false,"description":"Session-owned FIFO content projection used by snapshots and live events.","properties":{"head_id":{"default":null,"type":["string","null"]},"notification_count":{"default":0,"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"notification_previews":{"description":"Ordered display-only previews of waiting notifications, each bounded like\nPendingSubmissionSummary::preview. Older runtimes omit this field.","items":{"type":"string"},"type":"array"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RewindTarget":{"additionalProperties":false,"properties":{"disabled_reason":{"type":["string","null"]},"eligible":{"type":"boolean"},"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"id":{"$ref":"#/$defs/RewindTargetId"},"preview":{"type":"string"},"timestamp_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"truncate_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"turn_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"warning":{"type":["string","null"]}},"required":["id","expected_head_entries","truncate_entries","turn_index","preview","eligible"],"type":"object"},"RewindTargetId":{"additionalProperties":false,"properties":{"segment_id":{"type":"string"},"user_input_entry_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["segment_id","user_input_entry_index"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Explicitly selected, source-qualified Feature invocation. The host never\npromotes free text to this variant and validates it against the installed\ndeclaration before execution.","properties":{"invocation":{"$ref":"#/$defs/FeatureInvocation"},"kind":{"const":"feature_invoke","type":"string"}},"required":["kind","invocation"],"type":"object"},{"additionalProperties":false,"description":"Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.","properties":{"kind":{"const":"flow","type":"string"},"selector":{"type":"string"}},"required":["kind","selector"],"type":"object"},{"additionalProperties":false,"description":"Unknown variant from a newer client. Worker treats this as an\nunresolved input — surfaces an alert and inserts a placeholder.\nRound-trip is lossy: re-serializing yields `{\"kind\":\"unknown\"}`.","properties":{"kind":{"const":"unknown","type":"string"}},"required":["kind"],"type":"object"}]},"SessionContentPart":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"refusal","type":"string"},"refusal":{"type":"string"}},"required":["kind","refusal"],"type":"object"}]},"SessionEntryProvenance":{"enum":["human_input","worker_input","flow_instruction","backend_instruction","model_output","tool_output","derived_summary","legacy_unknown"],"type":"string"},"SessionMessageRole":{"enum":["user","assistant"],"type":"string"},"SessionSnapshot":{"additionalProperties":false,"description":"Canonical, storage-independent projection of committed session history.\n\nWorker protocols expose this DTO instead of append-log records. New\nstorage variants can therefore be added without teaching every client how\nto replay the durable log format.","properties":{"entries":{"items":{"$ref":"#/$defs/SessionSnapshotEntry"},"type":"array"},"pending_submissions":{"$ref":"#/$defs/PendingSubmissionsSnapshot","default":{"head_id":null,"notification_count":0,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Snapshot-only replay of persisted invocation/usage records. These are\naccounting metadata, never model context or conversation rows.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"invoke","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/InvokeKind"}},"required":["kind","trigger","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"usage","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","input_tokens","cache_read_input_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Display-only accounting reconstructed from persisted run/usage records.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_stats","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"requests":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"upload_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","elapsed_ms","requests","upload_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"attachment_id":{"description":"Stable, bounded identity derived from the owning Session entry, attachment\nposition, and immutable image bytes. It is a locator, not a bearer secret.","type":"string"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"type":"string"}},"required":["attachment_id","media_type","byte_len"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"restore_observation_token":{"description":"Opaque Runtime-owned Restore observation identity, never a display-state hash.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"type":"object"},"SubscriptionWorkerAvailability":{"description":"Whether the Workspace Server has a current Runtime observation for a Worker.\n\n`Unavailable` deliberately does not imply `Stopped`: it means the last observed\nlifecycle state is retained while the Runtime observation source is unavailable.","enum":["observed","unavailable"],"type":"string"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nCommands operate on the controller state at admission time, not a caller's\npreviously observed Run. A controller accepts command ids in strictly\nincreasing order; retrying an accepted id cannot apply it to a later Run.\nExact retries must retain the same command kind. Removed preconditions are\nrejected at the wire boundary rather than treated as arrival-time commands.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"last_finished_submission_request_id":{"description":"Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.","type":["string","null"]},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; export const RUST_SERIALIZED_SUBSCRIPTION_FRAME_FIXTURES: readonly unknown[] = [{"protocol_version":2,"frame":"response","message":{"result":"subscribed","payload":{"request_id":"fixture-request","subscription_id":"fixture-subscription","selector":{"topic":"workspace_workers"},"snapshot":{"topic":"workers","data":{"workers":[{"restore_observation_token":"fixture-restore-observation","worker_id":"fixture-worker","runtime_id":"fixture-runtime","resource_key":"fixture-worker-resource","availability":"observed","state":"idle","has_running_internal_workers":false,"workspace_id":"fixture-workspace","workdir_attachments":[{"alias":"external","working_directory_id":"fixture-external-workdir"}]}]}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"pending_submissions_changed","data":{"pending":{"notification_count":0,"head_id":null,"submissions":[]}}}}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"snapshot","data":{"session":{"pending_submissions":{"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"state":{"last_command_id":0,"state":{"kind":"idle"}},"internal_workers":[{"worker":{"session_id":"fixture-child-session","name":"fixture-child","parent_session_id":"fixture-parent-session","kind":"sub_worker"},"session":{"pending_submissions":{"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"status":"idle"}]}}}}}}}]; const utf8 = new TextEncoder(); diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index 04f9e6730..93145145c 100644 --- a/web/workspace/src/lib/generated/repository-api.ts +++ b/web/workspace/src/lib/generated/repository-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-repository-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Output: web/workspace/src/lib/generated/repository-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check diff --git a/web/workspace/src/lib/generated/runtime-api.ts b/web/workspace/src/lib/generated/runtime-api.ts index fd85dc19c..388f6867b 100644 --- a/web/workspace/src/lib/generated/runtime-api.ts +++ b/web/workspace/src/lib/generated/runtime-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-runtime-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/runtime-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check diff --git a/web/workspace/src/lib/generated/ticket-api.ts b/web/workspace/src/lib/generated/ticket-api.ts index 1559c0419..8818a7ad0 100644 --- a/web/workspace/src/lib/generated/ticket-api.ts +++ b/web/workspace/src/lib/generated/ticket-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/ticket-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check diff --git a/web/workspace/src/lib/generated/workdir-api.ts b/web/workspace/src/lib/generated/workdir-api.ts index 82cb27e33..b40098ed6 100644 --- a/web/workspace/src/lib/generated/workdir-api.ts +++ b/web/workspace/src/lib/generated/workdir-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-workdir-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/workdir-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index 65cf310d0..8814b40e8 100644 --- a/web/workspace/src/lib/generated/worker-launch-api.ts +++ b/web/workspace/src/lib/generated/worker-launch-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-worker-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4) +// Input contract: 0.1.0 (sha256:fc25104c077ab8c5c43f77611f505171585d2d40fd54c6538774ac17aae6cce3) // Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/worker-launch-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check diff --git a/web/workspace/src/lib/workspace/api/workspace-model.ts b/web/workspace/src/lib/workspace/api/workspace-model.ts index 53b4a8efd..c776a15f1 100644 --- a/web/workspace/src/lib/workspace/api/workspace-model.ts +++ b/web/workspace/src/lib/workspace/api/workspace-model.ts @@ -135,7 +135,7 @@ function repositoryArray(value: unknown, path: string): unknown[] { return parsed; } -function repositorySourceRevision(value: unknown, path: string): number { +function nonnegativeSafeInteger(value: unknown, path: string): number { const parsed = integer(value, path); if (parsed < 0) { throw new Error(`${path} must be between 0 and Number.MAX_SAFE_INTEGER`); @@ -833,7 +833,7 @@ export function parseRepositorySshConnectionProbeResponse( } const WORKSPACE_DELETION_MAX_OPERATION_ID_BYTES = 128; -const WORKSPACE_DELETION_MAX_REVISION_BYTES = 128; +const WORKSPACE_DELETION_MAX_UPDATED_AT_BYTES = 128; const WORKSPACE_DELETION_MAX_BLOCKERS = 1024; const WORKSPACE_DELETION_MAX_CHILD_OPERATION_IDS = 4096; const WORKSPACE_DELETION_MAX_RESOURCE_VALUE_BYTES = 128; @@ -872,7 +872,6 @@ const deletionStates = new Set([ ]); const deletionBlockerKinds = new Set([ "last_accessible_workspace", - "revision_conflict", "dirty_workdir", "worker_removal_blocked", "workdir_removal_blocked", @@ -981,7 +980,7 @@ export function parseWorkspaceDeletionPreflightResponse( expected_workspace_updated_at: deletionBoundedString( item.expected_workspace_updated_at, "Workspace deletion preflight.expected_workspace_updated_at", - WORKSPACE_DELETION_MAX_REVISION_BYTES, + WORKSPACE_DELETION_MAX_UPDATED_AT_BYTES, ), can_delete: boolean( item.can_delete, @@ -1089,7 +1088,7 @@ export function parseHostListResponse(value: unknown): HostListResponse { ["workspace_id", "limit", "items", "source", "diagnostics"], "host list response", ); - const limit = repositorySourceRevision( + const limit = nonnegativeSafeInteger( response.limit, "host list response.limit", ); diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 9180fdbd1..45cb8ed87 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -1278,7 +1278,6 @@ Deno.test("Worker Console route resolves logical Worker authority before Runtime consolePage.includes('sendWorkerControl("cancel")') && consolePage.includes("lifecycleMethod(command)") && consolePage.includes("command_id: commandId") && - !consolePage.includes("expected_worker_state_revision") && !consolePage.includes("expected_execution_generation") && consolePage.includes("onsubmit={handleComposerSubmit}") && consolePage.includes("disabled={!composerEditable}") && diff --git a/web/workspace/src/lib/workspace/merge-request-status.test.ts b/web/workspace/src/lib/workspace/merge-request-status.test.ts index 5e3ba1d44..c02bac2ce 100644 --- a/web/workspace/src/lib/workspace/merge-request-status.test.ts +++ b/web/workspace/src/lib/workspace/merge-request-status.test.ts @@ -177,7 +177,7 @@ Deno.test("Merge Request routes distinguish recorded approval from live source o ), ); assertEquals(detail.includes("Recorded integration approval"), true); - assertEquals(detail.includes("Live source revision"), true); + assertEquals(detail.includes("Live source ref"), true); assertEquals(detail.includes("Source observation error"), true); }); diff --git a/web/workspace/src/lib/workspace/merge-request-status.ts b/web/workspace/src/lib/workspace/merge-request-status.ts index d3b186bdd..36ce766aa 100644 --- a/web/workspace/src/lib/workspace/merge-request-status.ts +++ b/web/workspace/src/lib/workspace/merge-request-status.ts @@ -92,7 +92,7 @@ export function sourceReviewFreshness( return `Fresh source review required: no effective verdict exists for ${source}.`; } -// Source/integration approval can survive rescope; only this exact revision +// Source/integration approval can survive rescope; only this exact item snapshot // and linked-result attestation approves the current Ticket requirements. export function currentRequirementApprovalStatus( evidence: TicketEvidenceSummary, diff --git a/web/workspace/src/lib/workspace/resource-links.ts b/web/workspace/src/lib/workspace/resource-links.ts index b1672a08a..d1e9fb219 100644 --- a/web/workspace/src/lib/workspace/resource-links.ts +++ b/web/workspace/src/lib/workspace/resource-links.ts @@ -1,6 +1,6 @@ import type { DriveEntryRef } from "#lib/generated/drive-api.ts"; -/** Human-facing latest-only link: name, path, revision and bearer tokens are absent. */ +/** Human-facing latest-only link: name, path, content digest and bearer tokens are absent. */ export function driveHref(entry: DriveEntryRef): string { if ( !/^[1-9][0-9]{0,18}$/.test(entry.node_id) || diff --git a/web/workspace/src/lib/workspace/settings/profile-api.ts b/web/workspace/src/lib/workspace/settings/profile-api.ts index c5310fa32..413d54172 100644 --- a/web/workspace/src/lib/workspace/settings/profile-api.ts +++ b/web/workspace/src/lib/workspace/settings/profile-api.ts @@ -97,7 +97,7 @@ function optionalBoundedString( return boundedStringValue(value, context, maxBytes); } -function optionalRevision( +function optionalByteCount( value: unknown, context: string, ): number | null | undefined { @@ -250,7 +250,7 @@ function parseWorkspaceProfileSourceSummary( 502, ); } - const sizeBytes = optionalRevision(item.size_bytes, "Profile source catalog"); + const sizeBytes = optionalByteCount(item.size_bytes, "Profile source catalog"); if (sizeBytes === undefined || sizeBytes === null) { throw new ProfileApiError( "Profile source catalog returned an invalid response.", diff --git a/web/workspace/src/lib/workspace/settings/workdir-checkout.test.ts b/web/workspace/src/lib/workspace/settings/workdir-checkout.test.ts index 0262296fc..8c9dab92a 100644 --- a/web/workspace/src/lib/workspace/settings/workdir-checkout.test.ts +++ b/web/workspace/src/lib/workspace/settings/workdir-checkout.test.ts @@ -53,9 +53,9 @@ Deno.test("Git Workdir with a selector shows selector at current ref", () => { Deno.test("non-Git Workdir does not receive Git hash formatting", () => { assertEquals( formatCurrentWorkdirCheckout( - workdir("snapshot", "revision-value"), + workdir("snapshot", "snapshot-id"), "archive", ), - "snapshot · revision-value", + "snapshot · snapshot-id", ); }); diff --git a/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.test.ts b/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.test.ts index d9cea3469..035d1fd93 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.test.ts @@ -47,7 +47,7 @@ function attachment( }; } -Deno.test("logical config sidebar label is not fabricated as a repository revision", () => { +Deno.test("logical config sidebar label is not fabricated as a repository checkout", () => { const meta = sidebarWorkdirMeta([attachment("workspace-config", { display_name: "Workspace configuration", source: { kind: "workspace_config", access: "read_write", content_path: "/workspace-config", purpose: "Configuration" }, @@ -87,6 +87,17 @@ Deno.test("Worker sidebar Workdir label uses the creation branch before observat assertEquals(meta.text, "main:develop"); }); +Deno.test("Worker sidebar Workdir label shows the creation ref before a current checkout is observed", () => { + const meta = sidebarWorkdirMeta([ + attachment("checkout", { + creation_selector: "refs/tags/release", + creation_ref: "0123456789abcdef0123456789abcdef01234567", + }), + ]); + + assertEquals(meta.text, "main:creation-ref@0123456789ab"); +}); + Deno.test("Worker sidebar Workdir label marks detached HEAD instead of reusing creation branch", () => { const meta = sidebarWorkdirMeta([ attachment("checkout", { diff --git a/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.ts b/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.ts index 2743dd851..653d0c643 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-workdir-meta.ts @@ -38,10 +38,10 @@ function attachmentLabel(attachment: SidebarWorkdirAttachment): string { const repository = workdir?.source.kind === "repository" ? clean(workdir.source.repository_key) : clean(attachment.repository_key); - return `${repository ?? "unknown-repo"}:${revisionLabel(attachment)}`; + return `${repository ?? "unknown-repo"}:${checkoutLabel(attachment)}`; } -function revisionLabel(attachment: SidebarWorkdirAttachment): string { +function checkoutLabel(attachment: SidebarWorkdirAttachment): string { const workdir = attachment.working_directory; if (!workdir) { return `unknown@${shortReference(attachment.working_directory_id)}`; @@ -66,7 +66,7 @@ function revisionLabel(attachment: SidebarWorkdirAttachment): string { const creationReference = clean(workdir.creation_ref); if (creationSelector || creationReference) { return explicitReferenceFallback( - "revision", + "creation-ref", creationReference ?? creationSelector, ); } @@ -84,7 +84,7 @@ function branchName(selector: string | null): string | null { } function explicitReferenceFallback( - kind: "detached" | "revision", + kind: "detached" | "creation-ref", reference: string | null, ): string { return reference ? `${kind}@${shortReference(reference)}` : kind; diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/memory-page.browser.spec.ts b/web/workspace/src/routes/w/[workspaceId]/memory/memory-page.browser.spec.ts index 2a7d0aeb0..e8d29fdb8 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/memory-page.browser.spec.ts +++ b/web/workspace/src/routes/w/[workspaceId]/memory/memory-page.browser.spec.ts @@ -75,7 +75,7 @@ test("renders the subject index as the Memory product entry", () => { .toBeNull(); expect(screen.getByText("subject-1")).not.toBeNull(); expect(screen.getByText("Not connected")).not.toBeNull(); - expect(screen.queryByText("Store revision", { exact: true })).toBeNull(); + expect(screen.queryByText("Memory fingerprint", { exact: true })).toBeNull(); expect(screen.getByText("1 shown · more available")).not.toBeNull(); expect(screen.getByRole("link", { name: "First page" }).getAttribute("href")) .toBe("/w/workspace-1/memory"); diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/settings.browser.spec.ts b/web/workspace/src/routes/w/[workspaceId]/settings/settings.browser.spec.ts index 801a713c1..317cd2fb8 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/settings.browser.spec.ts +++ b/web/workspace/src/routes/w/[workspaceId]/settings/settings.browser.spec.ts @@ -114,7 +114,6 @@ test("saved settings have no editable form, badges or raw data expanded; each te ); expect(document.querySelectorAll("details[open]")).toHaveLength(0); expect(document.querySelector(".badge")).toBeNull(); - expect(screen.queryByText("Revision", { exact: true })).toBeNull(); expect( screen.getByText(identityFixture().public_bundle!.public_key_fingerprint), ).toBeTruthy(); diff --git a/web/workspace/test/config-source/api.test.ts b/web/workspace/test/config-source/api.test.ts index a7c3bea39..10426f333 100644 --- a/web/workspace/test/config-source/api.test.ts +++ b/web/workspace/test/config-source/api.test.ts @@ -197,12 +197,12 @@ Deno.test("config source API rejects mismatched entry map paths", async () => { ); }); -Deno.test("config source API rejects removed snapshot revision fields and oversized entry content", () => { +Deno.test("config source API rejects unknown snapshot fields and oversized entry content", () => { assertThrows( () => parseWorkspaceConfigTreeResponse({ ...tree, - snapshot: { ...snapshot, revision: 7 }, + snapshot: { ...snapshot, unexpected_field: true }, }), ConfigSourceApiError, "invalid response", diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index cfcb00bc8..962409b6a 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -169,7 +169,6 @@ Deno.test("generated WASM diagnostics carry snapshot provenance", () => { }>; assertEquals(diagnostics[0].path, "workspace.dcdl"); assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(snapshot).digest); - assertEquals("revision" in diagnostics[0], false); assertEquals(diagnostics[0].kind, "syntax"); }); @@ -938,7 +937,6 @@ Deno.test("generated WASM returns to the same content digest and enforces entry content: "{ broken = ; }", }]) as ConfigTreeSnapshot; assertEquals(changed.digest === base.digest, false); - assertEquals("revision" in changed, false); const diagnostics = analyze_snapshot(changed, "main.dcdl", undefined) as Array<{ tree_digest: string }>; assertEquals(diagnostics.length > 0, true); assertEquals(diagnostics.every((diagnostic) => diagnostic.tree_digest === changed.digest), true); diff --git a/web/workspace/test/repository-access/model.test.ts b/web/workspace/test/repository-access/model.test.ts index 46c2a88b3..38225a5fb 100644 --- a/web/workspace/test/repository-access/model.test.ts +++ b/web/workspace/test/repository-access/model.test.ts @@ -74,7 +74,7 @@ Deno.test("Repository Access distinguishes permission, conflict, field, and miss repositoryAccessRequestError(409, "rotate the key").message.includes( "Reload", ), - "409 should explain revision recovery", + "409 should explain conflict recovery", ); assert( repositoryAccessRequestError(422, "rotate the key").message.includes( diff --git a/web/workspace/test/visual-review/README.md b/web/workspace/test/visual-review/README.md index 843582e25..d43f0770c 100644 --- a/web/workspace/test/visual-review/README.md +++ b/web/workspace/test/visual-review/README.md @@ -43,7 +43,7 @@ capture error is expected. To repeat dark captures, use a scenario copy with ## Implementer-owned visual review -- Baseline revision: `e939cd4e4b1f6b63b81411fd7766062f2d69ad0d`. +- Baseline commit: `e939cd4e4b1f6b63b81411fd7766062f2d69ad0d`. - Before: `.svelte-kit/t717/visual/before-final-light` and `before-dark`. These use an isolated build under `.svelte-kit/t717/baseline-source`, with the changed production form, catalog API, model, and CSS restored from baseline diff --git a/web/workspace/tests/memory-api.test.ts b/web/workspace/tests/memory-api.test.ts index dedc746a2..dae4e6f6b 100644 --- a/web/workspace/tests/memory-api.test.ts +++ b/web/workspace/tests/memory-api.test.ts @@ -700,15 +700,14 @@ Deno.test("Memory changes preserve server order without interpreting opaque iden assertEquals(parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: "z/earlier", current_change_id: "a/current" }).change_id, "z/earlier"); }); -Deno.test("Memory identities reject numeric counters, empty and oversized strings, and legacy fields", () => { +Deno.test("Memory identities reject numeric counters, empty and oversized strings, and unknown fields", () => { for (const value of [3, "", "x".repeat(513)]) { assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: value }), "bounded identifier"); assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), current_change_id: value }), "bounded identifier"); assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), memory_fingerprint: value }), "bounded identifier"); } - assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), revision: 2 }), "unknown field"); - assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), behavior_revision: 2 }), "unknown field"); - assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), store_revision: 12 }), "unknown field"); + assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), unexpected_field: true }), "unknown field"); + assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), unexpected_field: true }), "unknown field"); }); Deno.test("Behavior CAS sends the observed text byte-for-byte including empty content", async () => { diff --git a/web/workspace/tests/profile-api.test.ts b/web/workspace/tests/profile-api.test.ts index 64477561b..2b2e1d6e5 100644 --- a/web/workspace/tests/profile-api.test.ts +++ b/web/workspace/tests/profile-api.test.ts @@ -93,6 +93,19 @@ Deno.test("profile settings requests use scoped API and strictly validate respon } }); +Deno.test("profile source byte counts accept nonnegative safe integers and reject missing or malformed values", () => { + for (const sizeBytes of [0, 128, Number.MAX_SAFE_INTEGER]) { + const value = profileSettingsFixture(); + (value.sources as Record[])[0].size_bytes = sizeBytes; + assertEquals(parseProfileSettingsResponse(value).sources[0].size_bytes, sizeBytes); + } + for (const sizeBytes of [undefined, null, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, "128"]) { + const value = profileSettingsFixture(); + (value.sources as Record[])[0].size_bytes = sizeBytes; + assertThrows(() => parseProfileSettingsResponse(value), ProfileApiError); + } +}); + Deno.test("workspace metadata requests use generated DTO shapes", async () => { const originalFetch = globalThis.fetch; const requests: Array<{ url: string; init?: RequestInit }> = []; @@ -247,8 +260,7 @@ Deno.test("Workspace signing identity parser validates active and pending public value.private_material_ref = "must-not-be-accepted"; }, (value: Record) => { - (value.identity as Record).revision = - Number.MAX_SAFE_INTEGER + 1; + (value.identity as Record).unexpected_field = true; }, (value: Record) => { (value.identity as Record).public_key = "x".repeat( @@ -287,7 +299,7 @@ Deno.test("workspace metadata parser rejects incomplete or stale response fields workspace_id: "workspace-test", display_name: "Workspace", created_at: "2026-01-01T00:00:00Z", - revision: "sha256:metadata", + updated_at: "2026-01-02T00:00:00Z", source: "workspace-config", diagnostics: [], workspace_path: "/legacy/path", diff --git a/web/workspace/tests/runtime-management.test.ts b/web/workspace/tests/runtime-management.test.ts index b5369bcc5..92df47a47 100644 --- a/web/workspace/tests/runtime-management.test.ts +++ b/web/workspace/tests/runtime-management.test.ts @@ -199,10 +199,10 @@ Deno.test("Runtime validators reject unknown object keys and enum variants", () }); Deno.test("Runtime validators reject invalid binding identities and bounded collection overflow", () => { - const unsafeRevision = structuredClone(detail()); - Object.assign(unsafeRevision.trust_key, { binding_id: 1 }); + const invalidBinding = structuredClone(detail()); + Object.assign(invalidBinding.trust_key, { binding_id: 1 }); assertThrows( - () => parseWorkspaceRuntimeDetail(unsafeRevision), + () => parseWorkspaceRuntimeDetail(invalidBinding), "must be a string", ); @@ -506,11 +506,11 @@ Deno.test("Runtime trust replacement preserves binding conflicts without retryin } }); -Deno.test("Runtime trust replacement rejects legacy counter conflict responses", async () => { +Deno.test("Runtime trust replacement rejects unknown fields and missing binding identity in conflict responses", async () => { const fetchImpl = (() => Promise.resolve(Response.json({ error: "stale_binding", message: "Stale trust", - current_revision: 3, + unexpected_field: true, }, { status: 409 }))) as typeof fetch; try { await createRemoteRuntime("workspace-a", { diff --git a/web/workspace/tests/skill-api.test.ts b/web/workspace/tests/skill-api.test.ts index a8351c0fb..5c79187fb 100644 --- a/web/workspace/tests/skill-api.test.ts +++ b/web/workspace/tests/skill-api.test.ts @@ -161,11 +161,11 @@ Deno.test("Skill parser fails closed on unknown fields and newer enum values", ( ); }); -Deno.test("Skill parser rejects removed revision fields and oversized collections or strings", () => { - const legacyRevision = catalogFixture(); - (legacyRevision.projection as Record).config_revision = 1; +Deno.test("Skill parser rejects unknown projection fields and oversized collections or strings", () => { + const legacyCounter = catalogFixture(); + (legacyCounter.projection as Record).unexpected_field = 1; assertContractError( - () => parseSkillCatalogResponse(legacyRevision), + () => parseSkillCatalogResponse(legacyCounter), "unknown fields", ); diff --git a/web/workspace/tests/workspace-model.test.ts b/web/workspace/tests/workspace-model.test.ts index 8ff49c4d5..cdf8feba8 100644 --- a/web/workspace/tests/workspace-model.test.ts +++ b/web/workspace/tests/workspace-model.test.ts @@ -45,7 +45,7 @@ Deno.test("workspace creation rejects missing and malformed repository response for ( const value of [undefined, false, {}, { ...currentInitialRepository, - source_revision: -1, + source_fingerprint: -1, }] ) { assertThrows( @@ -152,13 +152,13 @@ Deno.test("stale repository aliases fail closed at the JSON boundary", () => { ); }); -Deno.test("repository JSON rejects removed source revision counters", () => { +Deno.test("repository JSON rejects unknown fields", () => { const stale = structuredClone(repositoryList) as Record; const items = stale.items as Array>; - items[0].source_revision = 1; + items[0].unexpected_field = 1; assertThrows( () => parseRepositoryListResponse(stale), - ".source_revision is not part of the wire contract", + ".unexpected_field is not part of the wire contract", ); }); @@ -322,6 +322,21 @@ Deno.test("Workspace deletion DTOs fail closed and preserve durable operation st }), "unexpected is not part", ); + assertThrows( + () => + parseWorkspaceDeletionPreflightResponse({ + ...preflight, + expected_workspace_updated_at: "x".repeat(129), + }), + ".expected_workspace_updated_at is too long", + ); + for (const parse of [parseWorkspaceDeletionPreflightResponse, parseWorkspaceDeletionOperationResponse]) { + const value = parse === parseWorkspaceDeletionPreflightResponse ? preflight : operation; + assertThrows( + () => parse({ ...value, blockers: [{ ...operation.blockers[0], kind: "unknown_blocker" }] }), + ".kind is invalid", + ); + } assertThrows( () => parseWorkspaceDeletionOperationResponse({