diff --git a/docs/authentication/.review/README.md b/docs/authentication/.review/README.md new file mode 100644 index 0000000..80f5880 --- /dev/null +++ b/docs/authentication/.review/README.md @@ -0,0 +1,30 @@ +--- +artifact: swe-design-review-attestation +schema_version: 6 +scope_key: c2ebaf06c49668e199e08bccef68303c5cd0925e79b8f635860bb8fa8494d61b +scope: {"kind": "path", "primary_target": "docs/authentication/implementation-plan.md", "repository": ".", "selector": "docs/authentication/implementation-plan.md"} +review_content_identity_sha256: f46fc5843e78a8a5fd743a620ca1f795760d4e4d3d28bc7db44e7963eb32aaca +target_content_identity_sha256: 4edf646bb0369f9066ae6ae575c940373ef1595f1fb93965f7e450914ce66244 +baseline_content_identity_sha256: 8e02df1e7ff92b8aef06db429f86cd9b60ef8fdb1651422b9e6473cad85338a9 +target_documents: [{"executable": false, "path": "docs/authentication/README.md", "repository": ".", "sha256": "2bff627a30d975cbdde020b014638d6b061c2aba891815a2975998a1894a6ef9"}, {"executable": false, "path": "docs/authentication/implementation-plan.md", "repository": ".", "sha256": "38456f43776f8b47b1c5beb7cdbf5be5a34832267286a0135a489083980a4379"}] +baseline_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/milestone-3-lld.md", "repository": ".", "sha256": "566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +design_dependency_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/milestone-3-lld.md", "repository": ".", "sha256": "566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +plan_binding: {"base_revision": "7ca9b3d2a556852672eedaaa264ed94a44ba38af", "documents": [{"executable": false, "path": "docs/authentication/implementation-plan.md", "plan_key": "arbiter-authentication", "sha256": "38456f43776f8b47b1c5beb7cdbf5be5a34832267286a0135a489083980a4379", "slice_convention": "hierarchical-v1"}], "predecessors": [{"executable": false, "path": "docs/authentication/implementation-plan.md", "sha256": "b2899d82e5f0914b3a5088bdbbe0fa4dce7034ea6e8cc963d5b61df11acc3fcc"}]} +security_review: {"actual": "dbb", "binding_identity_sha256": "5357c8e3040ed8f3d37fa71fb3228a9a424b88332404fa6e576c478e40c33dad", "requested": "dbb", "reviewer": "/root/auth210_inventory_dbb_recovery", "schema_version": 1} +document_repository: "." +document_path: "docs/authentication/README.md" +document_revision_provenance: "7ca9b3d2a556852672eedaaa264ed94a44ba38af" +document_sha256: 2bff627a30d975cbdde020b014638d6b061c2aba891815a2975998a1894a6ef9 +verdict: clean +attested_at: 2026-10-28T11:48:40Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-6 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/authentication/.review/implementation-plan.md b/docs/authentication/.review/implementation-plan.md index e61fac7..9fab8d8 100644 --- a/docs/authentication/.review/implementation-plan.md +++ b/docs/authentication/.review/implementation-plan.md @@ -1,28 +1,29 @@ --- artifact: swe-design-review-attestation -schema_version: 5 +schema_version: 6 scope_key: c2ebaf06c49668e199e08bccef68303c5cd0925e79b8f635860bb8fa8494d61b scope: {"kind": "path", "primary_target": "docs/authentication/implementation-plan.md", "repository": ".", "selector": "docs/authentication/implementation-plan.md"} -review_content_identity_sha256: bc445c81f584d3d0a83d7826fb052aa4e943d26a4d627fd7d7a32b2bb0b8e711 -target_content_identity_sha256: a9d3c2a74376b48f8af8fc97c2e7c18069b97e5e9b209ce3658fcff8bf8083b1 -baseline_content_identity_sha256: ad1ec634421f4d4b16b5cb61c07844e945fa11d0737e7e59757b3b3aabbf21ce -target_documents: [{"executable": false, "path": "docs/authentication/implementation-plan.md", "repository": ".", "sha256": "c576091af919c3ec99f0e0edb5795cf4e7bd37a44415ae24742393822317ad3d"}] -baseline_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] -design_dependency_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] -plan_binding: {"base_revision": "0aa1b60a3c13a713e4e27c0645b0763c3d2fc6a1", "documents": [{"executable": false, "path": "docs/authentication/implementation-plan.md", "plan_key": "arbiter-authentication", "sha256": "c576091af919c3ec99f0e0edb5795cf4e7bd37a44415ae24742393822317ad3d", "slice_convention": "hierarchical-v1"}], "predecessors": []} +review_content_identity_sha256: f46fc5843e78a8a5fd743a620ca1f795760d4e4d3d28bc7db44e7963eb32aaca +target_content_identity_sha256: 4edf646bb0369f9066ae6ae575c940373ef1595f1fb93965f7e450914ce66244 +baseline_content_identity_sha256: 8e02df1e7ff92b8aef06db429f86cd9b60ef8fdb1651422b9e6473cad85338a9 +target_documents: [{"executable": false, "path": "docs/authentication/README.md", "repository": ".", "sha256": "2bff627a30d975cbdde020b014638d6b061c2aba891815a2975998a1894a6ef9"}, {"executable": false, "path": "docs/authentication/implementation-plan.md", "repository": ".", "sha256": "38456f43776f8b47b1c5beb7cdbf5be5a34832267286a0135a489083980a4379"}] +baseline_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/milestone-3-lld.md", "repository": ".", "sha256": "566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +design_dependency_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/milestone-3-lld.md", "repository": ".", "sha256": "566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +plan_binding: {"base_revision": "7ca9b3d2a556852672eedaaa264ed94a44ba38af", "documents": [{"executable": false, "path": "docs/authentication/implementation-plan.md", "plan_key": "arbiter-authentication", "sha256": "38456f43776f8b47b1c5beb7cdbf5be5a34832267286a0135a489083980a4379", "slice_convention": "hierarchical-v1"}], "predecessors": [{"executable": false, "path": "docs/authentication/implementation-plan.md", "sha256": "b2899d82e5f0914b3a5088bdbbe0fa4dce7034ea6e8cc963d5b61df11acc3fcc"}]} +security_review: {"actual": "dbb", "binding_identity_sha256": "5357c8e3040ed8f3d37fa71fb3228a9a424b88332404fa6e576c478e40c33dad", "requested": "dbb", "reviewer": "/root/auth210_inventory_dbb_recovery", "schema_version": 1} document_repository: "." document_path: "docs/authentication/implementation-plan.md" -document_revision_provenance: "0aa1b60a3c13a713e4e27c0645b0763c3d2fc6a1" -document_sha256: c576091af919c3ec99f0e0edb5795cf4e7bd37a44415ae24742393822317ad3d +document_revision_provenance: "7ca9b3d2a556852672eedaaa264ed94a44ba38af" +document_sha256: 38456f43776f8b47b1c5beb7cdbf5be5a34832267286a0135a489083980a4379 verdict: clean -attested_at: 2026-10-20T06:27:44Z +attested_at: 2026-10-28T11:48:40Z --- - + # SWE design-review attestation Review freshness binds target and baseline document bytes and executable state -listed in the version-5 header. Revisions are provenance only. +listed in the version-6 header. Revisions are provenance only. ## Durable review state diff --git a/docs/authentication/.review/milestone-3-lld.md b/docs/authentication/.review/milestone-3-lld.md new file mode 100644 index 0000000..79f66f8 --- /dev/null +++ b/docs/authentication/.review/milestone-3-lld.md @@ -0,0 +1,30 @@ +--- +artifact: swe-design-review-attestation +schema_version: 6 +scope_key: 66489811874712090b8cf783e2912a2c30971230158f693f9f856c843a312941 +scope: {"kind": "path", "primary_target": "docs/authentication/milestone-3-lld.md", "repository": ".", "selector": "docs/authentication/milestone-3-lld.md"} +review_content_identity_sha256: e0867deabcd610323cc406bc446579d94be5a1a851a23920805d0646d3f267d7 +target_content_identity_sha256: 3d8711b943bcc9225b635788047b92db86c772b42c43f82bf1d2ce71e7566099 +baseline_content_identity_sha256: 0c7c67fcd4a67e4cf2005caadd444b25aeb2082ecd8bc449d1424386a27cd631 +target_documents: [{"executable": false, "path": "docs/authentication/milestone-3-lld.md", "repository": ".", "sha256": "566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f"}] +baseline_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +design_dependency_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +plan_binding: null +security_review: {"actual": "dbb", "binding_identity_sha256": "c8a75df1717837e705d4821342d107fd1bf278fee889d7188a7561d02754d813", "requested": "dbb", "reviewer": "/root/pr75_e48c_inventory_dbb", "schema_version": 1} +document_repository: "." +document_path: "docs/authentication/milestone-3-lld.md" +document_revision_provenance: "e48c2feeee01707218733fe46e40cdd61fcbc9c6" +document_sha256: 566601529e5dabfa25cb575a9fc890ab625f358115ab0b836a53174a025bd25f +verdict: clean +attested_at: 2026-10-28T12:41:34Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-6 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/authentication/.review/portable-enrollment-lld.md b/docs/authentication/.review/portable-enrollment-lld.md new file mode 100644 index 0000000..e9f279f --- /dev/null +++ b/docs/authentication/.review/portable-enrollment-lld.md @@ -0,0 +1,30 @@ +--- +artifact: swe-design-review-attestation +schema_version: 6 +scope_key: d7084ee076d112d626ecf8763d342a9d7aabf0f7e3f15c864b4e69f32bb5e46c +scope: {"kind": "path", "primary_target": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "selector": "docs/authentication/portable-enrollment-lld.md"} +review_content_identity_sha256: 3507be6574a7311b65c6e57f49ac3e5f0d3b349661d9fc0c84da00d9ae7e0696 +target_content_identity_sha256: cca7a23e61b75d5310e9105f446813644e395ecc51cb241469d449321df6f505 +baseline_content_identity_sha256: ad1ec634421f4d4b16b5cb61c07844e945fa11d0737e7e59757b3b3aabbf21ce +target_documents: [{"executable": false, "path": "docs/authentication/portable-enrollment-lld.md", "repository": ".", "sha256": "7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1"}] +baseline_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +design_dependency_documents: [{"executable": false, "path": "docs/authentication/high-level-design.md", "repository": ".", "sha256": "b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57"}, {"executable": false, "path": "docs/authentication/requirements.md", "repository": ".", "sha256": "d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929"}] +plan_binding: null +security_review: {"actual": "dbb", "binding_identity_sha256": "355d9145e192b0a3980fc8d3a1c951915388fd8937a61001c94bd379d8b304ca", "requested": "dbb", "reviewer": "/root/pr75_e48c_inventory_dbb", "schema_version": 1} +document_repository: "." +document_path: "docs/authentication/portable-enrollment-lld.md" +document_revision_provenance: "e48c2feeee01707218733fe46e40cdd61fcbc9c6" +document_sha256: 7cbad8803d0ade4090289d22a681a01026782ade9785c731ac7a2d8a33b9c0e1 +verdict: clean +attested_at: 2026-10-28T12:35:44Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-6 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/authentication/README.md b/docs/authentication/README.md index 1134f53..dac773b 100644 --- a/docs/authentication/README.md +++ b/docs/authentication/README.md @@ -1,27 +1,80 @@ # Authentication and enrollment -Start with the [requirements baseline](requirements.md), then the -[high-level design](high-level-design.md), followed by the -[implementation plan](implementation-plan.md). All three passed DBB and -their respective SWE reviews. Their [requirements stamp](.review/requirements.md), -[HLD stamp](.review/high-level-design.md) and -[plan stamp](.review/implementation-plan.md) bind exact bytes and the appropriate -requirements/HLD dependencies. The delivery sequence is requirements, -high-level design, then an implementation plan with slices. Each stage -is reviewed and stamped at its own level, with DBB review of the final -document bytes before stamping. +Read the [requirements](requirements.md), [HLD](high-level-design.md), +[portable enrollment LLD](portable-enrollment-lld.md), +[milestone-3 LLD](milestone-3-lld.md), then the +[implementation plan](implementation-plan.md). The requirements/HLD remain the +unchanged governing design. The two LLDs specify the next proposed work, its +complete acceptance and already sized implementation cuts; the plan owns delivery +ordering and stable identities. This page summarizes those four design inputs +and the plan; it adds no separate product requirements. -The [earlier synthesis](design.md) and [candidate security contracts](security-contracts.md) -retain the mechanisms already assessed by DBB as historical inputs to the HLD. -They are not a completed high-level design or additional binding requirements. +Review evidence lives beside documents in `.review/`, including the +[requirements](.review/requirements.md), [HLD](.review/high-level-design.md), +[portable enrollment](.review/portable-enrollment-lld.md), +[milestone 3](.review/milestone-3-lld.md) and +[plan](.review/implementation-plan.md) attestations. A stamp binds exact bytes, +explicit design dependencies and its actual review evidence. An older stamp does +not approve changed text; every changed design must complete DBB/general review +and receive a current stamp before acceptance. Review approval is separate from +implementation authorization, merge and publication. + +## Current state and proposed next work + +Merged baseline: `25a8cf2196f36499ff92ce890f313901b51b4f2b`. +Authority/setup/local control and the milestone-2 fresh-pairing/activation/receipt +stack are merged; [PR74](https://github.com/omry/arbiter/pull/74) closed the latter. +All 13 reported post-merge checks passed in the +[baseline workflow run](https://github.com/omry/arbiter/actions/runs/40579826766). +That is source-baseline evidence, not +completion of all authentication requirements or publication approval. + +Existing-portable-identity reuse remains unimplemented. Fresh-key proof and +same-selector receipt resume do not preserve one agent across two servers. +The portable LLD's proposed `2.10.1–2.10.4` closes that gap with independently +registered issuer trust, original portable-key possession and fresh per-server +access/approval/grants. It retains the existing pairing and private publisher; +no key broker or second enrollment service is proposed. The first upgraded cut +also maintains unique agent bindings during fresh-v1 activation; safe intermediate +state is not deferred until v2 cutover. The old milestone-2 +22-attempt allowance is exhausted and does not authorize these four new cuts. + +Milestone 3 is also unimplemented. Its LLD specifies strict native mTLS, existing +ASGI/policy integration, live grants/denial and closed artifact behavior until the +encrypted store. It proposes 13 complete cuts, ordered so denial is ready before +public ordinary cutover. The server request-body deadline and shared enrollment +denial/contact fields are design specifications, not applied runtime changes. +The existing agent binding remains canonical. The production CLI switch and +server enablement share the same cut; earlier private-peer coverage preserves +the working product route. The first live enrollment-deny control and fixed-receipt +output guard share the same coordinator and cut; ordinary admission extends it +in the following cut. -The interview is closed. DBB resolved findings 1–5 at design level; -finding 6 uses the owner's accepted restore/migration procedure for cooperative -servers. Protocol, implementation and release readiness are not established. -The B1–B5 groups supplied the HLD agenda; their implementation evidence remains -outstanding. They did not require implementation detail for the initial -requirements stamp. No product authentication implementation has been authorized -by these documents. +The next design-reviewed implementation proposals are: + +| Work | Proposed PRs | Handwritten production/test/CI estimate | +| --- | ---: | ---: | +| `2.10` portable reuse | 4 | 2,150–3,015 | +| `3` native API/grants/denial | 13 | 6,790–9,410 | + +Detailed ownership, acceptance and per-cut estimates live once in their LLDs. +Docs/news/lock data are separate. Counts include code motion, fixtures and review +headroom; no later dynamic split is assumed. These are reviewable estimates, +not approved delivery allowances or guarantees. Preserve the existing 1,000-line +handwritten-code cap, including tests. The original 60-PR plan and milestone-3 +seven-PR estimate are historical. Milestones 4–11 retain scope/outlines, but need +source-based LLD sizing/review before execution; no credible total remaining-PR +forecast is claimed yet. + +Design and local/PR review do not authorize implementation. Starting `2.10`, +milestone 3 or later implementation requires separate operator authorization. All eleven milestones and +the combined publication gate remain required; a merged development stack is +not releasable authentication support. + +The [earlier synthesis](design.md) and [candidate security contracts](security-contracts.md) +are historical HLD inputs. The closed interview and earlier findings remain +provenance, not additional binding requirements or instructions to reopen questions. +These design documents themselves grant no product implementation authorization. ## Supporting material @@ -33,23 +86,15 @@ by these documents. exact-byte HLD pass. Earlier reports concern candidate architecture. The [initial plan review](reviews/plan-dbb.md) records the safe artifact-prefix and certificate-renewal repairs. The [plan sizing review](reviews/plan-sizing-dbb.md) - covers the current child-PR estimates and repaired activation/denial order. + records the original child-PR estimates and repaired activation/denial order; + it does not review the new LLD forecasts. - [Original draft and interview](../archive/authentication-and-enrollment/README.md): immutable historical evidence, not required reading or instructions to resume questioning. -The HLD covers B1–B5 and shared boundaries. Fourteen local Python/Go feasibility -checks passed; platform, production dependency, race and crash validation remain -implementation obligations. No product authentication is implemented by this work. - -The plan retains eleven milestones under stable identity `arbiter-authentication`, -split into sixty planned PRs with separate production/test estimates and confidence. -Target about 600 changed handwritten code lines per PR; the hard maximum is 1,000, -including tests. Estimates remain uncertain and can require further splits. -DBB required receipt recovery before activation and complete live denial before -ordinary API cutover; the reviewed order now enforces both. Earlier artifact-prefix -and renewal obligations remain. These are plan corrections, not implemented fixes. - -Next, with separate authorization: child PR 1.1, private authority initialization, -within milestone 1 (operator authority setup and authenticated local control). -No implementation slice or PR has started. +The HLD covers B1–B5 and shared boundaries. Its fourteen retained Python/Go +feasibility checks were design inputs, not proof of the later ordinary transport. +Native implementation, race/crash and installed-prefix evidence belongs to each +owning implementation cut. The reviewed LLDs retain receipt-before-activation, +denial-before-public-cutover and safe artifact-prefix obligations. This revision +updates the design and status; it implements no product behavior. diff --git a/docs/authentication/implementation-plan.md b/docs/authentication/implementation-plan.md index 0f30709..329bebf 100644 --- a/docs/authentication/implementation-plan.md +++ b/docs/authentication/implementation-plan.md @@ -5,48 +5,87 @@ slice_convention: hierarchical-v1 # Authentication and mutual enrollment: implementation plan -Stage: implementation-plan candidate, 2026-10-20. Governing documents are the +Stage: design-update candidate after the milestone-2 stack merge, 2026-10-28. +Governing documents are the [requirements](requirements.md), SHA-256 `d56e92a7cb01f131ba499650c4a5d7b2c8d17adf2cdc7d612eee7c5f53d07929`, and [HLD](high-level-design.md), SHA-256 `b6511414b3a9b485542df83c8f7abfc499dc8d06e522819b48e162a562d21c57`. -Their exact reviewed stamps are required dependencies of this plan's stamp. -Neither governing document changes in this stage. This plan authorizes no +Their exact reviewed stamps, the [portable enrollment LLD](portable-enrollment-lld.md) +and the [milestone-3 LLD](milestone-3-lld.md) are explicit design dependencies of +this plan's stamp. The LLDs own their detailed schemas, state transitions, +acceptance cases and already sized cuts. Neither requirements nor HLD changes. +This plan authorizes no implementation, release, live-account operation or deployment by itself. DBB must approve the exact final plan before its SWE design stamp. +## Current delivery state and next design scope + +Source baseline is merged main `25a8cf2196f36499ff92ce890f313901b51b4f2b`. +Milestone 1 authority/setup/local control and the milestone-2 fresh enrollment, +single activation and receipt stack are merged. The latter closed with +[PR74](https://github.com/omry/arbiter/pull/74); its 21 delivered PRs consumed the +22-attempt allowance, including one unshipped parent attempt. Those delivery +identities, approvals and spent allowance remain history, not permission for +additional work. All 13 reported post-merge checks passed in the +[baseline workflow run](https://github.com/omry/arbiter/actions/runs/40579826766). + +Milestone 2 still lacks the approved existing-portable-identity journey. Current +Go provisional creation always generates a new portable key; Python prepare +always assigns a new agent. Fresh-key possession and same-selector receipt +resume do not satisfy reuse of a registered portable identity at a second server. +This is an implementation gap, not an accepted scope deferral. Proposed `2.10` +closes it through the portable LLD before milestone 3 starts. These documents +specify that fix; no product fix is implemented by this design update. + +The original milestone-3 seven-PR estimate lacks the streaming, authority +integration and failure-test detail needed for a credible execution cut. Its approved +behavior is retained, with the LLD's 13 already sized proposed cuts replacing the +old boundaries. Milestones 4–11 retain their ordered scope and historical child +outlines below; their old estimates are not newly validated execution forecasts. +Refine them into source-based, reviewed LLDs with bounded complete cuts before +implementing each milestone. No cumulative remaining-PR or LOC promise follows +from adding those old outline counts to the newly sized work. + +This design and its local/PR review confer no implementation authorization. +Implementing `2.10`, milestone 3 or a later milestone requires separate operator authorization. No old +allowance is reset; no merge, release, deployment or live account work is authorized. +The [original plan at the merged baseline](https://github.com/omry/arbiter/blob/25a8cf2196f36499ff92ce890f313901b51b4f2b/docs/authentication/implementation-plan.md) +preserves the exact earlier text, forecasts and original cuts. + ## Delivery identities and rules `arbiter-authentication` owns this plan. Its numbered slices are `1` through `11`; references below use `arbiter-authentication/1`, for example. Under `hierarchical-v1`, an approved later split of `4` becomes `4.1`, `4.2`, etc.; never renumber another slice or reuse a completed identity. The parent retains the -aggregate acceptance obligation. A future execution ledger records exact PR URL, -revision, dependencies, checks, reviews and state for each canonical identity; -there are no PRs, code changes or completed implementation slices yet. +aggregate acceptance obligation. Delivery records bind exact PR URL, revision, +dependencies, checks, reviews and state for each canonical identity. Merged +deliveries are not renumbered or reclassified as unstarted by this plan revision. -The eleven numbered slices are milestones, not individual PRs. Their child -identities below are the intended reviewable PRs, each with its own bounded -behavior, negative/failure tests, documentation and applicable fragments. Work +The eleven numbered slices are milestones, not individual PRs. The current LLD +cuts are proposed reviewable PRs with bounded behavior, failure tests and relevant +guidance/fragments. Older child tables are historical outlines as labeled. Work serially in milestone order and then the listed child order (not numeric sort). Complete local review, checks, submission and the approved remote review cycle for the current PR before advancing; approval binds the final PR head, with restacking invalidating affected evidence as appropriate. -This document does not launch those cycles. If a slice becomes too large to -review, split it under its identity before implementation rather than hiding -another feature. A foundation PR must implement a real consumed boundary +This document does not launch those cycles. An LLD must produce already sized +implementable slices; dynamic resizing should be rare. If source evidence shows +a cut is insufficient, revise and review the design before implementing it rather +than treating an execution split as routine capacity. A foundation PR must implement a real consumed boundary (setup/status, registered pending work or an executable protocol peer), not empty interfaces, permissive stubs or a throwaway intermediate protocol. A synthetic peer may exercise a not-yet-public transport; it cannot authorize product traffic. -Until child 2.6, the live server has no reachable enrollment activation/ready-commit -route: 2.7/2.8 may use synthetic already-activated fixture rows only in tests, never -a production trust initializer. Until child 3.5, ordinary public server dispatch -remains closed on the new native boundary. Child 3.6 installs and tests complete -live denial first through the prepared inner handlers and real synthetic sockets. +The historical 2.7/2.8 cuts used synthetic already-activated fixture rows only in +tests before 2.6 enabled real activation; this is not a production trust initializer. +Ordinary public dispatch on the new native boundary remains closed until `3.5.2`. +Both `3.6.1` and `3.6.2` install and test complete live denial first through +private real peers and synthetic accounts. The eventual cutovers cannot rely on development-only publication policy as their safety mechanism; the respective runtime routes must actually remain closed. -Every implementation and review-fix commit subject, and its PR title, starts with -the short slice label in brackets. For example: +Outside the typed portable-correction inventory below, every implementation and +review-fix commit subject, and its PR title, starts with the short slice label in brackets. For example: `[authentication/1.1] Private authority initialization`. The repository already identifies Arbiter; do not repeat its name in the title prefix. This display label maps to canonical ledger identity `arbiter-authentication/1.1`; the plan key and @@ -94,9 +133,11 @@ Missing authority state fails closed and directs the operator to trusted setup. Dependencies below are direct semantic prerequisites. Serial delivery additionally requires the preceding PR to finish its review cycle. Final integration covers -cross-milestone interactions, not just each milestone in isolation. The listed child order is authoritative: milestone 2 runs 2.1–2.5, 2.7, 2.8, -2.6, 2.9; milestone 3 runs 3.1–3.4, 3.6, 3.5, 3.7. These orders preserve child -identities while repairing the reviewed cutover order. The explicit +cross-milestone interactions, not just each milestone in isolation. The historical +milestone-2 parent order was 2.1–2.5, 2.7, 2.8, 2.6, 2.9. Proposed next work is +`2.10.1 -> 2.10.2 -> 2.10.3 -> 2.10.4`, then the exact displayed milestone-3 +LLD order: `3.1.1–3.1.4`, `3.2.1–3.2.2`, `3.3`, `3.4`, `3.6.1–3.6.2`, +`3.5.1–3.5.2`, `3.7`. Existing parents retain their aggregate obligations. The explicit child dependencies below are technical prerequisites; serial delivery also requires the preceding child PR to finish its review cycle. An external dependency on a milestone means all of that milestone's children, not just its first PR. @@ -117,12 +158,20 @@ on a milestone means all of that milestone's children, not just its first PR. ## PR sizing and estimation basis -The current breakdown contains **60 planned PRs** across eleven milestones. -This count follows the boundaries below; it is not a delivery quota or a measured -implementation size. The earlier 30–45 PR conversation estimate was preliminary. -All children are unstarted. Preserve parent identities and aggregate acceptance; -if a child later needs splitting, use `2.7.1`, `2.7.2`, etc., without renumbering -other work. The execution ledger must retain the parent closure obligation. +The original 2026-10-20 breakdown contained **60 planned PRs**; the earlier +30–45 conversation estimate was preliminary. Both are historical, not the +current remaining queue. Only `2.10` and milestone 3 have newly sized LLDs here: + +| Proposed work | Cuts | Production | Tests/fixtures | Handwritten CI | Total | +| --- | ---: | ---: | ---: | ---: | ---: | +| `2.10` portable reuse | 4 | 850–1,220 | 1,300–1,770 | 0–25 | 2,150–3,015 | +| `3` native API/grants/denial | 13 | 2,350–3,360 | 4,440–5,980 | 0–70 | 6,790–9,410 | + +These are engineering estimates for review and later authorization, not approved +delivery allowances or guaranteed future diffs. Each full cut includes code +motion, helpers, fixtures, negative/race/native composition tests and correction +headroom. Shared harness code is charged once. The LLDs own the per-cut tables; +the plan does not maintain a second detailed sizing copy. Target **about 600 changed handwritten code lines** per PR. The **hard maximum is 1,000**, counting additions plus deletions in production code, tests and handwritten @@ -135,15 +184,16 @@ still contribute review burden. Do not introduce new generation tooling merely t avoid the limit. A large mechanical change should be a separate coherent PR. The target is flexible; the maximum is not. Before coding each child, check its -forecast against the inspected implementation and predecessor changes. If it -cannot fit, split it before proceeding. If actual changes approach/exceed 1,000, -stop expansion and split before submission. A changed split needs focused review +source-based forecast against predecessor changes. Do not begin an overlarge or +underspecified cut expecting to split it later. If actual evidence invalidates +the cut, stop expansion and return to the design boundary before submission. +A rare changed split needs focused review and updated dependency/acceptance mapping; changes to the stamped plan require DBB/SWE re-review and replacement stamp before treating that revision as approved. No additional owner interview is required for a mechanical split that preserves approved requirements, HLD, size policy and acceptance. -Ranges below estimate handwritten churn, including assertion/helpers and fixture +Historical ranges below estimate handwritten churn, including assertions/helpers and fixture construction. Production and test ranges are separate; the total is their sum. They are task-by-task engineering estimates, not statistical confidence intervals or a guarantee that the implementation will stay within the forecast. `Medium` @@ -154,7 +204,8 @@ to create one file for each PR. Existing code/tests are reused rather than copie only new/changed lines count. Shared harness work is charged to the first child that uses it, and later children budget their own scenario additions. -Inspection basis (2026-10-20, checkout `9d5cfc198bb4`): +Historical inspection basis for the original forecast (2026-10-20, +checkout `9d5cfc198bb4`): | Existing surface | Measured lines | Consequence for estimates | | --- | ---: | --- | @@ -167,11 +218,13 @@ Inspection basis (2026-10-20, checkout `9d5cfc198bb4`): | `server/tests/unit/test_native_http_protocol.py` / `server/tests/integration/test_cli_entrypoint.py` | 1,394 / 1,245 | Reuse synthetic ASGI and installed Go-wheel fixtures; charge new authenticated/process scenarios separately. | | `server/src/arbiter_server/storage.py` / `server/src/arbiter_server/config.py` | 57 / 376 | Private generation/authority/ACL recovery is mostly new work, not an existing backup subsystem. | -Read-through of these paths, native route/admission construction in `main.py` and -the HLD’s retained Python/Go probe results informs the estimates. Probe code is not assumed -to be production-ready or copied wholesale. Native Windows custody/durability, -full TLS adapter shutdown and dependency matrix work remain particularly uncertain. -No product tests or new feasibility probes were run for this sizing revision. +Read-through of these paths and the retained HLD probes informed the original +estimates. The two new LLDs use actual merged `25a8cf2` primitives and their named +missing work; old source-line counts above are not measurements of that baseline. +Probe code is not assumed production-ready or copied wholesale. No product code +or product tests were created/run by this design revision. Future streaming, +race/crash and installed-native acceptance remains implementation work, not proof +supplied by design checks or the existing baseline's green CI. The per-child tables define scope, a focused minimum acceptance check, code ranges, confidence and direct dependencies. Every child also owns relevant tests from the @@ -184,7 +237,12 @@ its applicable package fragment and command/operator guidance; final documentati passes only reconcile the complete journey. Documentation effort is not included in the handwritten-code totals, and must be called out before each PR starts. -### Estimate summary +### Historical estimate summary (2026-10-20) + +The following table preserves the original 60-PR forecast for comparison. It is +not a statement that those cuts remain unstarted, an actual delivered-LOC count, +or a newly reviewed forecast for the remaining milestones. In particular, the +old rows for 2 and 3 are superseded for future work by the status and LLDs above. | Milestone | Planned PRs | Production churn | Test churn | Combined churn | | --- | ---: | ---: | ---: | ---: | @@ -299,6 +357,9 @@ reviewer substitution. This plan does not authorize automatic merging/deployment ## Slice 1 — operator authority and local control +Historical note: the original child table in this section records the design cuts, not an unstarted queue. Authority/setup/local-control implementation is already merged at the source baseline above. + + Deliver a complete operator setup/status journey: create durable owner/server IDs, issuer and role-separated keys, external operator recovery file/public trust and private SQLite authority state; inspect/manage it through authenticated Unix @@ -334,6 +395,9 @@ operator command help and an `arbiter-server` news fragment. ## Slice 2 — pairing a real client and reconciling activation +Historical note: the original child table below retains parent identities and initial forecasts. The delivered stack used reviewed descendant cuts and is merged; existing-portable reuse remains the explicit `2.10` gap below. + + Deliver `enrol` from a real Go CLI against a real initialized Python server: generate provisional per-server/portable/recipient keys and 32-byte secret; closed server explains opening; trusted operator opens a bounded window, lists/selects @@ -372,17 +436,324 @@ custody guidance and fragments for server and native client packaging as applica | `2.6` — Single activation and receipt | With 2.7/2.8 present, wire the first reachable ready/activation commit: conditionally persist identity, credential, grants and result; promote only from authenticated receipt/retrieval. **Check:** Real Python/Go crash/lost-reply/restart at ready/commit/receipt proves one durable result, authenticated promotion and denial precedence; retries create no second identity or grant. | 280–440 | 320–520 | 600–960 | Low | 2.8 | | `2.9` — Pairing installation and overload closure | Complete prefix packaging/platform checks and process-level pairing failures, reusing earlier fixtures rather than deferring core assertions here. **Check:** Installed CLI pairs/reconciles; dependency builds, proxy fragmentation and simultaneous expiry/shutdown release real owners and keep health responsive. | 80–180 | 300–550 | 380–730 | Low | 2.6 | +### Proposed corrective parent `2.10` — existing portable identity enrollment + +The [portable enrollment LLD](portable-enrollment-lld.md) owns the exact source +selection, independently registered issuer, v2 schema, custody/proof, unique +agent binding, migration and receipt contract. Its P01–P10 acceptance and four +complete implementation cuts are normative for this proposed correction: +`2.10.1 -> 2.10.2 -> 2.10.3 -> 2.10.4`, then `3.1.1`. +The first schema/backfill cut also installs binding maintenance in every fresh-v1 +activation transaction; intermediate enrollments cannot evade later uniqueness or +denial. P05/P09 cover active/denied/inactive records created on both intermediate +prefixes. Each cut has its own real consumer and failure tests; `2.10.4` adds +installed fresh/reuse closure to the existing driver. The total is 2,150–3,015 handwritten +production/test/CI lines with all per-cut upper bounds below 1,000. This is no +reset of the exhausted milestone-2 allowance and no implementation authorization. +Existing-enrollment repair stays in milestone 5 and does not substitute for P01. + +## Execution inventory for the portable correction + +This inventory records only the already reviewed `2.10.1–2.10.4` cuts. +The retained `2` and `2.10` entries are aggregate planning parents, not +unstarted replacements for merged work. Original `2.1–2.9` delivery IDs, +findings, approvals and spent counters remain in their immutable history; +they are not re-admitted by this inventory. The two `replaced` states below +mean that executable responsibility is assigned to the four children, not that +completed deliveries were removed. Milestones 3–11 are not selected or admitted. + +The full canonical ID in each inventory title is the exact prefix required by +the current typed-delivery tools for these new cuts. Historical short display +labels stay unchanged. Paths bind the reviewed implementation ownership; a +necessary change outside them returns to reviewed inventory preparation. The +portable LLD remains the authority for behavior, acceptance and estimates. + +```swe-slice-inventory +{ + "schema_version": 1, + "slices": [ + { + "id": "arbiter-authentication/2", + "parent": null, + "state": "replaced", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2] Pairing and durable enrollment", + "responsibility": "Retain the original milestone-2 aggregate acceptance and immutable merged 2.1-2.9 delivery history; only its unimplemented 2.10 correction is admitted here.", + "paths": [ + ".github/workflows/ci.yml", + "client/go-cli/internal/cli/enrollment.go", + "client/go-cli/internal/cli/enrollment_test.go", + "client/go-cli/internal/identity/enrollment.go", + "client/go-cli/internal/identity/enrollment_receipt.go", + "client/go-cli/internal/identity/enrollment_receipt_test.go", + "client/go-cli/internal/identity/enrollment_test.go", + "client/go-cli/internal/identity/pairing_peer_test.go", + "client/go-cli/internal/identity/portable.go", + "client/go-cli/internal/identity/portable_test.go", + "client/go-cli/internal/identity/prepared.go", + "client/go-cli/internal/identity/prepared_test.go", + "client/go-cli/internal/identity/provisional.go", + "client/go-cli/internal/identity/provisional_store.go", + "client/go-cli/internal/identity/provisional_test.go", + "client/go-cli/internal/identity/registration_peer_test.go", + "docs/enrollment.md", + "server/news/+portable-enrollment-activation.feature.md", + "server/news/+portable-enrollment.feature.md", + "server/news/+portable-identity-codecs.feature.md", + "server/news/+portable-issuer-controls.feature.md", + "server/src/arbiter_server/authority.py", + "server/src/arbiter_server/enrollment.py", + "server/src/arbiter_server/enrollment_activation.py", + "server/src/arbiter_server/generations.py", + "server/src/arbiter_server/issuer_admin.py", + "server/src/arbiter_server/local_control.py", + "server/src/arbiter_server/main.py", + "server/src/arbiter_server/owner_issuers.py", + "server/src/arbiter_server/prepared.py", + "server/tests/integration/enrollment_terminal_driver.py", + "server/tests/integration/test_enrollment_cli_peer.py", + "server/tests/integration/test_enrollment_receipt_peer.py", + "server/tests/integration/test_installed_enrollment.py", + "server/tests/integration/test_provisional_peer.py", + "server/tests/unit/test_authority.py", + "server/tests/unit/test_enrollment_activation.py", + "server/tests/unit/test_enrollment_requests.py", + "server/tests/unit/test_local_control.py", + "server/tests/unit/test_main.py", + "server/tests/unit/test_owner_issuers.py", + "server/tests/unit/test_prepared.py", + "server/tests/unit/test_windows_control.py" + ], + "dependencies": [], + "acceptance": [ + "Preserve merged fresh pairing and receipt behavior; ordinary milestone-3 API remains unchanged." + ] + } + }, + { + "id": "arbiter-authentication/2.10", + "parent": "arbiter-authentication/2", + "state": "replaced", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2.10] Existing portable identity enrollment", + "responsibility": "Close only portable LLD P01-P10 through the four already reviewed cuts; preserve original 22 spent attempts and all historical delivery identities.", + "paths": [ + ".github/workflows/ci.yml", + "client/go-cli/internal/cli/enrollment.go", + "client/go-cli/internal/cli/enrollment_test.go", + "client/go-cli/internal/identity/enrollment.go", + "client/go-cli/internal/identity/enrollment_receipt.go", + "client/go-cli/internal/identity/enrollment_receipt_test.go", + "client/go-cli/internal/identity/enrollment_test.go", + "client/go-cli/internal/identity/pairing_peer_test.go", + "client/go-cli/internal/identity/portable.go", + "client/go-cli/internal/identity/portable_test.go", + "client/go-cli/internal/identity/prepared.go", + "client/go-cli/internal/identity/prepared_test.go", + "client/go-cli/internal/identity/provisional.go", + "client/go-cli/internal/identity/provisional_store.go", + "client/go-cli/internal/identity/provisional_test.go", + "client/go-cli/internal/identity/registration_peer_test.go", + "docs/enrollment.md", + "server/news/+portable-enrollment-activation.feature.md", + "server/news/+portable-enrollment.feature.md", + "server/news/+portable-identity-codecs.feature.md", + "server/news/+portable-issuer-controls.feature.md", + "server/src/arbiter_server/authority.py", + "server/src/arbiter_server/enrollment.py", + "server/src/arbiter_server/enrollment_activation.py", + "server/src/arbiter_server/generations.py", + "server/src/arbiter_server/issuer_admin.py", + "server/src/arbiter_server/local_control.py", + "server/src/arbiter_server/main.py", + "server/src/arbiter_server/owner_issuers.py", + "server/src/arbiter_server/prepared.py", + "server/tests/integration/enrollment_terminal_driver.py", + "server/tests/integration/test_enrollment_cli_peer.py", + "server/tests/integration/test_enrollment_receipt_peer.py", + "server/tests/integration/test_installed_enrollment.py", + "server/tests/integration/test_provisional_peer.py", + "server/tests/unit/test_authority.py", + "server/tests/unit/test_enrollment_activation.py", + "server/tests/unit/test_enrollment_requests.py", + "server/tests/unit/test_local_control.py", + "server/tests/unit/test_main.py", + "server/tests/unit/test_owner_issuers.py", + "server/tests/unit/test_prepared.py", + "server/tests/unit/test_windows_control.py" + ], + "dependencies": [], + "acceptance": [ + "Real A/B/Go journey preserves agent ID/portable key/certificate and fresh independent B access/grants." + ] + } + }, + { + "id": "arbiter-authentication/2.10.1", + "parent": "arbiter-authentication/2.10", + "state": "planned", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2.10.1] Local issuer trust and unique agent bindings", + "responsibility": "Implement the portable LLD first cut: OS-local issuer controls, exact certificate fingerprint trust, atomic schema/backfill and binding maintenance in every fresh-v1 activation/replay. Requires the merged 25a8cf2 fresh-enrollment baseline. No public reuse or milestone-3 work.", + "paths": [ + "server/src/arbiter_server/authority.py", + "server/src/arbiter_server/generations.py", + "server/src/arbiter_server/owner_issuers.py", + "server/src/arbiter_server/issuer_admin.py", + "server/src/arbiter_server/local_control.py", + "server/src/arbiter_server/main.py", + "server/src/arbiter_server/enrollment_activation.py", + "server/tests/unit/test_owner_issuers.py", + "server/tests/unit/test_authority.py", + "server/tests/unit/test_local_control.py", + "server/tests/unit/test_windows_control.py", + "server/tests/unit/test_main.py", + "server/tests/unit/test_enrollment_activation.py", + "server/news/+portable-issuer-controls.feature.md", + "docs/enrollment.md" + ], + "dependencies": [], + "acceptance": [ + "P02/P06/P09: exact issuer registration, removal/reactivation and revision; invalid profile rejected and local control remains OS authenticated.", + "Atomic migration preserves real old results/denial and rejects duplicate or malformed bindings.", + "Real post-upgrade fresh-v1 activation/replay/rollback maintains one agent/enrollment mapping, including later denied or inactive enrollments.", + "Retain 610-865 changed handwritten code/test/CI estimate and <=1000 hard cap." + ] + } + }, + { + "id": "arbiter-authentication/2.10.2", + "parent": "arbiter-authentication/2.10", + "state": "planned", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2.10.2] Portable source custody and versioned codecs", + "responsibility": "Implement the portable LLD second cut: strict enrolled-source loading and v2 private/public codecs consumed by executable Python/Go peers; reuse the native publisher and shared decoder with purpose-specific checks. Public reuse remains unreachable.", + "paths": [ + "client/go-cli/internal/identity/provisional.go", + "client/go-cli/internal/identity/provisional_store.go", + "client/go-cli/internal/identity/prepared.go", + "client/go-cli/internal/identity/portable.go", + "client/go-cli/internal/identity/provisional_test.go", + "client/go-cli/internal/identity/prepared_test.go", + "client/go-cli/internal/identity/portable_test.go", + "client/go-cli/internal/identity/enrollment_receipt.go", + "client/go-cli/internal/identity/enrollment_receipt_test.go", + "server/src/arbiter_server/enrollment.py", + "server/tests/unit/test_enrollment_requests.py", + "server/tests/integration/test_provisional_peer.py", + "server/news/+portable-identity-codecs.feature.md", + "docs/enrollment.md", + "client/go-cli/internal/identity/pairing_peer_test.go", + "client/go-cli/internal/identity/registration_peer_test.go" + ], + "dependencies": [ + "arbiter-authentication/2.10.1" + ], + "acceptance": [ + "P03/P08: reject altered source ID/key/certificate/hash, unsafe custody and prepared-only source; source bytes stay unchanged.", + "Source loading accepts a currently valid portable identity without A online or an A access-lifetime requirement.", + "Preserve old v1 stored-record/result receipt decoding and its historical portable validation; never relax ordinary TLS or new-enrollment validity.", + "Retain 490-695 changed handwritten code/test/CI estimate and <=1000 hard cap." + ] + } + }, + { + "id": "arbiter-authentication/2.10.3", + "parent": "arbiter-authentication/2.10", + "state": "planned", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2.10.3] Prepared reuse and single activation", + "responsibility": "Implement the portable LLD third cut through existing preparation/proofs and atomic activation, extending the same binding writer. Exercise real A/B/Go peers with fresh B access, explicit issuer trust/approval and exact receipt recovery. Public v2 remains closed.", + "paths": [ + "server/src/arbiter_server/prepared.py", + "server/src/arbiter_server/enrollment_activation.py", + "server/src/arbiter_server/enrollment.py", + "server/src/arbiter_server/owner_issuers.py", + "server/tests/unit/test_prepared.py", + "server/tests/unit/test_enrollment_activation.py", + "server/tests/integration/test_enrollment_cli_peer.py", + "server/tests/integration/test_enrollment_receipt_peer.py", + "client/go-cli/internal/identity/prepared.go", + "client/go-cli/internal/identity/prepared_test.go", + "client/go-cli/internal/identity/enrollment.go", + "client/go-cli/internal/identity/enrollment_test.go", + "client/go-cli/internal/identity/enrollment_receipt.go", + "client/go-cli/internal/identity/enrollment_receipt_test.go", + "server/news/+portable-enrollment-activation.feature.md", + "docs/enrollment.md", + "client/go-cli/internal/identity/pairing_peer_test.go", + "client/go-cli/internal/identity/registration_peer_test.go" + ], + "dependencies": [ + "arbiter-authentication/2.10.2" + ], + "acceptance": [ + "P01/P03/P05/P06/P07: preserve original agent/portable identity, generate independent B access/enrollment/recipient state; A may be offline.", + "Existing active/denied/inactive B binding rejects reuse, including real fresh-v1 enrollments created on both earlier upgraded prefixes.", + "Trust-revision removal/ready/commit races fail or reconcile once; lost final reply/restart/expiry returns the exact committed result.", + "Retain 550-745 changed handwritten code/test/CI estimate and <=1000 hard cap." + ] + } + }, + { + "id": "arbiter-authentication/2.10.4", + "parent": "arbiter-authentication/2.10", + "state": "planned", + "role": "implementation", + "intent": { + "title": "[arbiter-authentication/2.10.4] Public CLI reuse and installed acceptance", + "responsibility": "Implement the portable LLD fourth cut: explicit identity source selection, atomic v2-only public registration switch, installed A/B and fresh regression journeys, final migration/help/news and complete P01-P10 acceptance. Preserve all platforms and existing test harnesses.", + "paths": [ + "client/go-cli/internal/cli/enrollment.go", + "client/go-cli/internal/cli/enrollment_test.go", + "client/go-cli/internal/identity/enrollment.go", + "client/go-cli/internal/identity/enrollment_test.go", + "server/src/arbiter_server/main.py", + "server/src/arbiter_server/enrollment.py", + "server/tests/unit/test_enrollment_requests.py", + "server/tests/unit/test_main.py", + "server/tests/integration/enrollment_terminal_driver.py", + "server/tests/integration/test_installed_enrollment.py", + ".github/workflows/ci.yml", + "server/news/+portable-enrollment.feature.md", + "docs/enrollment.md" + ], + "dependencies": [ + "arbiter-authentication/2.10.3" + ], + "acceptance": [ + "P04/P08/P10 and all P01-P10: installed CLI completes fresh/reused enrollment and recovery on native CI; existing admission/time/attempt limits remain.", + "Reject identity plus resume, same-server reuse, unknown/invalid source and old public wire with actionable diagnostics and no saved identity deletion.", + "Confirm public server/client switch together, v1 saved receipt recovery survives, no live accounts and no milestone-3 API changes.", + "Retain 500-710 changed handwritten code/test/CI estimate and <=1000 hard cap." + ] + } + } + ] +} +``` + ## Slice 3 — native API, account grants and live denial Route every ordinary CLI discovery/schema/invocation/download constructor through the expected-server-verified inner TLS 1.3 transport, including errors and tool paths. Preserve API shape and existing policies after `(plugin, account)` grants. +Exercise the native factory through private peers in `3.5.1`; keep production +constructor selection on its working route until the atomic client/server switch +in `3.5.2`, with no anonymous fallback after that cut. Reject anonymous/proxy-header callers and hide ungranted account information from discovery and direct routes. Provide local grant add/remove and enrollment denial. Creator/account caller context is mandatory for subsequent artifact work. Implement shared coordinator admission and output cancellation across connections; ordinary TLS checks exact live enrollment/accepted key, certificate and role. +Use `agent_enrollments` as the sole agent-to-enrollment relation; live admission +must not create an independently authoritative `enrollments.agent_id` copy. Remove anonymous legacy API/artifact routing in the same cutover, invalidate server-managed ownerless artifacts without guessing ownership or touching mail/ client files. No artifact storage fallback may bypass caller context. Keep @@ -415,19 +786,41 @@ and cancellation/repeated failure release bounded staging and worker ownership. Use a legacy-store spy plus filesystem inspection, including restart cleanup; the final no-publication gate alone is not this acceptance evidence. -### Child PRs -| PR | Scope and focused acceptance | Production | Tests | Total | Confidence | Direct prerequisites | -| --- | --- | ---: | ---: | ---: | --- | --- | -| `3.1` — Ordinary inner mTLS caller context | Add strict access-leaf verification and single-request HTTP/1.1-to-ASGI adapter with immutable verified identity; ordinary API remains closed until 3.5. **Check:** Portable/cross-server/expired leaves fail; no proxy header or outer identity creates a caller; early body input stays admission-bounded. | 260–420 | 280–470 | 540–890 | Low | 2.9 | -| `3.2` — Live grants and admission revision | Implement local grant changes and a coordinator that admits work against current enrollment/account revision, retaining actual worker ownership. **Check:** Grant/revoke and dispatch barriers serialize; caches cannot authorize stale revisions; existing account policy remains the only operation policy. | 220–360 | 260–460 | 480–820 | Low | 3.1 | -| `3.3` — Authenticated discovery filtering | Apply verified grants to catalog, schema, policy/account summaries and direct lookup in the inner API. **Check:** Hidden accounts/policies cannot be learned by guessed paths, errors, previews or shared-account clients; existing payload contracts remain. | 160–300 | 250–430 | 410–730 | Medium | 3.2 | -| `3.4` — Authorized dispatch and closed artifact adapter | Bind account policy and immutable creator context to operation dispatch; replace plugin artifact creation/inspection with a closed adapter before native API cutover. **Check:** Real dispatch to an artifact-producing synthetic plugin rejects before legacy store entry, file/nonce/descriptor publication; upstream effects are reported honestly. | 220–370 | 280–500 | 500–870 | Low | 3.3 | -| `3.6` — Cross-connection live denial | Cancel affected admitted work/output on all connections at grant removal/enrollment denial, preserving pre-body admission and blocking-worker lifetime. Install and test this through prepared inner handlers and real synthetic sockets while the public ordinary boundary stays closed until 3.5. **Check:** Real sockets and slow/chunked bodies prove no post-denial dispatch/output; worker slots stay held until underlying calls exit; no uncertain replay. | 220–380 | 300–520 | 520–900 | Low | 3.4 | -| `3.5` — CLI transport cutover and anonymous closure | Route every Go discovery/invocation/artifact constructor to verified inner transport; atomically close anonymous legacy API/artifact routes on the server. **Check:** First reachable installed Python/Go ordinary prefix tests denial against queued admission, bodies, upstream dispatch and ongoing output with real worker retention; legacy routes stay closed and artifact creation leaves no plaintext/nonce. | 240–400 | 280–500 | 520–900 | Low | 3.6 | -| `3.7` — Native transport regression closure | Complete changed-cert/inspecting-relay/TCP-passthrough, installed-client and timeout/error compatibility checks across native routes. **Check:** Proxy sees no native secret; connect versus API/idle budgets, long active calls and eight-worker/1 MiB limits remain; no new bypass is needed. | 80–180 | 300–550 | 380–730 | Low | 3.5 | +### Current proposed cuts and dependencies + +The [milestone-3 LLD](milestone-3-lld.md) owns exact N01–N12 acceptance, complete +per-cut scope and production/test/CI ranges. Its 13 cuts replace the historical +seven rows; do not add the two forecasts together. All work remains unimplemented. + +| Cut | Direct implementation prerequisite | Owning result / acceptance | +| --- | --- | --- | +| `3.1.1` | `2.10.4` | Shared carrier/pump with real existing-control and streaming consumers; N03/N04. | +| `3.1.2` | `3.1.1` | Strict Python ordinary mTLS peer; N01. | +| `3.1.3` | `3.1.2` | Go native identity/dial and standard HTTP transport peer; N01/N02/N11. | +| `3.1.4` | `3.1.3` | Bounded single-request HTTP/ASGI, absolute request-receipt timer and pre-body admission; N03/N04. | +| `3.2.1` | `3.1.4` | Existing enrollment-row live fields/grants and migration; first OS enrollment-deny control and minimal shared coordinator guard the real fixed receipt through final output/actual-owner cleanup; N05/N07 receipt races. | +| `3.2.2` | `3.2.1` | Extend the same coordinator, writer/read epochs and actual worker ownership to ordinary authority; OS grant add/remove controls; N04/N05/N06/N07. | +| `3.3` | `3.2.2` | Discovery/direct/policy/error filtering; N06. | +| `3.4` | `3.3` | Real dispatch caller context, closed artifact dependency/inline/legacy cleanup; N09. | +| `3.6.1` | `3.4` | Cross-connection body/dispatch denial and retained actual workers; N07/N08. | +| `3.6.2` | `3.6.1` | Final output permission after backpressure and abort without revoked flush; N03/N07/N08. | +| `3.5.1` | `3.6.2` | Every CLI constructor and explicit enrollment selection through private real peers; production selection retains its working route. N10/N11. | +| `3.5.2` | `3.5.1` | Atomic production CLI/native server switch, anonymous closure and first installed denial/artifact prefix; N01/N04/N07/N09/N10/N12. | +| `3.7` | `3.5.2` | Installed relay/timeouts/native composition and full N01–N12 closure. | + +All `3.1` descendants close parent 3.1, both `3.2` descendants close 3.2, both +`3.6` descendants close 3.6 before cutover, and both `3.5` descendants close 3.5. +Later references to a parent still require all its descendants and aggregate +acceptance. The first public ordinary prefix includes denial and safe artifact +failure; final 3.7 is not a budget for missing earlier mechanisms or tests. ## Slice 4 — rotation, late completion and bounded fallback +Milestones 4–11 below retain their approved scope/order and historical outlines. +Their child boundaries and estimates require source-based LLD refinement/review +before implementation. This design revision does not claim those future cuts +are already sized or predictive after milestone-2 implementation changes. + Deliver on-contact due rotation, fixed grace, durable candidate save/confirmation and lost-response reconciliation. A pending deadline is recorded before asking for candidate; withholding/retry/old ordinary use cannot move it. Current, @@ -700,6 +1093,8 @@ have approved final heads and required checks, run the complete synthetic journe 1. Fresh private setup with no public CA/DNS or external identity account; enroll first and additional clients with one handoff/approval, restart and reconcile. + Reuse one portable identity at a separately registered second server with + distinct access/grants and no source-server availability requirement (P01–P10). 2. Authenticated discovery/direct invocation of mock SMTP/IMAP accounts with distinct grants, existing policies, grant change/denial and concurrent output. 3. Due rotation, fixed grace, expired-certificate control completion, inactivity, @@ -726,7 +1121,7 @@ live accounts to prove safety or mark unavailable checks as passed. Remaining failures require fixes/re-review or an explicit requirements/HLD decision; no scope reduction, platform drop or readiness claim is hidden in release wording. -Plan approval means this is an acceptable order and verification obligation, -not that any slice is implemented or has a PR. Execution begins only on separate -authorization. Reviews and exact-byte stamp evidence are stored outside this +Approval of this revision attests its design, ordering and verification +obligations. Implemented/merged state is reported explicitly above; the newly +proposed work begins only on separate implementation authorization. Reviews and exact-byte stamp evidence are stored outside this document so recording its verdict does not invalidate the reviewed plan. diff --git a/docs/authentication/milestone-3-lld.md b/docs/authentication/milestone-3-lld.md new file mode 100644 index 0000000..e4732b8 --- /dev/null +++ b/docs/authentication/milestone-3-lld.md @@ -0,0 +1,436 @@ +# Native API, grants and live denial: low-level design + +Status: design candidate; no product implementation or delivery authorization. +This refines [requirements](requirements.md) R10–R14 and [HLD](high-level-design.md) +B2/B3. It consumes the reviewed [portable enrollment LLD](portable-enrollment-lld.md). +Those three documents are its explicit design dependencies. Requirements/HLD bytes +remain unchanged. The original milestone-3 seven-PR forecast is historical; the +already sized proposal below replaces its cuts, not its approved product scope. + +Source basis: merged main `25a8cf2196f36499ff92ce890f313901b51b4f2b` plus the +unimplemented portable-enrollment contract. Current `ControlTLS` is a fixed +64-KiB exchange, not an ordinary stream. `PairingProtocol` owns carrier bounds; +`BlockingOperationPool` already retains actual threads after waiter cancellation; +`HTTPWork` supplies a worker/loop bridge. `main.py` still exposes anonymous API +and legacy artifact routes. Go `configuredHTTPClient` still constructs ordinary +HTTP clients. This document specifies changes to those paths, not a second API, +transport daemon, policy engine or authorization cache. + +## One ordinary channel and one selected enrollment + +The outer carrier is a binary WebSocket at `/api/v1/native/api`. Ordinary inner +TLS is exactly TLS 1.3 with ALPN `arbiter-native-v1`; the existing fixed control +endpoint keeps `arbiter-control-v1` and never reaches ASGI. Pairing remains Noise. +Disable compression, TLS tickets/resumption, early data, HTTP/2, pipelining and +redirects. Use one HTTP/1.1 request per inner connection, then close. This is a +simple subset of the HLD's one-in-flight rule and needs no connection pool or +operation replay protocol. + +Add the public selector setting `arbiter.enrollment` to existing client YAML and +the `enrollment=SELECTOR` override. It uses the existing canonical 22-character +selector, not a filename, key, URL or display name. Explicit override wins over +the config value. Missing selection gives enrollment/configuration guidance before +dial; never scan for a first identity or derive authority from URL. Enrollment +prints its final selector and this configuration guidance after durable promotion. +Changing `url=...` changes location only. Loading the selected record fixes the +expected server ID, remembered server authority and exact access key for this +logical request. Another server's record cannot authenticate a redirected URL. + +Reuse the strict stored-record decoder from portable enrollment with a distinct +`LoadNativeIdentity` validator. Accept only final `enrolled/` native-owned state, +validate current server authority, exact access profile/IDs/key correspondence and +complete receipt/promotion. The portable leaf is historical enrollment evidence; +its later expiry is not ordinary access authority. Never accept portable proof, +prepared-only state or a lifecycle time exception as ordinary authentication. +Owner recovery and current/predecessor rotation remain milestones 4/5, not an +authentication retry added here. + +Go's existing WebSocket `net.Conn` adapter carries `crypto/tls`. Verify the exact +remembered server authority, server-role SAN/ID, validity, key usage, EKU and +critical extensions before returning the stream. `GetClientCertificate` requires +the per-connection expected-server-verified marker before it supplies access +credentials. Do not put enrollment IDs, account names, application headers, +cookies, proxy authorization or nonces in the outer request. The dedicated native +outer dial may tolerate unverified carrier HTTPS; general/browser constructors +do not inherit that exception. Reject outer redirects and inherited proxy auth. + +Return the verified inner connection from standard `http.Transport.DialTLSContext`. +Set `DisableKeepAlives`, disable HTTP/2 and proxy use, and reject all redirects in +the owning `http.Client`. Retain existing `TimeoutTransport` around that standard +transport; do not build a custom HTTP RoundTripper or duplicate phase timers. +The original connect budget covers outer TCP/HTTPS, upgrade and inner handshake. +`GotConn` means inner verification completed. API response time, plugin overrides +and download header/idle-progress time keep their current semantics. No phase, +reconnect, ping or wrapper restarts a logical deadline. No automatic retry after +request submission, uncertain result or consuming artifact GET. + +## Shared bounded carrier and TLS stream + +Extract only the common private carrier ownership/pump from `pairing_carrier.py` +and `control_tls.py`. Existing pairing/control behavior remains consumed by real +regression peers after extraction; fixed control framing is not generalized into +HTTP. Ordinary TLS adds a stream consumer on that same owner. Serialize every +memory-BIO operation in the existing bounded crypto pool, including handshake, +read, write, outgoing BIO drain and teardown. Handle partial writes, `WantRead` +and `WantWrite`; do not assume one send consumes one buffer. No OpenSSL private +API, TLS-record parser or new cryptographic dependency is needed. + +Python ordinary TLS requires a client certificate. Verify its exact server-issued +access profile and remembered server IDs in the crypto worker. The resulting +immutable candidate identity is not yet an authorized caller: live enrollment, +accepted key, denial, inactivity and grants are checked through the coordinator +before ASGI admission. Portable and cross-server certificates fail even with a +generic valid clientAuth chain. No caller is reconstructed from HTTP headers, +outer TLS or WebSocket metadata. Expose only the server's selected ordinary leaf; +keep owner issuer and lifecycle-control trust selection separate. + +The existing `arbiter.limits.native_transport` bounds remain: + +| Owner/resource | Limit and release condition | +| --- | --- | +| Native carrier | 16 shared by pairing, fixed control and ordinary; reserve before upgrade, no wait queue. Release after stream cleanup and every actual crypto/plugin owner ends. | +| Handshake/crypto | Four concurrent bounded crypto workers; ordinary handshake uses the same gate, 10-second handshake deadline, 256-KiB cumulative ciphertext each direction, 32-KiB chain cap. | +| Pending native data | 256 KiB in each direction, including owned adapter, WebSocket, inner TLS and transport-pending copies. Reserve before a copy or feed; release only when its owner drains/discards. | +| WebSocket | Binary only, no compression, at most 64 KiB per message including fragmentation; existing byte/time control-frame limiter. | +| HTTP | 32-KiB header section, one request, 1-MiB configured operation-body ceiling; reserve actual operation work before pulling body data. | +| Request receipt | Server-owned absolute 30-second default from inner TLS completion through complete headers/body; configurable positive `arbiter.limits.native_transport.request_receive_timeout`. Fragments/pings never restart it. | +| Operation work | Existing configured eight actual blocking workers. A canceled waiter does not release a running worker or its carrier. | +| Client transfer | Existing four-per-process admission before GET/temp reservation; closed artifact paths do not create another transfer pool. | + +Handshake totals and ordinary pending-data credits are different counters. Do not +retain the fixed control's lifetime `_control_tls_fed` counter for a long HTTP +stream or reset a cumulative counter and claim that buffers are bounded. Use one +moving credit ledger per direction, on the existing carrier owner. It accounts +for simultaneous parser fragments, queued WS chunks, BIO input/output, decrypted +pending data, header assembly and transient copies. One held WS message and one +active BIO call per carrier suffice; no background read-ahead queue is needed. + +Feed/return TLS application chunks of at most 16 KiB. Drain outgoing BIO data +before offering more input or another application write. Because `pending()` +reports decrypted bytes rather than encrypted BIO occupancy, retain credits for +all fed ciphertext until the worker reaches `WantRead` with no pending plaintext; +then, only after the initial handshake completes, retain conservative space for +one incomplete TLS 1.3 record and its copy +(36 KiB total), in addition to still-owned adapter data. Disable post-handshake +client authentication and renegotiation; control/pairing keep their existing +cumulative limits. Never discount that +reserve while a partial record remains. If credits cannot be proved available, +pause input and drain; if the peer overruns the finite pending budget, abort +before another copy. An actual worker retains its input/output credit until its +completion callback, even after cancellation. + +Use public `asyncio.Protocol`, `pause_reading`, `pause_writing` and transport +write-buffer APIs. Do not require `BufferedProtocol` at the Python 3.10 floor: +its outer SSL implementation still calls application `data_received`. Handle a +borrowed callback in bounded slices without retaining the full callback or feeding +wsproto past available credit. Reject over-budget delivery before copying it; +OS/kernel buffers and allocator capacity are not a claimed total-process heap +limit. The implementation must test actual pending-copy accounting under plain +and outer-TLS callbacks, fragmentation, coalescing and both slow directions. + +The five-minute authenticated idle timer runs only with no admitted application +work or transfer progress. A long operation remains subject to its existing +operation timeout, not this idle timer. Control pings extend no deadline. Every +timeout/abort wakes blocked readers/writers; nothing retries an operation. +The independent request-receipt timer still runs while a worker waits for a body; +admission does not exempt that wait. Stop it only on complete bounded body receipt +or request termination. One absolute timer, rather than a second sliding idle +mechanism, bounds silent and drip-fed bodies. A smaller client budget may fail +first; raising a client budget does not extend the server's configured deadline. +After body completion this timer does not cut off an admitted long plugin call. + +## HTTP/1.1 to the existing ASGI application + +Reuse the installed HTTP parser (`h11` through the existing Uvicorn dependency) +for framing, not a new HTTP grammar. The private stream adapter builds one ASGI +scope with immutable `arbiter.caller`, verified server/agent/enrollment/key IDs and +the coordinator token. Only the server transport can construct that value. Reject +Upgrade, CONNECT, absolute-form targets, unsupported transfer encodings, ambiguous +Content-Length/Transfer-Encoding, trailers that change routing/authority, and a +second request. Preserve ordinary route paths, JSON envelopes and error codes. +The outer URL's Host and identity headers have no authorization role. + +Parse a bounded complete header before choosing a route. Reserve the existing +worker and a provisional coordinator token before ASGI `receive` can supply body +data. A coalesced early body stays within pending carrier credit; excess closes +without invoking the plugin. Enforce both declared Content-Length and cumulative +decoded/chunked bytes; a missing length does not bypass the 1-MiB ceiling. Do not +emit `100 Continue` before reservation. Streaming responses use the same bounded +send/backpressure path; never concatenate an unbounded response. +If the receipt timer expires, stop ASGI body delivery, wake the existing bridge +with timeout and abort the carrier. The underlying worker remains reserved until +its actual completion callback. It must not reach schema/plugin dispatch after +timeout, even if a delayed final body fragment arrives. Missing body bytes never +receive a fresh deadline through another request or connection wrapper. + +An operation ID selects a plugin before the body identifies its account. Before +body admission require a live enrollment with at least one grant for that plugin; +after bounded JSON decode require an explicit string `account`, check that exact +configured `(plugin, account)` grant, then use the existing schema validation and +account policy. There is no default first account or new operation allowlist. +Do not call plugin validation, account lookup or upstream work for a hidden +account. The existing IMAP/SMTP operations use this account argument; a synthetic +test plugin must use the same contract. Catalog-only requests use bounded work +with a token scoped to their returned visible accounts. + +## Live authority and local administration + +Keep exact `enrollments.prepared` bytes as immutable receipt history. Extend that +existing enrollment row, not a second denial/contact table, with `server_id`, +exact accepted access DER/SPKI fingerprint, `last_contact` and a +positive monotonic integer `live_revision`. Retain existing `denied` and +`inactive_after` as the single authoritative fields read by both ordinary access +and the fixed enrollment-receipt path. Existing opaque `revision` is immutable +approval provenance bound in the prepared result; never reinterpret it as a +monotonic live revision. Below, live token/revision checks mean `live_revision`. +Grant rows have unique `(enrollment_id, plugin, account)` and reference that same +enrollment. Do not add independently authoritative caches, mirror denial/cutoff +columns or mutate grants inside historical results. +Use portable enrollment's existing indexed `agent_enrollments` relation as the +sole agent-to-enrollment mapping; do not copy `agent_id` into `enrollments`. +Admission resolves the certificate's exact agent/enrollment pair through that +relation and the same enrollment row. Missing, conflicting or corrupt bindings +fail closed before ASGI admission. Migration validates the already backfilled +relation; fresh activation and exact replay retain its existing atomic writer. +No future key-version/rotation framework is implemented in this milestone. + +One atomic startup schema upgrade backfills current key/grants and preserves each +existing denial, deadline and exact prepared/result. Initial `last_contact` is +derived from that enrollment's recorded activation/inactivity origin, not startup +time. Bound each parsed result, iterate rather than load all enrollments, reject +duplicate/malformed relationships and unknown future versions, and reuse the +existing stopped-instance/publication reconciliation rules. No serving mixed +old/new authority schemas. Fresh portable-enrollment activation populates these +columns and grants in its existing commit transaction; no interim activated row +lacks live authority. Existing receipt reads continue using the same row's exact +prepared result and live denied/inactive_after. Deny atomically blocks both paths; +authenticated ordinary contact refreshes their shared cutoff without changing +historical approval/result bytes. Receipt retrieval itself keeps its existing +contact semantics; incomplete retrieval never advances the cutoff. + +Extend trusted `arbiter-server authority` controls with `grant add`, `grant remove` +and `enrollment deny`. Inputs are one exact enrollment ID and, for grant actions, +one configured plugin/account pair. Use bounded existing local-control messages +and peer credentials/Windows caller token. Add validates configured account and +does not clear enrollment denial/inactivity. Existing add/remove/deny is +idempotent; only an effective change advances live revision. Read summaries use +32-row keyset pages by enrollment ID and never expose private key material. +There is no remote grant endpoint, automatic regrant on password change, or +implicit different account when a configured name is repointed. + +Authenticate contact only after strict mTLS and current accepted-key/live-state +verification. Atomically refresh `last_contact` and the existing 180-day cutoff +for an active enrollment; an already inactive or denied record cannot renew itself. +Malformed unauthenticated traffic or incomplete control exchange cannot refresh +contact. Rotation/grace-specific contact behavior is added by milestone 4. + +## Coordinator, dispatch and denial linearization + +Use one event-loop-owned coordinator with at most one fail-fast authority writer +reservation and live tokens bounded by the carrier/worker caps. Reuse the existing +local-control worker for its SQLite writes and existing crypto worker for +activation. A writer acquires its loop reservation, performs DB work off-loop, +then publishes commit or rollback on the loop. No event-loop SQLite, waiting +thread lock, new executor, unbounded writer queue or per-request global policy +snapshot. All writes that affect ordinary authority use this reservation; busy +writers fail safely rather than wait behind blocked plugin calls. + +Read one bounded enrollment/grant snapshot in an existing worker, carrying the +coordinator's observed write epoch. Before issuing a token or accepting a new +account-specific read, the loop requires that epoch and exact live revision still +match and no writer barrier blocks the token. Stale/busy reads fail retryably +before dispatch, never retry an upstream operation. Already live tokens use the +coordinator's explicit validity/barrier state. On a committed unrelated change, +carry those bounded tokens forward to the new revision without widening their +account scope; affected tokens are invalidated. Do not reject unrelated running +output merely because a global epoch advanced. A read-only snapshot never +independently authorizes a request. + +| Token transition | Owner and required check | +| --- | --- | +| Reserved | Real worker has started; live enrollment and plugin grant permit bounded body reads. It is not yet upstream dispatch. | +| Body complete | Parse only bounded JSON; choose the exact account and live revision, validate input through the existing schema. | +| Dispatched | At the already executing worker's plugin-entry boundary, bridge to the loop for the final exact enrollment/account/revision check. That successful handoff is the dispatch linearization point. The worker's next action calls the existing plugin; no queue, task spawning, await or alternative-account selection follows it. | +| Output | Before every response chunk and final ciphertext write, recheck the token after any wait/backpressure. | +| Canceled/waiter ended | Stop body/output and abort carrier; retain real worker, bridge, token and carrier ownership until actual work ends. | +| Actual worker finished | Existing pool `on_finished` callback retires worker ownership on the loop; release carrier only after all crypto/stream owners also finish. | + +A deny/remove writer first places an affected-token barrier on admission, final +dispatch and output. It then performs the conditional SQLite transaction in the +existing admin worker. On commit, publish revision, invalidate affected tokens +on all connections, wake waiters and abort their carriers before reporting local +success. On rollback, remove the provisional barrier and permit old authority +again without replaying canceled or uncertain operations. A grant removal cancels +that account and catalog tokens whose visible output depends on it; enrollment +denial cancels all enrollment tokens. An unrelated grant does not widen or revoke +another token. Temporarily pausing admission for one authority writer is allowed; +unaffected in-flight plugin work is not canceled merely for a global epoch change. + +The final plugin-entry handoff, not TLS acceptance or early body reservation, +orders dispatch against denial. Denial's barrier first means no later dispatch. +Handoff first means already dispatched work: OS scheduling or upstream completion +may occur after denial and cannot be undone. Report that boundary honestly; never +promise to undo mail effects. Waiting for eight blocked workers is not a condition +of committing denial or suppressing their output. + +At output, drain TLS into bounded unsent buffers, await write readiness, then +check permission immediately before synchronous `transport.write`, with no await +between check and write. Do the same at plaintext/ASGI boundaries. Denial drops +unsent BIO/WS data and aborts raw transport without flushing close-notify or queued +application bytes. Bytes already handed to transport before the barrier cannot +be recalled. A late worker result cannot recreate caller authority or send a new +error after response/cancellation. `HTTPWork` waiter cleanup stops delivery but +does not discard its bridge/token until the pool's actual completion callback. + +Fixed enrollment-receipt control uses this same coordinator. After its existing +proof and exact enrollment/result verification, return the observed write epoch +with the bounded result. Acquire an enrollment-scoped receipt token on the loop +only if the epoch and current denial/inactivity state still permit it. This token +does not grant ordinary account authority or change historical result bytes. +Retain token and carrier ownership through actual crypto/send completion. + +The enrollment-deny barrier blocks new receipt admission and unsent receipt +output. Commit invalidates affected receipt tokens and aborts their carriers +without flushing queued application data. Rollback removes the provisional +barrier; unrelated enrollment or grant changes do not revoke receipt permission. +After every output wait/backpressure, check permission immediately before the +final synchronous raw ciphertext write through the same shared carrier owner, +with no await between check and write. The receipt's database snapshot alone is +not delivery permission. Bytes already handed to transport cannot be recalled. +Inactivity also blocks fresh receipt admission under the shared live state; +existing receipt contact semantics remain unchanged. + +Ship the fixed-receipt guard with the first live enrollment-deny control in +`3.2.1`. That cut introduces the minimal shared coordinator with the real fixed +receipt as its consumer, including its final-write barrier and actual-owner +cleanup. `3.2.2` extends this same coordinator to ordinary grant/body/dispatch +work and owns ordinary grant add/remove controls. Do not expose live denial and +defer receipt output integration to `3.6` or public ordinary cutover. No second +coordinator, authority table, executor, protocol or production bypass is needed. + +## Discovery, policy and artifact closure + +Use the same token/grant projection for overview, plugin/accounts, operation/schema, +policy summaries, previews, test/info routes and direct invocation. Show a plugin +only with visible granted accounts; filter counts/examples/guidance as well as +names. Direct hidden/unknown account and policy lookup returns the same generic +not-found response before account-specific validation or upstream work. A policy +is visible only through a granted account using it; this does not create a policy +grant. Current account policy remains the only operation policy. Preserve grants +for deliberate repointing; password changes require no grant edit. + +Before ordinary public cutover, supply a small closed `ArtifactStore` implementation +through the existing plugin dependency and `for_plugin` wrapper. Current concrete +type checks remain usable. It must not invoke the legacy constructor or any legacy +create/inspect/open/read/nonce method. Keep only inert limit properties needed by +schema responses; creation fails before payload staging with an authenticated +`artifact_unavailable` error. No bypass/config flag and no plaintext fallback. +Caller identity is fixed in ASGI scope and set through a scoped `ContextVar` in +the actual synchronous plugin worker, reset in `finally`; shared runtime context +is never mutated. The later encrypted store consumes this same caller boundary. + +Also close descriptor projection and inline extraction before legacy lookup: +`_operation_artifacts`, `_native_artifact_from_descriptor` and +`_inline_artifact_payload` cannot turn a plugin-supplied old URL/path/nonce into +usable bytes or a descriptor. Close old metadata/HEAD/GET and raw artifact routes, +including guessed paths. A synthetic artifact-producing operation must fail +before any legacy-store entry, payload, metadata or nonce; inspect both a legacy +spy and actual filesystem. Report prior upstream effects without retrying them. + +Under the existing exclusive startup owner, invalidate and incrementally remove +only server-managed ownerless legacy artifact index entries and their validated +files in the configured managed store. Do not instantiate its serving store, +follow links, scan mail or touch client files. Unknown/corrupt managed metadata +fails closed with operator guidance; no guessed ownership. Cleanup is bounded +per entry and repeatable across restart; deletion failure never re-enables serving. + +## Cutover and acceptance + +Private real peers exercise ordinary TLS/ASGI/coordinator paths before public +activation. No public bypass flag or test fixture creates product authority. +Build and exercise every Go discovery/schema/error/invocation/artifact constructor +with the selected native factory through private real peers first. Keep production +constructor selection on the existing path until the same cut switches it to the +native factory, closes ordinary anonymous API and legacy artifact routes, and +enables the complete reviewed ordinary channel. That cut is `3.5.2`; `3.5.1` +does not switch the product to an unavailable server route. After cutover there +is no anonymous fallback or bypass flag. +Only minimal health and bounded enrollment bootstrap remain anonymous. Missing +authority or projection fails closed. Fixed controls stay distinct. The first +installed ordinary prefix includes denial and closed-artifact acceptance; these +are not deferred to a final release checklist. + +| ID | Required real acceptance, assigned to the owning cuts below | +| --- | --- | +| N01 | Python/Go strict mutual TLS; wrong server/ALPN/role/issuer/key/time, portable/cross-server/prepared-only state and forged proxy headers never dispatch. | +| N02 | Real outer relay observes no access identity, account, operation, body or secret; changed outer cert/URL and TCP passthrough preserve expected inner trust; wrong server fails before client credentials. | +| N03 | Exact moving pending-byte accounting, all framing/chain/handshake caps, slow bidirectional peers, fragmented/coalesced WS/TLS/HTTP and valid 1-MiB body; no lifetime-byte cap on ordinary streams. | +| N04 | Pre-body eight-worker admission, Content-Length/chunked limits and Expect behavior. Fill all eight with silent/drip-fed bodies; the absolute server deadline, delayed final fragment, denial and shutdown wake waits, prevent dispatch and recover capacity only on actual worker exit. Fragments/pings/reconnect wrappers do not extend it. Health/admin denial remain responsive. | +| N05 | Existing rows, exact receipts, opaque approval revision, denies, grants and original inactivity survive one-way upgrade/restart. Admission uses the existing canonical agent binding after old-row backfill, fresh activation and exact replay; missing/conflicting/corrupt relations fail closed without a second agent-ID copy. Ordinary access and receipt read one denial/cutoff authority; denial blocks both, and active contact refresh changes their shared cutoff without changing historical bytes. | +| N06 | Discovery/count/schema/policy/error/preview/direct paths reveal only granted accounts and use existing policies; account repoint and password renewal preserve grants. | +| N07 | Controlled real-socket deny/remove races at queued read, body, final dispatch and cross-connection output. Fixed receipts also race snapshot return, token admission, blocked output and final raw write against deny commit; cover rollback, unrelated enrollment/grant changes, disconnect and actual-owner cleanup. No post-barrier dispatch or unsent delivery; already handed-off bytes cannot be recalled. Receipt cases run with the first live deny control in 3.2.1. | +| N08 | Denial/disconnect with blocked upstream calls retains all eight workers and carrier owners until actual exit; no automatic retry or false promise to recall prior effects. | +| N09 | Actual artifact-producing plugin/Go request rejects before legacy store entry or filesystem/nonce/descriptor/inline publication; old routes fail, restart cleanup is confined and failure stays closed. | +| N10 | Every real CLI constructor, including error/tool/artifact paths, uses explicit selected enrollment and verified native factory after atomic client/server cutover; the preceding product prefix keeps its working route while private peers exercise the new factory. No general outer TLS exception, URL identity inference, redirect or uncertain-operation replay. | +| N11 | Existing connect/API/plugin/download idle budgets through real peers: slow connect, no early headers, long active call, ping-only traffic, slow progressing stream and cancellation. | +| N12 | Installed supported Python/Go/native OS/CPU prefix actually executes N01/N04/N07/N09/N10 plus focused relay/timeouts; CI required variables are present and tests do not silently skip. | + +Reuse `test_native_http_protocol`, `test_artifact_http_admission` real-socket +fixtures and the existing Python/Go peer, installed-CLI and terminal drivers. +Own shared harness additions once; add targeted later cases. No live mail account, +full duplicated installed adversarial matrix or production prototype is authorized +by this design. Dependency/source inspection establishes API feasibility only; +native execution is implementation acceptance, not evidence already obtained. + +## Already sized implementation proposal + +Numbers are handwritten added + deleted production, tests/fixtures and CI, +including code motion, shared helper ownership and review-correction headroom. +Docs/news/lock data are separate. These bottom-up ranges replace the historical +3350–5840 code/test forecast; they are estimates for review, not measured future +diffs, approved delivery counts or a dynamic-splitting allowance. Each upper bound +fits the existing 1000-line cap before implementation starts. If source evidence +invalidates a cut, return to design rather than hide a late split in execution. + +| ID | Complete consumed result, scope and focused checks | Production | Tests/fixtures | CI | Total | +| --- | --- | ---: | ---: | ---: | ---: | +| `3.1.1` | Shared bounded carrier/lease/TLS pump extracted and consumed by existing pairing/control and real streaming Python/Go peers. Own shared streaming harness. N03/N04; no ordinary public route. | 320–440 | 300–420 | 0–5 | 620–865 | +| `3.1.2` | Strict Python ordinary mTLS and immutable candidate identity consumed by real peer; live admission still private. N01 and handshake limits. | 170–240 | 310–420 | 0–5 | 480–665 | +| `3.1.3` | Go strict native identity/dial plus standard HTTP transport, exercised against real Python; reuse stored decoder and existing timeout wrapper. N01/N02/N11. | 190–260 | 300–410 | 0–5 | 490–675 | +| `3.1.4` | One-request bounded HTTP/ASGI stream with immutable scope, real body/response peers and no public cutover. N03/N04, parser/body admission. | 230–320 | 350–470 | 0–5 | 580–795 | +| `3.2.1` | Live row extension/atomic backfill and OS enrollment-deny control with minimal shared coordinator consumed by the real fixed receipt. Guard receipt admission and final output, retain actual owners and test N07 receipt races/rollback/cleanup. Use the existing agent binding without a row copy; validate old/fresh/replay and corrupt relations. Extend activation once; preserve original receipt. N05. | 230–330 | 420–610 | 0–5 | 650–945 | +| `3.2.2` | Extend the same coordinator/writer reservation to ordinary authority and actual-worker callbacks, consumed by private real ASGI peer. Own OS grant add/remove controls. N04/N05/N06/N07 dispatch, busy/rollback and no stale snapshot authorization. | 220–300 | 330–430 | 0–5 | 550–735 | +| `3.3` | All discovery/direct-account/policy projections use exact live grants with current schemas and private peer. N06 including negative information leaks. | 160–230 | 320–440 | 0–5 | 480–675 | +| `3.4` | Real authorized plugin dispatch with caller context, closed concrete artifact dependency, descriptor/inline closure and confined startup invalidation. N09 with spy plus filesystem; reuse shared harness. | 230–320 | 350–460 | 0–5 | 580–785 | +| `3.6.1` | Denial closes all affected body/dispatch paths across actual sockets; actual blocked work retains slots/carriers. N07/N08, cutoff/rollback and admin responsiveness. | 190–270 | 350–470 | 0–5 | 540–745 | +| `3.6.2` | Permission after backpressure and final ciphertext-write boundary, abort without flushing revoked output. N03/N07/N08 streaming/late-worker cases. | 150–220 | 330–450 | 0–5 | 480–675 | +| `3.5.1` | Explicit selector/config/help and every CLI constructor exercises the verified native factory through complete private real peers; production selection stays on the existing working route. N10/N11. | 130–180 | 320–410 | 0–5 | 450–595 | +| `3.5.2` | Atomic production CLI constructor switch, public ordinary enablement and anonymous API/artifact closure, with no fallback; first installed authenticated prefix executes denial and closed-artifact journey. N01/N04/N07/N09/N10/N12. | 110–190 | 380–510 | 0–5 | 490–705 | +| `3.7` | Focused installed relay/outer-cert/passthrough and timeout composition, native prefix/packaging closure; add no deferred core mechanism. N02/N11/N12 and all parent N01–N12. | 20–60 | 380–480 | 0–10 | 400–550 | + +Order follows the displayed rows; existing 3.1/3.2/3.5/3.6 parents retain their +identities and acceptance. `2.10.4` precedes `3.1.1`; `3.6.2` precedes public +cutover. Each source/peer foundation has an executable current consumer, not an +empty interface. 3.1.1 owns shared carrier/harness code motion; later cuts own +only incremental scenarios. 3.5.2 owns the first installed ordinary journey; +3.7 extends it, not creates a second harness. The revised cut ranges move +20–40 production and 30–50 focused regression lines from +`3.5.1` into `3.5.2` for their atomic production switch; they are not counted twice. +Receipt/coordinator ownership moves a net 20–30 production and 20–30 test lines +from `3.2.2` into `3.2.1`; ordinary grant controls move the other way. Existing +fixed-control real-socket fixtures support 100–150 additional receipt-race test +lines in `3.2.1`. These are estimates, counted once; later ordinary and installed +coverage extends the shared harness rather than repeating the receipt matrix. +The proposed 13 PRs and aggregate range require separate implementation +authorization. No milestone-2 allowance is +reused and no milestone-3 product work begins as part of this design PR. +Aggregate: 2,350–3,360 production, 4,440–5,980 tests/fixtures and 0–70 handwritten +CI lines, or 6,790–9,410 total. The larger test share includes real streaming, +revocation/worker-lifetime and cross-language composition at their owning cuts. + +Primary API references supporting the adapter choice: +[Python 3.10 protocol API](https://docs.python.org/3.10/library/asyncio-protocol.html), +[Python 3.10 SSL protocol source](https://github.com/python/cpython/blob/v3.10.19/Lib/asyncio/sslproto.py), +[pyOpenSSL memory-BIO API](https://pyopenssl.org/en/stable/api/ssl.html). +The implementation must validate the installed pinned versions and supported Go +floor rather than treat newer local library/source inspection as native proof. diff --git a/docs/authentication/portable-enrollment-lld.md b/docs/authentication/portable-enrollment-lld.md new file mode 100644 index 0000000..69368da --- /dev/null +++ b/docs/authentication/portable-enrollment-lld.md @@ -0,0 +1,262 @@ +# Portable identity enrollment: low-level design + +Status: design candidate. This document specifies unimplemented work; it neither +creates trust nor authorizes implementation. The governing authorities are the +[requirements](requirements.md) (R04–R09, R11–R12) and +[HLD](high-level-design.md) (B1/B2/B3). Their reviewed bytes remain unchanged. +This closes the existing-portable-identity omission in milestone 2; it is not +milestone 5's repair of an existing enrollment. The old 22 delivery attempts +remain exhausted. The four proposed implementation PRs below need separate +implementation authorization after design review. + +Source basis: merged main `25a8cf2196f36499ff92ce890f313901b51b4f2b`. +`prepared.py:prepare` currently generates every agent ID and portable leaf; +`enrollment_activation.py:activate` accepts only the local owner issuer; +`identity/provisional.go` always generates a fresh portable key. Existing Noise, +proofs, certificate profiles, approval, private publisher and exact receipt remain +the implementation foundations. No new cryptographic library or protocol service +is introduced. + +## Observable journey and minimum solution + +1. A client already enrolled at A selects its saved enrolled selector explicitly: + `arbiter enrol url=https://B name=NAME identity=SOURCE_SELECTOR`. +2. B's operator has independently registered A's public owner-issuer certificate + through trusted local administration. Public issuer registration is not a new + secret handoff or an extra pairing fingerprint ceremony. +3. The client reads the portable key/certificate locally, generates fresh B access + and recipient keys, selector and PSK, saves provisional state and advertises. +4. B's operator opens enrollment and approves the exact request and B grants using + the existing single protected PSK handoff. Registered trust or a valid portable + certificate never approves a request or grants an account automatically. +5. The existing Noise exchange proves the fresh access key and the selected + portable private key over the exact advertisement and transcript. B keeps the + original agent ID/portable certificate, issues only B access, and performs the + same prepared-save/ready/conditional-commit/receipt/promotion sequence. + +A need not be online. Its private access key never authenticates B and is never +sent to B. A's grants, denial, URL and access lifetime are not B authorization. +An existing B enrollment for this agent, including a denied or inactive one, +rejects this journey and offers owner-approved existing-enrollment repair. It must +not create another B identity to evade denial or overwrite accepted access. + +Keep the portable key in each existing per-enrollment private bundle. Reuse the +native no-replace publisher and ownership checks. A separate global identity store, +key broker, account policy layer or reusable bearer capability is unnecessary. + +## Registered owner trust + +Extend the same private SQLite authority, under the existing instance owner: + +| Column | Constraint and use | +| --- | --- | +| `issuer_sha256` | Primary key: lowercase hex SHA-256 of canonical DER, exactly 64 characters. | +| `owner_id` | Canonical UUIDv4 from the exact owner-role SAN. | +| `certificate_der` | Canonical owner profile DER, 1–4,096 bytes. Immutable for this fingerprint. | +| `active` | Integer boolean. Removal retains the row as an inactive tombstone. | +| `revision` | Positive monotonically increasing integer for effective trust changes. | + +Register the initialized local owner issuer once. Repeated startup must not +reactivate an explicitly disabled row. Same active DER registration is idempotent; +removal and later reactivation advance revision. Same owner UUID may have multiple +explicitly registered issuer certificates: reuse of the operator recovery file +preserves owner UUID while independent server setup generates independent CA keys. +Select trust by exact registered certificate fingerprint, never by UUID alone. +The server cannot issue or renew a portable leaf under another server's key. + +Add trusted local controls under the existing OS-authenticated dispatcher: +`owner-issuer register`, `owner-issuer list`, `owner-issuer export`, and +`owner-issuer remove`. Their CLI surface is +`arbiter-server authority owner-issuer ACTION --state-dir ROOT`. +Register takes `--certificate-file PATH`; export/remove take `--issuer-sha256 ID`. +The CLI reads one bounded public DER file and sends its bytes over the existing +64-KiB local control exchange. Export writes public DER to stdout. List paginates +32 summaries containing owner ID, fingerprint, active state and revision; export +returns one certificate, not an unbounded certificate list. No private key input, +network registration, arbitrary server-side file path or silent replacement. +An inactive/expired issuer cannot authorize new enrollment. Removing issuer trust +does not retroactively revoke an already activated B access credential; that is +B's explicit enrollment-denial authority. + +Use one atomic, owner-only upgrade of the existing authority schema before +serving enrollment. Support reading the previous schema during interrupted +publication reconciliation, then upgrade it once; reject unknown future schema +versions. Preserve all original credentials and exact enrollment-result bytes. +The migration creates this registry and the following unique binding table: +`agent_enrollments(agent_id PRIMARY KEY, enrollment_id UNIQUE NOT NULL)`. +Backfill it from each bounded stored prepared result in the same transaction; +malformed or duplicate historical bindings fail startup with state preserved. +Existing empty/unactivated databases need no synthetic enrollment. Migration is +an extension of current SQLite/version handling, not an ORM or migration framework. +The same `2.10.1` cut also changes the existing public fresh-v1 activation writer: +insert its `agent_enrollments` binding in the very transaction that inserts the +enrollment/result. Exact committed replay must find the same agent/enrollment +mapping; a missing/conflicting binding fails closed, never creates a second row. +The backfill and this writer change ship together before an upgraded server can +accept another fresh request. Thus v1 enrollments created during `2.10.1` or +`2.10.2`, including subsequently denied/inactive ones, cannot escape uniqueness. +The later v2 path extends this same writer; it does not install a second mapping +path, defer maintenance until reuse exposure or rely on repeating migration. + +## Advertisement, custody and proof + +New enrollment uses one version-2 public schema. Keep the endpoint, request +64-KiB cap, original ten-minute deadline, twenty pending requests, frame/handshake +limits and five distinct authenticated attempts. The JSON object has exactly: + +| Field | Value | +| --- | --- | +| `version` | Integer `2`. | +| `selector`, `name` | Existing canonical selector and bounded display label. | +| `access_spki`, `portable_spki`, `recipient_spki` | Existing canonical key encodings and key separation. | +| `portable_identity` | `null` for fresh identity, or the exact object below. | + +The reused-identity object has exactly `owner_id`, `agent_id`, `issuer_sha256`, +and `certificate_der` (canonical standard base64, decoded size ≤4,096 bytes). +It does not supply a trust root. The server selects the independently registered +root by fingerprint and owner ID. Reject duplicate, unknown, differently cased, +wrong-type, malformed, trailing or noncanonical fields. The portable leaf's +public key must equal `portable_spki`; identity UUIDs must be distinct from each +other and the target server/enrollment IDs under existing profile rules. + +Anonymous registration performs bounded decoding only and retains these as +unverified claims. It cannot assert that possession has been proved. Before +approval, a bounded worker verifies the exact portable profile, current validity, +registered active issuer and proposed key. Approval retains issuer revision and +all exact request bindings; secret input is still protected and approval remains +immutable. The list/approval summary distinguishes a claimed portable identity +and certificate validation from the still-pending fresh possession proof. + +The raw advertisement digest already enters the Noise prologue and both signing +proofs. Consequently it binds all version-2 identity fields. Keep those proof +algorithms/tags and their role separation; do not add an independent signature +scheme. A fresh `access` proof plus the original portable-key proof is mandatory. +The fresh mode still assigns a new agent only after owner approval. + +New private provisional records use version 2 and retain a nullable source +identity containing exact owner DER, portable DER, owner/agent IDs and fingerprint. +Its bound is 24 KiB: two ≤4-KiB certificates, their base64 representation, existing +three bounded keys and metadata. Public advertisement stays under its existing +64-KiB cap. Prepared/enrolled wrapper bounds derive from the chosen private-record +version plus the existing maximum result representation. Version-1 private records +retain their original 8-KiB bound and exact advertisement reconstruction for old +receipt validation. No PSK is persisted. + +Extract one strict bounded stored-record decoder from existing prepared loading. +Keep purpose-specific validation above it: + +- `LoadPortableIdentity` accepts only the final `enrolled/` domain, validates native + ownership/canonical complete state, owner/portable profiles at current time and + portable private-key correspondence. It does not require A online, accept a + prepared-only identity or substitute A's access key. Source bytes are unchanged. +- Existing receipt loading validates current target-server/access authority and + authenticates the current receipt peer. Stored portable evidence is historical: + validate its full profile/chain at issuance, rather than impose its later + expiration as an unrelated receipt deadline. This is not a new-enrollment time + exemption or ordinary TLS exception. A not-yet-valid source leaf is rejected. +- Ordinary access loading belongs to the forthcoming milestone 3 LLD, a future + consumer rather than a governing input to this enrollment specification. + Parsing one stored bundle must not equate these purposes. + +`NewProvisionalFromIdentity` reuses only the validated portable key/certificate. +The target access and recipient keys remain independently generated. The existing +one enrollment owner spans source read, cleanup, save, handoff and promotion. +`identity=...` cannot combine with `resume=...`; a missing/invalid source fails +before registration or cleanup. Normal fresh enrollment omits `identity`. + +## Prepared result and single activation + +Preserve the existing nine-field prepared-result layout and exact receipt bytes. +For reuse, the fixed owner/agent IDs identify the selected portable issuer and +agent, not B's local owner issuer. Field 2 is that registered owner certificate; +field 3 is the original portable certificate unchanged. B supplies its own server +authority/leaf, fresh B access leaf, recipient and recovery public metadata as +before. The owner ID is never evidence that B holds the portable issuer key. + +Before prepare, verify the approved issuer revision and current portable validity. +Before ready, Go compares the returned owner/agent IDs, portable leaf and issuer +DER byte-for-byte with the selected local source in addition to existing result +validation. B access must bind the original agent, B server, fresh enrollment and +fresh access key. Same-server source selection is rejected with repair guidance. + +At activation, in one existing `BEGIN IMMEDIATE` transaction: + +1. Recheck exact approval, original request deadline, ready proof, proposed keys, + current accounts and issuer fingerprint/owner/active/revision. +2. Verify the portable profile at current time. Fresh mode requires the current + local issuer and newly issued portable leaf; reuse requires the exact approved + registered issuer and original leaf. Do not compare a reused issuer with B's + unrelated local owner credential. +3. Insert the unique agent/enrollment binding and the existing enrollment/result + row together. An existing binding rejects even if its enrollment is denied or + inactive. Same committed selector/enrollment/result replay may only read its + unchanged result; it does not create another binding. +4. Consume approval at the existing live commit decision and commit. A concurrent + issuer removal that commits first defeats activation; activation committed + first remains a valid B enrollment. No event-loop disk/crypto or registry lock + spans database work. SQLite's existing fail-fast writer exclusion suffices. + +Ready still follows complete private flush. The existing server-authenticated +receipt uses only B's registered access key and exporter/fresh nonce; no PSK, +portable credential or source-server assertion can retrieve another enrollment. +B denial/inactivity still prevents receipt retrieval. Changing B grants later +must not change the exact historical prepared result. Resume/promotion, failed +fsync, torn no-replace publication and original-expiry behavior remain intact. + +There is no published v1/v2 enrollment compatibility service. At final cutover, +all newly generated registrations use v2 and the public registration parser accepts +v2 only; old clients get a format error without an identity reset. Keep only the +v1 saved-state/result decoder needed to recover prior committed enrollment. +The earlier implementation slices below exercise the new codecs and activation +through real private peers while public registration stays on its existing safe +fresh path. The final slice switches the product path atomically. + +## Complete acceptance + +| ID | Required evidence and failure direction | +| --- | --- | +| P01 | Real A/B Python servers and Go client preserve portable key/cert/agent ID; B gets distinct access/enrollment/recipient state. A can be offline. | +| P02 | Same owner UUID with distinct CA keys succeeds only after exact B registration of A issuer. Unknown, inactive, mismatched or expired issuer/leaf fails. | +| P03 | Valid certificate without original private key, changed source ID/key/cert/hash, wrong PSK, replayed or changed transcript never reaches ready/activation. | +| P04 | Window, pending capacity, original deadlines, five authenticated attempts and duplicate-owner accounting remain unchanged for fresh and reused identity. | +| P05 | Existing active/denied/inactive B binding rejects new reuse; include real fresh-v1 enrollments committed on both intermediate `2.10.1` and `2.10.2` prefixes before later reuse. A denial alone grants no B authority and clears no B denial. Source access leaf never authenticates B. | +| P06 | Trust removal/re-add and approval/ready/commit races cannot revive an approval bound to an older issuer revision. Rollback preserves binding/result consistency. | +| P07 | Crash/lost successful or lost final reply, historical prepared expiry, portable expiry after commit, restart and promotion failure recover exactly one B result using B access. | +| P08 | POSIX/Darwin/Windows custody, source file replacement, private-public field leaks, failed save and old committed v1 receipt fixtures retain existing safety. | +| P09 | One-way upgrade preserves real old prepared/result/denial data, detects duplicate/malformed bindings, and fails interrupted migration atomically. From the first upgraded prefix, every new fresh-v1 activation and exact replay maintains one matching binding/result atomically; failed insertion rolls back both. | +| P10 | Installed fresh and reused journeys execute in existing native CI; old-wire rejection is actionable and does not delete valid saved identities. | + +Broad malformed-input matrices belong in unit/source-peer tests. Use one compact +installed two-server journey plus recovery on the existing terminal driver; do +not clone the entire adversarial matrix at installation level. No live accounts. + +## Implementation-ready cut proposed for review + +Counts are handwritten additions + deletions, including code motion, helpers, +fixtures and review-correction headroom. Docs/news/lock data are separate. These +are bottom-up engineering estimates from the named existing files, not measured +future changes or a guarantee. Each upper bound includes its local negative and +integration work and fits the existing 1,000-line hard limit. No later dynamic +split is assumed to make an overlarge row fit. + +| ID | Independently consumed result and ownership | Production | Tests/fixtures | CI | Total | +| --- | --- | ---: | ---: | ---: | ---: | +| `2.10.1` | Real OS-local issuer controls, atomic schema/backfill and same-transaction binding maintenance in the existing fresh-v1 activation writer. Extend authority/control/admin/activation; P02/P06/P09 plus post-upgrade activation/replay/rollback. | 260–370 | 350–490 | 0–5 | 610–865 | +| `2.10.2` | Go native source loading and v2 private/public codecs consumed by an executable Python/Go peer. Extend existing decoders/publisher; public reuse remains unreachable. P03/P08 and old receipt decoding. | 210–300 | 280–390 | 0–5 | 490–695 | +| `2.10.3` | B prepare/proof/v2 activation extends the existing binding writer, with historical receipt validation and a real A/B/Go peer. P01/P03/P05/P06/P07 includes prior-prefix v1 active/denied/inactive states; public v2 remains closed. | 230–310 | 320–430 | 0–5 | 550–745 | +| `2.10.4` | Wire explicit source selection and v2-only public registration into real CLI pairing; installed A/B and fresh regression journeys plus migration/help/news closure. P04/P08/P10 and all parent P01–P10. | 150–240 | 350–460 | 0–10 | 500–710 | + +Order is `2.10.1 -> 2.10.2 -> 2.10.3 -> 2.10.4`, then milestone 3. +Existing 2.1–2.9 delivery identities and their approvals are preserved. The new +parent `2.10` owns P01–P10; it is a proposed corrective scope, not a reset of +milestone 2's exhausted implementation allowance. Estimated total: 2,150–3,015 +lines including handwritten CI. The existing native CI matrix and terminal, +authority, activation and peer fixtures are reused; 2.10.2 owns source/codec +harness additions, 2.10.3 owns two-server composition, and 2.10.4 adds only the +installed variants. No external dependency adoption or platform drop is needed. +The first-cut writer ownership reallocates 20–40 production lines from 2.10.3 +to 2.10.1; 50–70 additional test lines cover intermediate activation integrity. +Aggregate production remains 850–1,220; tests are 1,300–1,770 and CI 0–25. +These are revised estimates, not measured future diffs. Core uniqueness tests run +where the first upgraded writer exists; later composition uses its existing P05 +budget to test reuse of those real intermediate states.