diff --git a/docs/.review/MULTI_IDENTITY_D_DESIGN.md b/docs/.review/MULTI_IDENTITY_D_DESIGN.md index 72a26f3e..e0095855 100644 --- a/docs/.review/MULTI_IDENTITY_D_DESIGN.md +++ b/docs/.review/MULTI_IDENTITY_D_DESIGN.md @@ -1,20 +1,20 @@ --- artifact: swe-design-review-attestation schema_version: 4 -scope_key: 1c2bd3927c554157c98ca0577b3d8c17a87d2be79f963a96232646924639dd2b -scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_D_DESIGN.md"} -review_content_identity_sha256: 7fcd5f133d5fd491fec89984afe8951bf522fea064af42c7417c08d316aa0989 -target_content_identity_sha256: fee62dbad94f6815fdccc1920a7180864dfef8e5ad9238e0fdee689a87db5573 -baseline_content_identity_sha256: 146b305ae1732ad14e8c5eef04d4f5ec47db383706172645f8cf324fde970166 -target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}] -baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] -design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] +scope_key: 84dfc9009e4659d1b853c82e116ec151a71a46124836472b7e702c130fca06e5 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md"} +review_content_identity_sha256: b28cffd88510deebb9138863c2d36e2e6043ab650bc3b4e10a36bdff1ada6927 +target_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +baseline_content_identity_sha256: 284179960f35a7db15d62cf21421166c0738e48b66ff87d495ec3ede995267d9 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] document_repository: "." document_path: "docs/MULTI_IDENTITY_D_DESIGN.md" -document_revision_provenance: "7cf9a7f64175c29eda3c28eba446ad9f1d05352a" +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" document_sha256: 078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435 verdict: clean -attested_at: 2026-10-26T21:41:50Z +attested_at: 2026-11-01T21:05:36Z --- diff --git a/docs/.review/MULTI_IDENTITY_E_DESIGN.md b/docs/.review/MULTI_IDENTITY_E_DESIGN.md index 40082dec..684e0d51 100644 --- a/docs/.review/MULTI_IDENTITY_E_DESIGN.md +++ b/docs/.review/MULTI_IDENTITY_E_DESIGN.md @@ -1,20 +1,20 @@ --- artifact: swe-design-review-attestation schema_version: 4 -scope_key: 9b4170e67f9abd8153de58192aec49a1a7bb74a0d3299729c834de814d6b2fd5 -scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_E_DESIGN.md"} -review_content_identity_sha256: 1ba7e2037c10966cb8dd71e1e174183f1a122ee21ffa392274d3f38beafa086c -target_content_identity_sha256: d06b7220bf6bead2879bbc8c59ba5cfc8759c21b3f6a9867587b7cf539c15c37 -baseline_content_identity_sha256: 17972b262e20f4dccbd006b043997b1dc12e2bb7f58cd0bf32f21bba95041036 -target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "f5058bb465d6af9d58b98eb6fde19edd4ed710aff1cc2588173757d37cdbbc58"}] -baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "fe188bb8a80f7f235e99b023283749a3a9e3172e4adf70b70b495d80e50e9218"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] -design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "fe188bb8a80f7f235e99b023283749a3a9e3172e4adf70b70b495d80e50e9218"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] +scope_key: 84dfc9009e4659d1b853c82e116ec151a71a46124836472b7e702c130fca06e5 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md"} +review_content_identity_sha256: b28cffd88510deebb9138863c2d36e2e6043ab650bc3b4e10a36bdff1ada6927 +target_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +baseline_content_identity_sha256: 284179960f35a7db15d62cf21421166c0738e48b66ff87d495ec3ede995267d9 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] document_repository: "." document_path: "docs/MULTI_IDENTITY_E_DESIGN.md" -document_revision_provenance: "944d564fc3565cf91f3d72af4f4730fbd6155b3a" -document_sha256: f5058bb465d6af9d58b98eb6fde19edd4ed710aff1cc2588173757d37cdbbc58 +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8 verdict: clean -attested_at: 2026-10-30T08:48:41Z +attested_at: 2026-11-01T21:05:36Z --- diff --git a/docs/.review/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md b/docs/.review/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md index f853db6b..00473f85 100644 --- a/docs/.review/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md +++ b/docs/.review/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md @@ -3,18 +3,18 @@ artifact: swe-design-review-attestation schema_version: 4 scope_key: 759c4832028b9f6f5a5d726cff691c90a0792176e2d7c93e57732a8f63e9d5c7 scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md"} -review_content_identity_sha256: 03039cd4c3611cfa0be4f53dc4ae5204cc2aa979772565ca810c52ac9e69f3b2 -target_content_identity_sha256: 0339661e75fd610092b903a1352c65ef98de35c4963c613923f0c9852193c597 -baseline_content_identity_sha256: 779702b5fa170b026d6a04df2aeaa97ce006b96bcba41897ca39babd8b956e83 -target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}] -baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] -design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] +review_content_identity_sha256: deee38df7715833ef4ba01b1ef58d4bab0b5e417ecdb3db11751a772d839cf33 +target_content_identity_sha256: 143a1b6174c17a225083cf80284ef404dc8fb566d43be06f296dde8347b0b539 +baseline_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json", "repository": ".", "sha256": "728d53f86612af0d6ba45c3d0fe84e61b41ac5e4f622b43132d2314775e389ab"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md", "repository": ".", "sha256": "3013953a1cafa68f6b1175818a167fab5d61f406970a9f0c61621e23d8809e98"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md", "repository": ".", "sha256": "8a073c695effc36a3c2fd2baba3d8fcf85627e152968682154833270da627a5e"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] document_repository: "." document_path: "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md" -document_revision_provenance: "7cf9a7f64175c29eda3c28eba446ad9f1d05352a" -document_sha256: 3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9 +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14 verdict: clean -attested_at: 2026-10-26T21:34:44Z +attested_at: 2026-11-01T20:59:58Z --- diff --git a/docs/.review/MULTI_IDENTITY_MAIL_RECONCILIATION.md b/docs/.review/MULTI_IDENTITY_MAIL_RECONCILIATION.md index 46ca44fa..b9ca1e27 100644 --- a/docs/.review/MULTI_IDENTITY_MAIL_RECONCILIATION.md +++ b/docs/.review/MULTI_IDENTITY_MAIL_RECONCILIATION.md @@ -1,20 +1,20 @@ --- artifact: swe-design-review-attestation schema_version: 4 -scope_key: da00dad9dd49b0dd2382b413199155873a61d4895395fe8065b2a0c936b48d0a -scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md"} -review_content_identity_sha256: 3fe0a699612176b60e5ab6a44aded0a13758f005000101c1bcff262d462f275d -target_content_identity_sha256: ba63be4406185d978f1794a3c22b2598a80288a0cff47ce018ff1e8ef745870c -baseline_content_identity_sha256: a68d5bec4bca25c403f41460bd202236954d42c9524b3fe50a7210e0af055387 -target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "fe188bb8a80f7f235e99b023283749a3a9e3172e4adf70b70b495d80e50e9218"}] -baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] -design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "3ef44a7a0be43648f5df1faad39b16101978662c58c87d6fa8e7409093a58fe9"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] +scope_key: 84dfc9009e4659d1b853c82e116ec151a71a46124836472b7e702c130fca06e5 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md"} +review_content_identity_sha256: b28cffd88510deebb9138863c2d36e2e6043ab650bc3b4e10a36bdff1ada6927 +target_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +baseline_content_identity_sha256: 284179960f35a7db15d62cf21421166c0738e48b66ff87d495ec3ede995267d9 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] document_repository: "." document_path: "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md" -document_revision_provenance: "7cf9a7f64175c29eda3c28eba446ad9f1d05352a" -document_sha256: fe188bb8a80f7f235e99b023283749a3a9e3172e4adf70b70b495d80e50e9218 +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95 verdict: clean -attested_at: 2026-10-26T21:49:29Z +attested_at: 2026-11-01T21:05:36Z --- diff --git a/docs/.review/MULTI_IDENTITY_SECURITY_CONTRACT.md b/docs/.review/MULTI_IDENTITY_SECURITY_CONTRACT.md index 6a24d1e4..d07c3deb 100644 --- a/docs/.review/MULTI_IDENTITY_SECURITY_CONTRACT.md +++ b/docs/.review/MULTI_IDENTITY_SECURITY_CONTRACT.md @@ -3,18 +3,18 @@ artifact: swe-design-review-attestation schema_version: 4 scope_key: 84dfc9009e4659d1b853c82e116ec151a71a46124836472b7e702c130fca06e5 scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md"} -review_content_identity_sha256: e58c41923187ec24edf769969f960b0514db4f8a0d9f41e18d06a3f08aa05160 -target_content_identity_sha256: 779702b5fa170b026d6a04df2aeaa97ce006b96bcba41897ca39babd8b956e83 -baseline_content_identity_sha256: null -target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826"}] -baseline_documents: [] +review_content_identity_sha256: b28cffd88510deebb9138863c2d36e2e6043ab650bc3b4e10a36bdff1ada6927 +target_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +baseline_content_identity_sha256: 284179960f35a7db15d62cf21421166c0738e48b66ff87d495ec3ede995267d9 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] design_dependency_documents: [] document_repository: "." document_path: "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md" -document_revision_provenance: "7cf9a7f64175c29eda3c28eba446ad9f1d05352a" -document_sha256: b049714f4a85d0f3fb204615897374377690564627bfcdd03e50c945fb042826 +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76 verdict: clean -attested_at: 2026-10-26T21:23:17Z +attested_at: 2026-11-01T21:05:36Z --- diff --git a/docs/MULTI_IDENTITY_E_DESIGN.md b/docs/MULTI_IDENTITY_E_DESIGN.md index 9853c955..80ce91d0 100644 --- a/docs/MULTI_IDENTITY_E_DESIGN.md +++ b/docs/MULTI_IDENTITY_E_DESIGN.md @@ -8,11 +8,18 @@ summary: Concrete remaining E contracts, shared admission boundaries and source- ## Authority, scope and stopping point +F/G continuation: the maintainer approved reuse of admission for the same +unchanged container on 2026-11-01. The campaign/security contracts state that +target. The E implementation and evidence below retain their delivered +one-shot, zero-restart meaning; they do not qualify the new retry protocol. +F/G owns its engineering, review and production proof before activation. + This document refines the E contract in the [campaign plan](MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). Its other explicit governing dependencies are the authoritative [security contract](MULTI_IDENTITY_SECURITY_CONTRACT.md), the approved native-process [mail reconciliation](MULTI_IDENTITY_MAIL_RECONCILIATION.md), and the -[D design](MULTI_IDENTITY_D_DESIGN.md). Those four documents remain unchanged. +[D design](MULTI_IDENTITY_D_DESIGN.md). The delivered E baseline retained those +four documents; the F/G retry amendment above is a later governing change. Source, test receipts and supporting investigations are context, not extra authorities. The input implementation is merged E2.2 at diff --git a/docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md b/docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md index d70aa149..86ac3d50 100644 --- a/docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md +++ b/docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md @@ -1,6 +1,6 @@ --- -status: A/B landed; C internal integration delivered; full-campaign architecture approved; remaining group delivery includes LLD preparation and review; public mapped execution remains inhibited -updated: 2026-10-27 +status: A–E delivered; F/G controller LLD and slice review in progress; H–K remain; public mapped execution remains inhibited +updated: 2026-11-01 summary: Full-campaign architecture and contracts for isolated single-installation identities, with implementation-ordered slices and separate proof and approval gates. --- @@ -35,6 +35,16 @@ and root-run mapped operations reject before mutation; the existing system-unit path does not establish that rootless runtime account. This is the initial mapped runtime scope; it does not claim system-scope support. Ordinary single-identity behavior keeps its backend-independent default. +## Current delivery state — 2026-11-01 + +A–E are delivered. D's declarations/image evidence and E's private mapped +mechanisms do not open public mapped execution. The next work is review and +mechanism engineering for the [F/G draft](#fg-engineering-and-slice-inventory--draft), +followed by its design publication/PR cycle. The separately requested RDA +cancellation hotfix precedes F/G implementation. H–K remain unfinished. +The old post-C table and D continuation below retain historical planning and +evidence. Do not select or reset completed D/E from those sections. + ## Approved scope boundary — 2026-10-19 - Implement mapping input, validation, state and recovery for one workload @@ -64,7 +74,12 @@ The superseded proposal made that a delivery dependency. The scope correction above removes that dependency while retaining explicit mappings and manual external coordination. It does not approve a composition API. -## Remaining scope: implementation-ordered delivery table +## Historical post-C forecast: implementation-ordered delivery table + +This table preserves the post-C forecast, including the then-unfinished D/E. +D/E are now delivered. It is not the current unfinished-work count or estimate. +The F/G engineering section below owns the proposed next inventory; H–K retain +their provisional parent estimates until their own LLD review. Letters are delivery groups, numbers are slices within the group. Read top to bottom. Earlier independent work may run sooner (notably K1). Group subtotals @@ -227,8 +242,11 @@ retained data and unmanaged services. Reploy serializes and checks one installation's own authority. Host snapshots and installation locks are not a global allocator or an atomic lock over administrator changes. Operators must stop and fence affected workloads before changing delegation or reservations. -Every new controlled execution admission still refreshes the required host -inventory and delegation checks; earlier success is never a cached permission. +Every new controlled creation, replacement or external-entry admission still +refreshes the required host inventory and delegation checks. Only the same +unchanged, previously admitted container may reuse its approval for engine +restart, subject to the qualified mechanism and fences below. Earlier success +does not authorize a new object or changed execution authority. The host operator, Reploy implementation and accepted blueprint are trusted. Application code, application-controlled data and ordinary image hooks are @@ -262,6 +280,9 @@ untrusted; image locking identifies bytes rather than establishing trust. These are logical contracts; reuse and extend existing records rather than create a parallel state store or admission cache. +An unchanged-container retry record is a confined startup projection of the +existing exact-object admission transaction, not a second mapping authority or +a cache that can admit new objects. | Record | Required binding | Producer and consumer | | --- | --- | --- | @@ -346,7 +367,9 @@ ownership or authorize a previously omitted role. All Reploy-controlled application paths use one admission boundary: initial start, application command, test, shell, controlled session, lifecycle hook, -runtime exec and restart. An unsupported path +runtime exec and replacement. An engine restart of the same unchanged admitted +container may reuse that container's original approval through a qualified +startup mechanism. An unsupported path rejects before payload execution. Native processes may fork, exec, select mapped identities with admitted capabilities and restart workers inside already accepted workload authority; they cannot use those actions to enlarge that authority. @@ -383,17 +406,23 @@ The host operation authorizes each requested role entry against fresh host and installation authority. Reuse the existing command/session transport to invoke the trusted launch boundary, binding the installation, generation, mapping revision, exact owned container, role and attempt. This is operation -evidence, not a new bearer token, admission cache or always-connected replacement -protocol. Stale, disconnected or incomplete requests launch no application code; +evidence, not a bearer token for other objects or an always-connected replacement +protocol. The approved unchanged-container retry exception requires a separate +exact-object approval record; it cannot reinterpret a one-shot frame as reusable +permission. Stale, disconnected or incomplete first-admission requests launch no application code; ambiguous replies retain their operation and never authorize a blind replay. The required foreground master's exit ends its container instance. Native worker replacement may use existing admitted authority; it is not a new host admission. Whole-container recreation and external entries still require fresh admission. Completion of a transient hook or explicit app command does not itself end the workload. -An installed user-service may apply its existing restart policy only by -re-entering host admission for a new instance. Detached staging requires an -explicit new host operation. No engine restart policy bypasses these checks. +An installed host service must re-enter host admission when creating a new +container. A qualified engine policy may retry the same unchanged admitted +container without a new host invocation. A new detached staged container still +requires an explicit host operation. Reploy stop, preparation and revocation +must fence all owners that can retry or create a replacement before completing +the protected operation; removing the exact owned container is a candidate +common engine fence, not permission to delete retained storage. Foreground/session ownership loss follows existing bounded stop/cleanup and retains uncertainty when stopping cannot be proved. An already admitted detached instance needs no continuously running host broker. Native worker changes cannot @@ -453,12 +482,17 @@ to start and inspect later. J may add only its explicit narrow interface rights. ### G contract: activation, restart and revocation -The existing host operation or foreground user-service runner owns application -admission and the permitted whole-container restart chain. User-service startup re-enters the normal host authority and -generation checks under the selected OS user. Reuse the existing user-service -lifecycle; do not delegate autonomous workload restart to an engine policy -that can bypass Reploy. Non-root foreground staging/run/test follows the same -admission. System units and root-run mapped operations remain ineligible. +Reploy owns application admission, recovery policy and stop/revocation state. +Reuse existing host service/control integration where applicable rather than +assuming a user-service runner exists or introducing a shared daemon. A host +manager invokes fresh admission for a new container under the selected OS user. +A qualified engine may execute configured retries of the same unchanged +admitted container; it may not create replacements, reinterpret policy or undo +a completed Reploy fence. The LLD must cover supported user/system and +OS/engine combinations and their actual gaps separately. Non-root foreground +staging/run/test follows the same admission. System units, system-scope and +root-run mapped operations remain ineligible; the recovery amendment does not +expand that cohort. Installing from staging creates a separate destination mapping owner with an explicit independently validated mapping decision. It copies portable build @@ -654,7 +688,7 @@ Evidence gates remain independent from design approval. Completing this section does not claim the one-shot launch, storage, network, restore or interface mechanisms have passed. Their owning LLD must provide the exact proof plan before code. -The current 37 remaining slice estimates are conditional. The explicit +The historical post-C 37-slice estimates are conditional. The explicit host-operation/one-shot launch boundary, exclusive storage and interface obligations may require decimal splits, especially E2/E3, F2 and J1/J3. Re-estimate against the selected LLD before implementation; do not force a new control plane into @@ -797,8 +831,10 @@ input files cannot retarget an operation midway; a later operation rereads input and performs normal revision checks. A changed desired mapping never overrides accepted or uncertain reservations. Check/status distinguish desired missing or conflicting input from recorded candidate/accepted/retained authority. Existing -runtime restart uses recorded accepted maps plus fresh admission, not a mutable -sidecar. Removing overrides or introducing `???` cannot erase stored reservations. +runtime replacement uses recorded accepted maps plus fresh admission, not a +mutable sidecar. A qualified unchanged-container retry uses its original exact +admission binding; removing overrides or introducing `???` cannot erase stored +reservations or authorize a different container. Range length does not authorize work proportional to the represented ID count. Validate interval arithmetic and delegated containment before account/group @@ -958,7 +994,7 @@ belongs only to the inhibited pending intent. The source stays valid, unchanged and separately reserved. Do not inherit its mapping ID, reservation history, pending operation, inhibition or owned resources. No source-to-destination ownership transfer or cross-directory commit is needed. -The exact public install flags remain part of service-design. +Destination input follows the current F/G override contract below; no mapping-specific install flags are selected. Only immutable build/image authority crosses this boundary automatically. Existing path-update/config/data transfer routes must reject mapped source storage that @@ -1189,8 +1225,10 @@ and user-service runtime account must be the same non-root numeric UID/GID. Stor daemon or restart owner. C1/G1 must positively identify the selected local rootless engine and its operating account and pin its endpoint. Configure/admission rejects root, sudo-mediated account substitution, a different engine account, and installed -`scope: system` before mapped workload mutation. Every start, restart, recreation, -exec and ownership-preparation path rechecks this binding and delegated authority. +`scope: system` before mapped workload mutation. Every new container creation, +replacement, external exec and ownership-preparation path rechecks this binding +and delegated authority. A qualified unchanged-container engine retry reuses +the admitted binding instead of refreshing host inventory on every process start. Unproved endpoint/runtime-account evidence rejects rather than trusting the caller or a socket path alone. Existing system installation resolves an application account separately but its systemd unit runs as root; that account is not a rootless engine @@ -1217,12 +1255,15 @@ retain those recorded effects and inhibition for exact recovery; do not claim atomic rollback of a multi-object preparation. F1/F2 test account/delegation changes between configuration and preparation, with unchanged objects on rejection. -Every execution admission, including start, restart, recreation and runtime -exec, also refreshes delegation, engine identity and the complete authoritative bulk +Every new-container, replacement or external-entry execution admission also +refreshes delegation, engine identity and the complete authoritative bulk UID/GID principal inventory. Rerun the interval collision checks and reject unavailable, incomplete or conflicting evidence before payload release; staging or earlier execution evidence cannot substitute. Verify effective kernel maps -before release as well. If host delegation changes +before first release as well. The qualified same-container engine-retry exception +retains the exact original admitted authority and still performs its trusted +local startup checks; it cannot authorize another container or changed plan. +If host delegation changes concurrently, Reploy cannot prevent that administrator action; it must reject known disagreement and never report revocation complete while resources remain. @@ -1711,8 +1752,613 @@ policy before child launch, and preparation/network checks before service activation. It replaces the earlier group table with forward prerequisites. No obsolete enrollment/global-allocator implementation remains in B. + + +## F/G engineering and slice inventory — draft + +The accepted direction is a lean host controller per persistent installation, +reusing the existing embedded runner and local engine transports. This section +is the canonical **F/G delivery candidate inventory**. The earlier F/G parent +rows preserve the original forecast; they are not an additional implementation +queue. D/E delivery is complete and is not reset by this replan. Publication and implementation require current local/independent/remote review, +agreed public contracts and the applicable proof gates. No public mapped guard +opens from this document alone. + +### Accepted public surface and scope + +- Managed mounts use optional `owner`, `group`, `permissions` and separate + `directories`/`files` maps. Omitted owner resolves from the applicable runtime + context; omitted group uses that owner's primary group. Omitted permissions + preserve existing creation behavior. Directories are prepared parent-first, + before files. No recursive repair, foreign-tree adoption, general content + templating or implicit role-to-user conversion is introduced. +- `environment.mounts.root` with `target: /` describes the intrinsic image root, + not an external bind. Omitted means read-only. `writable: true` explicitly + permits application image-root changes; trusted Reploy startup/control paths + remain protected. Other mounts keep their independent writable policies. + For the initial mapped cohort, a writable managed mount under the default + read-only root must target an absent, empty or directory-only image location. + Any existing image file beneath its resolved destination causes refusal, + including data-only files. Root writable opt-in relinquishes application-image + replacement protection, not trusted-path protection. Ordinary installations + do not acquire this mapped-only restriction. +- Mapping input moves directly to schema-1 `podman.identity_mapping`, with + explicit `staging` and `deployed` decisions and whole-array UID/GID overrides. + This is the undeployed feature revision, not a parallel legacy mapping path. + Destination admission is independent and never copies staging reservations, + ownership, secret values or an admission result. +- `environment.workload.restart` is the Reploy-owned policy, with `no` as the + default, `on-failure[:N]` and `always`. `N` is a positive decimal retry limit. + All conditions honor explicit stop for the current activation. Automatic + boot/login enablement is separate: `up`/`down` do not change it, and existing + `enable`/`disable` controls do not implicitly start/stop. An enabled installation + can start at its next qualified boot/login even after `down` or with retry + policy `no`. Revocation, preparation and uncertain-cleanup fences still survive + every activation. No `unless-stopped` value is selected. + Existing ordinary `docker.workload.restart` remains compatible; + specifying both fields is an error. No silent ordinary-install migration or + engine-policy fallback is allowed when the new controller is unavailable. + +```yaml +environment: + workload: + command: mail + restart: "on-failure:5" +``` + +Reploy owns recovery policy. The engine performs effects with its independent +automatic restart disabled for controller-owned workloads. The OS manager +supervises Reploy. A confirmed surviving workload can be reobserved after +controller loss without replaying startup or private delivery. A failed mapped +instance uses exact retirement and fresh container admission; E's consumed +one-shot release frame cannot restart an old container. The earlier permission +for a separately qualified unchanged-container engine retry remains a contract +permission, not a second mechanism to implement in this plan. + +Initial mapped execution remains non-root local rootless Linux Podman through +the Docker-compatible local socket. Ordinary Docker/Podman controller behavior +is covered separately on currently supported host/scope combinations. Native +Windows/macOS system installs, remote engines, mapped Docker and mapped system +scope remain outside this delivery. Secret-manager and clipboard features are +future consumers, not implementation work hidden in these slices. + +### Destination input and override contract + +Use the existing typed deployment overrides mechanism. The blueprint's +`podman.identity_mapping.deployed` declaration supplies destination defaults; +destination-local `overrides.yaml` can replace selected mapping values through +the same strict override model. A supplied UID or GID array replaces that whole +array; omitted values retain the selected declaration. Host starts are actual +host IDs, not Podman's intermediate parent-namespace IDs. No mapping-specific +install flags, second override language or mandatory separate mapping file is +introduced. Existing `--port` behavior is outside this change. + +The delivered mapping loader currently handles staging only. G1.1 relocates +and phase-selects the existing typed override consumers; G1.3 connects the +strict destination-local reader to first installation and updates. Read and +validate one destination input snapshot under the destination operation lock, +bind its environment and selected declaration, and use that same captured intent +for the pending operation. Source staging overrides are not destination input. +A fresh destination's override file supplies desired configuration only; it +cannot establish ownership or authorize adoption of existing data. Existing +installation/directory checks and paired-lock ordering still apply. + +The existing exact/bounded validators check the complete effective candidate, +fresh host delegation, principals, retained reservations and source/destination +overlap before effects. A missing override uses the blueprint's deployed values. +An update or retry preserves the destination's retained assignments; changing +an override cannot silently remap them. These consumers remain implementation +work, not a claim that installed override support already exists. + +```yaml +podman: + identity_mapping: + schema: 1 + staging: + profile: bounded + uids: [{container_start: 0, host_start: 100000, count: 65536}] + gids: [{container_start: 0, host_start: 100000, count: 65536}] + deployed: + profile: bounded + uids: [{container_start: 0, host_start: 200000, count: 65536}] + gids: [{container_start: 0, host_start: 200000, count: 65536}] +``` + +These are illustrative administrator-provided ranges. Their presence is not +proof that the host has delegation or that another installation has not reserved +them. Never interpret them as Podman's intermediate parent-namespace IDs. + +### Engineering contracts and ordering + +The [controller engineering draft](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-engineering.md) +defines separate manager enablement and current-activation intent, control epochs, +exclusive kernel-backed owner leases, +per-process receipts, bounded engine calls, retry accounting, exact reattachment +and manager-stop lock ordering. Extend the existing machine-local deployment +state; do not add a second registry or portable authority. Current-activation stop and durable +revocation/preparation inhibition outrank recovery within their respective scope. A wait error, missing engine +observation or successful stop command is not proof that the runtime is retired. + +Storage preparation receives a frozen manifest of declared relative objects, +held managed-root descriptors, selected geometry and fresh operation/revision +authority. It returns exact observed object identities and metadata, or an +inhibited failure. No user path, image code, callback or recursively traversed +application tree runs with setup authority. Reject symlink/mount/alias/type +substitution. Existing recorded data is verified; it is not silently repaired. +Observe every relevant attachment and fence its registered recovery owner before +protected mutation. G connects its actual controller adapter to this F contract; +F does not depend on a future G implementation to qualify unattached staging. + +The [mechanism engineering contract](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-mechanism-engineering.md) +defines the bounded build-time image-destination projection and its actual +mount observations. Extend the verified image stream and existing build lock; +do not discover a general executable/dependency closure or export the image on +every launch. Bind literal/resolved destinations and content facts to the locked +immutable image, platform, root policy and exact mount selection. Missing or +stale required evidence means rebuild or refusal. Preserve ordinary absent-field +canonical identity. F1.1.2 owns this producer and validation integration once. +F3.1.2 replaces the private E closure placeholder with this enforceable policy +and complete actual root/mount/source observations; update request, validator +and provider together. Trusted paths and ancestors remain independently +protected, including a generation-verified read-only `/reploy-probe` file bind +when the application root is writable. Reuse the packaged verifier artifact +and private operation/installation workspace with exact lifetime cleanup. + +Network setup must occur while the trusted mapped process is inhibited, before +host release. Reuse the nftables installer, which currently runs too late inside +the post-release sandbox path. F's production provider must obtain effective +namespace/mount/code/kernel observations bound to the exact frozen plan and +pinned process; returning request digests or configured engine strings is +insufficient. Final authority transitions still run on the qualified native +exec path, close all setup descriptors and leave no stronger resident Reploy +identity process in the container. A final capability that defeats the required +network boundary remains an explicit pre-effect refusal until qualified. + +Keep public/local/ambiguous egress meanings, but do not treat allowed egress as +peer permission. Admit only private per-installation namespaces and declared +endpoint publication. Observe actual bindings and cleanup, including IPv6, +resolvers, host gateway and direct peer paths. Publication requires effective +pre-start evidence; an absent compatible observation is a failed gate. + +The controller transport reuses existing Unix and native Windows pipe dialers +and PTY attachment. Add narrow in-process exact-container wait and required +non-TTY stream framing, half-close, cancellation and backpressure. Remove the +lifetime-long CLI child rather than adding a controller beside it. Keep bounded +short-lived CLI operations where their existing behavior is useful. Compare +the complete process tree under idle, streaming and recovery; the standalone +waiter measurement does not establish full-controller memory savings. + +E's `StartupComplete` requirement needs a production source. G1.5.1 owns the +bounded private exec-status channel in the supporting mechanism contract: +prepare/pin its FIFO before release, verify the exact startup header, unlink +before application execution, and retain a writer until atomic close-on-exec. +The qualified raw tail detaches its descriptor table, moves the private status +writer to close-on-exec descriptor 3, closes other high descriptors immediately, +then executes or writes a fixed errno and exits without Go work. It retains E's +existing close-range operation instead of requiring a newer range flag. +The host requires clean channel EOF plus a fresh positive kernel live-image +observation through the exact existing process pin; EOF, running state, argv +or an app marker alone cannot claim startup. Preserve final credentials and +private descriptor isolation, requalify the affected transition, and keep the +guard if evidence is unavailable. Actual hooks/readiness and detached handoff +remain separate completion steps. A lost uncommitted channel means exact +retirement/recovery, never fabricated acknowledgement or private-input replay. + +Private `.env` readers/parsers and current once-per-process semantics already +exist. Use a distinct internal mapped startup mode: after pre-release network +setup and host admission, persist transfer intent, send an exact bounded private +preamble, the unchanged E release frame and then application stdin. Startup +uses the existing private parser and unbuffered descriptor reader, then gives +the environment to the existing final native transition. No bytes after the E +frame are reinterpreted, and no application bytes are prefetched. The original +admission deadline bounds the entire sequence. Partial/invalid/cancelled input +executes no application; retire an uncertain instance before fresh admission. +Reattachment never resends input to a surviving process. Mapped final exec's +guard remains until this composition and descriptor/credential closure qualify. +No stronger occupied-namespace exec, retained secret snapshot, engine Env value, +secret argv/metadata or silent omission is introduced. Preserve ordinary FIFO +compatibility, the separate RDA hotfix and controlled-session parent-loss +contract. The supporting contract records the concrete framing and proof. + +### Mechanism readiness and proof gates + +The supporting [prerequisite audit](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md) +distinguishes existing interfaces from missing providers. The following are +load-bearing work, not completed qualifications: + +| Gate | Required result before dependent work | Evidence currently available | +| --- | --- | --- | +| F1.0 | Qualify the accepted conservative image-content boundary, confined descriptors and writable-root trusted artifact protection. | Disposable shadow/ancestor negative controls and 22 layered projection cases pass; eligible-engine descriptor/source-race and product observation checks remain. | +| F3.0 | Eligible rootless engine network/namespace/effective-policy and endpoint mechanism, including final capability constraints. | Actual pre-release nft setup/readback and peer egress controls pass in isolated fixtures; eligible-engine DNS/IPv6/private-peer/endpoints remain. | +| G1.0 | Direct wait/stream/EOF and mapped private-input feasibility without stronger occupied-namespace setup. | WSL Docker wait/exit/cancellation, seven private-parser cases and five refined exec-status process/FIFO cases pass; actual engine/native/final-authority and startup-race qualification remains. | +| G1.9–G1.14 | Actual manager/account/scope, login/boot, engine availability and recovery on supported hosts. | Linux system runner and native dialers exist. New user/desktop adapters and real native qualification remain work. | + +F1.1.1–F4.2 remain qualification-gated until the relevant F1.0/F3.0 results +settle their actual providers. G1.4–G1.13 additionally depend on G1.0's safe +transport/private-input composition. Native adapter slices remain qualification- +incomplete until their actual host checks pass. These are not declarations of +implementation readiness merely because candidate names and ranges exist. + +A failed gate stops its dependents and returns to engineering. It cannot be +closed by a document rewrite, copied evidence, new qualification flag or an +unapproved compatibility restriction. If a mechanism needs broader authority, +new image/consumer obligations, weaker guarantees or materially larger scope, +return for the affected architecture decision before production work. Qualification +rows may reuse valid existing evidence; they do not require a fabricated code +change or an automatic extra PR when no fixture change is needed. + +### Counting and forecast + +Use the existing delivery counting convention: added or rewritten handwritten +physical LF lines, including comments, blanks and shipped configuration. Count +moved code at its destination, do not subtract deletions, and report mechanical +moves/deletions separately. Production/configuration and test/fixture counts +are separate. Exclude prose, generated files, dependencies and observations. +Do not compare these values directly with historical delivered gross additions +plus removals. No working-hour or token budget is inferred. + +The [sizing basis](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md) +records inspected seams and component allocations. Ranges price incremental +work against those source seams, not complete reused files. Common state, fixtures and transports are owned once. Missing +native adapters are priced explicitly. Upper bounds remain conditional on the +mechanism gates; this forecast is not an implementation-readiness claim. +Target 500 production lines per implementation PR; hard maximum 750. Split +before implementation if new evidence invalidates a range. Do not weaken a +parent requirement to fit the target. + +| Group | Candidates | Production/configuration LOC | Test/fixture LOC | +| --- | ---: | ---: | ---: | +| F | 13 | 2,690–4,440 | 6,850–11,200 | +| G | 19 | 4,130–7,010 | 10,700–17,850 | +| **Combined** | **32** | **6,820–11,450** | **17,550–29,050** | + +The original logical-parent forecast was 2,400–4,200 production and +4,300–7,050 test lines. The revised draft adds concrete sandbox observation, +entry coverage, crash/stop state, direct transport and native manager work that +the parent estimates did not price adequately. The intermediate retained-input +engine-retry forecast is historical analysis, not another delivery queue. Its +line-count convention excluded unchanged moves, so do not report its numerical +delta as a strictly normalized before/after result. + +### Ordered candidates and completion checks + +The order below is the proposed delivery order. Dependencies are actual prior +results, including proof outcomes. The proposed decimal children replace their +F/G parent candidates only after review and supported campaign adoption. + +| Slice | Concrete change | Production LOC | Test/fixture LOC | Prerequisites | +| --- | --- | ---: | ---: | --- | +| F1.0 | Prove the managed-data and immutable-code boundary | 0–0 | 350–650 | E4.2 | +| F1.1.1 | Declare managed metadata and intrinsic root policy | 280–430 | 450–750 | F1.0 | +| F1.1.2 | Bind image-destination projection to the generation | 350–550 | 700–1100 | F1.1.1 | +| F1.2 | Prepare declared objects through pinned descriptors | 300–480 | 600–950 | F1.1.2 | +| F1.3 | Confine preparation to selected subordinate authority | 240–400 | 450–750 | F1.2 | +| F2.1 | Record storage ownership and exclusive preparation | 280–460 | 550–850 | F1.3 | +| F2.2 | Fence recovery owners before protected storage effects | 240–420 | 550–900 | F2.1 | +| F3.0 | Prove eligible rootless network and effective observation | 0–0 | 450–800 | F2.2 | +| F3.1.1 | Prepare trusted network policy before mapped release | 180–330 | 350–650 | F3.0 | +| F3.1.2 | Supply production mount and kernel sandbox observations | 300–500 | 650–1000 | F1.1.2; F3.1.1 | +| F3.2 | Enforce private installation network boundaries | 260–440 | 550–900 | F3.1.2 | +| F4.1 | Publish and observe only declared endpoints | 260–430 | 500–800 | F3.2 | +| F4.2 | Qualify storage and network together | 0–0 | 700–1100 | F4.1 | +| G1.0 | Prove controller transport and interrupted startup boundaries | 0–0 | 450–800 | F3.0 | +| G1.1 | Select phase mappings under the Podman input | 280–450 | 550–900 | F1.1.1 | +| G1.2 | Connect approved identity maintenance commands | 200–340 | 400–650 | G1.1 | +| G1.3 | Install an independent destination owner | 320–520 | 600–1000 | G1.2; F4.2 | +| G1.4 | Declare Reploy restart policy and durable controller state | 300–480 | 550–900 | G1.0; G1.3 | +| G1.5.1 | Produce trusted mapped startup-completion evidence | 350–600 | 800–1300 | G1.4; F1.3; F3.1.2 | +| G1.5.2 | Recover workloads through the shared controller loop | 300–500 | 650–1050 | G1.5.1; F2.2 | +| G1.6 | Wait on the engine without a persistent Docker CLI child | 160–290 | 350–600 | G1.5.2 | +| G1.7 | Replace lifetime stream children with bounded direct attachment | 260–440 | 550–950 | G1.6 | +| G1.8 | Integrate private input with the mapped final transition | 230–410 | 550–900 | G1.7; F1.3 | +| G1.9 | Install and control the Linux user controller | 180–320 | 400–700 | G1.8 | +| G1.10 | Add the macOS user controller adapter | 220–400 | 450–800 | G1.9 | +| G1.11 | Add the native Windows user controller adapter | 250–440 | 500–850 | G1.10 | +| G1.12 | Connect checked persistent controls and replacement | 300–500 | 600–1000 | G1.11 | +| G1.13 | Connect fresh commands and tests with their existing semantics | 260–440 | 550–950 | G1.12 | +| G1.14 | Qualify common controller behavior on supported hosts | 0–0 | 700–1150 | G1.13 | +| G2.1 | Revoke active runtime while retaining ownership | 260–440 | 500–850 | G1.14; F2.2 | +| G3.1 | Reconcile interrupted activation and exact cleanup | 260–440 | 550–900 | G2.1 | +| G3.2 | Qualify the complete public multi-identity launch path | 0–0 | 1000–1600 | G3.1; F4.2 | + +#### F1.0 — Prove the managed-data and immutable-code boundary + +Disposable descriptor/alias/mount and image-shadow probes. Reuse the accepted image-content projection and actual shadow/ancestor proofs; qualify the remaining confined-descriptor and trusted-source lifetime races before dependent production preparation. + +Prerequisites: E4.2. Existing source seams: `internal/dockerdeploy/mapped_sandbox_proof.go`, `internal/dockerdeploy/runtime_host_filesystem_linux.go`. + +Completion: Empty/absent/directory-only destinations work under read-only root; populated image destinations refuse writable managed mounts. Protected-path escape and trusted-source replacement refuse before effects, including under root writable opt-in. Ordinary installs retain their behavior. Failed eligible-engine proof blocks dependents; new restrictions or weaker guarantees require design review. + +#### F1.1.1 — Declare managed metadata and intrinsic root policy + +Accepted owner/group/permissions and separate relative directories/files maps. Intrinsic root defaults read-only, with explicit environment.mounts.root writable policy. Preserve ordinary omitted-field identity. + +Prerequisites: F1.0. Existing source seams: `internal/blueprint/syntax.go`, `internal/blueprint/model.go`, `internal/dockerdeploy/execution_plan.go`. + +Completion: Resolve contextual owners and primary groups, reject unsafe paths/unknown principals before effects, bind the root policy canonically without admitting an external bind over /. + +#### F1.1.2 — Bind image-destination projection to the generation + +Collect bounded layer metadata during the existing immutable-image stream, apply whiteout/alias/type rules and emit the closed per-destination projection defined in the supporting contract. Bind optional mapped-only evidence in the existing build lock to image/config/platform/rootfs, mount selection and root policy. Connect validation, publication and exact reuse without a second store or launch-time full export. + +Prerequisites: F1.1.1. Existing source seams: `internal/dockerdeploy/image_identity_layers.go`, `internal/dockerdeploy/image_identity_archive.go`, `internal/deploy/build_lock.go`, `internal/dockerdeploy/current_build_reuse.go`. + +Completion: The 22 projection controls and aggregate parser/resource/cancellation limits pass; forged/missing/stale evidence cannot admit populated writable mounts. Ordinary omitted-field canonical bytes remain stable. Rebuilt image/mount-policy evidence is required after changes; portable evidence never replaces host admission. + +#### F1.2 — Prepare declared objects through pinned descriptors + +Descriptor-relative inert directory/file creation and existing-object verification, parent-first directories then files. No symlink/mount crossing, alias/type substitution, recursive ownership repair or application code. + +Prerequisites: F1.1.2. Existing source seams: `internal/dockerdeploy/runtime_host_filesystem_linux.go`. + +Completion: Exact declared metadata succeeds; substitution, hardlink and foreign-tree races leave protected targets unchanged. Count shared adversarial fixtures here once. + +#### F1.3 — Confine preparation to selected subordinate authority + +Reviewed setup-helper invocation with freshly validated selected geometry, pinned handoff descriptors, numeric ownership and bounded cancellation. Reuse existing mapping/writer authority. + +Prerequisites: F1.2. Existing source seams: `internal/deploy/identity_mapping_operations.go`, `internal/probe/mapped_exec_linux.go`. + +Completion: Private subordinate ownership and recipient access work without host-root/operator aliases, unrelated mutations, application execution or a surviving privileged helper. The earlier namespace/file probe is partial feasibility evidence only. + +#### F2.1 — Record storage ownership and exclusive preparation + +Managed resource identities and preparation/attachment inhibition in the existing transaction. Capture selected handles, revisions and pending effects; no independent resource registry. + +Prerequisites: F1.3. Existing source seams: `internal/deploy/state_v1.go`, `internal/deploy/identity_mapping_native_state.go`. + +Completion: Concurrent attach/prepare and stale geometry refuse before writes; crashes retain exact ownership and reservations. A process exit or API success alone cannot prove an unattached resource. + +#### F2.2 — Fence recovery owners before protected storage effects + +Exact retirement and restart-owner fence contract consumed by later controller adapters. Keep the operation lock out of external manager-stop waits; require fresh retirement observations before preparation. + +Prerequisites: F2.1. Existing source seams: `internal/dockerdeploy/current_workload_lifecycle_mapped.go`, `internal/deploy/control_lease.go`. + +Completion: No registered engine/controller owner can recreate during preparation or undo stop/revocation. Lost observation retains inhibition; retained application data and reservations remain intact. + +#### F3.0 — Prove eligible rootless network and effective observation + +Disposable proof on an E-compatible rootless Podman path: trusted pre-release setup, namespace-pinned effective rules, DNS, private peer boundary and endpoint transport. Establish which final capabilities preserve that boundary. + +Prerequisites: F2.2. Existing source seams: `internal/probe/network_firewall_linux.go`, `internal/dockerdeploy/podman_mapped_waiter.go`. + +Completion: Positive allowed egress/endpoints and direct peer, gateway and IPv6 negatives with exact cleanup. Existing C patched-guest and ordinary Docker results do not close this gate. Failure blocks dependent code rather than weakening policy. + +#### F3.1.1 — Prepare trusted network policy before mapped release + +Move only trusted policy setup before the release wait, using the frozen plan and existing nftables installer. Keep application execution after the host gate and final native transition; do not copy the installer. + +Prerequisites: F3.0. Existing source seams: `internal/probe/mapped_wait.go`, `internal/probe/sandbox_exec_linux.go`, `internal/probe/network_firewall_linux.go`. + +Completion: Effective setup exists while payload is inhibited; failure/timeout never releases the application; no setup network authority survives the final transition. + +#### F3.1.2 — Supply production mount and kernel sandbox observations + +Actual F provider bound to exact inhibited PID/namespaces, locked image-destination projection, complete actual root/mount/source state and compiled capability-conditioned kernel policy. Replace the unimplemented complete-code-closure placeholder in request, validator and provider together. Reuse the packaged verifier for a protected top-level read-only bind under writable root. Obtain effective observations, not echoed request digests or engine strings alone. + +Prerequisites: F1.1.2, F3.1.1. Existing source seams: `internal/dockerdeploy/mapped_sandbox_proof.go`, `internal/dockerdeploy/podman_mapped_waiter.go`. + +Completion: Real protected-path/source/ancestor, root-policy and effective-kernel positives/negatives; stale PID, unplanned/substituted mount, missing projection, writable image-shadow and unverified filter facts refuse before release. Root opt-in never exposes trusted Reploy bytes or broadens capabilities. + +#### F3.2 — Enforce private installation network boundaries + +Canonical private namespace/attachment shape, resolver and host-gateway handling, public/local/ambiguous policy and capability-conditioned refusals. No shared private network or implicit peer reachability. + +Prerequisites: F3.1.2. Existing source seams: `internal/dockerdeploy/podman_mapped_policy.go`, `internal/probe/network_firewall_linux.go`. + +Completion: Selected transport preserves intended allowed traffic and denies undeclared TCP/UDP/abstract-socket peers. Unsupported capability or transport combinations fail explicitly before effects. + +#### F4.1 — Publish and observe only declared endpoints + +Typed create/inspect endpoint fields, staged/deployed port selection and exact network/publication ownership and cleanup. + +Prerequisites: F3.2. Existing source seams: `internal/dockerdeploy/execution_plan.go`, `internal/dockerdeploy/podman_application_identity.go`. + +Completion: Only declared addresses/ports are reachable; mismatched publication blocks payload. Partial teardown retains uncertainty and never removes a foreign object. + +#### F4.2 — Qualify storage and network together + +Integrated exact/bounded preparation, attachment, effective sandbox, network, endpoint and failure evidence on the qualified engine. Reuse child fixtures without repeating their entire test inventory. + +Prerequisites: F4.1. Existing source seams: `internal/dockerdeploy/current_workload_lifecycle_mapped_test.go`. + +Completion: Observable supported-boundary filesystem/network positives and refusal/no-mutation negatives close F's parent obligations; injected fixtures alone do not. + +#### G1.0 — Prove controller transport and interrupted startup boundaries + +Bounded disposable direct wait/attach/EOF/cancellation and mapped private-input feasibility. Reuse the successful WSL Docker wait proof; add missing Podman and native transport evidence where load-bearing. + +Prerequisites: F3.0. Existing source seams: `internal/dockerdeploy/docker_pty_attach.go`, `internal/dockerdeploy/private_workload_environment_inject.go`. + +Completion: Exact exit status, no duplicate/foreign start, private input confinement and no setup descriptors/authority at exec. Missing native tests remain named adapter readiness gates; no false universal qualification. + +#### G1.1 — Select phase mappings under the Podman input + +Direct schema-1 relocation to podman.identity_mapping with staging/deployed selection, typed whole-array overrides and independent install mapping selection. Update every opt-in/retained-state guard and resolved serialization path. + +Prerequisites: F1.1.1. Existing source seams: `internal/blueprint/syntax.go`, `internal/deploy/state_v1.go`. + +Completion: Correct phase/override precedence; no old undeployed mapping path, fallback profile, authority copied from staging or declaration-removal bypass. Existing typed overrides supply changed values without a mapping-specific CLI parser or source-sidecar adoption. + +#### G1.2 — Connect approved identity maintenance commands + +Public check/configure/status/recover wiring to delivered identity operations with bounded contexts, diagnostics and held-lock authority. Reuse approved command contracts. + +Prerequisites: G1.1. Existing source seams: `internal/cli/cli.go`, `internal/deploy/identity_mapping_operations.go`. + +Completion: Actual commands reach existing operations; pending/dormant/revoked state stays authoritative; commands do not launch payload or adopt copied state. + +#### G1.3 — Install an independent destination owner + +Destination locks, strict destination-local override snapshot, publication, independent mapping decision, immutable portable-build reuse, overlap/revision checks and partial install recovery. + +Prerequisites: G1.2, F4.2. Existing source seams: `internal/dockerdeploy/provider_install_run.go`, `internal/dockerdeploy/provider_install_plan.go`. + +Completion: Staging and user install have separate reservations; source state/data remain unchanged. Never copy source pending operations, admission, managed ownership or secrets. + +#### G1.4 — Declare Reploy restart policy and durable controller state + +environment.workload.restart parsing/serialization and legacy conflict checks; separate manager enablement and activation intent, control epoch, controller lease and durable attempt/retry accounting in existing deployment state. Shared controller test fixture belongs here once. + +Prerequisites: G1.0, G1.3. Existing source seams: `internal/blueprint/syntax.go`, `internal/blueprint/model.go`, `internal/deploy/deployment_state_v1.go`, `internal/deploy/control_lease.go`. + +Completion: No automatic exit retry by default. Start/stop, enable/disable and retry policy remain independent. Controller recovery cannot undo a stop within the same activation or reset its allowance; a genuine enabled boot/login or explicit start can create a new activation only after recovery gates pass. Security inhibition survives all activations. Unknown state and competing owners refuse. + +#### G1.5.1 — Produce trusted mapped startup-completion evidence + +Connect E's private startup confirmation to the bounded managed status FIFO, exact binding, unlink-before-release and fresh post-EOF kernel observation. Refine the final native tail to preserve only the close-on-exec status descriptor, close other high descriptors immediately and report fixed failure without Go work. Keep final authority unchanged. Reuse the existing process pin and lifecycle receipt; no application marker, tracer, resident helper or extra authority store. + +Prerequisites: G1.4, F1.3, F3.1.2. Existing source seams: `internal/dockerdeploy/current_workload_lifecycle_mapped.go`, `internal/dockerdeploy/mapped_process_linux.go`, `internal/probe/mapped_exec_linux.go`. + +Completion: Actual inhibited-versus-executed startup is distinguished on the supported mapped path. Both profiles/native tuples pass same-binary, failure/cancel/kill/exit-race and private-fd controls. Missing/stale/ambiguous status or kernel evidence refuses completion. Old E close-before-exec results are not relabelled new-tail qualification; uncertain startup retires before another admission. Hooks/readiness and detachment keep their own gates. + +#### G1.5.2 — Recover workloads through the shared controller loop + +Adapt the existing runner for qualified user ownership, host-manager/workload failure separation, policy/backoff, exact reattachment and interrupted-attempt recovery. No persistent approval file or secret snapshot. + +Prerequisites: G1.5.1, F2.2. Existing source seams: `internal/dockerdeploy/installed_service_container.go`, `internal/dockerdeploy/installed_service_wait.go`, `internal/dockerdeploy/workload_lifecycle.go`. + +Completion: Confirmed surviving processes are observed without reinjection; failed mapped instances retire before fresh admission. Lost engine observation never implies death or authorizes a second workload. + +#### G1.6 — Wait on the engine without a persistent Docker CLI child + +Narrow in-process wait using existing local Unix/named-pipe dialers, pinned endpoint/API version, bounded result decoding and request/body cancellation. Retain short-lived CLI operations where appropriate. + +Prerequisites: G1.5.2. Existing source seams: `internal/dockerdeploy/installed_service_wait.go`, `internal/dockerdeploy/docker_endpoint.go`, `internal/dockerdeploy/docker_pty_attach_windows.go`. + +Completion: Preserve actual exit status and error distinction; disconnect/cancel releases resources and triggers checked reconciliation. Compare whole-process-tree idle memory, not just a minimal probe binary. + +#### G1.7 — Replace lifetime stream children with bounded direct attachment + +Reuse PTY transport and add only required non-TTY framing, incremental stdout/stderr handling, stdin half-close, backpressure and independent exit observation. Preserve attach-before-payload ordering. + +Prerequisites: G1.6. Existing source seams: `internal/dockerdeploy/docker_pty_attach.go`, `internal/dockerdeploy/live_run_container.go`, `internal/dockerdeploy/live_run_mapped.go`. + +Completion: Large/slow streams have bounded buffers; EOF is not exit status; actual stdin/cancellation and exact cleanup survive on supported transports. Preserve the separately delivered RDA hotfix contract. + +#### G1.8 — Integrate private input with the mapped final transition + +Reuse .env validation and one-shot delivery through the distinct bounded private-preamble startup mode, followed by the unchanged E frame and exact application stdin. Persist transfer intent after admission, retain the original deadline and feed the existing native final transition. No stronger occupied-namespace exec or secret bytes in durable state, argv or engine metadata. + +Prerequisites: G1.7, F1.3. Existing source seams: `internal/probe/private_workload_environment_linux.go`, `internal/probe/mapped_exec_linux.go`, `internal/dockerdeploy/private_workload_environment_inject.go`. + +Completion: One accepted recipient/process receives dummy input exactly once; other roles/transient commands do not. Interrupted transfer retires safely; final descriptors, credentials and capabilities remain correct before enabling the guarded path. + +#### G1.9 — Install and control the Linux user controller + +Per-installation systemd user adapter, embedded executable/account/endpoint binding, checked stop rather than raw Compose, readiness and removal. Reuse system-unit rendering without granting mapped system scope. + +Prerequisites: G1.8. Existing source seams: `internal/dockerdeploy/provider_install_systemd.go`, `internal/dockerdeploy/provider_install_host_commands.go`, `internal/dockerdeploy/install_types.go`. + +Completion: Actual user lifecycle and engine readiness; explicit manager/lingering prerequisites and WSL distribution limits. No auto elevation, hidden host configuration or fallback recovery owner. + +#### G1.10 — Add the macOS user controller adapter + +Per-installation launchd user adapter for the common ordinary-workload controller: install/control, account/session binding, engine/VM readiness and removal. No system backend or mapped macOS promise. + +Prerequisites: G1.9. Existing source seams: `internal/dockerdeploy/platform.go`, `internal/dockerdeploy/provider_install_host_commands.go`. + +Completion: Native user login/logout, controller failure, explicit stop, update/removal and engine recovery. An unqualified adapter cannot accept the new public Reploy restart field by silently delegating it. + +#### G1.11 — Add the native Windows user controller adapter + +Per-installation Task Scheduler user adapter, exact executable/identity, process lifetime/duplicate-instance settings, controller recovery and removal. Reuse native pipe/file validation; no Windows system service or mapped runtime expansion. + +Prerequisites: G1.10. Existing source seams: `internal/dockerdeploy/platform.go`, `internal/dockerdeploy/docker_pty_attach_windows.go`, `internal/cli/control.go`. + +Completion: Actual native Desktop Linux-engine lifecycle, quoting/paths, controller death, stop intent, login and removal. No default task duration termination, duplicate controller or claims based on WSL tests. + +#### G1.12 — Connect checked persistent controls and replacement + +Public up/down/restart/status/log/health and existing enable/disable dispatch, hooks/readiness and exact replacement with activation-scoped intent, independent enablement and manager fencing. Preserve legacy ordinary behavior unless explicitly selected by the new policy. + +Prerequisites: G1.11. Existing source seams: `internal/cli/control.go`, `internal/dockerdeploy/current_workload_lifecycle.go`, `internal/dockerdeploy/current_workload_lifecycle_mapped.go`. + +Completion: Up requires actual startup/handoff and never changes enablement; enable/disable do not implicitly start/stop. Repeated up has no duplicate injection; stop/restart cannot deadlock or race a new attempt. Real boot/login activation remains distinct from controller crash recovery. Ordinary behavior stays explicit; unsupported mapped paths remain guarded. + +#### G1.13 — Connect fresh commands and tests with their existing semantics + +Actual staged/test/hook/deployed command adapters and correct fresh-container classification. Preserve stdin/EOF/TTY, role and output/status semantics; no default raw-shell role or unqualified occupied exec. + +Prerequisites: G1.12. Existing source seams: `internal/dockerdeploy/current_app_command_run.go`, `internal/dockerdeploy/current_shell_run.go`, `internal/dockerdeploy/mapped_entry_plan.go`. + +Completion: Supported command journeys use the common complete gate; actual unqualified host-native/occupied/shared-session paths refuse before effects. Enum names cannot misclassify ordinary fresh deployed commands. + +#### G1.14 — Qualify common controller behavior on supported hosts + +Native Linux user/system ordinary, WSL, native Windows user and macOS user manager/session qualification with actual supported engines. System-scope mapped remains refused. Shared state tests are reused, not counted again. + +Prerequisites: G1.13. Existing source seams: `internal/dockerdeploy/platform_test.go`, `internal/dockerdeploy/installed_service_wait_test.go`. + +Completion: Install/start/stop/restart/update/rollback/remove, controller crash, engine outage and host/session activation evidence with exact versions/locations, retained data and full process-tree memory/CPU observations. Unsupported combinations remain explicit. + +#### G2.1 — Revoke active runtime while retaining ownership + +Active revocation composition, stopped/inhibited intent, every registered retry/replacement fence, exact stop/removal and retained mapping/data state. + +Prerequisites: G1.14, F2.2. Existing source seams: `internal/deploy/identity_mapping_operations.go`, `internal/dockerdeploy/current_workload_lifecycle_mapped.go`. + +Completion: Success means runtime retired and no recovery owner can restart it. Later admission rejects; data/reservations remain. Ambiguous cleanup cannot report revocation success. + +#### G3.1 — Reconcile interrupted activation and exact cleanup + +Complete pending-operation matrix across install, preparation, admission, private transfer, replacement, manager effects, revocation and network cleanup. Reuse B recovery and existing ownership checks. + +Prerequisites: G2.1. Existing source seams: `internal/deploy/identity_mapping_native_state.go`, `internal/dockerdeploy/live_run_recovery.go`. + +Completion: Each interruption has one supported safe continuation. No stale PID/copied receipt/name-only object authorizes adoption, duplicate start, data deletion or reservation release. + +#### G3.2 — Qualify the complete public multi-identity launch path + +Source-bound public exact/bounded stage/build/configure/run/test/install/start/retry/stop/revoke journeys and full refusal/cleanup matrix. Close every generic-launch row through supported interfaces. + +Prerequisites: G3.1, F4.2. Existing source seams: `internal/dockerdeploy/current_workload_lifecycle_mapped_test.go`, `.github/workflows/ci.yml`. + +Completion: No public guard opens before all required positive and negative evidence passes. Require every exact-head CI job and the separate remote reviews; injected/cross-build/WSL results cannot substitute for unavailable native qualifications. + +### Parent obligation ownership and delivery boundary + +| Parent outcome | Owning candidates and integrated check | +| --- | --- | +| F1 declared objects, fresh subordinate authority and immutable code/mount boundary | F1.0–F1.3 and F3.1.2; integrated F4.2. | +| F2 exclusive prepare/attach, crash inhibition and all restart owners | F2.1–F2.2; actual controller connection G1.5.2/G1.12; revocation G2.1; crash matrix G3.1. | +| F3 public/local egress plus private, non-peer-reachable namespace/policy | F3.0–F3.2; effective pre-release observations F3.1.2; integrated F4.2. | +| F4 declared endpoints, actual publication and exact cleanup | F4.1–F4.2; interrupted cleanup G3.1. | +| G1 independent install, explicit destination maps, every supported entry, hooks/readiness and recovery | G1.0–G1.14; exact retirement/new admission G1.5.2/G1.12; fresh commands G1.13. | +| Supported OS/scope/engine behavior without new mapped cohorts | G1.9–G1.14; existing unsupported system/remote/mapped cases remain refused. | +| G2 active revocation, retained ownership and future admission refusal | G2.1 with F2.2 and actual manager fences. | +| G3 uncertain effects, exact-resource cleanup and generic-launch | G3.1 and complete public positive/negative qualification G3.2. | + +Preserve public mapped inhibition through all intermediate slices. G3.2 alone +can close generic-launch after the actual required evidence and approvals. A +clean design review cannot substitute for runtime or native-platform proof. +Every implementation PR requires all current exact-head `ci.yml` jobs before +approval, including Linux, Bash evidence, every smoke target and both Windows +host jobs; derive the actual current names when running its PR cycle. + +Finish affected LLD review and stamps, checkpoint/publish the reviewed design +and complete its PR cycle before dependent implementation. The separately +authorized RDA cancellation hotfix remains an independent delivery after F/G +design and before F/G implementation. Do not merge, release or resume completed +E work. Later H–K are not part of this delivery selection. + + + ## Required design and evidence gates +F/G engineering inputs are the [prerequisite audit](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md), +[lifecycle analysis](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-lifecycle-options.md) +and [restart/fencing evaluation](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-restart-fencing-evaluation.md). +The [per-installation controller comparison](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-evaluation.md) +records the selected direction for detailed engineering. Its +[controller draft](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-engineering.md) +separates restart conditions from durable operator intent and targets removal +of lifetime-long Docker CLI children. Detailed slices and qualification remain +subject to the affected reviews; ordinary-install migration is not implied. +Proposed amendments in supporting analysis do not change the governing contract +without the affected review and maintainer decision. + Gates are work inside delivery, not extra code PRs. Review establishes design quality; only the maintainer can approve a new intended behavior or authority. The approved full-campaign architecture supplies the common intended-behavior @@ -1954,9 +2600,12 @@ Preserve that mapping and every slice-prefixed commit title. Do not create a duplicate PR or rewrite an approved ancestor. -## Current design request and D continuation +## Historical D design request and continuation — 2026-10-27 + +The following records the request at that time. D/E are now delivered; these +old runs and stopping instructions are not the current continuation. -The architecture PR #282 is published and approved, unmerged. Draft PR #284 +At that time, architecture PR #282 was published and approved, unmerged. Draft PR #284 owns the consolidated D design and native-process reconciliation. Current run `run_5yye79jpd9dbcb98rg135yrv00` finishes the approved amendment, document review, stamps, publication and exact-head PR cycle, records the continuation point and diff --git a/docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md b/docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md index 4eb01f6d..9cd5ad14 100644 --- a/docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md +++ b/docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md @@ -270,9 +270,13 @@ overlapping host-ID exception. ## Lifetime, replacement and runtime exec The foreground stock master is the required workload entry. Its exit ends that -container instance. The existing host owner may recreate it only through fresh -admission; no engine restart policy bypasses that check. Revocation stops the -whole container, including package-managed descendants, and fences replacement. +process instance and its package-managed descendants. A qualified engine policy +may restart the same unchanged admitted container using its original approval. +The existing host owner may create a replacement only through fresh admission. +Every process start still applies the trusted final credential/capability and +sandbox transition. Revocation stops the whole container and fences both +engine retry and host-owned replacement before success; retained data and +mapping reservations remain protected. The application may fork, exec, change among accepted identities, and replace its own workers within its already admitted workload authority. Those internal diff --git a/docs/MULTI_IDENTITY_SECURITY_CONTRACT.md b/docs/MULTI_IDENTITY_SECURITY_CONTRACT.md index c41c4d5f..a6abec15 100644 --- a/docs/MULTI_IDENTITY_SECURITY_CONTRACT.md +++ b/docs/MULTI_IDENTITY_SECURITY_CONTRACT.md @@ -1,6 +1,6 @@ --- status: Active -updated: 2026-10-27 +updated: 2026-11-01 summary: Podman-gated security contract and acceptance ledger for future isolated multi-identity Linux workloads. --- @@ -252,7 +252,9 @@ while an installation is inactive but its owned data remains. contract may allow read-only inputs through filtered materialization and writable inputs only through an explicit reservation and migration boundary. 6. **Fail closed without mutation.** Missing, revoked, overlapping, stale, or - inconsistent delegation and identity state fails before workload start. + inconsistent delegation and identity state fails before new-container or + external-entry admission. Only the explicitly qualified unchanged-container + retry exception below may reuse prior host admission. Failure does not silently repair, rewrite, or delete persistent state. A supported revocation operation also removes active workload resources and prevents autonomous restart before it reports success, while retained @@ -279,20 +281,47 @@ while an installation is inactive but its owned data remains. no shared network namespace or peer-reachable network attachment and cannot connect to each other's TCP, UDP, or Linux abstract Unix-socket services. -Every Reploy-controlled entry uses fresh host admission and the accepted -generation/role policy: initial start, external command/hook/session, runtime -exec and whole-container replacement. Normalize initial credentials, apply only -accepted capabilities, close private descriptors and verify the final sandbox. +Every Reploy-controlled creation, external command/hook/session, runtime exec +and whole-container replacement uses fresh host admission and the accepted +generation/role policy. The same exact unchanged, previously admitted container +may reuse that approval for engine-triggered process restarts. This exception +does not authorize another container, installation, engine, image, mapping, role, +mount configuration or runtime policy. Its safe startup mechanism and engine +behavior must be qualified before activation; an old one-shot release frame or +application-written marker is not such proof. Every process start still +normalizes initial credentials, applies only accepted capabilities, closes +private descriptors and verifies the final sandbox. Native forks, execs and worker replacement use already admitted workload authority; they do not authorize new mappings, mounts, secrets or capabilities. -The required foreground master's exit ends the instance. The host owns fresh -replacement and revocation, fences every restart owner and stops the entire -container. No engine restart policy or raw exec bypass is permitted. +The required foreground master's exit ends its process instance and its +descendants. A permitted engine restart may start that same admitted container +again within its unchanged authority. Reploy owns lifecycle policy, fresh +replacement and revocation. It fences every registered restart owner and proves +the required runtime retirement before preparation, cleanup or successful +revocation. Removing a container does not release its retained data or mappings. +No engine retry may recreate or adopt a container or bypass those completed +fences. Raw exec remains prohibited where its final transition is unqualified. + +The administrator must stop and fence affected workloads before changing host +delegation, accounts or reservations. Restart checks are not continuous +protection against such trusted administrator changes. Approval reuse does not +permit automatic repair, remapping or substitution to keep an invalid workload +running. Reploy-owned recovery semantics must cover supported user/system and +OS/engine combinations; this does not expand the mapped backend cohort. Container-provided executable code and dependencies are read-only by default; reject writable mounts that shadow protected code paths. Mutation requires explicit opt-in and gives up replacement protection for the affected code. -Trusted Reploy startup/control code remains protected. Binary-exclusive privilege +Trusted Reploy startup/control code remains protected. For the initial mapped +cohort, the default root policy uses a conservative image-content boundary: +an application-declared writable managed mount must resolve to an absent, +empty or directory-only image destination. Existing image files, including +data-only files, cause refusal. The existing root-writable opt-in relinquishes +application-image replacement protection; it never exposes trusted Reploy code. +This restriction does not change ordinary installations. The +[F/G mechanism contract](supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-mechanism-engineering.md) +defines bounded immutable-image projection and actual mount observation. +Binary-exclusive privilege inheritance is not promised by immutability: already privileged image code remains able to use its admitted authority. No host AppArmor/SELinux/IPE dependency is selected for this stage. Effective syscall policy must independently deny diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-controller-slice-forecast.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-controller-slice-forecast.json new file mode 100644 index 00000000..48af1b93 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-controller-slice-forecast.json @@ -0,0 +1,28 @@ +--- +artifact: swe-design-review-attestation +schema_version: 4 +scope_key: 759c4832028b9f6f5a5d726cff691c90a0792176e2d7c93e57732a8f63e9d5c7 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md"} +review_content_identity_sha256: deee38df7715833ef4ba01b1ef58d4bab0b5e417ecdb3db11751a772d839cf33 +target_content_identity_sha256: 143a1b6174c17a225083cf80284ef404dc8fb566d43be06f296dde8347b0b539 +baseline_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json", "repository": ".", "sha256": "728d53f86612af0d6ba45c3d0fe84e61b41ac5e4f622b43132d2314775e389ab"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md", "repository": ".", "sha256": "3013953a1cafa68f6b1175818a167fab5d61f406970a9f0c61621e23d8809e98"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md", "repository": ".", "sha256": "8a073c695effc36a3c2fd2baba3d8fcf85627e152968682154833270da627a5e"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [] +document_repository: "." +document_path: "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json" +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 728d53f86612af0d6ba45c3d0fe84e61b41ac5e4f622b43132d2314775e389ab +verdict: clean +attested_at: 2026-11-01T20:59:58Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-4 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-prerequisite-audit.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-prerequisite-audit.md new file mode 100644 index 00000000..aa53b50f --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-prerequisite-audit.md @@ -0,0 +1,28 @@ +--- +artifact: swe-design-review-attestation +schema_version: 4 +scope_key: 759c4832028b9f6f5a5d726cff691c90a0792176e2d7c93e57732a8f63e9d5c7 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md"} +review_content_identity_sha256: deee38df7715833ef4ba01b1ef58d4bab0b5e417ecdb3db11751a772d839cf33 +target_content_identity_sha256: 143a1b6174c17a225083cf80284ef404dc8fb566d43be06f296dde8347b0b539 +baseline_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json", "repository": ".", "sha256": "728d53f86612af0d6ba45c3d0fe84e61b41ac5e4f622b43132d2314775e389ab"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md", "repository": ".", "sha256": "3013953a1cafa68f6b1175818a167fab5d61f406970a9f0c61621e23d8809e98"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md", "repository": ".", "sha256": "8a073c695effc36a3c2fd2baba3d8fcf85627e152968682154833270da627a5e"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [] +document_repository: "." +document_path: "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md" +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 3013953a1cafa68f6b1175818a167fab5d61f406970a9f0c61621e23d8809e98 +verdict: clean +attested_at: 2026-11-01T20:59:58Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-4 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-sizing-basis.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-sizing-basis.md new file mode 100644 index 00000000..e3ad5e13 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/.review/fg-sizing-basis.md @@ -0,0 +1,28 @@ +--- +artifact: swe-design-review-attestation +schema_version: 4 +scope_key: 759c4832028b9f6f5a5d726cff691c90a0792176e2d7c93e57732a8f63e9d5c7 +scope: {"kind": "path", "primary_target": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "selector": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md"} +review_content_identity_sha256: deee38df7715833ef4ba01b1ef58d4bab0b5e417ecdb3db11751a772d839cf33 +target_content_identity_sha256: 143a1b6174c17a225083cf80284ef404dc8fb566d43be06f296dde8347b0b539 +baseline_content_identity_sha256: f42b08528a26c7ad252a8503cc9a13d6322ca0f53930e5dac2b56402fd450474 +target_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_IMPLEMENTATION_PLAN.md", "repository": ".", "sha256": "6f278d93ec80c42f9b905f990c585c1c9db7082cd7e7c2a827e9b840582b6c14"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json", "repository": ".", "sha256": "728d53f86612af0d6ba45c3d0fe84e61b41ac5e4f622b43132d2314775e389ab"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md", "repository": ".", "sha256": "3013953a1cafa68f6b1175818a167fab5d61f406970a9f0c61621e23d8809e98"}, {"executable": false, "path": "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md", "repository": ".", "sha256": "8a073c695effc36a3c2fd2baba3d8fcf85627e152968682154833270da627a5e"}] +baseline_documents: [{"executable": false, "path": "docs/MULTI_IDENTITY_D_DESIGN.md", "repository": ".", "sha256": "078e63e84d432c2aad03120ca30e40ec4572a8970e6bf6be9d5008608a30f435"}, {"executable": false, "path": "docs/MULTI_IDENTITY_E_DESIGN.md", "repository": ".", "sha256": "7757ebe99b2135d9c7bf78ab29c844c2f1d956420a9fab6fea9dcdd0986f40a8"}, {"executable": false, "path": "docs/MULTI_IDENTITY_MAIL_RECONCILIATION.md", "repository": ".", "sha256": "78f9f1a1de6098b2b5653c5cd10096e7b0e5023230b1e9365638e317cc64fb95"}, {"executable": false, "path": "docs/MULTI_IDENTITY_SECURITY_CONTRACT.md", "repository": ".", "sha256": "f6049a45f86b96b5a7c6616926804871d77e8b86bf3305cc42d3910320fc5b76"}] +design_dependency_documents: [] +document_repository: "." +document_path: "docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md" +document_revision_provenance: "87899abecced4c5f8b3513ecee66793ecdd2df37" +document_sha256: 8a073c695effc36a3c2fd2baba3d8fcf85627e152968682154833270da627a5e +verdict: clean +attested_at: 2026-11-01T20:59:58Z +--- + + +# SWE design-review attestation + +Review freshness binds target and baseline document bytes and executable state +listed in the version-4 header. Revisions are provenance only. + +## Durable review state + +No standing rejected or deferred decisions. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/summary.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/summary.json new file mode 100644 index 00000000..cfb3b3c6 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/summary.json @@ -0,0 +1,43 @@ +{ + "at": "2026-11-01T10:06:36.415555+00:00", + "command": [ + "go", + "test", + "-json", + "-count=1", + "./internal/dockerdeploy", + "-run", + "^(TestInstalledService(NonzeroExitRequiresExactManagerStop|StopIntentIsIndependentOfContainerExitCode|WaitDoesNotQueryManagerForSuccessOrInvalidExit|WaitPreservesEngineFailureWithoutManagerQuery|EngineWait.*|StartupCompletionDoesNotCancelServiceMonitoring|StartupParentCancellationIsNotTimeout|PrivateStartFailureUsesOneCleanupDeadline)|Test(ConfirmedWorkloadSurvivesPrivateEnvironmentPresenceTransitionsV1|PrivateEnvironmentFreshStartupUsesNewProcessClaimV1|InterruptedStartupAndStopResumeRespectHookPrerequisitesV1|FailedStartupStillStopsWhenJournalIOFailsV1|RecoveryStopRequiresObservedShutdownV1)|TestControlledSessionWatchdog(ParentLossRemovesOnlyManifestResources|RetriesParentLossCleanupWhileDockerIsUnavailable|StopsParentLossRetryAfterBootChanges))$", + "-timeout=120s" + ], + "exit_code": 0, + "elapsed_seconds": 3.9432481810072204, + "passed_top_level_tests": [ + "TestControlledSessionWatchdogParentLossRemovesOnlyManifestResources", + "TestControlledSessionWatchdogRetriesParentLossCleanupWhileDockerIsUnavailable", + "TestControlledSessionWatchdogStopsParentLossRetryAfterBootChanges", + "TestInstalledServiceEngineWaitUsesRemainingPreparationDeadline", + "TestInstalledServiceEngineWaitDefersWorkUntilSelectedEngineIsReady", + "TestInstalledServiceEngineWaitRetainsTimeoutAndUnderlyingFailure", + "TestInstalledServiceEngineWaitHonorsCancellationBeforeAndDuringBinding", + "TestInstalledServiceStartupCompletionDoesNotCancelServiceMonitoring", + "TestInstalledServiceStartupParentCancellationIsNotTimeout", + "TestInstalledServicePrivateStartFailureUsesOneCleanupDeadline", + "TestInstalledServiceNonzeroExitRequiresExactManagerStop", + "TestInstalledServiceStopIntentIsIndependentOfContainerExitCode", + "TestInstalledServiceWaitDoesNotQueryManagerForSuccessOrInvalidExit", + "TestInstalledServiceWaitPreservesEngineFailureWithoutManagerQuery", + "TestPrivateEnvironmentFreshStartupUsesNewProcessClaimV1", + "TestConfirmedWorkloadSurvivesPrivateEnvironmentPresenceTransitionsV1", + "TestRecoveryStopRequiresObservedShutdownV1", + "TestInterruptedStartupAndStopResumeRespectHookPrerequisitesV1", + "TestFailedStartupStillStopsWhenJournalIOFailsV1" + ], + "skipped": [], + "stdout_sha256": "45ad93d32aeb27e55fdd3e301d16660f44f4a4fb5d7b02ffffa8c32ac8db13a2", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "qualification": "Production functions exercised through existing fixtures; no live controller crash, host restart, native desktop or mapped integration qualification.", + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "raw_evidence_directory": "temp/reviews/fg-controller-evaluation-20261101", + "review": "current-agent evidence inspection only; no independent review or stamp" +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/wait-memory-summary.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/wait-memory-summary.json new file mode 100644 index 00000000..5a55ef87 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/controller-evidence/wait-memory-summary.json @@ -0,0 +1,245 @@ +{ + "at": "2026-11-01T10:39:03.852354+00:00", + "scope": "Disposable client/wait proof only; not full Reploy or a reproduction of the reported workload.", + "engine": { + "Platform": { + "Name": "Docker Desktop 4.93.0 (240920)" + }, + "Version": "29.8.1", + "ApiVersion": "1.56", + "MinAPIVersion": "1.40", + "Os": "linux", + "Arch": "amd64", + "Components": [ + { + "Name": "Engine", + "Version": "29.8.1", + "Details": { + "ApiVersion": "1.56", + "Arch": "amd64", + "BuildTime": "Tue Sep 15 16:25:38 2026", + "Experimental": "false", + "GitCommit": "464cd50", + "GoVersion": "go1.26.8", + "KernelVersion": "6.18.54.1-microsoft-standard-WSL2", + "MinAPIVersion": "1.40", + "Module": "github.com/moby/moby/v2", + "ModuleVersion": "v2.0.0+unknown", + "Os": "linux" + } + }, + { + "Name": "containerd", + "Version": "v2.3.5", + "Details": { + "GitCommit": "1294c24a7da8e5a793ed378161673abe94118892" + } + }, + { + "Name": "runc", + "Version": "1.5.1", + "Details": { + "GitCommit": "v1.5.1-0-g8f2685a4" + } + }, + { + "Name": "docker-init", + "Version": "0.19.0", + "Details": { + "GitCommit": "de40ad0" + } + } + ], + "GitCommit": "464cd50", + "GoVersion": "go1.26.8", + "KernelVersion": "6.18.54.1-microsoft-standard-WSL2", + "BuildTime": "2026-09-15T16:25:38.000000000+00:00" + }, + "kernel": "6.18.54.1-microsoft-standard-WSL2", + "endpoint": "unix:///var/run/docker.sock", + "image": "sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d", + "label": "reploy.fg-controller-probe=621ef1a9c2bd4279819bfec5ead656a2", + "samples": [ + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + }, + { + "docker_cli_kib": { + "Rss": 28068, + "Pss": 26904, + "Private_Clean": 18148, + "Private_Dirty": 7604 + }, + "go_direct_kib": { + "Rss": 8160, + "Pss": 6683, + "Private_Clean": 0, + "Private_Dirty": 6660 + } + } + ], + "limitations": [ + "Minimal standalone Go probe excludes Reploy's other runtime work.", + "Single idle container; no native Windows/macOS or Podman qualification.", + "No leak diagnosis or approved resource threshold." + ], + "container_id": "f86c166d08fa1253ab5e7b6335cc87f0688c7f85179f4f2105b02d3a9a596eb1", + "cancel_request_leaves_workload_running": true, + "wait_results": { + "docker_cli_exit_status": 7, + "direct_api": { + "elapsed_seconds": 8.066579979, + "event": "exited", + "status": 7 + } + }, + "median_kib": { + "docker_cli_kib": { + "Rss": 28068.0, + "Pss": 26904.0, + "Private_Clean": 18148.0, + "Private_Dirty": 7604.0 + }, + "go_direct_kib": { + "Rss": 8160.0, + "Pss": 6683.0, + "Private_Clean": 0.0, + "Private_Dirty": 6660.0 + } + }, + "ok": true, + "cleanup": { + "removed": true, + "no_labelled_containers_remain": true + }, + "source_sha256": { + "wait_probe.go": "7189f2df440f546133ec16a3837d669f5d09ac7e460531b727ed14be7c28fafc", + "wait_probe.py": "8fe1a8dcaab1ea2ebc8c5e132d42373c9ee4c03c5a931cf995e8b7a78b21eede" + }, + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "raw_result_sha256": "c9efc3aedbab65c202af8a3c3de6e6eac82c5ba16b6e27bd84611759732692a8", + "raw_result": "temp/reviews/fg-controller-evaluation-20261101/wait-probe-result.json" +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-engineering.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-engineering.md new file mode 100644 index 00000000..afb69781 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-engineering.md @@ -0,0 +1,291 @@ +# Lean controller engineering draft + +This draft supports the [F/G implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). +The maintainer accepted the lean per-installation controller direction for +detailed engineering. This is not a reviewed implementation plan or approval +to migrate existing ordinary installations. The comparison is in +[the evaluation](fg-controller-evaluation.md). + +## Responsibility and scope + +Reuse the embedded Reploy executable and extend the internal +`_service-container` entry. Install one foreground controller per persistent +installation under its existing host account. The OS manager supervises this +process. Reploy owns workload recovery policy and uses the selected engine for +effects and observations. The engine restart policy is `no` for this path. +There is no shared daemon, extra network listener, broker or new service account. + +The common core handles installation state, attempts, admission, private input, +readiness, exact workload monitoring and checked retirement. Host adapters +install/start/stop/remove the controller and report actual manager state. They +do not independently choose workload restart semantics. Engine adapters report +actual object identity, state, status and errors; endpoint loss never selects a +different engine. + +New mapped installations use this path after generic-launch qualification. +Ordinary installations retain their behavior until their applicable compatibility +and migration checks pass and the migration is explicitly accepted. Native +Windows/macOS system installs remain unsupported. Initial mapped execution +remains non-root local Linux Podman. Foreground staged/test/command execution +shares the narrow transport and admission operations without installing services. + +Secret-manager access and clipboard integration are possible separate consumers +of this host-side lifetime. No such feature, credential source, permission, IPC +interface or plugin framework belongs to this F/G delivery. + +## Data ownership and interfaces + +The existing deployment state is authoritative. Extend its machine-local +deployment section with optional controller facts rather than creating a +second authority store. Bind the facts to the installation instance, selected +engine endpoint/identity and accepted generation. Keep them outside blueprint +resolution, provider identity and portable build evidence. + +The controller facts need these logical fields: + +- Current activation intent: running or stopped, independent of observed process + state, bound to its activation epoch and actual host/session identity. +- Automatic boot/login enablement: separate manager/installation configuration. + It is not inferred from the last `up`/`down` or from the workload retry policy. + Observe the existing manager setting; do not create a competing mutable mirror. +- Monotonic control epoch: changes on stop, restart, revocation and replacement + intent so a stale owner cannot commit another admitted start. +- Current controller owner ID and its held lease identity, using existing + kernel-backed lease primitives. A PID or a manager unit name alone does not + establish exclusive ownership. +- Current attempt ID, operation/pending intent and exact resource ownership. + Reuse the existing mapping/native operation records and startup receipt for + their respective facts instead of duplicating their authority. +- Retry accounting sufficient to preserve the selected policy across controller + crashes: failed attempt count, outcome and next eligible attempt. Do not let + repeated controller restarts reset a finite workload retry allowance. + +`WorkloadLifecycleV1` remains the receipt for one process startup. Its `up`, +transfer-claimed and completed fields do not prove activation intent or another +process's startup. Readers and writers must preserve new control facts when +updating generation, installation or mapping state. Unknown/inconsistent facts +fail closed; no automatic adoption or downgrade by an old runtime is allowed. + +Internal calls carry typed installation, epoch, generation, engine and full +container bindings. Do not add public receipt files or caller-supplied +qualification booleans. Short-lived existing CLI operations can remain where +they preserve bounded startup/cleanup and private input semantics; eliminate +CLI children that merely wait or carry streams for the workload lifetime. + +## Ownership and control ordering + +Acquire the installation operation lock before examining or publishing ownership. +If the retained controller lease is held, another invocation must not become a +second recovery owner. If it is abandoned, retain and reconcile its exact +pending attempt before publishing a new owner. Acquire the new owner lease +before publishing the owner record. Hold the lease over monitoring lifetime; +release the operation lock while only monitoring. Reuse the current lease +primitive without pretending a queue-entry lease already provides this protocol. + +Public `up`, `down` and `restart` retain existing control admission/queue behavior. +For an installed controller, use its host-manager adapter rather than starting +an unmanaged competing loop. The control transaction publishes current activation intent, +epoch and pending intent under the operation lock. It releases that lock around +an external manager call that may require Reploy cleanup, then reacquires and +verifies actual state. It must not deadlock against its own stop child. + +Starting/admitting an attempt holds the existing admitted operation ownership +through the bounded effects and release. Before every protected effect, verify +the epoch, owner and inhibition state. If an engine result is ambiguous, persist +uncertainty and retain the exact attempt/create binding. A late response or +manager restart cannot clear the pending intent or authorize another create. + +## Recovery journeys + +| Durable intent and observation | Required action | +| --- | --- | +| Stopped current activation, no owned runtime | Keep stopped during recovery of that activation. `up`/`restart` can request another activation; an enabled actual boot/login activation is separate. | +| Stopped activation with surviving runtime | Complete checked stop/retirement before any new activation; preserve uncertainty until verified. | +| Running current activation and same confirmed process | Reattach observation after endpoint, generation, container ID and process-start binding checks. Do not reinject input or replay startup hooks. | +| Current activation has interrupted admission or transfer | Reconcile the exact incomplete attempt. Never resend merely because the controller restarted. Retire an uncertain process before a new admitted attempt. | +| Confirmed workload failure and retry permitted | Retire all required descendants and attachments; retain data/reservations; create/admit a replacement for mapped execution; confirm its startup before reporting up. | +| Clean workload exit | Apply the selected policy. A process exit alone is not explicit operator stop. | +| Engine unavailable, wait disconnected or state inconsistent | Keep unknown/pending state. Retry bounded observations with backoff. Do not infer death, duplicate the workload, switch endpoint or claim stop success. | +| Revocation or security inhibition | Inhibit starts and all replacement owners before cleanup. Suppress untrusted hooks where required by the existing contract. Verify exact retirement before success. | + +E's mapped waiter is one-shot and bound to a create-time deadline, process and +zero restart count. Reconnecting to a surviving process does not release it +again. A failed mapped workload uses a fresh container/admission; simply +restarting the old engine object cannot reuse E's one-shot frame. This design +therefore needs neither a retained admission file nor secret snapshots for +engine-only retries. The earlier permission for qualified unchanged-container +engine retries remains an available contract permission, not a second mechanism +to implement alongside this controller. + +An OS manager restores a dead controller. Workload retries happen in Reploy, +with durable accounting, so desktop manager retry timing does not define the +application policy. All engine calls except intentional lifetime observation +have bounded contexts; lifetime observations are cancellable and reconciled +after disconnect. Process supervision is not proof of arbitrary-hang recovery. +Any stronger cross-platform hang guarantee requires separate evidence. + +## Private inputs and authority + +Reuse `.env` validation and the existing FIFO relay for ordinary compatibility. +Mapped fresh startup uses the distinct bounded private preamble defined in the +[mechanism contract](fg-mechanism-engineering.md#private-startup-input), before +the unchanged E frame and exact application stdin. Its parsed environment feeds +the existing final native transition. Do not adapt the occupied-exec FIFO relay +into a stronger mapped helper. Retain the mapped guard until composition, +credential/descriptor closure and actual engine transport qualify. + +Refresh sources only for a new admitted process. A confirmed surviving process +retains its environment without reinjection. Missing or invalid input prevents +the next required delivery. Partial/ambiguous transfer requires exact retirement +before replacement. Never put values in state, engine configuration, argv, +units, task XML, logs or output. No durable secret snapshot is introduced. + +The host controller stays outside application namespaces under the installation +account. It does not restore a stronger resident Reploy identity supervisor +inside the container or weaken same-UID roles sharing authority. + +## Narrow engine transport + +Reuse `dialLocalDockerEndpointV1`: Unix sockets on Linux/macOS and the existing +`go-winio` named-pipe dialer on Windows. Reuse endpoint pinning and direct PTY +attachment. The Podman identity client remains Podman-specific; do not weaken +its engine checks to create a misleading shared client. + +Add only the lifetime operations actually needed: + +1. Exact-container HTTP wait with bounded status/error decoding, request/body + cancellation, selected API version and lost-observation classification. +2. Direct attachment where the existing lifetime-long CLI is replaced. Preserve + startup-before-application ordering, stdin/EOF and TTY behavior. Demultiplex + non-TTY stdout/stderr incrementally with bounded buffers and backpressure. + Reject malformed/truncated frames and prove half-close on each supported + local transport. Keep exit status independent of attachment EOF. +3. Exact-object reinspection after wait/stream loss. Never turn an HTTP failure, + missing observation or arbitrary 404 into successful retirement without the + existing ownership and cleanup checks. + +Do not pull in a full engine SDK or rewrite all short-lived create/inspect/exec +calls merely to remove a persistent wait child. Do not automatically retry +create or secret transfer after an uncertain effect. Preserve private error +redaction and no redirect or remote-endpoint fallback. + +The [wait/memory proof](controller-evidence/wait-memory-summary.json) establishes +API waiting, real exit code and cancellation on the current WSL2/Docker Desktop +Unix socket. It does not establish attachment, native named-pipe behavior, Podman +or the complete Reploy controller. Windows's existing dialer is reusable code; +that is different from qualified native stream/EOF behavior. + +## Platform allocation and acceptance + +Price shared state/recovery and engine transports once. Price systemd user +installation separately from adapting the existing system unit. Price macOS +user launchd and Windows user Task Scheduler adapters separately. Preserve the +existing system-scope limits and host account; no elevation or login-session +substitution. WSL distribution lifetime and Desktop readiness require their +own evidence. Boot-before-login and login-session lifetime are not interchangeable. + +Qualification must cover two invocations competing for ownership; controller +death at every startup/transfer/stop boundary; surviving-process reattachment; +engine outage before and after effects; finite retry accounting; explicit stop; +revoke; update/rollback and uninstall; source input disappearance/change; and +retained mappings/data. Manager removal must not report success while its +owned workload remains uncertain. Existing controlled-session parent-loss +cleanup and foreground cancellation retain their separate contracts. + +Measure the full Reploy process tree at idle and during bounded streaming, +disconnect/recovery and repeated workload replacement. Record peak and steady +RSS/PSS/private memory where available, CPU, versions and installation count. +The minimal Go waiter is a feasibility baseline, not the controller's forecast. + +## Start/stop, automatic activation and retry policy + +The maintainer selected the systemd separation. These are three independent +controls, implemented through the existing Reploy installation/control surface: + +- `up`/`start` and `down`/`stop` control the current activation. They do not + enable or disable automatic boot/login activation. `restart` requests a + checked stop and a fresh start; it does not change enablement either. +- Existing `enable`/`disable` controls govern automatic manager activation. + They do not implicitly start or stop the current workload. Reuse the existing + installed systemd commands in `internal/cli/control.go`; extend checked + adapter dispatch where the new supported user managers require it. No new + command family, blueprint field or combined `--now` option is selected here. +- `environment.workload.restart` governs retry after workload exit during an + activation: `no` (default), `on-failure[:N]`, or `always`. `N` is a positive + decimal retry count, not a total start count. Explicit stop suppresses these + retries; it is not an instruction to disable the next boot/login activation. + No additional `unless-stopped` value is selected. + +For example, an enabled mail installation with `restart: "no"` starts when its +qualified boot/login activation runs. A workload exit leaves it stopped without +retry. `down` also stops it for the current activation. It starts at the next +eligible boot/login if still enabled. To keep it stopped there too, use the +separate existing `disable` control. Conversely, `up` can start a disabled +installation now without enabling it for later. `enable` alone does not mean +the workload is already running. + +Linux system installation already enables its unit independently of its start +command. Preserve that existing installation behavior. New user adapters must +expose and verify manager enablement through the same control distinction, +record their supported boot/login prerequisites, and never silently change an +existing ordinary installation's activation settings. Installation activation +and update preserve the selected enablement. Missing/ambiguous manager state +cannot be replaced by a desired-state boolean. WSL distribution startup, +Linux user-manager lingering and desktop user login retain their actual limits. + +### Activation and crash recovery + +Persist the current activation epoch, running/stopped intent, exact controller +owner/lease and attempt/retry facts in existing deployment/operation state. +This supports recovery; it is not a permanent on/off preference. A controller +crash or reconnect within the same activation does not undo `down`, reset an +exhausted retry allowance, or fabricate an explicit start. A fresh `up`/`restart` +or a genuine enabled boot/login activation can establish a new epoch only +after prior uncertain operations have been reconciled. A surviving exact process +is reobserved without startup hooks or private-input replay. + +The owning adapter must distinguish an actual activation from restoration of +the same controller using the existing controlled operation, host/session and +real manager evidence. A changed PID, invocation ID or controller restart alone +is not sufficient. No public caller boolean can assert a boot/login activation. +The native qualification gate must prove this classification for each adapter; +an unsupported or ambiguous case remains inhibited rather than bypass retry +policy. The Linux system control's existing direct `enable`/`disable` path is a +reuse seam, not proof that new user/native adapters already satisfy it. + +An explicit start begins its retry allowance. Consume retries only after a +confirmed workload failure and persist consumption before issuing the next +start effect. Engine unavailability before any workload start does not consume +it; fatal admission/configuration refusal remains blocked, not an automatic +create loop. A healthy interval does not silently reset the allowance. A genuine +new activation begins a new allowance; controller-only recovery preserves it. +Use bounded, cancellable observation/backoff, and qualify interruption before +and after retry consumption. Exact scheduling constants remain internal; they +cannot change which exits, activations or attempts are eligible. + +### Stop and security fences + +`down` records stop intent for the current activation before fencing the manager +and runtime. Releasing the operation lock around manager stop avoids cleanup +deadlock. Report success only after exact retirement is verified; a crash keeps +its pending stop and ownership recoverable. `disable` changes future activation +without being evidence that a currently running workload has stopped. + +Revocation, preparation inhibition and unresolved cleanup are durable security +constraints. Boot/login, `enable`, `up` or a new control epoch cannot clear them. +Only their existing checked recovery/authority transitions can do so. Every new +container still requires fresh host admission. This preserves the security +contract without turning ordinary stop into permanent disablement. + +Existing ordinary `docker.workload.restart` stays compatible; specifying it +together with the new field is an error. New mapped execution cannot fall back +to engine-owned retry. Ordinary installations without the new controller retain +their existing behavior until explicit reviewed migration. An unqualified +adapter must refuse the new policy rather than silently delegate it. + +## Readiness + +Concrete manager adapters, exact policy parsing/translation, native stream +proofs and F's storage/network observations remain engineering/qualification +work. This draft assigns their responsibility; it does not claim those gates +closed or that the whole F/G slice inventory is implementation-ready. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-evaluation.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-evaluation.md new file mode 100644 index 00000000..5f80a10d --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-evaluation.md @@ -0,0 +1,267 @@ +# Per-installation lifecycle controller evaluation + +Status: current-agent engineering evaluation, 2026-11-01. This document supports +the [campaign plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). It proposes a +direction that the maintainer accepted for detailed engineering. This is not +reviewed LLD approval, platform qualification, ordinary-install migration or a +production behavior change. The current draft is in +[controller engineering](fg-controller-engineering.md). + +## Recommendation + +Use the existing foreground installed-service runner as the basis for one +Reploy lifecycle controller per persistent installation. Let the host manager +supervise Reploy. Let Reploy reconcile the workload and execute its selected +recovery policy. Disable independent engine retries for installations using +this controller. Ordinary one-off commands do not need an installed service. + +This recommendation follows present requirements: automatic recovery, private +startup input, stop/revocation ordering and exact-object recovery. Possible +future features do not justify additional interfaces or a general framework. +There is no proposed shared privileged daemon, callback server, broker, or new +network endpoint. Use existing installed control commands and engine transports. + +The approved alternative remains valid: an unchanged admitted container may +reuse admission for engine retries. The controller would normally run the +existing admission path when it starts another process, so it need not exercise +that permission. Adopting a controller does not require reversing the approval +lifetime decision or claiming host-administrator edits are a new attack case. + +The controller direction has a better reuse argument than engine retries plus +retained private inputs. It is **not yet demonstrated to cost fewer lines across +all platforms**. Native manager integration and real crash recovery are the +largest remaining uncertainties. Do not adopt it on an asserted cost saving. + +## Concrete behavior + +For a persistent mail installation: + +1. The host manager starts the installation's embedded Reploy executable under + the installation account. It supplies paths and identifiers, never secrets. +2. Reploy reads the installation, current activation and independent enablement under the operation lock. + It pins the configured engine and observes the exact owned container. +3. If the same confirmed process is already running, Reploy resumes monitoring. + It does not inject secrets or replay startup hooks. +4. If the old process is conclusively stopped and policy permits another + attempt, Reploy uses the checked start path. A new mapped start applies the + required identity, capability, descriptor and sandbox transition. Existing + private input delivery runs once for that process. For the proposed mapped + controller path, retire the failed container and create/admit a replacement. + E's one-shot waiter cannot simply release a restarted old container: its + create-time deadline, consumed frame, PID and zero-restart checks still apply. + Preserve data and reservations through the existing replacement contract. +5. If Reploy crashes, the host manager starts it again. Step 2 decides whether + the workload survived; a manager restart is not proof of workload death. +6. An explicit stop records the stopped current activation before fencing + recovery and stopping the exact workload. Controller recovery honors that + stop. Independent enablement controls the next actual boot/login activation. + +These are proposed acceptance semantics, not claims that the current runner +implements all six steps. In particular, activation state, separate enablement and controller ownership +need engineering beyond the existing process-start record. + +## Source-backed reuse and gaps + +Paths below are relative to the source checkout at +`87899abecced4c5f8b3513ecee66793ecdd2df37`. + +| Source | Reuse | Missing qualification or adaptation | +| --- | --- | --- | +| `internal/dockerdeploy/installed_service_container.go` | Foreground host runner; operation lock; engine readiness; build checks; hooks/readiness; private injection; monitoring after releasing the operation lock. | Explicitly requires system scope and an unmapped document. Eligible mapped user installations cannot use it unchanged. | +| `internal/dockerdeploy/provider_install_systemd.go` | Per-installation embedded runtime, unit rendering, readiness notification and `Restart=on-failure`. | Only system scope. Stop directly runs Compose. No systemd user unit or controller heartbeat is implemented here. | +| `internal/dockerdeploy/installed_service_wait.go` | Exact-container wait; separates nonzero failure from manager stop intent. | Stop evidence is hard-coded to systemd system scope. Exit zero returns success; it does not implement `always` recovery. Engine observation failure is not proof of exit. | +| `internal/dockerdeploy/installed_service_engine_wait.go` and `installed_service_startup.go` | Bounded engine preparation, startup and cleanup. | Bound each attempt without exhausting recovery merely because the engine starts later. Define manager retry exhaustion and diagnostics. | +| `internal/dockerdeploy/workload_lifecycle.go` and `internal/deploy/workload_lifecycle.go` | Durable endpoint/runtime/container/start-time binding, startup completion and transfer claim. Confirmed process reuse avoids another injection. | A process record is not current activation intent, separate enablement, a retry counter, or exclusive lifetime ownership of a controller. Incomplete startup must be reconciled. | +| `internal/deploy/control_lease.go` and `internal/dockerdeploy/control_admission.go` | Existing locks, owner leases and queued control admission are useful primitives. | Leases belong to queue entries; they do not already establish one installation controller. Admission currently refuses mapped documents. Prove the ownership scheme and lock ordering rather than copying a second state store. | +| `internal/dockerdeploy/private_workload_environment*.go` | Defensive `.env` readers and private once-per-process relay, including Windows file validation. | Mapped execution still refuses private environment. Integrate that real gap; the ordinary feature is already implemented. | +| `internal/dockerdeploy/current_workload_lifecycle_mapped.go` | Exact retirement before replacement and refusal on uncertainty. | Production effect/observation composition remains an F/G obligation. A common controller does not provide the missing provider. | +| `internal/dockerdeploy/controlled_session_watchdog*.go` | Exact-resource cleanup and parent-loss handling for controlled sessions. | Linux only. It is not a workload restart service and must not be removed merely because a persistent controller exists. | + +## Competing mechanisms and actual cost drivers + +| Obligation | Engine retries with retained approval/input | Per-installation Reploy controller | +| --- | --- | --- | +| Restart scheduling | Engine implementation and effective-policy checks; portability differences remain. | Shared Reploy policy for workload attempts; host manager restores Reploy itself. | +| Mapped startup | New retained approval protocol, binding, publication and per-incarnation verification. | Adapt the existing checked start/admission composition; retain one-shot release unless a real requirement needs replacement. | +| Private `.env` on another start | Capture protected retained bytes, recipient delivery, lifecycle and cleanup; current relay cannot be called by the engine alone. | Reuse the existing private relay on a new checked start. No new retained-secret snapshot is needed solely for retries. | +| Controller crash | Native retries continue, but later host operations still reconcile exact object/process and fence preparation. | Restarted Reploy must reconcile a surviving process before creating or injecting anything. | +| Stop/revocation | Fence all engine retries and host replacement owners; prove exact retirement. | Persist stop intent and serialize it with all recovery starts; fence the manager and prove exact retirement. | +| Host boot/login | Engine availability and restart semantics; source/input and retained approval availability remain relevant. | New user/desktop manager adapters and startup prerequisites, plus existing engine readiness. | +| Engine unavailable | Engine owns its retries; Reploy cannot claim retirement or finish protected operations without evidence. | Record unknown state, retry observation with bounded attempts and backoff; never create a duplicate or switch endpoint. | +| Ongoing host resource cost | No additional persistent Reploy process solely for retries; current host entry paths can still retain Docker CLI children. | One Reploy process per persistent installation. Memory savings require replacing long-lived CLI children with in-process engine communication; measure the complete process tree. | + +Shared costs must be counted once: exact identity, activation intent, manager enablement, stop fencing, +ambiguous engine outcomes, mapping retention, update/rollback, and cleanup are +required by either architecture. A global controller service is a third design +with additional authority and availability costs; there is no present need for +it. + +The existing draft F/G estimates price the retained-approval/input candidate. +They are not a valid forecast for this alternative. Before publishing revised +totals, replace those candidate slices with source-grounded controller work and +price the platform adapters and qualification separately. Do not subtract the +whole private-environment implementation: it exists and both alternatives need +mapped integration. No working-hour forecast is used. + +## Persistent Docker CLI memory overhead + +The maintainer reports excessive memory from a Docker process retained for the +container's lifetime. Source confirms concrete lifetime coupling: + +- `internal/dockerdeploy/installed_service_wait.go` invokes `docker wait` from + the already resident installed Reploy runner. Both processes remain involved + until workload exit or interruption. +- `internal/dockerdeploy/live_run_mapped.go`, `waitMappedLiveExitV1`, also invokes + the CLI `wait` operation. Its foreground attachment has separate stream and + cancellation obligations. + +A controller that continues spawning `docker wait` merely keeps the same +overhead. Evaluate an in-process request to the selected engine for exact-object +waiting, and inventory any lifetime-long attach/log/exec children separately. +Both [Docker's Engine API](https://docs.docker.com/reference/api/engine/version/v1.40/) +and [Podman's compatible API](https://docs.podman.io/en/latest/_static/api.html?version=v5.0) +document container waiting. Existing local API plumbing is reusable evidence, +but the Podman-specific client in `podman_healthcheck.go` rejects Docker and +non-Linux identification. It is not already a portable general controller +transport. Reuse the existing Windows named-pipe dialer in +`docker_pty_attach_windows.go` and Unix dialer in `docker_pty_attach_unix.go`. +The complete wait/stream behavior on those endpoints still needs qualification. + +This need is independent of choosing a persistent controller. The current +runner's CLI wait could also be replaced directly. The controller comparison +must count that smaller alternative, and must not credit the controller with +savings achieved only by an unrelated transport change. Reuse the same narrow +transport work if the controller is selected; a full Docker SDK or wholesale +replacement of short-lived CLI operations is not a prerequisite. + +Acceptance should measure steady idle memory and CPU for the entire Reploy +process tree, including CLI/plugin children and any newly resident process. +Record versions, workload/stream mode, installation count, RSS and PSS/private +memory where available. Check that memory stays bounded during sustained output, +engine disconnection/reconnection and repeated workload recovery. Preserve exact +exit status, cancellation, stdin/EOF, stream backpressure and ownership checks. +These cases must not silently absorb or substitute for the separate RDA +cancellation hotfix. + +The subsequent [disposable wait proof](controller-evidence/wait-memory-summary.json) +measured ten idle samples on WSL2/Docker Desktop 4.93.0, Engine 29.8.1, using +the Unix socket. Median proportional-set memory was 26,904 KiB for `docker wait` +and 6,683 KiB for a minimal standalone Go API waiter. Both observed the same +real exit status 7. Cancelling another API wait left the workload running. The +exact labelled probe container was removed afterward. + +This measures an isolated transport client, not the complete Reploy controller +or the maintainer's reported workload. No leak, controller-wide saving or +resource limit is established. The first harness attempt mishandled cancellation +while reading a response body; its failed result and successful cleanup are +retained under the raw evidence directory's `attempt-01/`. The corrected probe +handles cancellation during both request setup and body reading. No production +transport changed. + +## Supervision and failure boundary + +The OS manager restores a dead Reploy process. Reploy reconciles and restores +its workload. This ends at the existing OS and engine trust boundary; it does +not require an infinite sequence of additional supervisors. + +However, process supervision alone does not detect a controller that remains +alive but makes no progress. Existing bounded engine calls reduce that risk; +they do not prove recovery from arbitrary deadlock. Systemd offers +[`WatchdogSec=`](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html). +No equivalent portable heartbeat implementation has been established for this +proposal. Do not advertise bounded arbitrary-hang recovery on all platforms. +Price any stronger promise separately before adopting another watcher. + +Required failure semantics for the proposed controller: + +- Controller loss during confirmed running state does not by itself revoke the + unchanged workload. Reattachment requires exact ownership and process proof. + This availability policy needs the architecture decision below. +- Controller loss during admission/private transfer leaves an incomplete + attempt. Never blindly resend secrets. Reconcile and stop/retire an uncertain + attempt before a fresh admitted start. +- A failed `wait` or unavailable engine preserves uncertainty. A running old + object and a missing observation must never lead to a second workload. +- Persist current-activation stop before acknowledgement. A controller crash + cannot erase it; a genuine enabled boot/login activation is a separate event. + Failed cleanup and security inhibition remain pending across either event. +- Do not hold the operation lock while waiting for a manager stop whose cleanup + needs the same lock. Record intent, release for the external action, then + reacquire and verify the result. +- Startup/retry policy must distinguish successful workload exit, failed exit, + intentional stop and controller failure. Preserve current public restart + meanings until an explicit compatibility decision changes them. +- Controller authority remains the installation account and selected endpoint. + It is not application identity and gains no new host privilege implicitly. + The controller stays outside the workload's namespaces. It does not reinstate + a stronger resident Reploy identity supervisor inside the container, weaken + descriptor closure, or change same-UID roles sharing authority. +- Private inputs remain in the existing protected source and one-shot delivery + path. Missing or invalid input prevents the next start; it must not silently + remove the setting or reuse unproved bytes. A surviving confirmed process + keeps its existing environment. + +Controlled sessions have an additional parent-loss cleanup contract. Persistent +installation reattachment must not silently weaken it. Reuse common operations +where suitable, but retain its exact cleanup owner until equivalent behavior is +proved. Likewise, transient shell/command execution does not acquire a permanent +service just to make the architecture uniform. + +## Platform work and prerequisites + +These are candidate mechanisms, not claims of implemented Reploy support. +Unsupported native Windows/macOS system installs stay unsupported. Initial +mapped execution stays local rootless Linux Podman; ordinary Docker coverage +does not expand that cohort. + +| Existing installation shape | Candidate adaptation | Principal gate | +| --- | --- | --- | +| Linux system | Adapt existing systemd unit/runner. | Checked stop rather than raw Compose; exact reattachment; stop intent; engine outage; successful-exit policy. | +| Linux user | Systemd user unit using the same executable and account. | New installation/control adapter. [Lingering](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html) starts the user manager at boot and retains it after logout; it is an explicit prerequisite if that persistence is promised, not a silent host change. | +| WSL2 | Linux adapter when the distribution actually runs the required manager. | Distribution startup/shutdown differs from Windows boot; engine may be Desktop outside the distribution. Do not promise a stopped distribution starts itself. | +| macOS user | Evaluate a per-installation launchd user agent. | New adapter; engine/VM readiness; removal and stop intent; actual native evidence. Apple's [launchd guidance](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) limits a user agent to the logged-in user session. This is not pre-login system support. | +| Native Windows user | Evaluate Task Scheduler under the existing user identity. | New adapter and native proof. A [logon trigger](https://learn.microsoft.com/en-us/windows/win32/taskschd/starting-an-executable-when-a-user-logs-on) is not boot-before-login support. [Restart count](https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-restartcount), [duplicate-instance policy](https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-multipleinstances), and the [default execution time limit](https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-executiontimelimit) require deliberate configuration and tests. | + +Manager crash retries and application retries are different. In particular, +Windows Task Scheduler's [restart interval](https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-restartinterval) +has a documented minimum of one minute. Do not delegate ordinary workload retry +timing to that facility and claim parity with a shared fast retry policy. +The minimal common design keeps workload retry accounting inside Reploy, with +durable limits across controller restarts, while each host manager restores +Reploy after its own failure. Exact policy translation remains LLD work. + +Native platform proofs must cover installation, readiness, workload exit, +controller death, engine outage/recovery, explicit stop, update/rollback and +removal. Reboot/login/logout qualification must use the actual supported scope +and prerequisites. Documentation and cross-compilation are not that evidence. + +## Evidence and readiness + +Nineteen top-level existing tests passed, with no skips, in 3.943 seconds wall +time. They exercised production functions through fixtures: engine readiness +and cancellation, system manager stop identity, nonzero/zero wait results, +monitoring lifetime, bounded failed-start cleanup, fresh process claims, +confirmed-process reuse, observed shutdown, interrupted lifecycle recovery, +journal failure, and controlled-session parent-loss cleanup. + +The [test summary](controller-evidence/summary.json) retains the exact command, +test names and output hashes. Raw events remain in +`temp/reviews/fg-controller-evaluation-20261101/`. +These establish useful existing primitives. They do not +qualify a new controller, real controller death, native managers, reboot, live +mapped execution, or universal hang recovery. The earlier +[engine experiments](fg-restart-fencing-evaluation.md) remain evidence for the +alternative; no live-engine experiment was repeated for this comparison. + +Current-agent review checked the comparison against the security contract, +existing public decisions and source boundaries. There is no independent or +remote review, design stamp, implementation acceptance, or PR approval for this +proposal. Full F/G LLD review still follows completed engineering. + +The maintainer accepted engineering this per-installation controller as the +persistent-workload recovery direction. The 2026-11-02 systemd-model decision +separates current start/stop, automatic activation enablement and exit retry. +Earlier durable-stop wording is superseded; security fences remain durable. Complete the interfaces, policy translation, failure +proofs and slice sizing before implementation. Stop and return for +direction if native supervision needs a privileged service, weaker integrity, +or broader platform support than described here. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json new file mode 100644 index 00000000..ad9ee00f --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-controller-slice-forecast.json @@ -0,0 +1,706 @@ +{ + "status": "Controller authoring forecast; not independently reviewed, human-accepted or implementation-ready. The canonical delivery inventory is the F/G engineering section of MULTI_IDENTITY_IMPLEMENTATION_PLAN.md; this JSON is its sizing projection.", + "unit": "Added or rewritten handwritten physical LF lines, including comments and blanks. Count moved code at destination; do not subtract deletions. Report deletions and mechanical moves separately. Production/configuration and tests/fixtures are separate; prose, generated files, dependencies and observations are excluded.", + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "limit": { + "production_target": 500, + "production_hard_max": 750 + }, + "confidence": "Conditional, with lowest confidence in code-closure, effective-policy and native platform proofs. A failing mechanism proof returns to engineering before dependent implementation; ranges are not budgets or measured results.", + "slices": [ + { + "id": "F1.0", + "title": "Prove the managed-data and immutable-code boundary", + "production": [ + 0, + 0 + ], + "tests": [ + 350, + 650 + ], + "depends": [ + "E4.2" + ], + "source": [ + "internal/dockerdeploy/mapped_sandbox_proof.go", + "internal/dockerdeploy/runtime_host_filesystem_linux.go" + ], + "owns": "Reuse conservative image projection and shadow/ancestor controls; qualify confined descriptors and trusted artifact/source lifetime races on eligible engine.", + "check": "Mapped empty/absent destinations work, populated writable ones refuse under RO root, trusted paths remain protected with RW root; ordinary behavior unchanged. Broader restrictions/weaker guarantees return to design." + }, + { + "id": "F1.1.1", + "title": "Declare managed metadata and intrinsic root policy", + "production": [ + 280, + 430 + ], + "tests": [ + 450, + 750 + ], + "depends": [ + "F1.0" + ], + "source": [ + "internal/blueprint/syntax.go", + "internal/blueprint/model.go", + "internal/dockerdeploy/execution_plan.go" + ], + "owns": "Accepted owner/group/permissions and separate relative directories/files maps. Intrinsic root defaults read-only, with explicit environment.mounts.root writable policy. Preserve ordinary omitted-field identity.", + "check": "Resolve contextual owners and primary groups, reject unsafe paths/unknown principals before effects, bind the root policy canonically without admitting an external bind over /." + }, + { + "id": "F1.1.2", + "title": "Bind image-destination projection to the generation", + "production": [ + 350, + 550 + ], + "tests": [ + 700, + 1100 + ], + "depends": [ + "F1.1.1" + ], + "source": [ + "internal/dockerdeploy/image_identity_layers.go", + "internal/dockerdeploy/image_identity_archive.go", + "internal/deploy/build_lock.go", + "internal/dockerdeploy/current_build_reuse.go" + ], + "owns": "Bounded metadata projection within verified immutable-image stream; canonical mapped-only build-lock evidence, validation/publication and exact reuse. No launch-time full export or separate authority store.", + "check": "Layer/alias/whiteout/type controls and aggregate limits/cancellation; missing/stale/forged evidence refuses, ordinary omitted-field bytes stable, host admission remains independent." + }, + { + "id": "F1.2", + "title": "Prepare declared objects through pinned descriptors", + "production": [ + 300, + 480 + ], + "tests": [ + 600, + 950 + ], + "depends": [ + "F1.1.2" + ], + "source": [ + "internal/dockerdeploy/runtime_host_filesystem_linux.go" + ], + "owns": "Descriptor-relative inert directory/file creation and existing-object verification, parent-first directories then files. No symlink/mount crossing, alias/type substitution, recursive ownership repair or application code.", + "check": "Exact declared metadata succeeds; substitution, hardlink and foreign-tree races leave protected targets unchanged. Count shared adversarial fixtures here once." + }, + { + "id": "F1.3", + "title": "Confine preparation to selected subordinate authority", + "production": [ + 240, + 400 + ], + "tests": [ + 450, + 750 + ], + "depends": [ + "F1.2" + ], + "source": [ + "internal/deploy/identity_mapping_operations.go", + "internal/probe/mapped_exec_linux.go" + ], + "owns": "Reviewed setup-helper invocation with freshly validated selected geometry, pinned handoff descriptors, numeric ownership and bounded cancellation. Reuse existing mapping/writer authority.", + "check": "Private subordinate ownership and recipient access work without host-root/operator aliases, unrelated mutations, application execution or a surviving privileged helper. The earlier namespace/file probe is partial feasibility evidence only." + }, + { + "id": "F2.1", + "title": "Record storage ownership and exclusive preparation", + "production": [ + 280, + 460 + ], + "tests": [ + 550, + 850 + ], + "depends": [ + "F1.3" + ], + "source": [ + "internal/deploy/state_v1.go", + "internal/deploy/identity_mapping_native_state.go" + ], + "owns": "Managed resource identities and preparation/attachment inhibition in the existing transaction. Capture selected handles, revisions and pending effects; no independent resource registry.", + "check": "Concurrent attach/prepare and stale geometry refuse before writes; crashes retain exact ownership and reservations. A process exit or API success alone cannot prove an unattached resource." + }, + { + "id": "F2.2", + "title": "Fence recovery owners before protected storage effects", + "production": [ + 240, + 420 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "F2.1" + ], + "source": [ + "internal/dockerdeploy/current_workload_lifecycle_mapped.go", + "internal/deploy/control_lease.go" + ], + "owns": "Exact retirement and restart-owner fence contract consumed by later controller adapters. Keep the operation lock out of external manager-stop waits; require fresh retirement observations before preparation.", + "check": "No registered engine/controller owner can recreate during preparation or undo stop/revocation. Lost observation retains inhibition; retained application data and reservations remain intact." + }, + { + "id": "F3.0", + "title": "Prove eligible rootless network and effective observation", + "production": [ + 0, + 0 + ], + "tests": [ + 450, + 800 + ], + "depends": [ + "F2.2" + ], + "source": [ + "internal/probe/network_firewall_linux.go", + "internal/dockerdeploy/podman_mapped_waiter.go" + ], + "owns": "Disposable proof on an E-compatible rootless Podman path: trusted pre-release setup, namespace-pinned effective rules, DNS, private peer boundary and endpoint transport. Establish which final capabilities preserve that boundary.", + "check": "Positive allowed egress/endpoints and direct peer, gateway and IPv6 negatives with exact cleanup. Existing C patched-guest and ordinary Docker results do not close this gate. Failure blocks dependent code rather than weakening policy." + }, + { + "id": "F3.1.1", + "title": "Prepare trusted network policy before mapped release", + "production": [ + 180, + 330 + ], + "tests": [ + 350, + 650 + ], + "depends": [ + "F3.0" + ], + "source": [ + "internal/probe/mapped_wait.go", + "internal/probe/sandbox_exec_linux.go", + "internal/probe/network_firewall_linux.go" + ], + "owns": "Move only trusted policy setup before the release wait, using the frozen plan and existing nftables installer. Keep application execution after the host gate and final native transition; do not copy the installer.", + "check": "Effective setup exists while payload is inhibited; failure/timeout never releases the application; no setup network authority survives the final transition." + }, + { + "id": "F3.1.2", + "title": "Supply production mount and kernel sandbox observations", + "production": [ + 300, + 500 + ], + "tests": [ + 650, + 1000 + ], + "depends": [ + "F1.1.2", + "F3.1.1" + ], + "source": [ + "internal/dockerdeploy/mapped_sandbox_proof.go", + "internal/dockerdeploy/podman_mapped_waiter.go" + ], + "owns": "Replace the private complete-code-closure placeholder with the locked bounded image-destination projection and actual complete root/mount/source/kernel observations. Protect the generation-verified top-level verifier bind even with writable root; no general executable classifier.", + "check": "Exact process/namespace/source/root-policy and effective-kernel controls; stale/substituted/unplanned mounts, missing projection and writable image-shadow refuse. Explicit root mutation never exposes trusted Reploy code." + }, + { + "id": "F3.2", + "title": "Enforce private installation network boundaries", + "production": [ + 260, + 440 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "F3.1.2" + ], + "source": [ + "internal/dockerdeploy/podman_mapped_policy.go", + "internal/probe/network_firewall_linux.go" + ], + "owns": "Canonical private namespace/attachment shape, resolver and host-gateway handling, public/local/ambiguous policy and capability-conditioned refusals. No shared private network or implicit peer reachability.", + "check": "Selected transport preserves intended allowed traffic and denies undeclared TCP/UDP/abstract-socket peers. Unsupported capability or transport combinations fail explicitly before effects." + }, + { + "id": "F4.1", + "title": "Publish and observe only declared endpoints", + "production": [ + 260, + 430 + ], + "tests": [ + 500, + 800 + ], + "depends": [ + "F3.2" + ], + "source": [ + "internal/dockerdeploy/execution_plan.go", + "internal/dockerdeploy/podman_application_identity.go" + ], + "owns": "Typed create/inspect endpoint fields, staged/deployed port selection and exact network/publication ownership and cleanup.", + "check": "Only declared addresses/ports are reachable; mismatched publication blocks payload. Partial teardown retains uncertainty and never removes a foreign object." + }, + { + "id": "F4.2", + "title": "Qualify storage and network together", + "production": [ + 0, + 0 + ], + "tests": [ + 700, + 1100 + ], + "depends": [ + "F4.1" + ], + "source": [ + "internal/dockerdeploy/current_workload_lifecycle_mapped_test.go" + ], + "owns": "Integrated exact/bounded preparation, attachment, effective sandbox, network, endpoint and failure evidence on the qualified engine. Reuse child fixtures without repeating their entire test inventory.", + "check": "Observable supported-boundary filesystem/network positives and refusal/no-mutation negatives close F's parent obligations; injected fixtures alone do not." + }, + { + "id": "G1.0", + "title": "Prove controller transport and interrupted startup boundaries", + "production": [ + 0, + 0 + ], + "tests": [ + 450, + 800 + ], + "depends": [ + "F3.0" + ], + "source": [ + "internal/dockerdeploy/docker_pty_attach.go", + "internal/dockerdeploy/private_workload_environment_inject.go" + ], + "owns": "Bounded disposable direct wait/attach/EOF/cancellation and mapped private-input feasibility. Reuse the successful WSL Docker wait proof; add missing Podman and native transport evidence where load-bearing.", + "check": "Exact exit status, no duplicate/foreign start, private input confinement and no setup descriptors/authority at exec. Missing native tests remain named adapter readiness gates; no false universal qualification." + }, + { + "id": "G1.1", + "title": "Select phase mappings under the Podman input", + "production": [ + 280, + 450 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "F1.1.1" + ], + "source": [ + "internal/blueprint/syntax.go", + "internal/deploy/state_v1.go" + ], + "owns": "Direct schema-1 relocation and staging/deployed selection through existing typed whole-array overrides. Update all consumers and retained-state guards; no mapping-specific install flags.", + "check": "Correct phase/override precedence, strict existing override decoding and whole-array replacement; no old undeployed mapping path, source-sidecar adoption, fallback profile or declaration-removal bypass." + }, + { + "id": "G1.2", + "title": "Connect approved identity maintenance commands", + "production": [ + 200, + 340 + ], + "tests": [ + 400, + 650 + ], + "depends": [ + "G1.1" + ], + "source": [ + "internal/cli/cli.go", + "internal/deploy/identity_mapping_operations.go" + ], + "owns": "Public check/configure/status/recover wiring to delivered identity operations with bounded contexts, diagnostics and held-lock authority. Reuse approved command contracts.", + "check": "Actual commands reach existing operations; pending/dormant/revoked state stays authoritative; commands do not launch payload or adopt copied state." + }, + { + "id": "G1.3", + "title": "Install an independent destination owner", + "production": [ + 320, + 520 + ], + "tests": [ + 600, + 1000 + ], + "depends": [ + "G1.2", + "F4.2" + ], + "source": [ + "internal/dockerdeploy/provider_install_run.go", + "internal/dockerdeploy/provider_install_plan.go" + ], + "owns": "Destination-local override snapshot under paired operation locks, independent mapping decision, publication/build reuse, overlap/revision checks and partial-install recovery. Existing staging loader is not installed support.", + "check": "Staging and user install have separate reservations; source state/data remain unchanged. Never copy source pending operations, admission, managed ownership or secrets." + }, + { + "id": "G1.4", + "title": "Declare Reploy restart policy and durable controller state", + "production": [ + 300, + 480 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "G1.0", + "G1.3" + ], + "source": [ + "internal/blueprint/syntax.go", + "internal/blueprint/model.go", + "internal/deploy/deployment_state_v1.go", + "internal/deploy/control_lease.go" + ], + "owns": "Restart policy and legacy conflict checks; current activation intent/epoch, independent observed manager enablement, controller lease and durable attempt/retry accounting. No permanent up/down preference or parallel enablement authority.", + "check": "Start/stop and enable/disable are independent. Controller recovery preserves activation stop and retry allowance; actual enabled boot/login or explicit start can establish a new activation after recovery. Security inhibition survives all activations." + }, + { + "id": "G1.5.1", + "title": "Produce trusted mapped startup-completion evidence", + "production": [ + 350, + 600 + ], + "tests": [ + 800, + 1300 + ], + "depends": [ + "G1.4", + "F1.3", + "F3.1.2" + ], + "source": [ + "internal/dockerdeploy/current_workload_lifecycle_mapped.go", + "internal/dockerdeploy/mapped_process_linux.go", + "internal/probe/mapped_exec_linux.go" + ], + "owns": "Managed per-attempt exec-status FIFO/header/unlink; qualified raw CLOEXEC tail/fixed errno; exact post-EOF kernel live-image observation bound to existing process/lifecycle ownership. No tracer, resident helper or app marker.", + "check": "Both-profile/native-tuple real startup, same-binary, errno/cancel/kill/exit-race, object substitution and private-fd controls. EOF or engine running alone cannot promote startup; old E evidence not relabelled; guard remains if proof unavailable." + }, + { + "id": "G1.5.2", + "title": "Recover workloads through the shared controller loop", + "production": [ + 300, + 500 + ], + "tests": [ + 650, + 1050 + ], + "depends": [ + "G1.5.1", + "F2.2" + ], + "source": [ + "internal/dockerdeploy/installed_service_container.go", + "internal/dockerdeploy/installed_service_wait.go", + "internal/dockerdeploy/workload_lifecycle.go" + ], + "owns": "Adapt the existing runner for qualified user ownership, host-manager/workload failure separation, policy/backoff, exact reattachment and interrupted-attempt recovery. No persistent approval file or secret snapshot.", + "check": "Confirmed surviving processes are observed without reinjection; failed mapped instances retire before fresh admission. Lost engine observation never implies death or authorizes a second workload." + }, + { + "id": "G1.6", + "title": "Wait on the engine without a persistent Docker CLI child", + "production": [ + 160, + 290 + ], + "tests": [ + 350, + 600 + ], + "depends": [ + "G1.5.2" + ], + "source": [ + "internal/dockerdeploy/installed_service_wait.go", + "internal/dockerdeploy/docker_endpoint.go", + "internal/dockerdeploy/docker_pty_attach_windows.go" + ], + "owns": "Narrow in-process wait using existing local Unix/named-pipe dialers, pinned endpoint/API version, bounded result decoding and request/body cancellation. Retain short-lived CLI operations where appropriate.", + "check": "Preserve actual exit status and error distinction; disconnect/cancel releases resources and triggers checked reconciliation. Compare whole-process-tree idle memory, not just a minimal probe binary." + }, + { + "id": "G1.7", + "title": "Replace lifetime stream children with bounded direct attachment", + "production": [ + 260, + 440 + ], + "tests": [ + 550, + 950 + ], + "depends": [ + "G1.6" + ], + "source": [ + "internal/dockerdeploy/docker_pty_attach.go", + "internal/dockerdeploy/live_run_container.go", + "internal/dockerdeploy/live_run_mapped.go" + ], + "owns": "Reuse PTY transport and add only required non-TTY framing, incremental stdout/stderr handling, stdin half-close, backpressure and independent exit observation. Preserve attach-before-payload ordering.", + "check": "Large/slow streams have bounded buffers; EOF is not exit status; actual stdin/cancellation and exact cleanup survive on supported transports. Preserve the separately delivered RDA hotfix contract." + }, + { + "id": "G1.8", + "title": "Integrate private input with the mapped final transition", + "production": [ + 230, + 410 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "G1.7", + "F1.3" + ], + "source": [ + "internal/probe/private_workload_environment_linux.go", + "internal/probe/mapped_exec_linux.go", + "internal/dockerdeploy/private_workload_environment_inject.go" + ], + "owns": "Distinct private preamble before unchanged E release and application stdin; reuse bounded parser/descriptor reader and native mapped exec. Transfer intent follows admission; original deadline, no stronger occupied exec, no retained secret snapshot.", + "check": "One accepted recipient/process receives dummy input exactly once; other roles/transient commands do not. Interrupted transfer retires safely; final descriptors, credentials and capabilities remain correct before enabling the guarded path." + }, + { + "id": "G1.9", + "title": "Install and control the Linux user controller", + "production": [ + 180, + 320 + ], + "tests": [ + 400, + 700 + ], + "depends": [ + "G1.8" + ], + "source": [ + "internal/dockerdeploy/provider_install_systemd.go", + "internal/dockerdeploy/provider_install_host_commands.go", + "internal/dockerdeploy/install_types.go" + ], + "owns": "Per-installation systemd user adapter, embedded executable/account/endpoint binding, checked stop rather than raw Compose, readiness and removal. Reuse system-unit rendering without granting mapped system scope.", + "check": "Actual user lifecycle and engine readiness; explicit manager/lingering prerequisites and WSL distribution limits. No auto elevation, hidden host configuration or fallback recovery owner." + }, + { + "id": "G1.10", + "title": "Add the macOS user controller adapter", + "production": [ + 220, + 400 + ], + "tests": [ + 450, + 800 + ], + "depends": [ + "G1.9" + ], + "source": [ + "internal/dockerdeploy/platform.go", + "internal/dockerdeploy/provider_install_host_commands.go" + ], + "owns": "Per-installation launchd user adapter for the common ordinary-workload controller: install/control, account/session binding, engine/VM readiness and removal. No system backend or mapped macOS promise.", + "check": "Native user login/logout, controller failure, explicit stop, update/removal and engine recovery. An unqualified adapter cannot accept the new public Reploy restart field by silently delegating it." + }, + { + "id": "G1.11", + "title": "Add the native Windows user controller adapter", + "production": [ + 250, + 440 + ], + "tests": [ + 500, + 850 + ], + "depends": [ + "G1.10" + ], + "source": [ + "internal/dockerdeploy/platform.go", + "internal/dockerdeploy/docker_pty_attach_windows.go", + "internal/cli/control.go" + ], + "owns": "Per-installation Task Scheduler user adapter, exact executable/identity, process lifetime/duplicate-instance settings, controller recovery and removal. Reuse native pipe/file validation; no Windows system service or mapped runtime expansion.", + "check": "Actual native Desktop Linux-engine lifecycle, quoting/paths, controller death, stop intent, login and removal. No default task duration termination, duplicate controller or claims based on WSL tests." + }, + { + "id": "G1.12", + "title": "Connect checked persistent controls and replacement", + "production": [ + 300, + 500 + ], + "tests": [ + 600, + 1000 + ], + "depends": [ + "G1.11" + ], + "source": [ + "internal/cli/control.go", + "internal/dockerdeploy/current_workload_lifecycle.go", + "internal/dockerdeploy/current_workload_lifecycle_mapped.go" + ], + "owns": "Checked up/down/restart/status/log/health and existing enable/disable control surface, hooks/readiness, exact replacement, activation-scoped stop and independent manager enablement. No silent legacy migration.", + "check": "Up requires actual startup/handoff and does not enable; enable/disable do not start/stop. No duplicate injection, manager-stop deadlock, recovery-policy bypass or unqualified mapped path." + }, + { + "id": "G1.13", + "title": "Connect fresh commands and tests with their existing semantics", + "production": [ + 260, + 440 + ], + "tests": [ + 550, + 950 + ], + "depends": [ + "G1.12" + ], + "source": [ + "internal/dockerdeploy/current_app_command_run.go", + "internal/dockerdeploy/current_shell_run.go", + "internal/dockerdeploy/mapped_entry_plan.go" + ], + "owns": "Actual staged/test/hook/deployed command adapters and correct fresh-container classification. Preserve stdin/EOF/TTY, role and output/status semantics; no default raw-shell role or unqualified occupied exec.", + "check": "Supported command journeys use the common complete gate; actual unqualified host-native/occupied/shared-session paths refuse before effects. Enum names cannot misclassify ordinary fresh deployed commands." + }, + { + "id": "G1.14", + "title": "Qualify common controller behavior on supported hosts", + "production": [ + 0, + 0 + ], + "tests": [ + 700, + 1150 + ], + "depends": [ + "G1.13" + ], + "source": [ + "internal/dockerdeploy/platform_test.go", + "internal/dockerdeploy/installed_service_wait_test.go" + ], + "owns": "Native Linux user/system ordinary, WSL, native Windows user and macOS user manager/session qualification with actual supported engines. System-scope mapped remains refused. Shared state tests are reused, not counted again.", + "check": "Install/start/stop/restart/update/rollback/remove, controller crash, engine outage and host/session activation evidence with exact versions/locations, retained data and full process-tree memory/CPU observations. Unsupported combinations remain explicit." + }, + { + "id": "G2.1", + "title": "Revoke active runtime while retaining ownership", + "production": [ + 260, + 440 + ], + "tests": [ + 500, + 850 + ], + "depends": [ + "G1.14", + "F2.2" + ], + "source": [ + "internal/deploy/identity_mapping_operations.go", + "internal/dockerdeploy/current_workload_lifecycle_mapped.go" + ], + "owns": "Active revocation composition, stopped/inhibited intent, every registered retry/replacement fence, exact stop/removal and retained mapping/data state.", + "check": "Success means runtime retired and no recovery owner can restart it. Later admission rejects; data/reservations remain. Ambiguous cleanup cannot report revocation success." + }, + { + "id": "G3.1", + "title": "Reconcile interrupted activation and exact cleanup", + "production": [ + 260, + 440 + ], + "tests": [ + 550, + 900 + ], + "depends": [ + "G2.1" + ], + "source": [ + "internal/deploy/identity_mapping_native_state.go", + "internal/dockerdeploy/live_run_recovery.go" + ], + "owns": "Complete pending-operation matrix across install, preparation, admission, private transfer, replacement, manager effects, revocation and network cleanup. Reuse B recovery and existing ownership checks.", + "check": "Each interruption has one supported safe continuation. No stale PID/copied receipt/name-only object authorizes adoption, duplicate start, data deletion or reservation release." + }, + { + "id": "G3.2", + "title": "Qualify the complete public multi-identity launch path", + "production": [ + 0, + 0 + ], + "tests": [ + 1000, + 1600 + ], + "depends": [ + "G3.1", + "F4.2" + ], + "source": [ + "internal/dockerdeploy/current_workload_lifecycle_mapped_test.go", + ".github/workflows/ci.yml" + ], + "owns": "Source-bound public exact/bounded stage/build/configure/run/test/install/start/retry/stop/revoke journeys and full refusal/cleanup matrix. Close every generic-launch row through supported interfaces.", + "check": "No public guard opens before all required positive and negative evidence passes. Require every exact-head CI job and the separate remote reviews; injected/cross-build/WSL results cannot substitute for unavailable native qualifications." + } + ] +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-lifecycle-options.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-lifecycle-options.md new file mode 100644 index 00000000..86bbdec4 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-lifecycle-options.md @@ -0,0 +1,228 @@ +# F/G lifecycle: Reploy-owned recovery and existing integration + +This is supporting engineering analysis, not an approved service design or +evidence that public mapped installation works. The owning document is +[the multi-identity implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). +The maintainer requires Reploy to own restart and recovery. Systemd is a +candidate host mechanism. The maintainer accepted engineering a lean controller +per persistent installation. Detailed service adapters and user-lingering +prerequisites remain subject to the final design and platform qualification. + +The [restart/fencing evaluation](fg-restart-fencing-evaluation.md) records +actual Docker and rootless Podman observations. The maintainer subsequently +approved admission reuse for the same exact unchanged container. The +[controller comparison](fg-controller-evaluation.md) evaluates another mechanism +for Reploy-owned recovery. The controller direction is selected for engineering; +it is not implemented or qualified. See the +[controller draft](fg-controller-engineering.md) for the current state model. + +## Required boundary + +The [security contract](../../MULTI_IDENTITY_SECURITY_CONTRACT.md#required-security-properties) +requires fresh host admission for new container creation, external entry +and whole-container replacement. An unchanged admitted container may reuse its +approval on a permitted retry; each process still applies its final privilege +and sandbox transition. Reuse does not admit a replacement or changed object. +Revocation must prevent further starts and prove exact runtime cleanup while +retaining mappings and storage. This boundary does not itself require automatic +restart or a particular persistent host process. The maintainer separately +requires Reploy-owned recovery so that engine-specific restart behavior does not +define the product contract. + +## Existing implementation + +| Evidence | Established behavior | Limit | +| --- | --- | --- | +| `internal/dockerdeploy/platform.go`, `installBackendForScope` | Linux user-scope installs select the Docker-managed backend. | This backend does not install a systemd user unit. | +| `provider_install_host_commands.go` | The managed backend starts detached containers with Compose. | It is not a Reploy restart loop or a production mapped-admission adapter. | +| `podman_mapped_policy.go` | Mapped create explicitly sets `RestartPolicy` to `no` with zero retries. | This disables engine restart; it does not implement automatic Reploy restart. | +| `podman_mapped_waiter.go` | Startup observation checks the engine restart policy and restart count before release. | A generated request alone is not the effective observation. | +| `current_workload_lifecycle_mapped.go` | Private replacement orders exact old retirement before fresh admission; uncertainty blocks the next start. | Its trusted effect/observation provider is still an F/G handoff. | +| `mapped_entry_adapter.go` | The public guard remains; private replacement refuses before retiring the old instance because production composition is missing. | A refusal test is not working replacement qualification. | +| `control_script.go` and `internal/cli/control.go` | Existing installed wrappers dispatch explicit runtime controls through Reploy. | G must connect and qualify the mapped branch; ordinary behavior is not mapped proof. | +| `installed_service_container.go` and `provider_install_systemd.go` | A foreground Reploy runner and systemd restart exist for system installations. | The runner explicitly requires system scope and an unmapped document. Initial mapped system scope is prohibited. It cannot be claimed as an existing mapped user-service backend. | + +The earlier architecture says to reuse an existing user-service runner. The +source does not provide that runner for the eligible user-scope backend. The +engineering mismatch is real. Installing a new service is one possible +resolution, not a requirement derived from the security boundary. + +## Selected responsibility and mechanism comparison + +Reploy owns the recovery decision, its mapping/generation checks and the +start/stop/revocation transaction. A host manager may invoke Reploy again. The +container engine performs the selected workload operations and reports their +results. The approved contract permits unchanged-container retries under +Reploy's policy. The delivered E implementation still enforces zero restarts; +the contract amendment alone does not change that implementation. Compare a +retained-approval/input mechanism with the controller before choosing which new +path to engineer. Both retain stop/preparation/revocation fences. + +This distinction is not manual versus automatic invocation. Systemd can invoke +the same checked Reploy entry under the selected non-root account. The current +adapter's system-scope-only restriction is an implementation boundary to +engineer, not a fundamental reason these checks cannot run under systemd. + +First evaluate adapting the existing per-application unit rendering and +foreground runner. Do not infer a need for an additional shared daemon, broker +or general service-management framework. The selected manager scope, host +setup, stop/revocation ordering and engine-availability behavior must be +specified and reviewed before dependent implementation. + +### Explicit operation ownership + +Reuse installed controls for explicit `up`, `down` and `restart`. Each new start +must pass the same locked generation, mapping, storage and sandbox admission. +Keep engine restart disabled. A failed workload remains stopped until another +explicit operation. Do not promise automatic startup after reboot. + +This avoids a new service manager dependency. It still requires real G work: +destination ownership, start/replacement composition, detached lifetime handoff, +revocation and exact crash cleanup. It is not a shortcut around those gates. + +If a blueprint requests automatic engine restart, fail before effects with an +actionable unsupported-policy error; do not silently discard the requested +behavior. Ordinary non-mapped restart policies keep their existing behavior. + +This is not sufficient as the full recovery design. Retain it as the common +checked operation invoked by either an operator or the selected host manager; +do not silently narrow the agreed goal to manual recovery only. + +### Automatic host-owned restart + +For automatic recovery, host-side execution must invoke Reploy again. It must +participate in installation ownership, +stop/revocation, updates, removal and failure recovery. A systemd user unit is a +candidate mechanism. It would introduce user-service and, for unattended boot +and logout persistence, host user-manager configuration requirements. + +Do not select this mechanism merely because an old plan referred to a runner. +Compare adaptation of existing service installation with simpler viable +implementations and size the actual additional work. Do not introduce a shared +daemon or broker. + +Engineering must cover these cases consistently. The approved admission reuse +narrows the first item only when a selected mechanism supports an unchanged +container retry; the transaction fences remain: + +- A permitted retry reloads the selected installation, current generation, + retained mapping and fresh host authority before a new workload start. +- Explicit stop and revocation fence the recovery owner before success; an + engine failure must not be mistaken for permission to undo operator stop. +- An unavailable selected engine produces a bounded readiness wait or diagnosed + failed attempt. It does not select a different engine, map, profile or owner. +- Loss of observation retains uncertainty and owned-resource records. A retry + must prove retirement before creating a replacement. +- A host manager schedules invocation. Reploy applies the recovery semantics; + neither Podman nor Docker defaults define those semantics. + +Existing configured restart-policy meanings and their portability need to be +traced before changing defaults or exposing syntax. The design must cover the +existing ordinary Docker install paths as well as mapped execution, and price +any required shared lifecycle changes explicitly. This does not authorize an +unreviewed rewrite of ordinary behavior or add mapped Docker support. Initial +mapped execution still requires eligible local rootless Linux Podman; owning +recovery does not remove that execution dependency. + +## Install-scope and operating-system coverage + +Reploy-owned recovery is a product responsibility, not a Linux/systemd-only +contract. Specify common outcomes first, then identify the host adapter and +actual prerequisites for each supported installation shape. A candidate systemd +implementation cannot count as a complete cross-platform design. + +The current source dispatch and scope validation establish this starting matrix +(`platform.go` and `install_types.go`). These rows describe existing install +support, not qualification of new recovery behavior: + +| Host and install scope | Current implementation | Recovery design obligation | +| --- | --- | --- | +| Linux, system | Systemd system unit and foreground Reploy runner | Reuse and assess startup, stop, retry and engine-readiness behavior; distinguish host controller authority from application UID/GID. Mapped system scope remains ineligible. | +| Linux, user | Detached Docker-managed Compose; no Reploy-owned user unit | Engineer recovery under the installation's actual non-root account. Evaluate adaptation of existing per-application integration before adding machinery. | +| WSL2, supported Linux scopes | Linux dispatch inside the distribution | Identify the actual init/service-manager and engine location. Distribution startup, host boot and user login are different events; do not claim native Linux boot behavior from a WSL run. | +| Native macOS, user | Docker-managed backend, including supported local VM-backed engines | Cover host login/reboot, runtime/VM availability and Reploy recovery ownership. Do not treat engine auto-start or Linux systemd behavior as proof. | +| Native Windows, user | Docker Desktop managed backend | Cover Windows session/login/reboot and Desktop/engine readiness through the actual native entry path. WSL execution is not native Windows evidence. | +| Native macOS or Windows, system | Rejected: no native system-service backend | Preserve explicit unsupported behavior. This coverage requirement does not silently promise a new system-install backend. | + +Do not infer current Reploy implementation support from the broader substrate +possibilities in `INSTALL_SCOPE_RUNTIME_SUPPORT.md`; that document explicitly +describes host/runtime capability, not CLI implementation status. + +For every applicable row, the design and acceptance inventory must cover: + +- Install, first start, failure recovery, explicit stop/restart, update/rollback + interaction and removal; a recovery owner must not undo a recorded stop. +- Host reboot, login/logout and missing host-manager prerequisites, with exact + supported persistence guarantees and actionable unsupported cases. +- Engine initially absent, slow to become ready, lost during operation, and + available again. Preserve the selected endpoint and installation identity. +- Concurrent control, controller failure and ambiguous engine results; prove + owned-resource retirement before replacement and retain uncertainty otherwise. +- Revocation and cleanup where applicable. A new or changed mapped container + requires fresh admission; same-container reuse needs its selected mechanism + and fresh per-process transition. Ordinary installs keep their own checks. +- Host permissions and ownership, safe logs/diagnostics, and lack of accidental + privilege escalation or silent fallback between user/system scopes. + +Track common contract tests separately from native integration evidence. A +cross-compiled binary or an injected runner test is not a native reboot/session +or service-manager proof. Identify unavailable environments and pending checks +honestly, and include real adapter/platform work in the slice estimates rather +than pricing Linux as if it covered all supported hosts. + +## Docker and Podman independence + +The recovery contract must cover both Docker and Podman. Reploy owns restart +decisions, desired running/stopped state, retry handling, admission, cancellation, +revocation where applicable, and uncertain-operation recovery. These semantics +must not be inherited from engine defaults or independently reimplemented with +different meanings in each adapter. + +Engine-specific integration translates workload operations such as create, +start, inspect, wait, stop and removal into the selected supported transport. +It returns actual observations, errors and operation identity to the shared +lifecycle. Reuse existing adapter boundaries; this separation does not require +a new generic engine framework or a native Podman backend. + +The host manager invokes Reploy; Reploy's lifecycle invokes the selected engine. +An unavailable or restarted engine must not silently change recovery policy, +installation ownership or the selected endpoint. Engine-specific readiness and +error handling remain necessary even when the recovery decision is shared. + +Cross the supported OS/install-scope matrix with the actual engine support +matrix. Run common lifecycle contract cases against both engine adapters, and +retain engine-specific integration evidence for readiness, lost observations, +stop/replacement and cleanup. Do not infer Docker behavior from Podman results +or vice versa. Do not invent unsupported combinations to complete a matrix. + +Recovery portability is separate from execution-feature support. In particular, +the initial mapped cohort remains local rootless Linux Podman. Docker coverage +must exercise the supported ordinary installation path; it does not imply +mapped Docker execution is available. + +## Focused verification + +On source head `87899abecced4c5f8b3513ecee66793ecdd2df37`, seven existing test +functions passed in a focused run (package execution 3.692 seconds): + +- `TestPlanProviderInstallHostCommandsV1DockerManagedUsesExactInstalledInputs` +- `TestMappedCreatePolicyOverlayPreservesOrdinaryRequestV1` +- `TestMappedRuntimePolicyRefusalBeforeFrameV1` +- `TestMappedWorkloadReplacementOrdersRetirementAndFreshAdmissionV1` +- `TestMappedWorkloadRetirementUncertaintyBlocksReloadV1` +- `TestRunCurrentWorkloadLifecycleV1RejectsFreshPrivateRestartPoliciesV1` +- `TestRunCurrentShellV1OrdersStaleCheckFinalGateAndInteractiveContainer` + +These are source-level and injected fixture checks. They do not qualify a live +Podman installation, systemd user services, boot/logout behavior or the final +public inspection/admission/launch path. No production code or host service was +changed by this analysis. + +## Readiness + +Recovery ownership is resolved: Reploy owns it. The concrete adaptation, +operational prerequisites, state/effect ordering and qualification remain +engineering work. Do not treat the ownership decision as acceptance of an +unexamined systemd design. F's independent storage/network engineering can +proceed, but public activation remains inhibited until all governing F/G proofs +pass. Final slices and revised LOC totals must include the selected real work. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-mechanism-engineering.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-mechanism-engineering.md new file mode 100644 index 00000000..a6ba203d --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-mechanism-engineering.md @@ -0,0 +1,371 @@ +# F/G mechanism engineering + +This is supporting engineering for the +[implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). +It preserves the accepted root-mount, storage, mapping and controller decisions. +The maintainer accepted the conservative mount compatibility limit below on +2026-11-02, for the initial mapped cohort only. This document does not mark a delivery slice complete or open a public mapped execution guard. + +## Source and proof boundary + +Source head: `87899abecced4c5f8b3513ecee66793ecdd2df37`. +The [evidence manifest](mechanism-evidence/manifest.json) binds the actual +disposable sources and results. The Podman tests used a fresh private VFS store, +rootless Podman 4.9.3, crun 1.14.1 and WSL2 kernel +`6.18.54.1-microsoft-standard-WSL2`. Fixture UID 0 mapped to subordinate host +UID 100000. No application was released through Reploy's public path. + +These are mechanism proofs, not eligible-engine qualification, a complete +both-profile campaign, native desktop proof, endpoint/peer-isolation acceptance +or completion of F/G. The archived scripts are evidence; replay them in a new +directory rather than overwrite the retained receipts. + +## Private startup input + +The existing mapped release reader consumes exactly one bounded frame and leaves +all following stdin bytes for the application. Keep that qualified v1 behavior. +The existing private FIFO relay uses another container exec and does not compose +with the mapped final transition. The production mapped private-input guard +therefore remains correct until the new composition is qualified. + +The smaller candidate is a distinct internal private-startup mode: + +1. Create the fresh container with trusted startup only and attach stdin before + release. Bind the mode to the frozen plan and exact installation/attempt. +2. Establish the required network policy before waiting for host admission. +3. After all host admission checks pass, persist transfer intent and send a + bounded private preamble, the unchanged E release frame, then application + stdin. Never place values in argv, engine configuration or durable state. +4. Trusted startup consumes the exact private length with the existing bounded + descriptor reader and validates assignments with the existing parser. It then + validates the unchanged E frame. It must not prefetch application bytes. +5. Supply the parsed environment to the existing native mapped exec transition. + That transition still owns credentials, capabilities, descriptor closure and + exec. No stronger process enters an application-occupied namespace. +6. A partial, invalid, stale or cancelled transfer executes no application. + Reconcile and retire an uncertain instance before admitting a replacement. + Reattachment to a confirmed surviving process never resends the preamble. + +The original admission deadline still bounds the whole startup. Reading private +input must not extend it. A new internal mode avoids changing the meaning of +bytes after a v1 release frame. Ordinary FIFO delivery keeps its existing +compatibility contract; no second mapped relay is introduced. + +The disposable Go overlay used the actual private assignment parser, mapped +descriptor reader and release parser. Seven new cases passed: binary stdin +preservation, missing preamble, oversized length, truncated input, invalid +assignments, wrong release binding and cancellation. Existing mapped waiter, +descriptor and native exec-tail checks also passed. Dummy values reached a +child environment without entering its argv. This is pipe/process composition, +not proof of the complete engine or native Windows attachment path. + +## Network setup before release + +`mappedWaitV1` currently waits before calling `sandboxAndExecApplicationV1`; +the latter installs nftables. F needs effective observations before release. +Move the existing installer into the trusted pre-wait preparation phase and +keep one installer owner. Final credentials/capabilities still follow admission. +Do not leave an in-container supervisor or add a service. + +The final disposable proof used two private container network namespaces and +a veth pair. Both in-container setup and a bounded host helper joining the +pinned namespace could install and read actual IPv4/IPv6 kernel tables before +any application release. A valid connection passed before filtering, failed +under deny-local policy and passed under allow-local policy. This establishes +the setup/readback mechanism; it does not qualify full egress, DNS, undeclared +peer denial, IPv6 traffic or endpoint publication. + +Use the existing trusted startup for installation. A production observer must +bind readback to the exact held namespace and process, reject stale identity, +and verify rules rather than echo the request. Host setup is a proved fallback +mechanism, not a second selected installer. No host-wide firewall was changed. + +The first fixture used loopback as a negative despite the intentional loopback +allow rule. The second lacked the synthetic resolver file and left the first +case's deny table in the shared fixture. These are fixture defects; their +receipts remain. The corrected third attempt passed. No production policy was +changed to obtain the pass. All private containers, images and stores were +removed. + +## Writable root and protected startup + +The root-mount decision permits an explicitly writable application root while +keeping trusted Reploy code protected. The mount proof established: + +- Read-only image root rejects direct image-code writes. +- A separate writable mount can hide and replace image code despite read-only + root. This successful attack is a negative control, not a product pass. +- A read-only bind at the top-level `/probe` path denied write, unlink and + rename while ordinary application image files remained writable. +- Protecting only `/opt/reploy/probe` was insufficient when `/opt` remained + writable: renaming that ancestor allowed a replacement at the original path. + +The production design must protect trusted path ancestors too. The existing +top-level `/reploy-probe` location is a smaller protection boundary than adding +a nested path hierarchy. Its read-only backing must be the verified artifact +for the selected generation, retained for the exact owned container lifetime. +An arbitrary host binary or unverified source path is not sufficient. + +The original storage receipt reported an empty private engine inventory but +left recreated, caller-owned Podman lock directories. The +[cleanup follow-up](mechanism-evidence/storage-cleanup-followup.json) proves +their removal and absence of matching proof processes. The original receipt +was preserved rather than rewritten. + +## Accepted initial image-mount boundary + +The maintainer accepted the smaller initial mapped boundary: with the default +read-only application image root, reject an application-declared writable managed +mount if its destination covers any existing image files. Empty, absent or +directory-only image destinations remain usable. This deliberately rejects +prepopulated data-only destinations too. For example, `/var/lib/mail` is usable +when empty; image-supplied default files beneath it cause refusal. Existing +`environment.mounts.root.writable: true` opts into broad application-image +mutation and relinquishes this application replacement protection. Trusted +Reploy startup/control paths remain protected independently. No new flag is +introduced. This limit does not change ordinary installations. + +This replaces the unimplemented E interface's assumption that F will discover +a complete general code/dependency closure. A classifier based on executable +bits or ELF dependencies would miss scripts, plugins and dynamic lookup. +Instead, default root read-only policy protects image files and the conservative +mount check prevents writable managed mounts from hiding them. F must change +the request/proof contract and its real provider together. Historical E injected +proofs remain private-interface evidence, not qualification of the new provider. + +### Image destination projection + +Use the existing verified immutable image-save stream during build preparation. +Extend its metadata collection; do not extract image files, execute image code, +add a general filesystem snapshot, or repeat image export on every launch. +Verify the selected config, platform, ordered layer hashes and rootfs subject +using D's existing immutable-image authority. Apply whiteouts to lower layers +before same-layer additions, merge directories, and remove descendants when a +non-directory replaces a directory. Treat hardlinks and every non-directory +entry as image content, regardless of mode or size. Resolve destination aliases +within the image root, never on the host. Compare path components, not lexical +prefixes. Reject cycles, root aliases, non-directory destinations, ambiguous +layer traversal through non-directory ancestors and unsupported archive forms. + +The disposable projection passed 22 cases, including same-layer and opaque +whiteouts, hardlinks, symlink aliases, zero-size/data files, root aliases and +file/directory replacement. Every case also rejects a mismatched rootfs subject. +The first 17-case attempt is retained. The prototype is not the production +bounded reader: F1.1.2 must retain D's parser limits and cancellation behavior +and add aggregate accounting before qualification. + +Keep the existing 128-layer, 100,000-header-per-layer, 4,096-byte-path and +64-GiB archive ceilings. Bound the projection across the archive to one million +distinct indexed nodes, 64 MiB of retained path/link metadata and 32 alias hops. +Charge implicit directories and metadata extension records too; count bytes +before allocation, never only after a map insertion. The selected operation +context bounds total work and cancellation interrupts reading. A limit failure +rejects preparation, never switches to a weaker mount policy. These are internal +parser limits, not a new public size option. Existing supported D archive forms +and strict protected-account handling remain unchanged. + +The published result is a small closed projection for declared destinations, +not the working index. Bind it to the immutable image/config/platform/rootfs, +normalized mount selection and root policy. Record literal/resolved destination, +absent/directory/non-directory kind and whether image content exists beneath it. +Store it as an optional mapped-only part of the existing build lock/generation; +the lock digest covers it. Absent fields preserve ordinary canonical identity. +Reuse requires the exact image and policy bindings; no caller boolean, changed +mount list, substituted lock or automatic rebinding can supply evidence. +A missing/stale required projection means rebuild or refusal. Changing the image +or mount policy creates new evidence before use. This adds no parallel authority +store and does not make a portable build into a host admission. + +### Actual mount and trusted artifact observation + +Before release, F's provider pins the inhibited process/root and namespaces and +reads the complete effective mount table. Bind literal and resolved targets, +source identity, read-only state and root policy to the locked plan. Reject +unplanned mounts, stale processes, writable aliases, substituted sources and +overlap with trusted startup/control paths, including their ancestors. Apply +the accepted image-content rule to declared writable managed mounts. Read-only +application mounts retain existing protected-path overlap checks; this approval +does not impose a blanket new image-layout restriction on them. Engine-created +mounts must match the exact qualified runtime contract, not an exemption by name. + +For explicit writable root, protect the existing top-level `/reploy-probe` with +a read-only file bind. Use the startup verifier already named by +`ApplicationRuntimeLayerV1.Verifier.Artifact` and `.Size`. Extract the packaged +artifact for the locked platform, verify those bytes against the selected +generation, and retain them in the existing private operation/installation +workspace for the exact owned-container lifetime. The workload cannot access or +rewrite its host backing path. Admission rechecks the retained source and actual +bind; retirement precedes removing its backing. Reuse existing owned-reference +and pending-cleanup machinery. Do not invent a second binary store or allow an +arbitrary host executable. A missing matching artifact refuses before release. +The top-level placement avoids the demonstrated writable-ancestor attack; +all trusted paths still receive ancestor and mount-substitution checks. + +## Trusted application-start completion + +E's `mappedWorkloadEvidenceV1` is a private requirement, not an implemented +production provider. `StartupComplete` cannot come from an engine-running bit, +a release write, argv or application output. A trustworthy producer is needed +before the controller can claim successful mapped startup. + +### Selected bounded mechanism + +Use one private exec-status FIFO for the fresh inhibited instance, prepared in +its existing managed runtime workspace. This is not an application input or a +new service. The host opens and pins its read end before the reviewed preparer +sets the FIFO's subordinate setup ownership. Trusted startup alone opens the +write end before application release. F1's descriptor/type/owner/link checks +apply; record the exact resource and attempt before effects. Both ends must +match the same observed FIFO object. Never open a caller-selected external path. + +Send a bounded closed startup header over this private channel. Bind it to the +normal installation/operation/generation/revision/image/plan tuple and exact +owned process, using the trusted constructor's binding. This metadata contains +no secret value and is not a portable approval token. Reuse the existing bounded +canonical frame primitives; cap the header at 64 KiB, reject duplicates, unknown +fields and noncanonical/partial/wrong headers. The disposable magic/token header +is only a proof fixture; production uses this exact typed binding. + +After the header and descriptor identities match, the host unlinks the FIFO +through its held managed-root descriptor and verifies link count zero while +retaining its read end. No application runs yet. Only then may the normal host +admission/release sequence proceed. Startup retains the write descriptor through +final exec, with explicit raw-descriptor ownership. No private path or writer can be +reopened by the workload. A pipe alone is insufficient if its path remains +available after release. + +Open the writer as a raw nonblocking, close-on-exec descriptor, outside Go +`os.File` finalizer ownership. Verify that it is above the standard streams +and has both flags before entering the native tail. Prepare every pointer and +failure buffer before that boundary. The tail performs these raw operations: + +1. Detach the current thread's descriptor table with + `close_range(status_fd + 1, UINT_MAX, CLOSE_RANGE_UNSHARE)`. This preserves + the status writer and lower descriptors while closing the higher range. +2. If the writer is not descriptor 3, use `dup3(status_fd, 3, O_CLOEXEC)` in + this now-private table. Do not overwrite descriptor 3 in Go's shared table. + If the writer is already descriptor 3, retain its verified close-on-exec flag. +3. Close descriptors 4 and above immediately with ordinary `close_range`. +4. Execute the application. Successful kernel exec closes descriptor 3 before + untrusted code starts and destroys the other Go threads/table references. + +On any operation failure, write one fixed four-byte errno record through the +still-owned nonblocking writer, then exit through the raw tail. Track the +writer's actual descriptor until a successful duplication changes it. No error +return, Go runtime call, allocation, logging, callback or cleanup executes after +table detachment. Retry only interrupted bounded writes. This uses E's existing +close-range/UNSHARE operation and ordinary descriptor close-on-exec; it does not +require the newer whole-range `CLOSE_RANGE_CLOEXEC` flag or raise the kernel floor. + +Once armed, every normal trusted startup failure must produce a failure record +before closing/exiting. Do not allow an ordinary return/finalizer to close the +writer while an inhibited waiter remains live. Raw descriptor ownership and +prebuilt pointer/buffer liveness must remain explicit in every leader/nonleader +branch. The existing final +UID/GID/groups, P/E/B/I/A, securebits and NNP transition still runs before this +tail. This is a proposed refinement of descriptor closure, not reuse of E's +old close-before-exec qualification as proof of new behavior. It preserves the +required absence of setup descriptors in untrusted code. Change and qualify +the single mapped transition; do not add a resident observer or stronger exec. + +### Host acceptance and uncertainty + +The host accepts only the exact armed channel, its complete header, clean EOF +with no failure/trailing bytes, and a fresh positive post-EOF kernel live-image +observation through the existing held process-directory/pidfd pin. Bind the +same start identity, namespaces, maps, generation and container observation; +read the actual kernel executable object, not argv or an app marker. An inode +change is not required: a valid application may exec the same binary. +EOF alone, or EOF plus an unready pidfd alone, is ambiguous with process death +and is not success. The qualified observer must distinguish exit teardown from +a live executable image after the channel closes, including nonleader exec. +If the kernel/process permissions or runtime cannot supply this observation, +refuse completion rather than invent `StartupComplete`. + +This proves the selected exec handoff, not application health. Run existing +required hooks/readiness afterward before the persistent lifecycle becomes up. +An already exited process does not produce a live startup/handoff claim. +Transient entry completion retains its separate exact-exit semantics; do not +reinterpret a finite command as a persistent master's startup. Detachment needs +its own qualified transport and a fresh matching live-instance observation. + +If the controller dies before the existing startup/lifetime transaction records +completion, the status descriptors and unlinked object cannot be reconstructed +as approval. Reconcile and retire that exact uncertain instance before another +admission. Never resend private input or synthesize an acknowledgement from +engine presence. A confirmed recorded surviving process can be reobserved +without another startup protocol or input delivery. Retirement precedes removing +any remaining owned backing; ambiguous cleanup retains inhibition/reservations. + +### Proof, alternatives and required qualification + +The selected reserved-descriptor refinement passes five disposable process +cases: same-binary nonleader exec with natural and deliberately high source +descriptors, missing executable, failure before exec and cancellation before +release. The compiler output contains six raw syscall call sites and no Go +runtime calls in the tail. The natural writer was descriptor 4; the forced +writer was descriptor 64. The source-descriptor-3 branch remains an explicit +product qualification case. Both success cases observed a live kernel executable +after EOF, no inherited private descriptors and no remaining status path. +The first two failed fixture receipts are retained: the fixture wrongly assumed +Go left descriptor 3 free. The corrected fixture duplicates its raw writer to an +available high descriptor without overwriting Go's shared table. + +These are bounded process/FIFO feasibility results on this WSL2 kernel, not +complete mapped credential or adversarial-race qualification. Earlier successful +whole-range-close-on-exec proofs remain historical evidence; they are not the +selected kernel requirement. Exact sources and receipts are listed in the +[mechanism evidence manifest](mechanism-evidence/manifest.json). +Existing pinned executable observation remains useful, but inode change alone +misses same-binary exec. An application marker is untrusted and cannot replace +this channel. A host tracer/privileged service is materially broader and is not +selected. The FIFO reuses F's managed runtime preparation instead of adding a +host listener, durable acknowledgement file or another authority store. + +The selected refinement also passes all five cases through raw rootless +Podman 4.9.3, using a fresh isolated VFS store, read-only image, empty capability +set, NNP and fixture map `0 -> host 100000`. The host pins the FIFO before +subordinate ownership, observes the inhibited kernel process, unlinks the +status/canary paths before release and uses non-TTY stdin. Both same-binary +cases acknowledge; all three failure/cancellation cases refuse. No private +descriptor reaches the application. This is not Reploy's compatible-create +path, full credential transition, both profiles or accepted native ABI coverage. + +The original container receipt records successful cases and incomplete cleanup. +The [reserved-descriptor cleanup follow-up](mechanism-evidence/startup-status-reserved-fd-container-cleanup-followup.json) +records termination of the exact fixture pause helper through its verified +private environment, UID, command, start identity and pidfd. Caller-owned lock +directories were then removed; no fixture process or private root remained. +The earlier failed follow-up and original receipts are retained. Replays must +remove the engine objects/store, residual lock directories and exact pause +helper; store absence alone does not prove complete helper cleanup. +G1.5.1 must prove the full supported mapped tuple before using its provider: +both profiles and admitted ABIs; actual subordinate FIFO access and unlink; +same-object/type/owner/link/alias checks; GC and nonleader descriptor-table +liveness; source descriptor 3 and high-descriptor branches; complete/partial/stale headers and errno records; invalid frame, +timeout, cancellation, exec and close-range failures; kills and exit-teardown +races before/after arm, release, EOF and kernel observation; exact process/PID +reuse refusals; final credentials/caps and private/high-fd absence. Reuse E's +applicable tests without relabelling their old tail as new qualification. +No success-shaped EOF may promote a dead, inhibited, wrong or uncertain process. +Missing supported runtime evidence leaves the product startup guard in place. + +## Effect on readiness and slicing + +The private-input composition and network ordering now have concrete, +source-backed candidates. Their product providers, bounded transport, +interruption/reconciliation tests and supported-platform qualification remain +with their G1.0/G1.8 and F3 children. Do not count these prototypes again as +automatically required implementation changes when their exact evidence can +be reused. + +The image-layout decision and layered projection mechanism now have evidence. +F1.0 still owns the complete confined-descriptor/trusted-source race matrix on +the eligible engine, F3.0 owns eligible network/endpoint qualification, and G1.0 +owns the remaining direct/native transport proofs. The completed disposable +results are reusable inputs, not declarations that those product gates passed. +F1.1.2 separately prices the previously missing image projection/build-lock +producer. G1.5.1 separately owns the actual startup-completion provider and qualification; +its process/FIFO proof does not close that gate. Required review loops and final +LLD acceptance remain. Existing D/E +delivery is unchanged. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md new file mode 100644 index 00000000..4f4c2857 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-prerequisite-audit.md @@ -0,0 +1,72 @@ +# F/G prerequisite audit + +This working engineering audit supports +[the implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). +It records existing mechanisms and remaining work. It is not a reviewed LLD, +an approved replacement slice map, or runtime qualification. + +## Rules for the next design draft + +- Trace each added mechanism to a concrete requirement and the existing path + that cannot satisfy it. An old reference to a component is not evidence that + it exists or that a new component is needed. +- Reuse accepted mapping, transaction, admission, startup and cleanup machinery. + Do not introduce a shared daemon, global allocator, broker or parallel state + store. The accepted per-installation controller reuses the existing runner; + engineer and price its actual adapter/state work explicitly. +- Preserve the accepted public storage and mapping decisions. Do not reintroduce + superseded flags or a compatibility version for an undeployed mapping API. +- Treat a private interface, injected observation or refusal test as a handoff, + not a completed production provider or a positive compatibility proof. +- Keep all public mapped activation guards until the actual generic-launch + evidence passes. Unresolved engineering cannot become a qualification boolean. + +## Requirement-to-source assessment + +| Requirement or accepted decision | Existing foundation | Work the LLD must actually own | +| --- | --- | --- | +| Stable private exact/bounded mappings and retained reservations | `internal/deploy/identity_mapping_geometry.go`, `identity_mapping_state_file.go` and operation-lock/state writers | Reuse the same state and guards for the selected destination. Do not add a mapping registry or allocation service. | +| `podman.identity_mapping` with staging/deployed selection | Strict schema-1 parser and typed whole-array overrides exist under the old runtime placement. Endpoint phase selection already exists. | Move the undeployed public input directly, update all opt-in detection and serialization/typed-override consumers, and preserve actual-host-ID meaning. G1.3 must connect destination-local overrides; the delivered loader is staging-only. No mapping-specific flags or schema-2 compatibility path. | +| Independent installed owner | Existing installation transaction, immutable build references, pending intents and owner-path checks | Connect independent destination admission, source/destination revision and overlap checks, and exact partial-publication recovery. Do not copy reservations, admissions or mapped data. | +| Existing contextual owner defaults; optional explicit owner/group/permissions; directories before files | Contextual runtime-user resolution and descriptor-owned private metadata helpers exist. | Add only declared inert-object metadata and confined preparation. `fsowner` protects private metadata; it is not already the application-storage preparer. No recursive chown/chmod, foreign-tree adoption or content/template system. | +| Fresh ownership authority before preparation, exclusive attach/prepare and crash inhibition | Existing operation lock, retained identity state and inhibited native-intent machinery | Define the exact managed resource record, effect ordering and recovery observations. A local lock or stopped process does not prove that every attachment is gone. Reuse the transaction; do not invent a global resource manager. | +| Intrinsic image root is read-only by default; explicit root-mount writable opt-in | Existing sandbox defaults to read-only root and rejects `/` as an ordinary mount destination. | Give the intrinsic root policy its own representation inside the mount model. Do not admit a bind over `/`, apply ordinary managed-data preparation to root, or disable overlap validation for other mounts. | +| Trusted startup/control paths remain protected even with writable application root | Existing canonical runtime protected paths and the private mapped sandbox proof interface | Engineer the actual production observation and protection. Current mapped create and proof validators insist on read-only root; changing blueprint parsing alone cannot implement the approved opt-in. | +| Pre-release mount/code/kernel/network verification | `mapped_sandbox_proof.go` defines the expected request/proof binding; E supplies no production provider. | Establish evidence provenance and effective observations. Do not count the interface as working F implementation or add an arbitrary dependency-discovery framework without showing necessity. | +| Existing egress policy plus private, non-peer-reachable installations | Existing nftables policy compiler/installer, declared endpoints and Podman request transport | Prove the selected rootless network transport, DNS, peer denial, endpoint publication and exact cleanup. User-facing public/local egress meanings stay unchanged. | +| Effective network policy before application execution | The mapped waiter waits for host release, then calls `sandboxAndExecApplicationV1`; that function installs nftables rules. | Resolve the ordering with F's requested pre-release effective observation. Do not report installed rules before they exist or silently move application execution ahead of admission. Choose the smallest implementation after tracing the existing trusted transition. This remains an engineering prerequisite. | +| Host-owned checked replacement and active revocation | Private E retirement/reload ordering and workload-lifecycle state exist; production effect providers and replacement composition remain unavailable. | Implement and qualify exact retirement, fresh reload, startup confirmation, lifetime handoff, restart fencing and uncertain cleanup. Never obtain a pass by removing a required positive journey. | +| Correct process-entry semantics and explicit roles | Existing command-to-executable roles and ordinary transient shell path; private E entry classifiers/refusals | Classify the actual adapter rather than assuming every operation called `shell` is occupied exec. D already refuses a shell/raw path without an accepted definition. Do not invent a default shell role or replace genuine occupied exec with a fresh container. | +| No engine restart bypass under the selected controller | Mapped create sets restart to `no`; observation rejects changed policy/count. | Retain one-shot startup and fresh mapped replacement after failure. The approved permission for qualified unchanged-container retry remains available, but this controller does not need a parallel retained-approval mechanism. | + +## Recovery direction and remaining engineering + +See [the lifecycle analysis](fg-lifecycle-options.md). The maintainer requires +Reploy-owned restart and recovery, with systemd as a possible host mechanism. +Systemd can invoke the same checked operations; manual versus automatic +invocation is not the security boundary. The maintainer accepted the lean +per-installation controller direction, including replacing lifetime-long Docker +CLI children. Engineer and size the adapters and boot/session behavior; the +direction does not approve new platform scope or silent ordinary-install +migration. Current start/stop, independent boot/login enablement and exit retry are separate. +Security/preparation/recovery fences remain durable across all activations. + +## Proof and sizing limits + +The successful source-level tests and their limits are recorded in the lifecycle +analysis. The local `podman --version` reports 4.9.3 and warns that +`/run/user/1000` is absent. A binary version is not a running eligible rootless +engine, compatible transport proof, or network/mount qualification. Existing +campaign guest evidence must be resolved and checked for reuse before scheduling +new disposable engine proofs; no Podman checkout or host service is to be changed. + +The original combined F/G forecast remains 2,400–4,200 production lines and +4,300–7,050 test/fixture lines. This audit does not replace it with a guessed +number. Revised counts must come from the selected mechanisms, actual missing +providers, failure/recovery/platform checks and ordered slice responsibilities. +Count shared work once, identify the unit, and retain qualification-only slices +as zero production when appropriate. Target 500 production lines per PR; split +before the 750-line maximum. + +The engineering authoring function is not complete. No fresh design stamp, +review approval, implementation readiness or public mapped support is claimed. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-restart-fencing-evaluation.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-restart-fencing-evaluation.md new file mode 100644 index 00000000..051967a7 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-restart-fencing-evaluation.md @@ -0,0 +1,168 @@ +# F/G restart and preparation fencing evaluation + +This records the approved unchanged-container retry direction for +[the multi-identity implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md), +with mechanism evidence and remaining engineering gates. The maintainer approved +the scoped approval-lifetime amendment. No production restart policy, startup +code, host service or governing stamp was changed. + +## Recommendation and boundary + +Evaluate engine-native retry for the same previously admitted container, while +Reploy owns lifecycle policy, explicit stop/revocation and preparation fences. +Do not introduce a callback service solely to repeat host checks after a trusted +administrator changes the system. The existing trust model assigns coordination +of those changes to the administrator. + +Container creation/replacement and changed image, mapping or mount configuration +still need normal Reploy admission. A proposed permission to retry an unchanged +container must not become transferable authority for another container, +installation, engine, generation, role or configuration. + +The governing text is amended to reflect that scoped decision and still needs +affected review. The behavior is not implemented by enabling a restart flag or +reusing a release frame. + +## What was tested + +The probe used an existing local BusyBox image, pinned by full image ID, with no +pulls, host bind mounts, published ports or production container changes. +Both engine probes used the Docker-compatible interface. Rootless Podman ran +against a new temporary VFS store and a short-lived private API process, not +the user's existing Podman store or a configured host service. + +| Environment | Observed version and location | +| --- | --- | +| Host | WSL2, kernel `6.18.54.1-microsoft-standard-WSL2` | +| Docker substrate | `Docker Desktop (containerized)`, Linux amd64; Engine 29.8.1, API 1.56 | +| Podman substrate | Local Linux Podman 4.9.3, rootless; separate VFS store under `/tmp/reploy-fg-podman-pg0j371k/store`, runtime under its sibling `runtime` directory | +| Podman test connection | Private Unix socket under the same temporary directory; Docker client API fixed to 1.41 for the compatibility probe | +| Image | `sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d` | +| Raw local evidence/scripts | `temp/reviews/fg-restart-fence-20261101/` | + +| Observation | Docker | Podman 4.9.3 | +| --- | --- | --- | +| Same container automatically restarts after its main process exits, without Reploy running | Passed; same full ID and restart count increased | Passed; same full ID and restart count increased | +| Change an existing policy to `no`, then kill the process and observe no retry | Passed | Compatible update returned an error; policy remained `always`. This is not a supported fence on this tested path. | +| Remove the exact container while it is automatically restarting/running | Passed; absent afterward | Passed; absent afterward | +| Explicitly stop an `always` container and observe it stay stopped for three seconds | Passed | Passed | +| Remove the stopped container and observe it remain absent | Passed | Passed | +| Container removal preserves a separate named volume and its sentinel contents | Passed | Passed | + +All disposable containers and volumes were cleaned up. The temporary Podman API +process exited, and its isolated pause namespace was retired after checking that +the temporary store had no containers or volumes. The existing Podman store and +configured services were not used. + +The first harness attempts encountered absence-message handling errors; they +are retained as failed attempts. The final runs used explicit container inspect +and recorded unsupported policy update separately. No failed capability was +silently converted into a positive result. + +The [compact evidence inventory](restart-fence-evidence/summary.json) identifies +the actual final results, limits and hashes of the local raw files/scripts. + +## What the observations support + +Removing the owned container is a common candidate fence: that engine object +cannot execute another retry once it is absent. Its separate retained data need +not be removed. This resembles existing Reploy `down`/recreation behavior and +avoids depending on a runtime policy update that the tested Podman path lacks. + +Removal alone is not a complete F/G transaction or proof that all processes and +storage attachments are gone. It also does not fence an independent host unit +that could create a replacement container. Those remain explicit owning checks. + +The candidate transaction is: + +1. Under the existing installation lock, persist the inhibited operation and + exact owned resources before any effect. +2. Fence every registered recovery owner that could create a replacement. + A manager stop must not deadlock against an operation lock needed by its + own cleanup path. +3. Stop and remove the exact owned container without deleting retained + application storage. Use independently validated ownership, not a name-only + lookup or application-reported identity. +4. Prove exact runtime retirement and that the relevant storage is unattached. + An API success, stopped PID or absence message alone is insufficient. +5. Only then prepare or modify declared storage. +6. On a later permitted start, create a new container through full admission. + Restore allowed recovery only after the transaction's completion checks. + +If a process, network request or engine fails during these steps, preserve the +pending operation, ownership and reservations. Do not report success, mutate +storage early, or enable retries against ambiguous preparation state. Recovery +reconciles exact resources under the same transaction before continuing. + +Reploy stop/revocation is complete only after its fence is established. A +persisted intent is not itself an engine-side stop. This is distinct from +promising instantaneous interruption at the moment an operator requests stop. + +Container removal discards its writable layer. The default immutable image and +separate managed data fit that model, but the explicit writable-image-root +option must document this boundary. Do not promise preservation of image-root +edits when Reploy removes/recreates a container. + +## Approved startup direction and mechanism boundary + +The current E implementation intentionally prevents direct engine retry: + +- `internal/probe/mapped_wait.go` requires a fresh frame on stdin and a fixed + deadline embedded in the create arguments. +- `internal/dockerdeploy/podman_mapped_waiter.go` consumes one release attempt, + verifies the pinned process and requires restart count zero and policy `no`. +- `internal/mappedlaunch/release.go` binds the frame to a particular operation, + revision, image, plan and container. It is not a reusable approval protocol. + +There are two distinct choices: + +1. **Fresh permission for every start.** Retain the per-start requirement. A + host runner or reachable callback receiver must provide it. A callback moves + the availability requirement; it does not remove it. +2. **Reuse approval for the unchanged container.** Permit the engine to restart + that exact previously admitted object without contacting Reploy. A new or + changed container still requires admission. This needs a deliberately + designed and proven way to distinguish first admission from permitted retry; + neither a writable application flag nor replaying the old frame is a proof. + +The maintainer selected the second direction. The original engine-fencing +probes did not prove its startup mechanism. A subsequent bounded +[startup-input proof](fg-startup-input-engineering.md) tested a host-controlled, +read-only record on both engines. That candidate is not production integration +or a signing service, general admission cache or callback endpoint. + +## Compatibility and required next proofs + +- Review the affected amendment before dependent implementation. Affected text + includes the security contract's per-entry admission + and engine-restart prohibition; the campaign's E/G lifetime sections; mail + lifetime reconciliation; and the E handoff's zero-restart assumptions. +- Keep credential/capability reduction, private descriptor closure, trusted-code + protection and effective sandbox checks on every actual process start. Reusing + host admission must not mean skipping the trusted local startup transition. +- Resolve startup hooks, readiness, private environment injection and secrets + for retry. A restarted process does not necessarily retain ephemeral tmpfs + data; these probes did not establish those paths. Preserve the once-per-instance + delivery contract unless an explicit amendment changes it. +- Qualify the full mapped startup/retry/fence sequence on the eligible Podman + backend, including process loss and each transaction crash boundary. An + ordinary rootless container is not mapped qualification. +- Cover registered system/user recovery owners and actual supported OS/engine + combinations. A WSL2 Docker Desktop run is not native Windows, macOS or native + Linux service-manager/reboot evidence. +- Test daemon/VM restart, host reboot and crash durability in disposable + environments. No live host or existing engine was restarted by these probes. +- Normalize policy meanings explicitly. Podman 4.9.3 documents `unless-stopped` + as identical to `always`; do not promise Docker's reboot semantics by passing + the same spelling to both engines. + +The documentation baseline is +[Docker restart policy behavior](https://docs.docker.com/engine/containers/start-containers-automatically/), +[Podman 4.9.3 restart policy behavior](https://docs.podman.io/en/v4.9.3/markdown/podman-run.1.html#restart-policy) +and [Podman 4.9.3 update scope](https://docs.podman.io/en/v4.9.3/markdown/podman-update.1.html). +These are contract inputs, not replacements for the actual versioned proof. + +This investigation does not finalize F/G slices or revised line forecasts. Any +startup-protocol adaptation, policy compatibility and cross-platform proof work +must be counted in its actual owning slices. It does not justify silently adding +a new service framework or removing a required positive acceptance journey. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md new file mode 100644 index 00000000..5bdab0db --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-sizing-basis.md @@ -0,0 +1,149 @@ +# F/G sizing basis + +This supports the canonical inventory in +[the implementation plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md#fg-engineering-and-slice-inventory--draft). +All values are conditional forecasts, not measured implementation, budgets, +working-hour estimates or guarantees that a mechanism will fit. The accepted +production target is 500 lines per implementation PR and the hard limit is 750. + +## Unit and current source + +Count added or rewritten handwritten physical LF lines, including comments, +blank lines, helpers and shipped configuration. Count moved code at destination; +report deletions and mechanical moves separately. Tests/fixtures have their own +column. Exclude prose, generated files, dependencies and raw observations. +This matches the governing plan's convention. Delivered C/E gross additions +plus removals are a different metric and must not be compared directly. + +The inspection used source head +`87899abecced4c5f8b3513ecee66793ecdd2df37`. Selected current implementation +footprints, including comments and blanks, were: + +| Inspected seams | Existing physical lines | Interpretation | +| --- | ---: | --- | +| Installed runner, wait and engine-readiness functions | 403 | Existing lifecycle core to adapt; not 403 new lines. | +| Systemd/host-command/platform/scope adapters | 627 | Linux system integration exists; native user adapters are missing. | +| Direct PTY, Unix/named-pipe dialers and endpoint pinning | 451 | Reuse instead of acquiring a full SDK or another transport family. | +| Deployment/process state, leases and private input handoff | 1,282 | Reuse state and validation; reconcile activation intent, enablement and process receipts. | +| Mapped proof/entry/wait/exec and nftables integration | 1,383 | Private interfaces and refusal paths are not production F observations. | + +The complete file list and counts are retained in +`temp/reviews/fg-controller-evaluation-20261101/source-footprint.json`. These +groups are source-reading references, not a sum of planned changes. + +## Production/configuration allocations + +Each row decomposes its range into concrete incremental responsibilities. +Existing helpers remain shared and are not charged as full new modules. A zero +production qualification row carries test/fixture/evidence work rather than +invented product code. The canonical slice descriptions carry its prerequisites +and observable completion check. + +| Slice | Incremental allocations, low–high physical lines | Sum | +| --- | --- | ---: | +| F1.1.1 | Syntax/model 80–130; metadata/canonical resolution 80–120; intrinsic-root rendering 120–180 | 280–430 | +| F1.1.2 | Layer/index projection 140–220; closed evidence/validation 100–160; build-lock/publication/reuse integration 110–170 | 350–550 | +| F1.2 | Descriptor primitives 120–200; path/alias validation 100–150; create/verify 80–130 | 300–480 | +| F1.3 | Setup protocol 80–130; identity binding 80–120; cancellation/cleanup 80–150 | 240–400 | +| F2.1 | Resource state 90–140; writers/pending effects 120–200; freshness 70–120 | 280–460 | +| F2.2 | Owner fence 80–130; retirement observations 100–170; cleanup recording 60–120 | 240–420 | +| F3.1.1 | Setup phase 70–130; frozen plan/call ordering 70–120; bounded failures 40–80 | 180–330 | +| F3.1.2 | Image/mount observation 120–200; effective kernel evidence 120–200; exact process/proof binding 60–100 | 300–500 | +| F3.2 | Network topology 80–130; peer/capability constraints 100–170; resolver/interface rules 80–140 | 260–440 | +| F4.1 | Typed publication 100–170; effective observation 100–160; exact cleanup 60–100 | 260–430 | +| G1.1 | Schema/phase mapping 120–190; retained-state guards 80–130; typed override phase/consumer wiring 80–130 | 280–450 | +| G1.2 | Dispatch 60–100; argument validation 50–90; authority/errors 90–150 | 200–340 | +| G1.3 | Destination transaction 120–200; independent immutable reuse 100–160; failure recovery 100–160 | 320–520 | +| G1.4 | Policy syntax/canonical form 100–160; activation/control facts 100–160; leases/retry accounting 100–160 | 300–480 | +| G1.5.1 | Managed channel/typed binding 110–190; native tail/liveness 70–120; pinned post-exec observation 90–150; lifecycle/failure cleanup 80–140 | 350–600 | +| G1.5.2 | Controller loop 120–200; exact reattachment 120–200; retry handling 60–100 | 300–500 | +| G1.6 | API wait 80–140; decode/cancel 40–80; lifecycle wiring 40–70 | 160–290 | +| G1.7 | Non-TTY framing 120–200; stdin/half-close 80–140; entry wiring 60–100 | 260–440 | +| G1.8 | Input parsing/startup 80–140; final transition 80–140; host binding 70–130 | 230–410 | +| G1.9 | User unit 80–140; controls 60–100; scope validation 40–80 | 180–320 | +| G1.10 | User agent definition 80–150; controls 80–140; binding 60–110 | 220–400 | +| G1.11 | Task/principal definition 100–180; controls 90–160; ownership/removal 60–100 | 250–440 | +| G1.12 | Control/enablement dispatch 100–170; manager transaction 100–170; hooks/completion 100–160 | 300–500 | +| G1.13 | Actual entry classification 100–170; adapter wiring 80–140; output/status 80–130 | 260–440 | +| G2.1 | Revoke admission 80–140; owner fences 80–140; observed retirement 100–160 | 260–440 | +| G3.1 | Recovery cases 80–140; resource cleanup 100–160; bounded diagnostics 80–140 | 260–440 | + +F1.0, F3.0, F4.2, G1.0, G1.14 and G3.2 are zero-production proof/qualification +candidates. They may reuse valid evidence. A failed mechanism proof invalidates +dependent readiness and may require a new engineering estimate; it is not a +reason to pad production or remove a required check. + +## Tests, integration and failure work + +Each canonical row has a separate test/fixture range. These costs cover different +work rather than mirroring implementation: + +- F1 owns shared filesystem attack fixtures and subordinate-owner checks. + Include path replacement, link/mount/type aliases, stale authority, foreign + objects, contextual defaults and failures without protected mutation. +- F2 owns attachment/preparation concurrency and interruption matrices, including + success-shaped engine errors and uncertain manager cleanup. Reuse existing + operation-lock and resource fixture builders. +- F3/F4 own real kernel/network/publication integration and explicit transport + proof. Ordinary nftables tests do not replace eligible rootless positive and + negative journeys. F4.2 owns the combined parent journey, not a full duplicate + of every child assertion. +- G1.4 owns the shared controller state/lease fixture. G1.5.1–G1.8 add actual startup evidence, policy, + surviving-process, stream, private input and final-transition fault cases using + that fixture; they do not each recreate it. +- G1.9–G1.11 price native adapter tests independently. G1.14 owns the cross-platform + lifecycle/session qualification, including versions/locations and complete + process-tree resource observations. A cross-compiled binary or WSL pass is + insufficient native evidence. +- G2/G3 own active revocation and interruption across actual assembled effects. + G3.2 owns the complete public exact/bounded acceptance matrix and exact-head + CI/review closeout. Missing native or engine access remains visible, not a pass. + +Docs, Changie prose and review bookkeeping are separate work excluded from both +line columns. Necessary fixture scripts count as tests. Observed timing and +tokens are reported only when actually available; no working-hour forecast is +included. + +## Interpretation of the change + +The original logical-parent F/G ranges were 2,400–4,200 production and +4,300–7,050 tests. The current controller draft is 6,820–11,450 production and +17,550–29,050 tests across 26 implementation and six qualification candidates. +These are candidate boundaries, not a promise of 32 PRs. + +The increase primarily exposes missing ownership, effective sandbox and native +integration work. The new Linux-user/macOS-user/Windows-user adapters alone +account for 650–1,160 production and 1,350–2,350 test lines. Controller state, +loop, wait/stream and mapped private-input integration account for +1,600–2,720 production and 3,450–5,700 test lines, with shared work charged once. +These are component totals, not arithmetic claims that every line is added +scope relative to the original overlapping parents. + +The intermediate engine-retry candidate required retained approval/private +snapshots instead. It remains historical alternative analysis and used a +different rule for unchanged moves. Do not publish a precise normalized saving +against it. The lean-controller direction has demonstrated a smaller isolated +wait client, not yet a smaller complete cross-platform implementation. + +## Image-projection refinement + +F1.1.2 prices a missing producer separately from mount declarations. The +disposable metadata prototype reuses D's archive/immutable binding; its source +and result are in [mechanism evidence](mechanism-evidence/manifest.json). +Production must add aggregate allocation limits, typed canonical evidence and +build/publication/reuse integration. The 350–550 production and 700–1,100 test +range covers these incremental obligations, not another complete image reader. +F3.1.2 consumes that result and owns actual host observations, so the producer +is not counted there again. Its existing range remains conditional on the +eligible-engine proof; review can require a split before implementation. + +## Startup-completion producer refinement + +G1.5 was relying on E's private `StartupComplete` interface as though its real +producer existed. G1.5.1 now owns that missing producer and qualification before +G1.5.2 consumes it. The 350–600 production range prices managed channel/binding, +the raw-tail/liveness refinement, actual process observation and recovery +integration; 800–1,300 tests price both-profile/native/exit-race and private-fd +controls. The four-case process proof is reusable feasibility evidence, not +full engine/credential qualification. A failed required proof blocks the provider +and dependent loop; it cannot be closed by a completion boolean or weaker up. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-slice-estimates-draft.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-slice-estimates-draft.json new file mode 100644 index 00000000..de314812 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-slice-estimates-draft.json @@ -0,0 +1,28 @@ +{ + "status": "Historical engine-retry authoring forecast, superseded for delivery by selected lean-controller engineering. Not reviewed or approved implementation slices.", + "unit": "Physical handwritten LF lines added or rewritten; comments and blank lines included. Production/configuration and tests/fixtures are separate. Unchanged moves are excluded; report deletions separately. Do not compare this unit directly with E's authored additions-plus-removals total.", + "limit": {"production_target": 500, "production_hard_max": 750}, + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "slices": [ + {"id":"F1.1","title":"Declare managed metadata and intrinsic root policy","production":[300,450],"tests":[600,850],"depends":["E4.2"],"owns":"Environment mount parsing, root exclusion from external mount materialization, canonical policy binding and owner/group/permissions/directories/files validation.","check":"Absent root remains read-only and ordinary hashes remain stable; explicit root writability is bound; unknown owners, unsafe relative paths and root-as-external-bind reject before effects."}, + {"id":"F1.2","title":"Prepare inert objects through pinned descriptors","production":[300,500],"tests":[650,1000],"depends":["F1.1"],"owns":"Trusted Linux directory/file primitive; no symlink, mount crossing, alias, type substitution or recursive ownership repair.","check":"Directories precede children and files; exact declared creation succeeds; adversarial replacement and foreign existing objects leave protected targets unchanged."}, + {"id":"F1.3","title":"Run the preparer under selected subordinate authority","production":[250,450],"tests":[550,900],"depends":["F1.2"],"owns":"Fresh host authority, bounded namespace/setup-helper protocol, confined handoff directory, selected numeric ownership, helper cancellation and exit.","check":"Expected host ownership and recipient access on the private geometry; no host-root/operator alias, unrelated path mutation, application code or surviving setup process."}, + {"id":"F2.1","title":"Record managed resources and exclude attached storage","production":[300,500],"tests":[650,1000],"depends":["F1.3"],"owns":"Extend existing deployment ownership/pending records for managed data and startup inputs; serialize prepare/restore against every relevant attachment.","check":"Concurrent attach/prepare and stale revision fail before writes; crashes retain inhibition, exact handles and reservations without global allocation or ownership guessing."}, + {"id":"F2.2","title":"Fence retry and replacement before protected effects","production":[250,450],"tests":[650,1000],"depends":["F2.1"],"owns":"Common stop/removal transaction, registered host restart owners, exact engine-object retirement, retained data and ambiguous-response recovery.","check":"Stop/revocation/preparation cannot complete while a retry owner or attachment remains; API errors preserve state; no manager-stop/operation-lock deadlock or volume deletion."}, + {"id":"F3.0","title":"Prove the eligible rootless network transport","production":[0,0],"tests":[400,700],"depends":["F2.2"],"owns":"Disposable proof on the selected E-compatible engine: private network namespace, effective policy observation, DNS, peer denial and endpoint transport. Reuse existing nftables tests and fixtures.","check":"Positive public/local/declared endpoint journeys plus direct peer/gateway/IPv6 negatives, with exact cleanup. Failure blocks F3.1/F3.2/F4 rather than selecting a weaker network or engine."}, + {"id":"F3.1","title":"Prepare and observe network policy before first release","production":[300,500],"tests":[650,1000],"depends":["F3.0"],"owns":"Split trusted setup from final exec, reuse existing nftables installer, bind effective reports to the pinned inhibited process, and implement the production sandbox observation provider.","check":"Rules and mount/kernel observations exist before approval; stale/substituted or post-payload reports cannot authorize release; every retry repeats local setup."}, + {"id":"F3.2","title":"Enforce private installation network boundaries","production":[300,500],"tests":[650,1100],"depends":["F3.1"],"owns":"Canonical backend network shape, private namespaces/propagation, direct peer and host-gateway boundaries, resolver exceptions and unsupported capability/policy refusal.","check":"Existing public/local/ambiguous semantics hold without a shared private network or undeclared peer access; incompatible capability sets fail explicitly rather than silently losing rights."}, + {"id":"F4.1","title":"Publish and verify declared endpoints","production":[300,500],"tests":[600,900],"depends":["F3.2"],"owns":"Typed compatible create/inspect endpoint fields, staged/deployed publication selection, exact endpoint/network ownership and teardown.","check":"Only declared ports/addresses are reachable; mismatched publication fails before payload; partial cleanup retains uncertainty and never removes a foreign object."}, + {"id":"F4.2","title":"Qualify storage and network together","production":[0,0],"tests":[700,1100],"depends":["F4.1"],"owns":"Integrated exact/bounded preparation, attachment, network, endpoint and failure evidence on the qualified engine.","check":"Observable filesystem/ownership/network positives and refusal/no-mutation negatives close F's rows; unit/injected fixtures alone cannot close the gate."}, + {"id":"G1.1","title":"Select phase mappings under the Podman input","production":[300,500],"tests":[650,1000],"depends":["F1.1"],"owns":"Direct schema-1 relocation to podman.identity_mapping, staging/deployed selection, typed overrides, install UID/GID flags and all opt-in/policy-only/retained-state guards.","check":"Correct phase and whole-array override precedence; no legacy input path, profile fallback, build-authority contamination or guard bypass when a declaration disappears."}, + {"id":"G1.2","title":"Connect approved identity maintenance commands","production":[250,450],"tests":[550,850],"depends":["G1.1"],"owns":"Public check/configure/status/recover wiring to the delivered identity operations, bounded contexts, diagnostics and current held-lock state. Reuse existing approved command contracts.","check":"Actual public command journeys reach existing operations, preserve pending/dormant/revoked authority and never launch a workload or adopt copied state."}, + {"id":"G1.3","title":"Install an independent destination owner","production":[350,550],"tests":[750,1100],"depends":["G1.2","F4.2"],"owns":"Destination lock/publication, independent mapping decision, portable immutable build reuse, source/destination revision and overlap checks, rollback-safe installed controls.","check":"Actual staging-to-user-install has separate reservations; source state/data are unchanged; partial publication recovers exactly without copying authority or mapped application data."}, + {"id":"G1.4","title":"Publish per-container startup approval","production":[350,550],"tests":[650,1000],"depends":["F3.1","G1.3"],"owns":"Exact container/create binding, canonical bounded approval record, pinned-inode commit protocol, first-start waiting and retry reuse; lifecycle receipt separates container from observed process start.","check":"No payload before durable first admission; no old-frame or wrong-object reuse; partial record, missing file, replay and process-change cases fail; no callback or private descriptor reaches the app."}, + {"id":"G1.5","title":"Deliver captured private inputs on each process start","production":[250,450],"tests":[650,1000],"depends":["G1.4","F1.3"],"owns":"Parsed .env runtime snapshot for the intended persistent workload, recipient ownership, native startup read integration and cleanup/export exclusions. Reuse F preparation and E final exec.","check":"Initial/retry processes receive the captured values once, other identities/transient commands do not gain access, and values stay out of metadata/engine configuration/arguments/logs. Final FD/credential proof passes before enabling the branch."}, + {"id":"G1.6","title":"Connect persistent lifecycle and engine handoff","production":[350,550],"tests":[750,1150],"depends":["G1.5","F2.2"],"owns":"Actual up/down/restart, hooks/readiness, exact replacement, observed incarnation reconciliation, host-to-engine ownership handoff, cancellation and compatible retry policy handling.","check":"Startup/handoff complete before up; repeated up does not resend private input; retries do not undo stop; failures retain exact ownership, and unavailable engines cannot cause duplicate or foreign replacement."}, + {"id":"G1.7","title":"Connect fresh command and test entries without changing semantics","production":[300,500],"tests":[700,1100],"depends":["G1.6"],"owns":"Application/test/hook and deployed-command adapters, correct fresh-container classification, stdin/EOF/TTY and host-observed exit/output handoff where qualified.","check":"Existing staged and installed transient commands preserve arguments, output, status and roles; raw shell without an accepted definition, real unqualified occupied exec and shared-network session paths remain explicit pre-effect refusals under their existing governing limits."}, + {"id":"G2.1","title":"Revoke active runtime while retaining ownership","production":[300,500],"tests":[600,950],"depends":["G1.7","F2.2"],"owns":"Active revocation command/effect composition, all retry/replacement fences, exact stop/removal confirmation and retained data/mapping state.","check":"Success means owned runtime absent and no registered recovery owner can restart it; future admission rejects while storage and reservations remain; ambiguous cleanup never reports successful revocation."}, + {"id":"G3.1","title":"Reconcile interrupted activation and cleanup","production":[300,500],"tests":[650,1050],"depends":["G2.1"],"owns":"Complete pending-operation crash matrix across installation, approval publication, startup, replacement, revocation and input/network cleanup. Reuse B recovery and exact ownership.","check":"Every interruption has a safe supported continuation; no copied receipt, stale PID, unexpected object or uncertain write permits adoption, blind replay, data deletion or reservation release."}, + {"id":"G3.2","title":"Qualify public launch and shared lifecycle compatibility","production":[0,0],"tests":[1000,1600],"depends":["G3.1","F4.2"],"owns":"Actual public exact/bounded stage/build/configure/run/test/install/retry/stop/revoke journeys, full refusal matrix, supported OS/scope/engine ordinary compatibility and evidence ledger.","check":"Close generic-launch only from source-bound supported-interface evidence and exact-head CI. Keep unsupported combinations explicit; WSL/injected/cross-build results do not replace native platform or reboot tests."} + ] +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-startup-input-engineering.md b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-startup-input-engineering.md new file mode 100644 index 00000000..23818096 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/fg-startup-input-engineering.md @@ -0,0 +1,224 @@ +# F/G startup inputs and unchanged-container retries + +This is engineering for the approved unchanged-container retry direction in the +[campaign plan](../../MULTI_IDENTITY_IMPLEMENTATION_PLAN.md). It defines a +candidate for review, not a completed production mechanism. The initial mapped +cohort, capabilities, secret rules and public activation guards remain unchanged. + +This is the unselected engine-retry alternative in the +[controller comparison](fg-controller-evaluation.md). Retained approval and +private-input snapshots are not prerequisites for a host controller that +creates and admits replacements and reuses the existing one-shot delivery. Do +not implement or size both alternatives as mandatory work. The maintainer +selected lean controller engineering; this alternative retains useful protocol +analysis but is not the selected delivery mechanism. + +## Reuse and mechanism choice + +Retain the existing immutable trusted startup, exact engine-object ownership, +deployment transaction, private runtime masking, confined preparation and final +native exec transition. Replace the invocation-only stdin release on the new +mapped public path with a bounded host-controlled startup record that survives +ordinary process restarts of that container. Do not carry two public mapped +startup mechanisms for the same purpose or reinterpret old one-shot evidence as +proof of the new mechanism. E's recorded evidence remains historical. + +An always-reachable callback receiver would retain per-start host availability +and introduce a service/transport requirement. It is not needed for the approved +same-container approval lifetime. A writable application flag would be smaller +but is not authoritative before first admission. The candidate uses a narrowly +mounted, read-only host-controlled record instead. No general broker, engine +socket, shared daemon, signing service or allocator is introduced. + +## Required records and ownership + +The existing deployment transaction remains authoritative. It retains the +installation and exact engine endpoint/object ID, create nonce, accepted +generation/image/runtime-policy identities, mapping geometry, retry policy, +owned startup-input handles and operation progress. Startup material is only a +projection for that exact container; it cannot configure or admit an object. + +Separate the immutable container identity from a process start: + +- Exact container: selected engine plus full container ID and independently + verified immutable create binding. A name or application claim is insufficient. +- Process incarnation: freshly observed PID/start time and namespace identity. + These change on engine retry and are never reused as current process proof. +- Startup completion: applies to the observed incarnation. An old `up` record + or handoff claim does not prove a new process completed startup. + +`WorkloadLifecycleV1` currently binds one process start. G must extend or version +its mapped receipt without silently weakening ordinary one-shot readers. A +locked host operation may reconcile a new observed start only after proving it +is the exact admitted unchanged container. No object adoption by name, stale PID +or copied lifecycle file is permitted. + +## First-admission protocol + +1. Hold the installation operation lock; read current generation and host + authority through existing readers. Persist an inhibited intent and fresh + create nonce before effects. +2. Prepare the startup record under the private host runtime area using held + descriptors and exclusive creation. Its initial state grants nothing. Prepare + only required per-recipient startup inputs through the confined preparer. +3. Create the selected container with immutable trusted startup arguments, + expected nonce/plan binding and narrowly scoped read-only input mounts. Never + mount the deployment directory, state store, source `.env`, engine socket or + a directory containing other recipients' material into the workload. +4. Record the returned full container ID. Start only trusted Reploy setup code. + It prepares the local sandbox/network state and remains inhibited; no image + entrypoint, application hook or application code runs with setup authority. +5. Verify the exact owned process and F's effective mount/network/kernel + observations. Complete the existing mapping/generation admission transaction. +6. After the exact-object admission is durable, publish its bounded startup + record through the held file descriptor and flush it. A malformed, partial or + mismatched record grants nothing. An uncertain publish keeps the exact owner + and inhibited recovery record; it never triggers blind permission transfer. +7. Trusted startup accepts only its fixed path and immutable expected binding, + closes the record descriptor and performs the final credential/capability, + descriptor and sandbox transition before directly executing the application. + +The startup record must be regular bounded data with validated ownership, +identity and mount protection. The host must verify the same inode is mounted +and written; atomic pathname replacement alone does not update a single-file +bind mount. No symlink, hardlink alias or application-selected source path is +accepted. The prototype's short comparison string is not the production wire +contract; the candidate production contract is below. + +### Candidate record and publication format + +Use the existing closed canonical JSON and digest helpers. The binding contains +the installation ID, positive engine-identity digest, fresh 256-bit create nonce, +generation/build-lock digest, immutable image digest and canonical entry-plan +digest. The plan already binds role, initial credentials/groups/capabilities, +arguments, mappings, mount policy and network policy. It excludes mutable PID, +start time and engine-assigned container ID. Freeze that binding in trusted +create arguments. It is not selected through application arguments or env. + +The record has exactly `schema`, `state`, `binding`, `container_id`, +`initial_operation` and `initial_revision`. Schema is +`mapped-container-approval-v1`; the only committed state is `admitted`. +`container_id` is the engine's full lowercase hexadecimal ID. Operation and +revision identify the actual admission commit, not a revision to be refreshed +on every retry. Reject unknown/duplicate keys and noncanonical encodings. + +The file consists of the eight-byte magic `RPLYAP1\n`, a four-byte big-endian +JSON length, 1–65,536 canonical JSON bytes, and a 32-byte SHA-256 digest of +`mapped-container-approval-v1\0` followed by those JSON bytes. Its exact length +must be `44 + JSON length`; no trailing data is accepted. The digest detects +partial/inconsistent publication. It is not a signature or authentication key. +Authority comes from the validated host-controlled inode, read-only view and +exact-object transaction, not from anyone knowing the non-secret binding. + +An initial empty/invalid record grants nothing. The sole host writer first +writes body/digest to its held inode, then the commit header and length, and +flushes the file. It does this only after the admission state is durably +committed. The reader accepts only a full valid snapshot matching the frozen +binding, with a regular single-link file and the expected read-only mount. +Host-side ownership and exact source identity are checked before attachment; +an overflow UID inside the private map is not treated as proof of host ownership. + +Permission is immutable for that container. A different plan, nonce or engine +requires a new container and pending record. A torn/uncertain first publication +retains the exact owner. Recovery may reconcile/reconstruct only that exact +committed projection after fresh host verification; it cannot adopt an object, +replay a one-shot secret transfer or infer approval from file existence. + +The trusted host creator must generate a new nonce and record for every create, +bind the returned ID before commit and reject copied authority. The startup +process cannot independently learn its full engine object ID without engine +access; the design does not pretend that matching a submitted ID authenticates +the caller. This trusted creation/association duty is an explicit proof target. + +## Retry protocol + +The engine restarts that same container under Reploy's configured permitted +policy. Trusted startup repeats local sandbox setup, validates the fixed +read-only record and its immutable create binding, then repeats the final +authority reduction and exec. It does not need a new stdin frame, expired +creation deadline, host inventory scan or reachable callback. + +Waiting for an initial uncommitted record has a bounded per-process deadline; +it must not reuse an absolute deadline from the original creation. Missing, +corrupt, substituted or inconsistent material fails before payload execution. +A new container receives a fresh binding and unapproved record even if image, +application name and logical configuration are identical. + +A Reploy-controlled stop/revocation/preparation transaction fences every +registered replacement owner and removes or otherwise positively fences the +exact engine object before protected effects. Owned record cleanup follows +proved runtime retirement. It is not a substitute for that retirement. A +runtime-held inode can remain readable after host unlink, so unlinking a record +must never be the sole revocation mechanism. + +## Private environment and secret constraints + +The existing private workload file is `.env`, represented by +`PrivateWorkloadEnvironmentFileName`; `.env.private` was an inaccurate name in +the initial discussion. The current relay sends parsed values once per process +startup and removes its FIFO names. The mapped transition currently rejects that +transport. A permission record does not recreate private environment values. + +The candidate extension prepares a private runtime snapshot of only the parsed +values intended for the persistent workload, not raw `.env` comments or a whole +deployment directory. It is separate from the non-secret approval record and +from portable image/state/lock/artifact metadata. Values must not enter engine +environment inspection, arguments, manifests, hashes published in metadata, +logs, review packets or application output. Retain current `.env` validation, +limits and masking. + +For the private mapped Linux cohort, prepare recipient-owned files through the +same confined subordinate-identity preparer required by F, with only that +recipient's access. A world-readable secret mount is not an acceptable shortcut. +Same-UID roles and a master permitted to assume that UID retain their existing +authority; the mechanism must not promise new isolation between them. + +Trusted startup reads the recipient input without retaining private descriptors +or broader credential authority across exec. Reuse E's last native transition; +prepare all environment buffers before its descriptor-table detach/raw exec. +Do not put ordinary Go code after the audited raw transition or retain a new +privileged reader beside the application. The exact read/transition integration +needs its native proof before the guard is removed. The candidate Linux reader +uses the already locked setup thread and accepted identity-setting authority to +read as the recipient filesystem identity, then verifies restoration before +preparing the existing last-transition buffers. It must not add a DAC-bypass +grant or retain a stronger reader process. If that exact transition cannot be +proved, keep the guard and return to engineering rather than making the secret +file readable to other identities. + +Each new process receives its captured input once. Repeated `up` must not resend +to a live process. Engine retry uses the same captured inputs; an explicit +replacement prepares a new snapshot. Rotation/source refresh belongs to its +explicit lifecycle operation, not silent per-retry host-file rereading. Runtime +snapshots stay private, are excluded from portable copies/export, and remain +owned until exact runtime/input cleanup succeeds. This must not become a new +general secret-reference or rotation feature; I retains that work. + +Other supported host/engine paths retain their actual private-environment +guarantees. Do not infer POSIX recipient ownership from Windows ACLs, Desktop +file sharing or a WSL result. Existing unsupported private-environment restart +combinations remain explicit until their input path is qualified; never claim +new cross-platform support from this Linux proof. + +## Bounded observations and limitations + +The [actual startup evidence](restart-fence-evidence/startup-summary.json) records: + +- Docker Desktop 29.8.1 and isolated rootless Podman 4.9.3: startup remained + blocked before host approval, then ran and retried with no new host grant. +- Both engines: root with an explicit DAC-bypass capability could not modify + the approval through the read-only bind. A separate writable-mount control + allowed a change despite file mode `0444`. +- Both engines: a different launch nonce did not match the earlier record. + Fresh nonce selection and exact-container association remain host duties. +- WSL2 subordinate-only namespace probe: the trusted preparer created a dummy + recipient file at UID/GID 200, observed externally as 100200/100200. Recipient + 200 could read it; identity 201 could not. All subordinate-owned test objects + were removed. No account/delegation edits, image code or real secrets were used. + +The engine fixtures use trusted BusyBox/shell code. They prove the storage and +retry transport properties, not the production Go startup boundary. The +namespace probe is not complete preparation/race or secret-delivery proof. +Neither establishes crash-safe permission publication, live private-map +qualification, per-recipient engine mounts, cold boot or native Windows/macOS +behavior. Those checks must have explicit owning prerequisites/slices. diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-attempt01-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-attempt01-result.json new file mode 100644 index 00000000..a9cc48c6 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-attempt01-result.json @@ -0,0 +1,38 @@ +{ + "command": [ + "go", + "test", + "-json", + "-count=1", + "-overlay", + "/home/omry/dev/reploy/temp/reviews/fg-finalization-20261101/image-projection-overlay.json", + "./internal/dockerdeploy", + "-run", + "^TestFGImageDestinationProjection$", + "-timeout=120s" + ], + "exit_code": 0, + "elapsed_seconds": 5.914950288992259, + "passed": [ + "TestFGImageDestinationProjection/absent", + "TestFGImageDestinationProjection/empty", + "TestFGImageDestinationProjection/directories-only", + "TestFGImageDestinationProjection/data-is-protected-too", + "TestFGImageDestinationProjection/zero-size-file", + "TestFGImageDestinationProjection/prefix-is-not-overlap", + "TestFGImageDestinationProjection/whiteout-removes-lower", + "TestFGImageDestinationProjection/whiteout-keeps-same-layer", + "TestFGImageDestinationProjection/opaque-removes-lower", + "TestFGImageDestinationProjection/opaque-keeps-same-layer", + "TestFGImageDestinationProjection/absolute-image-alias", + "TestFGImageDestinationProjection/relative-image-alias", + "TestFGImageDestinationProjection/alias-to-populated-tree", + "TestFGImageDestinationProjection/alias-cycle", + "TestFGImageDestinationProjection/file-destination", + "TestFGImageDestinationProjection/layer-traverses-link", + "TestFGImageDestinationProjection/intrinsic-root-not-data", + "TestFGImageDestinationProjection" + ], + "failed": [], + "scope": "Disposable metadata projection bound to actual D-verified immutable archive/layers; no production reader change, extracted files, code classifier or admission qualification." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-result.json new file mode 100644 index 00000000..682fd7d0 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof-result.json @@ -0,0 +1,43 @@ +{ + "command": [ + "go", + "test", + "-json", + "-count=1", + "-overlay", + "/home/omry/dev/reploy/temp/reviews/fg-finalization-20261101/image-projection-overlay.json", + "./internal/dockerdeploy", + "-run", + "^TestFGImageDestinationProjection$", + "-timeout=120s" + ], + "exit_code": 0, + "elapsed_seconds": 6.408751765004126, + "passed": [ + "TestFGImageDestinationProjection/absent", + "TestFGImageDestinationProjection/empty", + "TestFGImageDestinationProjection/directories-only", + "TestFGImageDestinationProjection/data-is-protected-too", + "TestFGImageDestinationProjection/zero-size-file", + "TestFGImageDestinationProjection/prefix-is-not-overlap", + "TestFGImageDestinationProjection/whiteout-removes-lower", + "TestFGImageDestinationProjection/whiteout-keeps-same-layer", + "TestFGImageDestinationProjection/opaque-removes-lower", + "TestFGImageDestinationProjection/opaque-keeps-same-layer", + "TestFGImageDestinationProjection/absolute-image-alias", + "TestFGImageDestinationProjection/relative-image-alias", + "TestFGImageDestinationProjection/alias-to-populated-tree", + "TestFGImageDestinationProjection/alias-cycle", + "TestFGImageDestinationProjection/file-destination", + "TestFGImageDestinationProjection/layer-traverses-link", + "TestFGImageDestinationProjection/alias-to-root", + "TestFGImageDestinationProjection/hardlink-is-file", + "TestFGImageDestinationProjection/link-descendant-protects-subtree", + "TestFGImageDestinationProjection/file-replaced-by-directory", + "TestFGImageDestinationProjection/directory-replaced-by-file", + "TestFGImageDestinationProjection/intrinsic-root-not-data", + "TestFGImageDestinationProjection" + ], + "failed": [], + "scope": "Disposable metadata projection bound to actual D-verified immutable archive/layers; no production reader change, extracted files, code classifier or admission qualification." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof.jsonl b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof.jsonl new file mode 100644 index 00000000..24701042 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image-projection-proof.jsonl @@ -0,0 +1,96 @@ +{"Time":"2026-11-02T01:28:19.676476503+08:00","Action":"start","Package":"github.com/omry/reploy/internal/dockerdeploy"} +{"Time":"2026-11-02T01:28:19.680256437+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection"} +{"Time":"2026-11-02T01:28:19.680310484+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection","Output":"=== RUN TestFGImageDestinationProjection\n"} +{"Time":"2026-11-02T01:28:19.680317328+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absent"} +{"Time":"2026-11-02T01:28:19.680319171+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absent","Output":"=== RUN TestFGImageDestinationProjection/absent\n"} +{"Time":"2026-11-02T01:28:19.680649973+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absent","Output":"--- PASS: TestFGImageDestinationProjection/absent (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.680672626+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absent","Elapsed":0} +{"Time":"2026-11-02T01:28:19.680677304+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/empty"} +{"Time":"2026-11-02T01:28:19.68067887+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/empty","Output":"=== RUN TestFGImageDestinationProjection/empty\n"} +{"Time":"2026-11-02T01:28:19.680836712+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/empty","Output":"--- PASS: TestFGImageDestinationProjection/empty (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.680857518+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/empty","Elapsed":0} +{"Time":"2026-11-02T01:28:19.680860842+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directories-only"} +{"Time":"2026-11-02T01:28:19.680862195+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directories-only","Output":"=== RUN TestFGImageDestinationProjection/directories-only\n"} +{"Time":"2026-11-02T01:28:19.681059751+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directories-only","Output":"--- PASS: TestFGImageDestinationProjection/directories-only (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.681077137+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directories-only","Elapsed":0} +{"Time":"2026-11-02T01:28:19.681079498+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/data-is-protected-too"} +{"Time":"2026-11-02T01:28:19.681080944+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/data-is-protected-too","Output":"=== RUN TestFGImageDestinationProjection/data-is-protected-too\n"} +{"Time":"2026-11-02T01:28:19.681267859+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/data-is-protected-too","Output":"--- PASS: TestFGImageDestinationProjection/data-is-protected-too (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.681281673+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/data-is-protected-too","Elapsed":0} +{"Time":"2026-11-02T01:28:19.681284974+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/zero-size-file"} +{"Time":"2026-11-02T01:28:19.681503877+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/zero-size-file","Output":"=== RUN TestFGImageDestinationProjection/zero-size-file\n"} +{"Time":"2026-11-02T01:28:19.681885615+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/zero-size-file","Output":"--- PASS: TestFGImageDestinationProjection/zero-size-file (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.681901353+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/zero-size-file","Elapsed":0} +{"Time":"2026-11-02T01:28:19.68190417+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/prefix-is-not-overlap"} +{"Time":"2026-11-02T01:28:19.681905647+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/prefix-is-not-overlap","Output":"=== RUN TestFGImageDestinationProjection/prefix-is-not-overlap\n"} +{"Time":"2026-11-02T01:28:19.682523177+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/prefix-is-not-overlap","Output":"--- PASS: TestFGImageDestinationProjection/prefix-is-not-overlap (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.68254377+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/prefix-is-not-overlap","Elapsed":0} +{"Time":"2026-11-02T01:28:19.682547255+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-removes-lower"} +{"Time":"2026-11-02T01:28:19.682548706+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-removes-lower","Output":"=== RUN TestFGImageDestinationProjection/whiteout-removes-lower\n"} +{"Time":"2026-11-02T01:28:19.682795484+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-removes-lower","Output":"--- PASS: TestFGImageDestinationProjection/whiteout-removes-lower (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.682808756+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-removes-lower","Elapsed":0} +{"Time":"2026-11-02T01:28:19.682811105+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-keeps-same-layer"} +{"Time":"2026-11-02T01:28:19.682812477+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-keeps-same-layer","Output":"=== RUN TestFGImageDestinationProjection/whiteout-keeps-same-layer\n"} +{"Time":"2026-11-02T01:28:19.683020666+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-keeps-same-layer","Output":"--- PASS: TestFGImageDestinationProjection/whiteout-keeps-same-layer (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.683032796+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/whiteout-keeps-same-layer","Elapsed":0} +{"Time":"2026-11-02T01:28:19.683035692+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-removes-lower"} +{"Time":"2026-11-02T01:28:19.683037138+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-removes-lower","Output":"=== RUN TestFGImageDestinationProjection/opaque-removes-lower\n"} +{"Time":"2026-11-02T01:28:19.683233579+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-removes-lower","Output":"--- PASS: TestFGImageDestinationProjection/opaque-removes-lower (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.683250916+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-removes-lower","Elapsed":0} +{"Time":"2026-11-02T01:28:19.683253283+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-keeps-same-layer"} +{"Time":"2026-11-02T01:28:19.683254755+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-keeps-same-layer","Output":"=== RUN TestFGImageDestinationProjection/opaque-keeps-same-layer\n"} +{"Time":"2026-11-02T01:28:19.683496349+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-keeps-same-layer","Output":"--- PASS: TestFGImageDestinationProjection/opaque-keeps-same-layer (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.683508594+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/opaque-keeps-same-layer","Elapsed":0} +{"Time":"2026-11-02T01:28:19.683510778+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absolute-image-alias"} +{"Time":"2026-11-02T01:28:19.683513921+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absolute-image-alias","Output":"=== RUN TestFGImageDestinationProjection/absolute-image-alias\n"} +{"Time":"2026-11-02T01:28:19.683690574+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absolute-image-alias","Output":"--- PASS: TestFGImageDestinationProjection/absolute-image-alias (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.683714183+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/absolute-image-alias","Elapsed":0} +{"Time":"2026-11-02T01:28:19.683717575+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/relative-image-alias"} +{"Time":"2026-11-02T01:28:19.683719075+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/relative-image-alias","Output":"=== RUN TestFGImageDestinationProjection/relative-image-alias\n"} +{"Time":"2026-11-02T01:28:19.683863701+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/relative-image-alias","Output":"--- PASS: TestFGImageDestinationProjection/relative-image-alias (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.683877941+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/relative-image-alias","Elapsed":0} +{"Time":"2026-11-02T01:28:19.683880444+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-populated-tree"} +{"Time":"2026-11-02T01:28:19.683881885+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-populated-tree","Output":"=== RUN TestFGImageDestinationProjection/alias-to-populated-tree\n"} +{"Time":"2026-11-02T01:28:19.684036114+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-populated-tree","Output":"--- PASS: TestFGImageDestinationProjection/alias-to-populated-tree (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.684051347+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-populated-tree","Elapsed":0} +{"Time":"2026-11-02T01:28:19.684055877+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-cycle"} +{"Time":"2026-11-02T01:28:19.684061145+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-cycle","Output":"=== RUN TestFGImageDestinationProjection/alias-cycle\n"} +{"Time":"2026-11-02T01:28:19.684545285+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-cycle","Output":"--- PASS: TestFGImageDestinationProjection/alias-cycle (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.684604791+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-cycle","Elapsed":0} +{"Time":"2026-11-02T01:28:19.684625059+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-destination"} +{"Time":"2026-11-02T01:28:19.684627508+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-destination","Output":"=== RUN TestFGImageDestinationProjection/file-destination\n"} +{"Time":"2026-11-02T01:28:19.684894875+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-destination","Output":"--- PASS: TestFGImageDestinationProjection/file-destination (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.68491509+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-destination","Elapsed":0} +{"Time":"2026-11-02T01:28:19.684918151+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/layer-traverses-link"} +{"Time":"2026-11-02T01:28:19.684919642+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/layer-traverses-link","Output":"=== RUN TestFGImageDestinationProjection/layer-traverses-link\n"} +{"Time":"2026-11-02T01:28:19.685117555+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/layer-traverses-link","Output":"--- PASS: TestFGImageDestinationProjection/layer-traverses-link (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.685136261+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/layer-traverses-link","Elapsed":0} +{"Time":"2026-11-02T01:28:19.685138757+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-root"} +{"Time":"2026-11-02T01:28:19.685140216+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-root","Output":"=== RUN TestFGImageDestinationProjection/alias-to-root\n"} +{"Time":"2026-11-02T01:28:19.685323298+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-root","Output":"--- PASS: TestFGImageDestinationProjection/alias-to-root (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.685346212+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/alias-to-root","Elapsed":0} +{"Time":"2026-11-02T01:28:19.685349172+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/hardlink-is-file"} +{"Time":"2026-11-02T01:28:19.685350594+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/hardlink-is-file","Output":"=== RUN TestFGImageDestinationProjection/hardlink-is-file\n"} +{"Time":"2026-11-02T01:28:19.685492409+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/hardlink-is-file","Output":"--- PASS: TestFGImageDestinationProjection/hardlink-is-file (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.685514248+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/hardlink-is-file","Elapsed":0} +{"Time":"2026-11-02T01:28:19.685522839+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/link-descendant-protects-subtree"} +{"Time":"2026-11-02T01:28:19.685524623+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/link-descendant-protects-subtree","Output":"=== RUN TestFGImageDestinationProjection/link-descendant-protects-subtree\n"} +{"Time":"2026-11-02T01:28:19.685649947+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/link-descendant-protects-subtree","Output":"--- PASS: TestFGImageDestinationProjection/link-descendant-protects-subtree (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.685661643+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/link-descendant-protects-subtree","Elapsed":0} +{"Time":"2026-11-02T01:28:19.685663656+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-replaced-by-directory"} +{"Time":"2026-11-02T01:28:19.685665024+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-replaced-by-directory","Output":"=== RUN TestFGImageDestinationProjection/file-replaced-by-directory\n"} +{"Time":"2026-11-02T01:28:19.685837693+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-replaced-by-directory","Output":"--- PASS: TestFGImageDestinationProjection/file-replaced-by-directory (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.685849559+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/file-replaced-by-directory","Elapsed":0} +{"Time":"2026-11-02T01:28:19.685851831+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directory-replaced-by-file"} +{"Time":"2026-11-02T01:28:19.685853217+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directory-replaced-by-file","Output":"=== RUN TestFGImageDestinationProjection/directory-replaced-by-file\n"} +{"Time":"2026-11-02T01:28:19.686098293+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directory-replaced-by-file","Output":"--- PASS: TestFGImageDestinationProjection/directory-replaced-by-file (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.686119921+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/directory-replaced-by-file","Elapsed":0} +{"Time":"2026-11-02T01:28:19.686122293+08:00","Action":"run","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/intrinsic-root-not-data"} +{"Time":"2026-11-02T01:28:19.686123773+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/intrinsic-root-not-data","Output":"=== RUN TestFGImageDestinationProjection/intrinsic-root-not-data\n"} +{"Time":"2026-11-02T01:28:19.686431183+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/intrinsic-root-not-data","Output":"--- PASS: TestFGImageDestinationProjection/intrinsic-root-not-data (0.00s)\n"} +{"Time":"2026-11-02T01:28:19.686453171+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection/intrinsic-root-not-data","Elapsed":0} +{"Time":"2026-11-02T01:28:19.686456312+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection","Output":"--- PASS: TestFGImageDestinationProjection (0.01s)\n"} +{"Time":"2026-11-02T01:28:19.686461242+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Test":"TestFGImageDestinationProjection","Elapsed":0.01} +{"Time":"2026-11-02T01:28:19.686639311+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Output":"PASS\n"} +{"Time":"2026-11-02T01:28:19.68688593+08:00","Action":"output","Package":"github.com/omry/reploy/internal/dockerdeploy","Output":"ok \tgithub.com/omry/reploy/internal/dockerdeploy\t0.010s\n"} +{"Time":"2026-11-02T01:28:19.686914692+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/dockerdeploy","Elapsed":0.01} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test.go.txt new file mode 100644 index 00000000..676063a5 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test.go.txt @@ -0,0 +1,126 @@ +// Disposable design prototype. No production archive/evidence code is changed. +package dockerdeploy + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "path" + "strings" + "testing" + + "github.com/omry/reploy/internal/canonical" + "github.com/omry/reploy/internal/deploy" +) + +type fgImageNode struct { kind byte; link string } +type fgImageIndex map[string]fgImageNode + +func (nodes fgImageIndex) remove(prefix string, childrenOnly bool) { + for name:=range nodes { if (!childrenOnly && name==prefix)||prefix=="."&&name!="."||strings.HasPrefix(name,prefix+"/") { delete(nodes,name) } } +} + +func fgProject(layers [][]byte, expected canonical.Digest) (fgImageIndex,error) { + ids:=[]canonical.Digest{} + for _,layer:=range layers { ids=append(ids,canonical.Digest(fmtHealthDigest(layer))) } + actual,err:=deploy.RootFSSubject(ids) + if err!=nil || actual!=expected { return nil,fmt.Errorf("unbound image layers") } + nodes:=fgImageIndex{".":{kind:tar.TypeDir}} + for _,layer:=range layers { + type entry struct { name string; node fgImageNode } + entries:=[]entry{};deleted:=[]string{};opaque:=[]string{};seen:=map[string]bool{} + reader:=tar.NewReader(bytes.NewReader(layer)) + for { + h,err:=reader.Next();if err==io.EOF { break };if err!=nil { return nil,err } + name,err:=imageIdentityLayerPathV1(h.Name,h.Typeflag) + if err!=nil || seen[name] || isSparseImageHealthHeader(h) { return nil,fmt.Errorf("ambiguous image entry") };seen[name]=true + base:=path.Base(name) + if base==".wh..wh..opq" { opaque=append(opaque,path.Dir(name));continue } + if strings.HasPrefix(base,".wh.") { if len(base)==4{return nil,fmt.Errorf("invalid whiteout")};deleted=append(deleted,path.Join(path.Dir(name),base[4:]));continue } + entries=append(entries,entry{name,fgImageNode{h.Typeflag,h.Linkname}}) + } + // OCI whiteouts remove lower entries, never same-layer additions. + for _,name:=range deleted { nodes.remove(name,false) } + for _,name:=range opaque { nodes.remove(name,true) } + for _,e:=range entries { + // Do not guess how an engine handles layer writes through non-directory + // ancestors. Query-time image aliases are resolved separately below. + for parent:=path.Dir(e.name);parent!=".";parent=path.Dir(parent) { + if n,ok:=nodes[parent];ok && n.kind!=tar.TypeDir {return nil,fmt.Errorf("ambiguous layer ancestor")} + } + previous,exists:=nodes[e.name] + if exists && (previous.kind!=tar.TypeDir || e.node.kind!=tar.TypeDir) { nodes.remove(e.name,false) } + nodes[e.name]=e.node + for parent:=path.Dir(e.name);parent!=".";parent=path.Dir(parent) { if _,ok:=nodes[parent];!ok {nodes[parent]=fgImageNode{kind:tar.TypeDir}} } + if len(nodes)>1<<20 {return nil,fmt.Errorf("image metadata budget")} + } + } + return nodes,nil +} + +func (nodes fgImageIndex) empty(target string) (bool,string,error) { + if !path.IsAbs(target)||path.Clean(target)!=target||target=="/" {return false,"",fmt.Errorf("invalid data destination")} + resolved:=target + for hop:=0;hop<32;hop++ { + parts:=strings.Split(strings.TrimPrefix(resolved,"/"),"/");changed:=false + for i:=range parts { + prefix:=strings.Join(parts[:i+1],"/");node,exists:=nodes[prefix] + if !exists {continue} + if node.kind==tar.TypeSymlink { + destination:=node.link + if !path.IsAbs(destination) {destination=path.Join("/",path.Dir(prefix),destination)} + resolved=path.Join(destination,path.Join(parts[i+1:]...));changed=true;break + } + if node.kind!=tar.TypeDir {return false,resolved,fmt.Errorf("mount destination is not a directory")} + } + if changed {continue} + if resolved=="/" {return false,resolved,fmt.Errorf("data destination resolves to root")} + prefix:=strings.TrimPrefix(resolved,"/") + for name,node:=range nodes {if strings.HasPrefix(name,prefix+"/") && node.kind!=tar.TypeDir {return false,resolved,nil}} + return true,resolved,nil + } + return false,resolved,fmt.Errorf("image link cycle/depth") +} + +func TestFGImageDestinationProjection(t *testing.T) { + dir:=func(p string)healthArchiveMember{return healthArchiveMember{name:p,kind:tar.TypeDir}} + file:=func(p string)healthArchiveMember{return healthArchiveMember{name:p,content:[]byte("image-data-or-code")}} + link:=func(p,to string)healthArchiveMember{return healthArchiveMember{name:p,kind:tar.TypeSymlink,linkname:to}} + cases:=[]struct{name,target string;layers [][]healthArchiveMember;empty,invalid bool}{ + {"absent","/var/lib/mail",nil,true,false}, + {"empty","/var/lib/mail",[][]healthArchiveMember{{dir("var/lib/mail")}},true,false}, + {"directories-only","/var/lib/mail",[][]healthArchiveMember{{dir("var/lib/mail/queue")}},true,false}, + {"data-is-protected-too","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf")}},false,false}, + {"zero-size-file","/var/lib/mail",[][]healthArchiveMember{{{name:"var/lib/mail/empty"}}},false,false}, + {"prefix-is-not-overlap","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mailbox/code")}},true,false}, + {"whiteout-removes-lower","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf")},{{name:"var/lib/mail/.wh.default.conf"}}},true,false}, + {"whiteout-keeps-same-layer","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf"),{name:"var/lib/mail/.wh.default.conf"}}},false,false}, + {"opaque-removes-lower","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/queue/code")},{{name:"var/lib/mail/.wh..wh..opq"}}},true,false}, + {"opaque-keeps-same-layer","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/old")},{file("var/lib/mail/new"),{name:"var/lib/mail/.wh..wh..opq"}}},false,false}, + {"absolute-image-alias","/data",[][]healthArchiveMember{{file("var/lib/mail/code"),link("data","/var/lib/mail")}},false,false}, + {"relative-image-alias","/var/run/mail",[][]healthArchiveMember{{dir("run/mail"),link("var/run","../run")}},true,false}, + {"alias-to-populated-tree","/var/run/mail",[][]healthArchiveMember{{file("run/mail/code"),link("var/run","../run")}},false,false}, + {"alias-cycle","/a",[][]healthArchiveMember{{link("a","b"),link("b","a")}},false,true}, + {"file-destination","/data",[][]healthArchiveMember{{file("data")}},false,true}, + {"layer-traverses-link","/data",[][]healthArchiveMember{{link("alias","data")},{file("alias/code")}},false,true}, + {"alias-to-root","/data",[][]healthArchiveMember{{link("data","/")}},false,true}, + {"hardlink-is-file","/data",[][]healthArchiveMember{{file("original"),{name:"data/alias",kind:tar.TypeLink,linkname:"original"}}},false,false}, + {"link-descendant-protects-subtree","/data",[][]healthArchiveMember{{dir("data"),link("data/alias","/outside")}},false,false}, + {"file-replaced-by-directory","/data",[][]healthArchiveMember{{file("data")},{dir("data")}},true,false}, + {"directory-replaced-by-file","/data",[][]healthArchiveMember{{file("data/old")},{file("data")}},false,true}, + {"intrinsic-root-not-data","/",nil,false,true}, + } + for _,tc:=range cases {t.Run(tc.name,func(t *testing.T){ + layers:=[][]byte{healthArchive(t,identitySourceMembersV1()...)} + for _,members:=range tc.layers {layers=append(layers,healthArchive(t,members...))} + image,archive:=identitySourceArchiveV1(t,layers,nil) + policy,platform:=identitySourcePolicyV1(t) + if _,err:=readDockerImageIdentityArchiveV1(t.Context(),bytes.NewReader(archive),policy,image,platform);err!=nil{t.Fatal(err)} + index,err:=fgProject(layers,image.RootFSSubject) + var empty bool;var resolved string + if err==nil {empty,resolved,err=index.empty(tc.target)} + if (err!=nil)!=tc.invalid || err==nil&&empty!=tc.empty {t.Fatalf("empty=%t resolved=%q err=%v",empty,resolved,err)} + if _,err:=fgProject(layers,canonical.Digest("sha256:"+strings.Repeat("a",64)));err==nil {t.Fatal("unbound layers accepted")} + })} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test_attempt01.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test_attempt01.go.txt new file mode 100644 index 00000000..cb3f6603 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/image_projection_overlay_test_attempt01.go.txt @@ -0,0 +1,120 @@ +// Disposable design prototype. No production archive/evidence code is changed. +package dockerdeploy + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "path" + "strings" + "testing" + + "github.com/omry/reploy/internal/canonical" + "github.com/omry/reploy/internal/deploy" +) + +type fgImageNode struct { kind byte; link string } +type fgImageIndex map[string]fgImageNode + +func (nodes fgImageIndex) remove(prefix string, childrenOnly bool) { + for name:=range nodes { if (!childrenOnly && name==prefix)||prefix=="."&&name!="."||strings.HasPrefix(name,prefix+"/") { delete(nodes,name) } } +} + +func fgProject(layers [][]byte, expected canonical.Digest) (fgImageIndex,error) { + ids:=[]canonical.Digest{} + for _,layer:=range layers { ids=append(ids,canonical.Digest(fmtHealthDigest(layer))) } + actual,err:=deploy.RootFSSubject(ids) + if err!=nil || actual!=expected { return nil,fmt.Errorf("unbound image layers") } + nodes:=fgImageIndex{".":{kind:tar.TypeDir}} + for _,layer:=range layers { + type entry struct { name string; node fgImageNode } + entries:=[]entry{};deleted:=[]string{};opaque:=[]string{};seen:=map[string]bool{} + reader:=tar.NewReader(bytes.NewReader(layer)) + for { + h,err:=reader.Next();if err==io.EOF { break };if err!=nil { return nil,err } + name,err:=imageIdentityLayerPathV1(h.Name,h.Typeflag) + if err!=nil || seen[name] || isSparseImageHealthHeader(h) { return nil,fmt.Errorf("ambiguous image entry") };seen[name]=true + base:=path.Base(name) + if base==".wh..wh..opq" { opaque=append(opaque,path.Dir(name));continue } + if strings.HasPrefix(base,".wh.") { if len(base)==4{return nil,fmt.Errorf("invalid whiteout")};deleted=append(deleted,path.Join(path.Dir(name),base[4:]));continue } + entries=append(entries,entry{name,fgImageNode{h.Typeflag,h.Linkname}}) + } + // OCI whiteouts remove lower entries, never same-layer additions. + for _,name:=range deleted { nodes.remove(name,false) } + for _,name:=range opaque { nodes.remove(name,true) } + for _,e:=range entries { + // Do not guess how an engine handles layer writes through non-directory + // ancestors. Query-time image aliases are resolved separately below. + for parent:=path.Dir(e.name);parent!=".";parent=path.Dir(parent) { + if n,ok:=nodes[parent];ok && n.kind!=tar.TypeDir {return nil,fmt.Errorf("ambiguous layer ancestor")} + } + previous,exists:=nodes[e.name] + if exists && (previous.kind!=tar.TypeDir || e.node.kind!=tar.TypeDir) { nodes.remove(e.name,false) } + nodes[e.name]=e.node + for parent:=path.Dir(e.name);parent!=".";parent=path.Dir(parent) { if _,ok:=nodes[parent];!ok {nodes[parent]=fgImageNode{kind:tar.TypeDir}} } + if len(nodes)>1<<20 {return nil,fmt.Errorf("image metadata budget")} + } + } + return nodes,nil +} + +func (nodes fgImageIndex) empty(target string) (bool,string,error) { + if !path.IsAbs(target)||path.Clean(target)!=target||target=="/" {return false,"",fmt.Errorf("invalid data destination")} + resolved:=target + for hop:=0;hop<32;hop++ { + parts:=strings.Split(strings.TrimPrefix(resolved,"/"),"/");changed:=false + for i:=range parts { + prefix:=strings.Join(parts[:i+1],"/");node,exists:=nodes[prefix] + if !exists {continue} + if node.kind==tar.TypeSymlink { + destination:=node.link + if !path.IsAbs(destination) {destination=path.Join("/",path.Dir(prefix),destination)} + resolved=path.Join(destination,path.Join(parts[i+1:]...));changed=true;break + } + if node.kind!=tar.TypeDir {return false,resolved,fmt.Errorf("mount destination is not a directory")} + } + if changed {continue} + prefix:=strings.TrimPrefix(resolved,"/") + for name,node:=range nodes {if strings.HasPrefix(name,prefix+"/") && node.kind!=tar.TypeDir {return false,resolved,nil}} + return true,resolved,nil + } + return false,resolved,fmt.Errorf("image link cycle/depth") +} + +func TestFGImageDestinationProjection(t *testing.T) { + dir:=func(p string)healthArchiveMember{return healthArchiveMember{name:p,kind:tar.TypeDir}} + file:=func(p string)healthArchiveMember{return healthArchiveMember{name:p,content:[]byte("image-data-or-code")}} + link:=func(p,to string)healthArchiveMember{return healthArchiveMember{name:p,kind:tar.TypeSymlink,linkname:to}} + cases:=[]struct{name,target string;layers [][]healthArchiveMember;empty,invalid bool}{ + {"absent","/var/lib/mail",nil,true,false}, + {"empty","/var/lib/mail",[][]healthArchiveMember{{dir("var/lib/mail")}},true,false}, + {"directories-only","/var/lib/mail",[][]healthArchiveMember{{dir("var/lib/mail/queue")}},true,false}, + {"data-is-protected-too","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf")}},false,false}, + {"zero-size-file","/var/lib/mail",[][]healthArchiveMember{{{name:"var/lib/mail/empty"}}},false,false}, + {"prefix-is-not-overlap","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mailbox/code")}},true,false}, + {"whiteout-removes-lower","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf")},{{name:"var/lib/mail/.wh.default.conf"}}},true,false}, + {"whiteout-keeps-same-layer","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/default.conf"),{name:"var/lib/mail/.wh.default.conf"}}},false,false}, + {"opaque-removes-lower","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/queue/code")},{{name:"var/lib/mail/.wh..wh..opq"}}},true,false}, + {"opaque-keeps-same-layer","/var/lib/mail",[][]healthArchiveMember{{file("var/lib/mail/old")},{file("var/lib/mail/new"),{name:"var/lib/mail/.wh..wh..opq"}}},false,false}, + {"absolute-image-alias","/data",[][]healthArchiveMember{{file("var/lib/mail/code"),link("data","/var/lib/mail")}},false,false}, + {"relative-image-alias","/var/run/mail",[][]healthArchiveMember{{dir("run/mail"),link("var/run","../run")}},true,false}, + {"alias-to-populated-tree","/var/run/mail",[][]healthArchiveMember{{file("run/mail/code"),link("var/run","../run")}},false,false}, + {"alias-cycle","/a",[][]healthArchiveMember{{link("a","b"),link("b","a")}},false,true}, + {"file-destination","/data",[][]healthArchiveMember{{file("data")}},false,true}, + {"layer-traverses-link","/data",[][]healthArchiveMember{{link("alias","data")},{file("alias/code")}},false,true}, + {"intrinsic-root-not-data","/",nil,false,true}, + } + for _,tc:=range cases {t.Run(tc.name,func(t *testing.T){ + layers:=[][]byte{healthArchive(t,identitySourceMembersV1()...)} + for _,members:=range tc.layers {layers=append(layers,healthArchive(t,members...))} + image,archive:=identitySourceArchiveV1(t,layers,nil) + policy,platform:=identitySourcePolicyV1(t) + if _,err:=readDockerImageIdentityArchiveV1(t.Context(),bytes.NewReader(archive),policy,image,platform);err!=nil{t.Fatal(err)} + index,err:=fgProject(layers,image.RootFSSubject) + var empty bool;var resolved string + if err==nil {empty,resolved,err=index.empty(tc.target)} + if (err!=nil)!=tc.invalid || err==nil&&empty!=tc.empty {t.Fatalf("empty=%t resolved=%q err=%v",empty,resolved,err)} + if _,err:=fgProject(layers,canonical.Digest("sha256:"+strings.Repeat("a",64)));err==nil {t.Fatal("unbound layers accepted")} + })} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/manifest.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/manifest.json new file mode 100644 index 00000000..af121883 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/manifest.json @@ -0,0 +1,213 @@ +{ + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "scope": "Bounded design engineering proofs; not production provider qualification, completed F/G slices or generic-launch. Dummy input only; no credential sources.", + "files": [ + { + "path": "image-projection-proof-attempt01-result.json", + "sha256": "0f5f94f9e079b9d229a5ee2d0686522ccd2899558ff8a66cb67f043aa41c24da" + }, + { + "path": "image-projection-proof-result.json", + "sha256": "4b913944150d9a676903670f1aa2c89f60f82d00c8ff967e0a469420e5b0334d" + }, + { + "path": "image-projection-proof.jsonl", + "sha256": "761e3383ec45c9c8553eb73c28c49ea4ab9a04f50a95cd1a84957cfef6d93308" + }, + { + "path": "image_projection_overlay_test.go.txt", + "sha256": "e84d17bd5089db21c7729d9bd8ce47d14488f5579c1f81b1026f3382fad50836" + }, + { + "path": "image_projection_overlay_test_attempt01.go.txt", + "sha256": "023691daa8e52a2fbaea25a553a1be196fc2bb3c84d7af47e6fdeb4d19655a91" + }, + { + "path": "network-proof-attempt01-result.json", + "sha256": "886c814dba24d3d4b3cb760f034566226c58f5eb3dc8621d64f279690573a682" + }, + { + "path": "network-proof-attempt02-result.json", + "sha256": "4f18c6c11cae83721a2aeefbbf590cd7e8d95027297d634e9269e27ee7a95416" + }, + { + "path": "network-proof-result.json", + "sha256": "019b576fe85eae62647ed451034ee95d07ff0a0b6fd61c2723c5ef3fadbfd8a5" + }, + { + "path": "network_overlay_test.go.txt", + "sha256": "1e180891ab6a036323ed6ac7efdb3cbf58f3bf88d12d9bc9e58426754887b57f" + }, + { + "path": "network_overlay_test_attempt01.go.txt", + "sha256": "60f63c1496aded513986e03fb27cd92e33c38be0d967163fc029dd9f6597d756" + }, + { + "path": "network_overlay_test_attempt02.go.txt", + "sha256": "e5fdcd35efe38327b7f45da470beddf711fcbe4bee3ad5aa208d2fe085844089" + }, + { + "path": "private-input-proof-result.json", + "sha256": "46ada1c63fcc61f2c639644cec896926ca2d93b5a863bc80dd2b7ee4d8a9f291" + }, + { + "path": "private-input-proof.jsonl", + "sha256": "4523eb90b33e63cada8031ce194170f55b0fe2ed419c065b3f8d970ce7d0c50c" + }, + { + "path": "private_input_overlay_test.go.txt", + "sha256": "68068bec17491ecd654a0f1a3094b7000830f09deac9ebf5c9b8c5b6797db0b0" + }, + { + "path": "run_network_probe.py.txt", + "sha256": "2611310d5d3509d525e26373f01137b7cf19db5f571258843f363a9a31e6cef3" + }, + { + "path": "run_network_probe_attempt01.py.txt", + "sha256": "40cbab241fece9fae2b4d2e2c256528b1c09764e1b5e889ed86c9122761a9f60" + }, + { + "path": "run_network_probe_attempt02.py.txt", + "sha256": "49a8e8e4691d1b54fb79a347d86f5f9fc591918735b7f0ca9b6e746cc1929481" + }, + { + "path": "run_startup_status_container_probe.py.txt", + "sha256": "b653aa9616c132cd3f7df0f6b942598e0c75269a6b3f068cf658c5c483939f65" + }, + { + "path": "run_startup_status_probe.py.txt", + "sha256": "4414c52c4bb52ee906cb40761f49d73ff7cb82bce1a7f1365bca3304f69cca01" + }, + { + "path": "run_startup_status_probe_attempt01.py.txt", + "sha256": "f7613cf24c2e687a858f67f727d09ce6341c3525f810d5c28da6a0e5cfd63ff1" + }, + { + "path": "run_startup_status_probe_attempt02.py.txt", + "sha256": "ce2c5faaad89944043a81a4fb18a1e9ac275878d37096f765310cd1542a63edc" + }, + { + "path": "run_startup_status_reservedfd_container_probe.py.txt", + "sha256": "73c0eb9f1549551405fbd033c4e48d8e433f52fece81cf57cbfceb2e7e3f89bc" + }, + { + "path": "run_startup_status_reservedfd_probe.py.txt", + "sha256": "f7fe0e0214905d9376c54ba80b8e1bd346b3bcf0b92298f444152747fe8b3853" + }, + { + "path": "run_startup_status_reservedfd_probe_attempt01.py.txt", + "sha256": "de9e5bb1386d1bc4b9ab5947a4469c73278e06fd93d4d427ae169f240b8ea8b5" + }, + { + "path": "run_startup_status_reservedfd_probe_attempt02.py.txt", + "sha256": "bb714b059cde46c16f609b96dce3a673f268b189618814667227653f3cd5e302" + }, + { + "path": "run_storage_probe_attempt01.py.txt", + "sha256": "ab620a85cc6eae15f1defb49e274118ede537662d0055cf0d2215cc16a0801ae" + }, + { + "path": "startup-status-container-cleanup-followup.json", + "sha256": "3318ea1dc1cd8435cfae1100c480f420f539832a304ed2d8824293505e9bbc8d" + }, + { + "path": "startup-status-container-proof-result.json", + "sha256": "a061760424e23d8b718525d99a255a5e740517cf08d6c4b961c29e746e2ec07b" + }, + { + "path": "startup-status-proof-attempt01-result.json", + "sha256": "284cd26a9ea3a29cc45fd9d93613854ad74c983c19889545935c253b2f68f335" + }, + { + "path": "startup-status-proof-attempt02-result.json", + "sha256": "ab6f60357111a3a62e204d767adbfc46a03ee5739cea4e870ca05fca5da042b2" + }, + { + "path": "startup-status-proof-attempt03-result.json", + "sha256": "5a201ef63596a6d17e1c5210553a342209901dfd09a0b4dc87b0147ad50fb474" + }, + { + "path": "startup-status-proof-result.json", + "sha256": "fee17491636df7013733df6a39b0c2539c7c0d6ce231a9c1ef0b4b3f20bfe824" + }, + { + "path": "startup-status-reserved-fd-container-cleanup-followup-attempt01.json", + "sha256": "c23d65df8c5b679ada82491391dab03aeb149922162cd191ab48eb80fb77ec43" + }, + { + "path": "startup-status-reserved-fd-container-cleanup-followup.json", + "sha256": "07ce07dfc1f425c1953933310cec41d6748d7c94904e8b2db253b0ba71eed103" + }, + { + "path": "startup-status-reserved-fd-container-proof-result.json", + "sha256": "551a8f1f27f8aae2eddf5021888224c84fa378a5ccbc6404a411db544afa3044" + }, + { + "path": "startup-status-reserved-fd-proof-attempt01-result.json", + "sha256": "60ca567439429286ce071ce9436d6c9841ca31ace30ce96a2654ab7e25be2711" + }, + { + "path": "startup-status-reserved-fd-proof-attempt02-result.json", + "sha256": "ae3b84cc1445e79100255b4abca8061b05b5672536539ff5862a39332e582e52" + }, + { + "path": "startup-status-reserved-fd-proof-result.json", + "sha256": "36ba98010b1f4982708fb85f0fd2c9bcdae99029f5e720d859751a4f0713c0ae" + }, + { + "path": "startup-status-reserved-fd-tail.txt", + "sha256": "ddf9a21c836b93fdcc1c9dbc58c5f24145a386488488b88a33fa8c822bd4ae44" + }, + { + "path": "startup-status-tail.txt", + "sha256": "669adba70a202f9585934aa1828e34f99204da196a27d57e04ed8dfa6eadaf9d" + }, + { + "path": "startup_status_probe.go.txt", + "sha256": "86930283fb539e193603502247087a256f96a1b8d0182180a38d607d23899be1" + }, + { + "path": "startup_status_probe_attempt01.go.txt", + "sha256": "eb0c9983289d52516334cf4bb8907b7aa876185bf95babff504c04d0ad9ad112" + }, + { + "path": "startup_status_probe_attempt02.go.txt", + "sha256": "675362c01b059ba3113b9239499225e3a9107ad81fc105b44a554c7ec87e21e9" + }, + { + "path": "startup_status_probe_attempt03.go.txt", + "sha256": "96dd04223b9bc1874c86878bd75bce228e6950688ea1d3d268fbf62a011f7657" + }, + { + "path": "startup_status_reservedfd_probe.go.txt", + "sha256": "c9e9debe56aaccc49d26acd1b696de588d816ff46588ff5776945145f6f9556c" + }, + { + "path": "startup_status_reservedfd_probe_attempt01.go.txt", + "sha256": "a0d5a37e999b5de5b3bd6f2b51782ac3c20c989d7950dac4c8cd70d05bca29e5" + }, + { + "path": "startup_status_reservedfd_probe_attempt02.go.txt", + "sha256": "a0d5a37e999b5de5b3bd6f2b51782ac3c20c989d7950dac4c8cd70d05bca29e5" + }, + { + "path": "storage-cleanup-followup.json", + "sha256": "83c88d934d33052e601c1c7e60b9b4a8e9bd4f8e57e1475ee7b9880fb9956ebc" + }, + { + "path": "storage-proof-result.json", + "sha256": "86bc3ebe196f81d7cf3aee8b6cb130bbb26c5761907d0fb614dc4aad88d14296" + }, + { + "path": "storage_probe.go.txt", + "sha256": "7a9c67c92af3bce49eae667f97cf927c06e2ea857cb7b23f095ddecbfdb3022d" + } + ], + "storage": "Four fixture observations, including successful shadow/ancestor attacks as negative controls. Original cleanup receipt retained; follow-up proves exact private root removal.", + "private_input": "Seven preamble cases plus real existing parser/reader, mapped waiter/descriptor/exec-tail checks. Pipe/process composition only; not native desktop/engine attach qualification.", + "network": "Final inside-container and bounded-host setup variants pass with separate private peer positive/deny/allow controls and actual kernel table observation. First two failed fixture attempts retained; final private containers/image/store removed.", + "engine": "Podman4.9.3/crun1.14.1 on WSL2, raw private exact UID0 fixture; not the supported production mapped cohort or both-profile acceptance.", + "source_format": "Source archives keep an added .txt suffix so documentation does not create Go packages or executable test suites. Restore original names only in a fresh replay directory.", + "image_projection": "22 layered immutable-image metadata cases pass using the actual D archive binding. Root aliases, hardlinks and type replacement included; the earlier 17-case receipt remains. This is not the bounded production parser, build-lock integration or actual engine mount observation.", + "startup_status": "Selected reserved-fd refinement: five host and five raw isolated rootless Podman cases pass with source fd4/fd64, raw-only six-call-site compiled tail and no private descriptor leaks. Source-fd3 branch and full production qualification remain gates. Original failed fixture and incomplete cleanup receipts remain; exact cleanup follow-up proves no private root/process remains. Historical whole-range CLOEXEC proofs are not the selected kernel requirement." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt01-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt01-result.json new file mode 100644 index 00000000..cc26c3ca --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt01-result.json @@ -0,0 +1,50 @@ +{ + "scope": "private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification", + "attempt": "47f1f7b0cdb18d6ad80cd418", + "private_root": "/tmp/reploy-fg-network-0l_j_bfi", + "cases": [ + { + "mode": "trusted-setup-inside-private-container", + "exit_code": 1, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:38: deny-local negative failed\n--- FAIL: TestFGPreReleaseNetwork (0.01s)\nFAIL\n", + "stderr": "" + }, + { + "mode": "bounded-host-setup-in-pinned-network-namespace", + "exit_code": 1, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:38: deny-local negative failed\n--- FAIL: TestFGPreReleaseNetwork (0.01s)\nFAIL\n", + "stderr": "" + } + ], + "cleanup": [ + { + "container": "00920258ddcc2ca1edc0d04b1a195b2b7cc24854cb7b03a599145cf5dc406cc3", + "removed": true + }, + { + "image": "sha256:f352364254ccf9627f48de1310b205fb2578a923b94c4eec8bcafc1c080e3d45", + "removed": true + } + ], + "engine": { + "version": { + "APIVersion": "4.9.3", + "Version": "4.9.3", + "GoVersion": "go1.22.2", + "GitCommit": "", + "BuiltTime": "Thu Jan 1 07:30:00 1970", + "Built": 0, + "OsArch": "linux/amd64", + "Os": "linux" + }, + "kernel": "6.18.54.1-microsoft-standard-WSL2", + "rootless": true + }, + "host_setup_supported": false, + "private_root_removed": true, + "elapsed_seconds": 25.075334323002608, + "source_sha256": { + "run_network_probe.py": "40cbab241fece9fae2b4d2e2c256528b1c09764e1b5e889ed86c9122761a9f60", + "network_overlay_test.go": "60f63c1496aded513986e03fb27cd92e33c38be0d967163fc029dd9f6597d756" + } +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt02-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt02-result.json new file mode 100644 index 00000000..f1c56b25 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-attempt02-result.json @@ -0,0 +1,54 @@ +{ + "scope": "private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification", + "attempt": "006cefaa61ac909c69dda30b", + "private_root": "/tmp/reploy-fg-network-0kmdppfb", + "cases": [ + { + "mode": "trusted-setup-inside-private-container", + "exit_code": 1, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:47: read application DNS resolvers: open /etc/resolv.conf: no such file or directory\n--- FAIL: TestFGPreReleaseNetwork (0.31s)\nFAIL\n", + "stderr": "" + }, + { + "mode": "bounded-host-setup-in-pinned-network-namespace", + "exit_code": 1, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:42: unfiltered positive control failed\n--- FAIL: TestFGPreReleaseNetwork (0.30s)\nFAIL\n", + "stderr": "" + } + ], + "cleanup": [ + { + "container": "bfb5be829b06e735268c15f3b76fa732bc478120690797a111961f0253f468c4", + "removed": true + }, + { + "container": "77a3a67f47f15a3bc27aba596aa7ba372f7fcbca1a44c63c29a5d311db0eaa23", + "removed": true + }, + { + "image": "sha256:5d1cc413f87b412f66b45102b154beb421c3df107cf3c60288ef2d0b8dd05996", + "removed": true + } + ], + "engine": { + "version": { + "APIVersion": "4.9.3", + "Version": "4.9.3", + "GoVersion": "go1.22.2", + "GitCommit": "", + "BuiltTime": "Thu Jan 1 07:30:00 1970", + "Built": 0, + "OsArch": "linux/amd64", + "Os": "linux" + }, + "kernel": "6.18.54.1-microsoft-standard-WSL2", + "rootless": true + }, + "host_setup_supported": false, + "private_root_removed": true, + "elapsed_seconds": 29.80738508098875, + "source_sha256": { + "run_network_probe.py": "49a8e8e4691d1b54fb79a347d86f5f9fc591918735b7f0ca9b6e746cc1929481", + "network_overlay_test.go": "e5fdcd35efe38327b7f45da470beddf711fcbe4bee3ad5aa208d2fe085844089" + } +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-result.json new file mode 100644 index 00000000..e6be9653 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network-proof-result.json @@ -0,0 +1,54 @@ +{ + "scope": "private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification", + "attempt": "dcc925ca85104e73733be7d5", + "private_root": "/tmp/reploy-fg-network-6zr_3eh6", + "cases": [ + { + "mode": "trusted-setup-inside-private-container", + "exit_code": 0, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:56: FG_NETWORK_OBSERVATION {\"allowed_local\":true,\"application_release\":\"never performed\",\"baseline_positive\":true,\"denied_local\":true,\"namespace\":\"net:[4026532655]\",\"observed_kernel_tables\":2,\"uid_map\":\" 0 1 1\\n\"}\n--- PASS: TestFGPreReleaseNetwork (0.34s)\nPASS\n", + "stderr": "" + }, + { + "mode": "bounded-host-setup-in-pinned-network-namespace", + "exit_code": 0, + "stdout": "=== RUN TestFGPreReleaseNetwork\n fg_network_design_test.go:56: FG_NETWORK_OBSERVATION {\"allowed_local\":true,\"application_release\":\"never performed\",\"baseline_positive\":true,\"denied_local\":true,\"namespace\":\"net:[4026532655]\",\"observed_kernel_tables\":2,\"uid_map\":\" 0 1000 1\\n 1 100000 65536\\n\"}\n--- PASS: TestFGPreReleaseNetwork (0.37s)\nPASS\n", + "stderr": "" + } + ], + "cleanup": [ + { + "container": "dc44b54b31ed5c96fa72a463cfe5b6c2d1bb44cc337cb2089998caf71ea7025f", + "removed": true + }, + { + "container": "9854fcf5595dee14f590641d7dfc0b903478a9779536e9cc911886c92373f38b", + "removed": true + }, + { + "image": "sha256:51babf21c386e2412e8383fc60ff6b7064c79c9653cb2d494ee54d486e588bab", + "removed": true + } + ], + "engine": { + "version": { + "APIVersion": "4.9.3", + "Version": "4.9.3", + "GoVersion": "go1.22.2", + "GitCommit": "", + "BuiltTime": "Thu Jan 1 07:30:00 1970", + "Built": 0, + "OsArch": "linux/amd64", + "Os": "linux" + }, + "kernel": "6.18.54.1-microsoft-standard-WSL2", + "rootless": true + }, + "host_setup_supported": true, + "private_root_removed": true, + "elapsed_seconds": 31.25311080100073, + "source_sha256": { + "run_network_probe.py": "2611310d5d3509d525e26373f01137b7cf19db5f571258843f363a9a31e6cef3", + "network_overlay_test.go": "1e180891ab6a036323ed6ac7efdb3cbf58f3bf88d12d9bc9e58426754887b57f" + } +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test.go.txt new file mode 100644 index 00000000..ce54b7e3 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test.go.txt @@ -0,0 +1,57 @@ +// Disposable namespace proof, loaded via go test -overlay. +package probe + +import ( + "encoding/json" + "net" + "os" + "runtime" + "testing" + "time" + + "github.com/google/nftables" + "golang.org/x/sys/unix" +) + +func TestFGNetworkWaiter(t *testing.T) { + if os.Getenv("FG_NETWORK_WAITER")!="1" { t.Skip("private fixture only") } + t.Log("trusted inert waiter") + time.Sleep(90*time.Second) +} + +func TestFGNetworkPeer(t *testing.T) { + if os.Getenv("FG_NETWORK_PEER")!="1" { t.Skip("private fixture only") } + listener,err:=net.Listen("tcp4",":34567");if err!=nil { t.Fatal(err) };defer listener.Close() + go func(){for { c,e:=listener.Accept();if e!=nil{return};c.Close() }}() + time.Sleep(90*time.Second) +} + +func TestFGPreReleaseNetwork(t *testing.T) { + if os.Getenv("FG_NETWORK_PROOF")!="1" { t.Skip("private fixture only") } + runtime.LockOSThread(); defer runtime.UnlockOSThread() + if target:=os.Getenv("FG_JOIN_NETNS");target!="" { + fd,err:=unix.Open(target,unix.O_RDONLY|unix.O_CLOEXEC,0);if err!=nil { t.Fatal(err) };defer unix.Close(fd) + if err=unix.Setns(fd,unix.CLONE_NEWNET);err!=nil { t.Fatal(err) } + } + namespace,err:=os.Readlink("/proc/thread-self/ns/net") + if err!=nil || namespace==os.Getenv("FG_PARENT_NETNS") || namespace=="" { t.Fatalf("no private network namespace: %v",err) } + // Reset only the table installed by this fixture, after the namespace guard. + reset:=&nftables.Conn{} + previous,err:=reset.ListTables();if err!=nil { t.Fatal(err) } + for _,table:=range previous { if table.Name=="reploy" { reset.DelTable(table) } } + if err=reset.Flush();err!=nil { t.Fatal(err) } + listener,err:=net.Listen("tcp4","127.0.0.1:0");if err!=nil { t.Fatal(err) };defer listener.Close() + go func(){ for { conn,err:=listener.Accept();if err!=nil{return};conn.Close() } }() + address:=os.Getenv("FG_NETWORK_TARGET");if address=="" { t.Fatal("separate private peer control is required") } + dial:=func()bool{c,e:=net.DialTimeout("tcp4",address,300*time.Millisecond);if c!=nil { c.Close() };return e==nil} + if !dial() { t.Fatal("unfiltered positive control failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{});err!=nil { t.Fatalf("pre-release policy install: %v",err) } + c:=&nftables.Conn{} + tables,err:=c.ListTables();if err!=nil || len(tables)<2 { t.Fatalf("actual kernel table observation: %v",err) } + if dial() { t.Fatal("deny-local negative failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{AllowLocal:true});err!=nil { t.Fatal(err) } + if !dial() { t.Fatal("allow-local positive failed") } + maps,_:=os.ReadFile("/proc/self/uid_map") + observation,_:=json.Marshal(map[string]any{"namespace":namespace,"uid_map":string(maps),"observed_kernel_tables":len(tables),"baseline_positive":true,"denied_local":true,"allowed_local":true,"application_release":"never performed"}) + t.Logf("FG_NETWORK_OBSERVATION %s",observation) +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt01.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt01.go.txt new file mode 100644 index 00000000..1da3842f --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt01.go.txt @@ -0,0 +1,44 @@ +// Disposable namespace proof, loaded via go test -overlay. +package probe + +import ( + "encoding/json" + "net" + "os" + "runtime" + "testing" + "time" + + "github.com/google/nftables" + "golang.org/x/sys/unix" +) + +func TestFGNetworkWaiter(t *testing.T) { + if os.Getenv("FG_NETWORK_WAITER")!="1" { t.Skip("private fixture only") } + t.Log("trusted inert waiter") + time.Sleep(90*time.Second) +} + +func TestFGPreReleaseNetwork(t *testing.T) { + if os.Getenv("FG_NETWORK_PROOF")!="1" { t.Skip("private fixture only") } + runtime.LockOSThread(); defer runtime.UnlockOSThread() + if target:=os.Getenv("FG_JOIN_NETNS");target!="" { + fd,err:=unix.Open(target,unix.O_RDONLY|unix.O_CLOEXEC,0);if err!=nil { t.Fatal(err) };defer unix.Close(fd) + if err=unix.Setns(fd,unix.CLONE_NEWNET);err!=nil { t.Fatal(err) } + } + namespace,err:=os.Readlink("/proc/thread-self/ns/net") + if err!=nil || namespace==os.Getenv("FG_PARENT_NETNS") || namespace=="" { t.Fatalf("no private network namespace: %v",err) } + listener,err:=net.Listen("tcp4","127.0.0.1:0");if err!=nil { t.Fatal(err) };defer listener.Close() + go func(){ for { conn,err:=listener.Accept();if err!=nil{return};conn.Close() } }() + dial:=func()bool{c,e:=net.DialTimeout("tcp4",listener.Addr().String(),300*time.Millisecond);if c!=nil { c.Close() };return e==nil} + if !dial() { t.Fatal("unfiltered positive control failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{});err!=nil { t.Fatalf("pre-release policy install: %v",err) } + c:=&nftables.Conn{} + tables,err:=c.ListTables();if err!=nil || len(tables)<2 { t.Fatalf("actual kernel table observation: %v",err) } + if dial() { t.Fatal("deny-local negative failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{AllowLocal:true});err!=nil { t.Fatal(err) } + if !dial() { t.Fatal("allow-local positive failed") } + maps,_:=os.ReadFile("/proc/self/uid_map") + observation,_:=json.Marshal(map[string]any{"namespace":namespace,"uid_map":string(maps),"observed_kernel_tables":len(tables),"unfiltered_positive":true,"denied_local":true,"allowed_local":true,"application_release":"never performed"}) + t.Logf("FG_NETWORK_OBSERVATION %s",observation) +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt02.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt02.go.txt new file mode 100644 index 00000000..e837f6f7 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/network_overlay_test_attempt02.go.txt @@ -0,0 +1,52 @@ +// Disposable namespace proof, loaded via go test -overlay. +package probe + +import ( + "encoding/json" + "net" + "os" + "runtime" + "testing" + "time" + + "github.com/google/nftables" + "golang.org/x/sys/unix" +) + +func TestFGNetworkWaiter(t *testing.T) { + if os.Getenv("FG_NETWORK_WAITER")!="1" { t.Skip("private fixture only") } + t.Log("trusted inert waiter") + time.Sleep(90*time.Second) +} + +func TestFGNetworkPeer(t *testing.T) { + if os.Getenv("FG_NETWORK_PEER")!="1" { t.Skip("private fixture only") } + listener,err:=net.Listen("tcp4",":34567");if err!=nil { t.Fatal(err) };defer listener.Close() + go func(){for { c,e:=listener.Accept();if e!=nil{return};c.Close() }}() + time.Sleep(90*time.Second) +} + +func TestFGPreReleaseNetwork(t *testing.T) { + if os.Getenv("FG_NETWORK_PROOF")!="1" { t.Skip("private fixture only") } + runtime.LockOSThread(); defer runtime.UnlockOSThread() + if target:=os.Getenv("FG_JOIN_NETNS");target!="" { + fd,err:=unix.Open(target,unix.O_RDONLY|unix.O_CLOEXEC,0);if err!=nil { t.Fatal(err) };defer unix.Close(fd) + if err=unix.Setns(fd,unix.CLONE_NEWNET);err!=nil { t.Fatal(err) } + } + namespace,err:=os.Readlink("/proc/thread-self/ns/net") + if err!=nil || namespace==os.Getenv("FG_PARENT_NETNS") || namespace=="" { t.Fatalf("no private network namespace: %v",err) } + listener,err:=net.Listen("tcp4","127.0.0.1:0");if err!=nil { t.Fatal(err) };defer listener.Close() + go func(){ for { conn,err:=listener.Accept();if err!=nil{return};conn.Close() } }() + address:=os.Getenv("FG_NETWORK_TARGET");if address=="" { t.Fatal("separate private peer control is required") } + dial:=func()bool{c,e:=net.DialTimeout("tcp4",address,300*time.Millisecond);if c!=nil { c.Close() };return e==nil} + if !dial() { t.Fatal("unfiltered positive control failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{});err!=nil { t.Fatalf("pre-release policy install: %v",err) } + c:=&nftables.Conn{} + tables,err:=c.ListTables();if err!=nil || len(tables)<2 { t.Fatalf("actual kernel table observation: %v",err) } + if dial() { t.Fatal("deny-local negative failed") } + if err=installApplicationNetworkPolicyV1(applicationNetworkPolicyV1{AllowLocal:true});err!=nil { t.Fatal(err) } + if !dial() { t.Fatal("allow-local positive failed") } + maps,_:=os.ReadFile("/proc/self/uid_map") + observation,_:=json.Marshal(map[string]any{"namespace":namespace,"uid_map":string(maps),"observed_kernel_tables":len(tables),"baseline_positive":true,"denied_local":true,"allowed_local":true,"application_release":"never performed"}) + t.Logf("FG_NETWORK_OBSERVATION %s",observation) +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof-result.json new file mode 100644 index 00000000..9cb1d8de --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof-result.json @@ -0,0 +1,44 @@ +{ + "command": [ + "go", + "test", + "-json", + "-count=1", + "-overlay", + "/home/omry/dev/reploy/temp/reviews/fg-finalization-20261101/private-input-overlay.json", + "./internal/probe", + "-run", + "^Test(FGPrivateInput|MappedWaitHandoff|MappedWaitAborts|MappedStandardDescriptors|MappedProductionExecTail)", + "-timeout=120s" + ], + "exit_code": 0, + "elapsed_seconds": 0.9632900069991592, + "passed": [ + "TestFGPrivateInputComposition/valid-preserves-binary-stdin", + "TestFGPrivateInputComposition/no-private-preamble", + "TestFGPrivateInputComposition/oversized-before-allocation", + "TestFGPrivateInputComposition/truncated-private-payload", + "TestFGPrivateInputComposition/invalid-private-assignments", + "TestFGPrivateInputComposition/wrong-release-binding", + "TestFGPrivateInputComposition/cancel-incomplete-private-input", + "TestFGPrivateInputComposition", + "TestFGPrivateInputApp", + "TestMappedStandardDescriptorsV1", + "TestMappedProductionExecTailV1", + "TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1", + "TestMappedWaitAbortsBeforeContinuationV1/unbounded", + "TestMappedWaitAbortsBeforeContinuationV1/nil", + "TestMappedWaitAbortsBeforeContinuationV1/closed", + "TestMappedWaitAbortsBeforeContinuationV1/cancelled", + "TestMappedWaitAbortsBeforeContinuationV1/EOF", + "TestMappedWaitAbortsBeforeContinuationV1/partial", + "TestMappedWaitAbortsBeforeContinuationV1/malformed", + "TestMappedWaitAbortsBeforeContinuationV1/deadline", + "TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read", + "TestMappedWaitAbortsBeforeContinuationV1/regular_file", + "TestMappedWaitAbortsBeforeContinuationV1" + ], + "failed": [], + "skipped": [], + "scope": "Disposable framing composition with actual existing parsers/reader and native tail compile check; not production private-mode activation or container transport qualification." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof.jsonl b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof.jsonl new file mode 100644 index 00000000..c3575a02 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private-input-proof.jsonl @@ -0,0 +1,96 @@ +{"Time":"2026-11-02T00:04:19.880441521+08:00","Action":"start","Package":"github.com/omry/reploy/internal/probe"} +{"Time":"2026-11-02T00:04:19.882371398+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition"} +{"Time":"2026-11-02T00:04:19.882404977+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition","Output":"=== RUN TestFGPrivateInputComposition\n"} +{"Time":"2026-11-02T00:04:19.882420793+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/valid-preserves-binary-stdin"} +{"Time":"2026-11-02T00:04:19.882422716+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/valid-preserves-binary-stdin","Output":"=== RUN TestFGPrivateInputComposition/valid-preserves-binary-stdin\n"} +{"Time":"2026-11-02T00:04:19.884353888+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/valid-preserves-binary-stdin","Output":"--- PASS: TestFGPrivateInputComposition/valid-preserves-binary-stdin (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884376592+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/valid-preserves-binary-stdin","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884381089+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/no-private-preamble"} +{"Time":"2026-11-02T00:04:19.884382856+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/no-private-preamble","Output":"=== RUN TestFGPrivateInputComposition/no-private-preamble\n"} +{"Time":"2026-11-02T00:04:19.884470442+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/no-private-preamble","Output":"--- PASS: TestFGPrivateInputComposition/no-private-preamble (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884480189+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/no-private-preamble","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884482259+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/oversized-before-allocation"} +{"Time":"2026-11-02T00:04:19.884483402+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/oversized-before-allocation","Output":"=== RUN TestFGPrivateInputComposition/oversized-before-allocation\n"} +{"Time":"2026-11-02T00:04:19.884548957+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/oversized-before-allocation","Output":"--- PASS: TestFGPrivateInputComposition/oversized-before-allocation (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884558388+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/oversized-before-allocation","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884562316+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/truncated-private-payload"} +{"Time":"2026-11-02T00:04:19.884564547+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/truncated-private-payload","Output":"=== RUN TestFGPrivateInputComposition/truncated-private-payload\n"} +{"Time":"2026-11-02T00:04:19.884628552+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/truncated-private-payload","Output":"--- PASS: TestFGPrivateInputComposition/truncated-private-payload (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884638038+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/truncated-private-payload","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884639838+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/invalid-private-assignments"} +{"Time":"2026-11-02T00:04:19.884640951+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/invalid-private-assignments","Output":"=== RUN TestFGPrivateInputComposition/invalid-private-assignments\n"} +{"Time":"2026-11-02T00:04:19.884702867+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/invalid-private-assignments","Output":"--- PASS: TestFGPrivateInputComposition/invalid-private-assignments (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.88471922+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/invalid-private-assignments","Elapsed":0} +{"Time":"2026-11-02T00:04:19.88472184+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/wrong-release-binding"} +{"Time":"2026-11-02T00:04:19.884723096+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/wrong-release-binding","Output":"=== RUN TestFGPrivateInputComposition/wrong-release-binding\n"} +{"Time":"2026-11-02T00:04:19.884820407+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/wrong-release-binding","Output":"--- PASS: TestFGPrivateInputComposition/wrong-release-binding (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884837209+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/wrong-release-binding","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884839629+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/cancel-incomplete-private-input"} +{"Time":"2026-11-02T00:04:19.884840807+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/cancel-incomplete-private-input","Output":"=== RUN TestFGPrivateInputComposition/cancel-incomplete-private-input\n"} +{"Time":"2026-11-02T00:04:19.884912345+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/cancel-incomplete-private-input","Output":"--- PASS: TestFGPrivateInputComposition/cancel-incomplete-private-input (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884923006+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition/cancel-incomplete-private-input","Elapsed":0} +{"Time":"2026-11-02T00:04:19.88492587+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition","Output":"--- PASS: TestFGPrivateInputComposition (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884930428+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputComposition","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884932638+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputApp"} +{"Time":"2026-11-02T00:04:19.8849338+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputApp","Output":"=== RUN TestFGPrivateInputApp\n"} +{"Time":"2026-11-02T00:04:19.884955425+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputApp","Output":"--- PASS: TestFGPrivateInputApp (0.00s)\n"} +{"Time":"2026-11-02T00:04:19.884965102+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestFGPrivateInputApp","Elapsed":0} +{"Time":"2026-11-02T00:04:19.884966812+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedStandardDescriptorsV1"} +{"Time":"2026-11-02T00:04:19.884967888+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedStandardDescriptorsV1","Output":"=== RUN TestMappedStandardDescriptorsV1\n"} +{"Time":"2026-11-02T00:04:19.890305179+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedStandardDescriptorsV1","Output":"--- PASS: TestMappedStandardDescriptorsV1 (0.01s)\n"} +{"Time":"2026-11-02T00:04:19.890336003+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedStandardDescriptorsV1","Elapsed":0.01} +{"Time":"2026-11-02T00:04:19.890342032+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedProductionExecTailV1"} +{"Time":"2026-11-02T00:04:19.890343463+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedProductionExecTailV1","Output":"=== RUN TestMappedProductionExecTailV1\n"} +{"Time":"2026-11-02T00:04:20.265897954+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedProductionExecTailV1","Output":"--- PASS: TestMappedProductionExecTailV1 (0.38s)\n"} +{"Time":"2026-11-02T00:04:20.26593624+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedProductionExecTailV1","Elapsed":0.38} +{"Time":"2026-11-02T00:04:20.26594326+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1"} +{"Time":"2026-11-02T00:04:20.265944438+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1","Output":"=== RUN TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1\n"} +{"Time":"2026-11-02T00:04:20.266140594+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1","Output":"--- PASS: TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1 (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266173933+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitHandoffLeavesStdinAndClearsDeadlineV1","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266177082+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1"} +{"Time":"2026-11-02T00:04:20.266178257+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1\n"} +{"Time":"2026-11-02T00:04:20.266181014+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/unbounded"} +{"Time":"2026-11-02T00:04:20.266182021+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/unbounded","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/unbounded\n"} +{"Time":"2026-11-02T00:04:20.266228367+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/unbounded","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/unbounded (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266243037+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/unbounded","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266245683+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/nil"} +{"Time":"2026-11-02T00:04:20.266247076+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/nil","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/nil\n"} +{"Time":"2026-11-02T00:04:20.26625201+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/nil","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/nil (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266255182+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/nil","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266256999+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/closed"} +{"Time":"2026-11-02T00:04:20.266265874+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/closed","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/closed\n"} +{"Time":"2026-11-02T00:04:20.266291488+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/closed","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/closed (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266301481+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/closed","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266303029+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancelled"} +{"Time":"2026-11-02T00:04:20.266304174+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancelled","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/cancelled\n"} +{"Time":"2026-11-02T00:04:20.266351213+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancelled","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/cancelled (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266360688+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancelled","Elapsed":0} +{"Time":"2026-11-02T00:04:20.26636285+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/EOF"} +{"Time":"2026-11-02T00:04:20.266364058+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/EOF","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/EOF\n"} +{"Time":"2026-11-02T00:04:20.26638552+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/EOF","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/EOF (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266395316+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/EOF","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266396853+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/partial"} +{"Time":"2026-11-02T00:04:20.266398045+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/partial","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/partial\n"} +{"Time":"2026-11-02T00:04:20.266482137+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/partial","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/partial (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266491492+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/partial","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266493163+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/malformed"} +{"Time":"2026-11-02T00:04:20.266494458+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/malformed","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/malformed\n"} +{"Time":"2026-11-02T00:04:20.266591815+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/malformed","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/malformed (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.266604233+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/malformed","Elapsed":0} +{"Time":"2026-11-02T00:04:20.266606706+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/deadline"} +{"Time":"2026-11-02T00:04:20.266607942+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/deadline","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/deadline\n"} +{"Time":"2026-11-02T00:04:20.276995396+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/deadline","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/deadline (0.01s)\n"} +{"Time":"2026-11-02T00:04:20.277019669+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/deadline","Elapsed":0.01} +{"Time":"2026-11-02T00:04:20.277024259+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read"} +{"Time":"2026-11-02T00:04:20.277025326+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read\n"} +{"Time":"2026-11-02T00:04:20.277028545+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.27702987+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/cancel-during-read","Elapsed":0} +{"Time":"2026-11-02T00:04:20.27703636+08:00","Action":"run","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/regular_file"} +{"Time":"2026-11-02T00:04:20.277037425+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/regular_file","Output":"=== RUN TestMappedWaitAbortsBeforeContinuationV1/regular_file\n"} +{"Time":"2026-11-02T00:04:20.277405247+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/regular_file","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1/regular_file (0.00s)\n"} +{"Time":"2026-11-02T00:04:20.277422701+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1/regular_file","Elapsed":0} +{"Time":"2026-11-02T00:04:20.277425851+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1","Output":"--- PASS: TestMappedWaitAbortsBeforeContinuationV1 (0.01s)\n"} +{"Time":"2026-11-02T00:04:20.277427551+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Test":"TestMappedWaitAbortsBeforeContinuationV1","Elapsed":0.01} +{"Time":"2026-11-02T00:04:20.277430021+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Output":"PASS\n"} +{"Time":"2026-11-02T00:04:20.278030943+08:00","Action":"output","Package":"github.com/omry/reploy/internal/probe","Output":"ok \tgithub.com/omry/reploy/internal/probe\t0.397s\n"} +{"Time":"2026-11-02T00:04:20.278049948+08:00","Action":"pass","Package":"github.com/omry/reploy/internal/probe","Elapsed":0.398} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private_input_overlay_test.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private_input_overlay_test.go.txt new file mode 100644 index 00000000..74b4e479 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/private_input_overlay_test.go.txt @@ -0,0 +1,94 @@ +// Disposable design probe, loaded through go test -overlay. Not production code. +package probe + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/omry/reploy/internal/mappedlaunch" +) + +const fgDummyValue = "FG-DUMMY-PRIVATE-6238" + +// New private-mode preamble, before the unchanged E release frame. Reuse the +// real private assignment parser and real unbuffered bounded mapped fd reader. +func fgReadPreamble(input io.Reader) ([]string, error) { + var head [12]byte + if _, err := io.ReadFull(input, head[:]); err != nil { return nil, err } + n := binary.BigEndian.Uint32(head[8:]) + if string(head[:8]) != "RPLYENV1" || n == 0 || n > privateEnvironmentPayloadLimitV1 { + return nil, fmt.Errorf("invalid private preamble") + } + payload := make([]byte, n) + if _, err := io.ReadFull(input, payload); err != nil { return nil, err } + return privateEnvironmentAssignmentsV1(payload) +} + +func fgPreamble(payload []byte) []byte { + data := make([]byte, 12+len(payload)) + copy(data, "RPLYENV1") + binary.BigEndian.PutUint32(data[8:], uint32(len(payload))) + copy(data[12:], payload) + return data +} + +func TestFGPrivateInputComposition(t *testing.T) { + binding := mappedWaitBindingV1() + release, err := mappedlaunch.Frame(mappedlaunch.Request{Binding:binding, Container:strings.Repeat("b",64)}) + if err != nil { t.Fatal(err) } + canary := []byte("application\x00stdin\n") + payload := []byte("FG_PRIVATE="+fgDummyValue+"\n\n") + correct := append(append(fgPreamble(payload), release...), canary...) + badBinding := binding; badBinding.Operation = "other-operation" + wrong, _ := mappedlaunch.Frame(mappedlaunch.Request{Binding:badBinding, Container:strings.Repeat("b",64)}) + oversized := fgPreamble(nil) + binary.BigEndian.PutUint32(oversized[8:], privateEnvironmentPayloadLimitV1+1) + cases := []struct{name string; input []byte; cancel bool; valid bool}{ + {"valid-preserves-binary-stdin",correct,false,true}, + {"no-private-preamble",append(release,canary...),false,false}, + {"oversized-before-allocation",oversized,false,false}, + {"truncated-private-payload",fgPreamble(payload)[:15],false,false}, + {"invalid-private-assignments",append(fgPreamble([]byte("not-an-assignment\n\n")),release...),false,false}, + {"wrong-release-binding",append(fgPreamble(payload),wrong...),false,false}, + {"cancel-incomplete-private-input",correct[:15],true,false}, + } + for _, tc := range cases { t.Run(tc.name,func(t *testing.T) { + input, output, err := os.Pipe(); if err != nil { t.Fatal(err) } + defer input.Close(); defer output.Close() + ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second); defer cancel() + go func(){ _, _ = output.Write(tc.input); if tc.cancel { cancel() } else { _=output.Close() } }() + reader := mappedWaitInputV1{ctx:ctx, fd:int(input.Fd())} + assignments, err := fgReadPreamble(reader) + if err == nil { _,err=mappedlaunch.Read(reader,binding) } + if !tc.valid { if err == nil { t.Fatal("invalid input reached application boundary") }; return } + if err != nil { t.Fatal(err) } + remaining, err := io.ReadAll(input) + if err != nil || !bytes.Equal(remaining,canary) { t.Fatalf("application stdin altered: %v",err) } + // Dummy bytes are in stdin, not argv or the waiter's inherited environment. + argv,_ := os.ReadFile("/proc/self/cmdline") + if bytes.Contains(argv,[]byte(fgDummyValue)) || os.Getenv("FG_PRIVATE") != "" { t.Fatal("dummy input leaked into waiter metadata") } + app := exec.Command(os.Args[0],"-test.run=^TestFGPrivateInputApp$") + app.Env=append([]string{"FG_PRIVATE_PROBE_APP=1"},assignments...) + app.Stdin=bytes.NewReader(remaining) + result,err:=app.CombinedOutput() + if err != nil || !bytes.Equal(result,[]byte("private-env-and-stdin-ok\n")) { t.Fatalf("app contract: %v %q",err,result) } + }) } +} + +func TestFGPrivateInputApp(t *testing.T) { + if os.Getenv("FG_PRIVATE_PROBE_APP") != "1" { return } + data,err:=io.ReadAll(os.Stdin) + if err != nil || !bytes.Equal(data,[]byte("application\x00stdin\n")) || os.Getenv("FG_PRIVATE") != fgDummyValue { os.Exit(77) } + argv,_:=os.ReadFile("/proc/self/cmdline") + if bytes.Contains(argv,[]byte(fgDummyValue)) { os.Exit(78) } + fmt.Println("private-env-and-stdin-ok") + os.Exit(0) +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe.py.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe.py.txt new file mode 100644 index 00000000..4a495e0f --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe.py.txt @@ -0,0 +1,72 @@ +"""Private rootless network ownership/order proof, without application release.""" +from pathlib import Path +import hashlib,io,json,os,secrets,shutil,subprocess,tarfile,tempfile,time +OUT=Path(__file__).resolve().parent +WORK=Path(tempfile.mkdtemp(prefix="reploy-fg-network-"));TOKEN=secrets.token_hex(12) +ENV=dict(os.environ,XDG_RUNTIME_DIR=str(WORK/"xdg"),XDG_CONFIG_HOME=str(WORK/"config"),XDG_DATA_HOME=str(WORK/"data")) +for n in ("xdg","config","data"):(WORK/n).mkdir(mode=0o700) +PREFIX=["/usr/bin/podman","--remote=false","--root",str(WORK/"graph"),"--runroot",str(WORK/"run"),"--storage-driver","vfs","--cgroup-manager","cgroupfs","--events-backend","file"] +RESULT={"scope":"private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification","attempt":TOKEN,"private_root":str(WORK),"cases":[],"cleanup":[]};NAMES=[];IMAGE=None;START=time.monotonic() +def run(args,check=True,timeout=45,env=ENV): + p=subprocess.run(args,capture_output=True,text=True,timeout=timeout,env=env) + if check and p.returncode:raise RuntimeError(str((p.returncode,args[:3],p.stderr[-2000:]))) + return p +try: + overlay=OUT/"network-overlay.json";overlay.write_text(json.dumps({"Replace":{str(Path("internal/probe/fg_network_design_test.go").resolve()):str(OUT/"network_overlay_test.go")}})) + binary=WORK/"probe" + run(["go","test","-c","-overlay",str(overlay),"-o",str(binary),"./internal/probe"],env=dict(ENV,CGO_ENABLED="0"));binary.chmod(0o555) + info=json.loads(run(PREFIX+["info","--format","json"]).stdout);assert info["host"]["security"]["rootless"] + RESULT["engine"]={"version":info["version"],"kernel":info["host"]["kernel"],"rootless":True} + rootfs=WORK/"rootfs.tar" + with tarfile.open(rootfs,"w") as t: + for name in ("tmp","etc"): + h=tarfile.TarInfo(name);h.type=tarfile.DIRTYPE;h.mode=0o755;t.addfile(h) + data=binary.read_bytes();h=tarfile.TarInfo("probe");h.size=len(data);h.mode=0o555;t.addfile(h,io.BytesIO(data)) + data=b"nameserver 192.0.2.2\n";h=tarfile.TarInfo("etc/resolv.conf");h.size=len(data);h.mode=0o644;t.addfile(h,io.BytesIO(data)) + IMAGE=run(PREFIX+["import","--change","LABEL reploy.fg-network-proof="+TOKEN,str(rootfs),"localhost/reploy-fg-network:"+TOKEN]).stdout.strip() + parent=os.readlink("/proc/self/ns/net") + common=["--network","none","--cgroups","disabled","--uidmap","0:1:1","--gidmap","0:1:1","--cap-add","NET_ADMIN","--label","reploy.fg-network-proof="+TOKEN] + peer_name="reploy-fg-network-"+TOKEN+"-peer";NAMES.append(peer_name) + peer=run(PREFIX+["run","--detach","--name",peer_name,*common,"--env","FG_NETWORK_PEER=1",IMAGE,"/probe","-test.run=^TestFGNetworkPeer$","-test.v"]).stdout.strip() + peer_obj=json.loads(run(PREFIX+["inspect",peer]).stdout)[0];assert peer_obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + peer_pid=peer_obj["State"]["Pid"];assert peer_pid>0 + name="reploy-fg-network-"+TOKEN+"-waiter";NAMES.append(name) + container=run(PREFIX+["run","--detach","--name",name,*common,"--env","FG_NETWORK_WAITER=1",IMAGE,"/probe","-test.run=^TestFGNetworkWaiter$","-test.v"]).stdout.strip() + obj=json.loads(run(PREFIX+["inspect",container]).stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + pid=obj["State"]["Pid"];assert pid>0 + ip=shutil.which("ip");nsenter=shutil.which("nsenter");assert ip and nsenter + def ns_ip(target,*args): + return run(PREFIX+["unshare",nsenter,"--net=/proc/"+str(target)+"/ns/net",ip,*args]) + # Both veth ends exist only in our two private container namespaces. + ns_ip(pid,"link","add","fg-a","type","veth","peer","name","fg-b") + ns_ip(pid,"link","set","fg-b","netns",str(peer_pid)) + ns_ip(pid,"addr","add","192.0.2.1/30","dev","fg-a") + ns_ip(pid,"link","set","fg-a","up") + ns_ip(peer_pid,"addr","add","192.0.2.2/30","dev","fg-b") + ns_ip(peer_pid,"link","set","fg-b","up") + p=run(PREFIX+["exec","--env","FG_NETWORK_PROOF=1","--env","FG_PARENT_NETNS="+parent,"--env","FG_NETWORK_TARGET=192.0.2.2:34567",container,"/probe","-test.run=^TestFGPreReleaseNetwork$","-test.v"],check=False) + RESULT["cases"].append({"mode":"trusted-setup-inside-private-container","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + # Trusted host setup joins only the pinned inert fixture's network namespace. + helper_env=dict(ENV,FG_NETWORK_PROOF="1",FG_JOIN_NETNS=f"/proc/{pid}/ns/net",FG_PARENT_NETNS=parent,FG_NETWORK_TARGET="192.0.2.2:34567") + p=run(PREFIX+["unshare",str(binary),"-test.run=^TestFGPreReleaseNetwork$","-test.v"],env=helper_env,check=False) + RESULT["cases"].append({"mode":"bounded-host-setup-in-pinned-network-namespace","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + RESULT["host_setup_supported"]=p.returncode==0 +except Exception as e:RESULT["error"]=str(e) +finally: + for name in NAMES: + p=run(PREFIX+["inspect",name],check=False) + if p.returncode==0: + obj=json.loads(p.stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + p=run(PREFIX+["rm","--force",obj["Id"]],check=False);RESULT["cleanup"].append({"container":obj["Id"],"removed":p.returncode==0}) + if IMAGE: + p=run(PREFIX+["rmi",IMAGE],check=False);RESULT["cleanup"].append({"image":IMAGE,"removed":p.returncode==0}) + try: + assert json.loads(run(PREFIX+["ps","--all","--format","json"]).stdout)==[] + assert str(WORK).startswith("/tmp/reploy-fg-network-") and WORK.resolve()==WORK + run(PREFIX+["unshare","/usr/bin/rm","-rf",str(WORK)]) + if WORK.exists():shutil.rmtree(WORK) + RESULT["private_root_removed"]=not WORK.exists() + except Exception as e:RESULT["cleanup_error"]=str(e) + RESULT["elapsed_seconds"]=time.monotonic()-START + RESULT["source_sha256"]={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (Path(__file__),OUT/"network_overlay_test.go")} + (OUT/"network-proof-result.json").write_text(json.dumps(RESULT,indent=2)+"\n");print(json.dumps(RESULT,indent=2)) diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt01.py.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt01.py.txt new file mode 100644 index 00000000..e0af5c2f --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt01.py.txt @@ -0,0 +1,59 @@ +"""Private rootless network ownership/order proof, without application release.""" +from pathlib import Path +import hashlib,io,json,os,secrets,shutil,subprocess,tarfile,tempfile,time +OUT=Path(__file__).resolve().parent +WORK=Path(tempfile.mkdtemp(prefix="reploy-fg-network-"));TOKEN=secrets.token_hex(12) +ENV=dict(os.environ,XDG_RUNTIME_DIR=str(WORK/"xdg"),XDG_CONFIG_HOME=str(WORK/"config"),XDG_DATA_HOME=str(WORK/"data")) +for n in ("xdg","config","data"):(WORK/n).mkdir(mode=0o700) +PREFIX=["/usr/bin/podman","--remote=false","--root",str(WORK/"graph"),"--runroot",str(WORK/"run"),"--storage-driver","vfs","--cgroup-manager","cgroupfs","--events-backend","file"] +RESULT={"scope":"private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification","attempt":TOKEN,"private_root":str(WORK),"cases":[],"cleanup":[]};NAMES=[];IMAGE=None;START=time.monotonic() +def run(args,check=True,timeout=45,env=ENV): + p=subprocess.run(args,capture_output=True,text=True,timeout=timeout,env=env) + if check and p.returncode:raise RuntimeError(str((p.returncode,args[:3],p.stderr[-2000:]))) + return p +try: + overlay=OUT/"network-overlay.json";overlay.write_text(json.dumps({"Replace":{str(Path("internal/probe/fg_network_design_test.go").resolve()):str(OUT/"network_overlay_test.go")}})) + binary=WORK/"probe" + run(["go","test","-c","-overlay",str(overlay),"-o",str(binary),"./internal/probe"],env=dict(ENV,CGO_ENABLED="0"));binary.chmod(0o555) + info=json.loads(run(PREFIX+["info","--format","json"]).stdout);assert info["host"]["security"]["rootless"] + RESULT["engine"]={"version":info["version"],"kernel":info["host"]["kernel"],"rootless":True} + rootfs=WORK/"rootfs.tar" + with tarfile.open(rootfs,"w") as t: + for name in ("tmp","etc"): + h=tarfile.TarInfo(name);h.type=tarfile.DIRTYPE;h.mode=0o755;t.addfile(h) + data=binary.read_bytes();h=tarfile.TarInfo("probe");h.size=len(data);h.mode=0o555;t.addfile(h,io.BytesIO(data)) + IMAGE=run(PREFIX+["import","--change","LABEL reploy.fg-network-proof="+TOKEN,str(rootfs),"localhost/reploy-fg-network:"+TOKEN]).stdout.strip() + parent=os.readlink("/proc/self/ns/net") + common=["--network","none","--cgroups","disabled","--uidmap","0:1:1","--gidmap","0:1:1","--cap-add","NET_ADMIN","--label","reploy.fg-network-proof="+TOKEN] + name="reploy-fg-network-"+TOKEN+"-inner";NAMES.append(name) + p=run(PREFIX+["run","--name",name,*common,"--env","FG_NETWORK_PROOF=1","--env","FG_PARENT_NETNS="+parent,IMAGE,"/probe","-test.run=^TestFGPreReleaseNetwork$","-test.v"],check=False) + RESULT["cases"].append({"mode":"trusted-setup-inside-private-container","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + run(PREFIX+["rm",name]) + name="reploy-fg-network-"+TOKEN+"-waiter";NAMES.append(name) + container=run(PREFIX+["run","--detach","--name",name,*common,"--env","FG_NETWORK_WAITER=1",IMAGE,"/probe","-test.run=^TestFGNetworkWaiter$","-test.v"]).stdout.strip() + obj=json.loads(run(PREFIX+["inspect",container]).stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + pid=obj["State"]["Pid"];assert pid>0 + # Trusted host setup joins only the pinned inert fixture's network namespace. + helper_env=dict(ENV,FG_NETWORK_PROOF="1",FG_JOIN_NETNS=f"/proc/{pid}/ns/net",FG_PARENT_NETNS=parent) + p=run(PREFIX+["unshare",str(binary),"-test.run=^TestFGPreReleaseNetwork$","-test.v"],env=helper_env,check=False) + RESULT["cases"].append({"mode":"bounded-host-setup-in-pinned-network-namespace","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + RESULT["host_setup_supported"]=p.returncode==0 +except Exception as e:RESULT["error"]=str(e) +finally: + for name in NAMES: + p=run(PREFIX+["inspect",name],check=False) + if p.returncode==0: + obj=json.loads(p.stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + p=run(PREFIX+["rm","--force",obj["Id"]],check=False);RESULT["cleanup"].append({"container":obj["Id"],"removed":p.returncode==0}) + if IMAGE: + p=run(PREFIX+["rmi",IMAGE],check=False);RESULT["cleanup"].append({"image":IMAGE,"removed":p.returncode==0}) + try: + assert json.loads(run(PREFIX+["ps","--all","--format","json"]).stdout)==[] + assert str(WORK).startswith("/tmp/reploy-fg-network-") and WORK.resolve()==WORK + run(PREFIX+["unshare","/usr/bin/rm","-rf",str(WORK)]) + if WORK.exists():shutil.rmtree(WORK) + RESULT["private_root_removed"]=not WORK.exists() + except Exception as e:RESULT["cleanup_error"]=str(e) + RESULT["elapsed_seconds"]=time.monotonic()-START + RESULT["source_sha256"]={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (Path(__file__),OUT/"network_overlay_test.go")} + (OUT/"network-proof-result.json").write_text(json.dumps(RESULT,indent=2)+"\n");print(json.dumps(RESULT,indent=2)) diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt02.py.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt02.py.txt new file mode 100644 index 00000000..e0d69814 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_network_probe_attempt02.py.txt @@ -0,0 +1,71 @@ +"""Private rootless network ownership/order proof, without application release.""" +from pathlib import Path +import hashlib,io,json,os,secrets,shutil,subprocess,tarfile,tempfile,time +OUT=Path(__file__).resolve().parent +WORK=Path(tempfile.mkdtemp(prefix="reploy-fg-network-"));TOKEN=secrets.token_hex(12) +ENV=dict(os.environ,XDG_RUNTIME_DIR=str(WORK/"xdg"),XDG_CONFIG_HOME=str(WORK/"config"),XDG_DATA_HOME=str(WORK/"data")) +for n in ("xdg","config","data"):(WORK/n).mkdir(mode=0o700) +PREFIX=["/usr/bin/podman","--remote=false","--root",str(WORK/"graph"),"--runroot",str(WORK/"run"),"--storage-driver","vfs","--cgroup-manager","cgroupfs","--events-backend","file"] +RESULT={"scope":"private network namespace/setup feasibility on rootless Podman4.9.3; not product/eligible-engine or peer/endpoint qualification","attempt":TOKEN,"private_root":str(WORK),"cases":[],"cleanup":[]};NAMES=[];IMAGE=None;START=time.monotonic() +def run(args,check=True,timeout=45,env=ENV): + p=subprocess.run(args,capture_output=True,text=True,timeout=timeout,env=env) + if check and p.returncode:raise RuntimeError(str((p.returncode,args[:3],p.stderr[-2000:]))) + return p +try: + overlay=OUT/"network-overlay.json";overlay.write_text(json.dumps({"Replace":{str(Path("internal/probe/fg_network_design_test.go").resolve()):str(OUT/"network_overlay_test.go")}})) + binary=WORK/"probe" + run(["go","test","-c","-overlay",str(overlay),"-o",str(binary),"./internal/probe"],env=dict(ENV,CGO_ENABLED="0"));binary.chmod(0o555) + info=json.loads(run(PREFIX+["info","--format","json"]).stdout);assert info["host"]["security"]["rootless"] + RESULT["engine"]={"version":info["version"],"kernel":info["host"]["kernel"],"rootless":True} + rootfs=WORK/"rootfs.tar" + with tarfile.open(rootfs,"w") as t: + for name in ("tmp","etc"): + h=tarfile.TarInfo(name);h.type=tarfile.DIRTYPE;h.mode=0o755;t.addfile(h) + data=binary.read_bytes();h=tarfile.TarInfo("probe");h.size=len(data);h.mode=0o555;t.addfile(h,io.BytesIO(data)) + IMAGE=run(PREFIX+["import","--change","LABEL reploy.fg-network-proof="+TOKEN,str(rootfs),"localhost/reploy-fg-network:"+TOKEN]).stdout.strip() + parent=os.readlink("/proc/self/ns/net") + common=["--network","none","--cgroups","disabled","--uidmap","0:1:1","--gidmap","0:1:1","--cap-add","NET_ADMIN","--label","reploy.fg-network-proof="+TOKEN] + peer_name="reploy-fg-network-"+TOKEN+"-peer";NAMES.append(peer_name) + peer=run(PREFIX+["run","--detach","--name",peer_name,*common,"--env","FG_NETWORK_PEER=1",IMAGE,"/probe","-test.run=^TestFGNetworkPeer$","-test.v"]).stdout.strip() + peer_obj=json.loads(run(PREFIX+["inspect",peer]).stdout)[0];assert peer_obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + peer_pid=peer_obj["State"]["Pid"];assert peer_pid>0 + name="reploy-fg-network-"+TOKEN+"-waiter";NAMES.append(name) + container=run(PREFIX+["run","--detach","--name",name,*common,"--env","FG_NETWORK_WAITER=1",IMAGE,"/probe","-test.run=^TestFGNetworkWaiter$","-test.v"]).stdout.strip() + obj=json.loads(run(PREFIX+["inspect",container]).stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + pid=obj["State"]["Pid"];assert pid>0 + ip=shutil.which("ip");nsenter=shutil.which("nsenter");assert ip and nsenter + def ns_ip(target,*args): + return run(PREFIX+["unshare",nsenter,"--net=/proc/"+str(target)+"/ns/net",ip,*args]) + # Both veth ends exist only in our two private container namespaces. + ns_ip(pid,"link","add","fg-a","type","veth","peer","name","fg-b") + ns_ip(pid,"link","set","fg-b","netns",str(peer_pid)) + ns_ip(pid,"addr","add","192.0.2.1/30","dev","fg-a") + ns_ip(pid,"link","set","fg-a","up") + ns_ip(peer_pid,"addr","add","192.0.2.2/30","dev","fg-b") + ns_ip(peer_pid,"link","set","fg-b","up") + p=run(PREFIX+["exec","--env","FG_NETWORK_PROOF=1","--env","FG_PARENT_NETNS="+parent,"--env","FG_NETWORK_TARGET=192.0.2.2:34567",container,"/probe","-test.run=^TestFGPreReleaseNetwork$","-test.v"],check=False) + RESULT["cases"].append({"mode":"trusted-setup-inside-private-container","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + # Trusted host setup joins only the pinned inert fixture's network namespace. + helper_env=dict(ENV,FG_NETWORK_PROOF="1",FG_JOIN_NETNS=f"/proc/{pid}/ns/net",FG_PARENT_NETNS=parent,FG_NETWORK_TARGET="192.0.2.2:34567") + p=run(PREFIX+["unshare",str(binary),"-test.run=^TestFGPreReleaseNetwork$","-test.v"],env=helper_env,check=False) + RESULT["cases"].append({"mode":"bounded-host-setup-in-pinned-network-namespace","exit_code":p.returncode,"stdout":p.stdout,"stderr":p.stderr}) + RESULT["host_setup_supported"]=p.returncode==0 +except Exception as e:RESULT["error"]=str(e) +finally: + for name in NAMES: + p=run(PREFIX+["inspect",name],check=False) + if p.returncode==0: + obj=json.loads(p.stdout)[0];assert obj["Config"]["Labels"]["reploy.fg-network-proof"]==TOKEN + p=run(PREFIX+["rm","--force",obj["Id"]],check=False);RESULT["cleanup"].append({"container":obj["Id"],"removed":p.returncode==0}) + if IMAGE: + p=run(PREFIX+["rmi",IMAGE],check=False);RESULT["cleanup"].append({"image":IMAGE,"removed":p.returncode==0}) + try: + assert json.loads(run(PREFIX+["ps","--all","--format","json"]).stdout)==[] + assert str(WORK).startswith("/tmp/reploy-fg-network-") and WORK.resolve()==WORK + run(PREFIX+["unshare","/usr/bin/rm","-rf",str(WORK)]) + if WORK.exists():shutil.rmtree(WORK) + RESULT["private_root_removed"]=not WORK.exists() + except Exception as e:RESULT["cleanup_error"]=str(e) + RESULT["elapsed_seconds"]=time.monotonic()-START + RESULT["source_sha256"]={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (Path(__file__),OUT/"network_overlay_test.go")} + (OUT/"network-proof-result.json").write_text(json.dumps(RESULT,indent=2)+"\n");print(json.dumps(RESULT,indent=2)) diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_startup_status_container_probe.py.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_startup_status_container_probe.py.txt new file mode 100644 index 00000000..8d219965 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/run_startup_status_container_probe.py.txt @@ -0,0 +1,114 @@ +"""Private rootless Podman status-FIFO feasibility. No production services/store.""" +from pathlib import Path +import hashlib,io,json,os,secrets,select,shutil,subprocess,tarfile,tempfile,time + +OUT=Path(__file__).resolve().parent +WORK=Path(tempfile.mkdtemp(prefix='reploy-fg-container-status-')) +TOKEN=secrets.token_hex(12) +ENV=dict(os.environ,XDG_RUNTIME_DIR=str(WORK/'xdg'),XDG_CONFIG_HOME=str(WORK/'config'),XDG_DATA_HOME=str(WORK/'data')) +for n in ['xdg','config','data']:(WORK/n).mkdir(mode=0o700) +PODMAN=['/usr/bin/podman','--remote=false','--root',str(WORK/'graph'),'--runroot',str(WORK/'run'),'--storage-driver','vfs','--cgroup-manager','cgroupfs','--events-backend','file'] +RESULT={'scope':'Raw private rootless Podman fixture: status FIFO/subordinate access/unlink/host process observation and non-TTY stdin release. Not compatible API, production Reploy, full credential transition, supported ABI/profile or public-path qualification.','private_root':str(WORK),'cases':[],'cleanup':[]} +START=time.monotonic();NAMES=[];CHILDREN=[];IMAGE=None +def run(args,check=True,timeout=45,env=ENV): + p=subprocess.run(args,capture_output=True,text=True,timeout=timeout,env=env) + if check and p.returncode:raise RuntimeError((p.returncode,args[:3],p.stderr[-1800:])) + return p +def read_exact(fd,n,deadline): + data=b'' + while len(data)deadline:raise TimeoutError('status header') + select.select([fd],[],[],.05) + try:chunk=os.read(fd,n-len(data)) + except BlockingIOError:continue + if chunk:data+=chunk + return data +def snapshot(pid): + base=Path('/proc')/str(pid) + stat=(base/'stat').read_text().rsplit(') ',1)[1].split() + fd=os.open(base/'exe',os.O_PATH|os.O_CLOEXEC) + try:exe=os.fstat(fd) + finally:os.close(fd) + return {'pid':pid,'start':stat[19],'state':stat[0],'exe_device':exe.st_dev,'exe_inode':exe.st_ino,'uid_map':(base/'uid_map').read_text(),'gid_map':(base/'gid_map').read_text()} +try: + binary=WORK/'probe' + run(['go','build','-o',str(binary),str(OUT/'startup_status_probe.go')],timeout=90,env=dict(ENV,CGO_ENABLED='0')) + info=json.loads(run(PODMAN+['info','--format','json']).stdout) + assert info['host']['security']['rootless'] + RESULT['engine']={'version':info['version'],'kernel':info['host']['kernel'],'rootless':True} + image_tar=WORK/'rootfs.tar' + with tarfile.open(image_tar,'w') as archive: + for n in ['tmp','status']: + h=tarfile.TarInfo(n);h.type=tarfile.DIRTYPE;h.mode=0o755;archive.addfile(h) + content=binary.read_bytes();h=tarfile.TarInfo('probe');h.mode=0o555;h.size=len(content);archive.addfile(h,io.BytesIO(content)) + IMAGE=run(PODMAN+['import','--change','LABEL reploy.fg-status-proof='+TOKEN,str(image_tar),'localhost/reploy-fg-status:'+TOKEN]).stdout.strip() + for mode in ['success','exec-failure','before-exec-failure','cancel-before-release']: + runtime=WORK/('runtime-'+mode);runtime.mkdir(mode=0o711) + fifo=runtime/'status';os.mkfifo(fifo,0o600) + canary=runtime/'private';canary.write_bytes(b'private-descriptor-canary');canary.chmod(0o600) + fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + run(PODMAN+['unshare','/usr/bin/chown','1:1',str(fifo),str(canary)]) + assert fifo.stat().st_uid not in [0,os.getuid()] + name='reploy-fg-status-'+TOKEN+'-'+mode;NAMES.append(name);binding=secrets.token_hex(8) + command=PODMAN+['run','--interactive','--name',name,'--network','none','--cgroups','disabled','--uidmap','0:1:1','--gidmap','0:1:1','--cap-drop','ALL','--security-opt','no-new-privileges','--read-only','--label','reploy.fg-status-proof='+TOKEN,'--volume',str(runtime)+':/status:rw',IMAGE,'/probe',mode,'/status/status',binding,'/status/private','fixture'] + child=subprocess.Popen(command,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=ENV);CHILDREN.append(child) + deadline=time.monotonic()+15 + assert read_exact(fd,24,deadline)==b'RPLYEX01'+binding.encode() + assert select.select([child.stdout],[],[],5)[0] and child.stdout.readline()==b'ARMED\n' + obj=json.loads(run(PODMAN+['inspect',name]).stdout)[0] + assert obj['Config']['Labels']['reploy.fg-status-proof']==TOKEN + pid=obj['State']['Pid'];assert pid>0 + before=snapshot(pid);assert fifo.stat().st_uid==int(before['uid_map'].split()[1]) + try:os.read(fd,16);raise AssertionError('inhibited waiter acknowledged') + except BlockingIOError:pass + fifo.unlink();canary.unlink() + if mode=='cancel-before-release':run(PODMAN+['kill','--signal','KILL',obj['Id']]) + else:child.stdin.write(b'G');child.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_probe.go')],check=True,timeout=90,capture_output=True) + assembly=subprocess.run(['go','tool','objdump','-s','main.execTail',str(binary)],check=True,timeout=20,capture_output=True,text=True).stdout + calls=[line.strip() for line in assembly.splitlines() if 'CALL' in line] + assert 'TEXT ' in assembly and calls and all('syscall.RawSyscall' in line for line in calls) and 'runtime.' not in assembly + (OUT/'startup-status-tail.txt').write_text(assembly) + RESULT['compiler']=subprocess.run(['go','version'],check=True,capture_output=True,text=True).stdout.strip() + RESULT['binary_sha256']=hashlib.sha256(binary.read_bytes()).hexdigest() + RESULT['compiled_tail']={'raw_call_count':len(calls),'no_runtime_calls':True,'disassembly_sha256':hashlib.sha256(assembly.encode()).hexdigest()} + for mode in ['success','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + header=exact(fd,24,deadline);assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + # A live inhibited process is not startup completion. Its writer is held. + try: + premature=os.read(fd,16) + raise AssertionError('unexpected status data/EOF before release: '+repr(premature)) + except BlockingIOError:pass + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_probe.go')],check=True,timeout=90,capture_output=True) + for mode in ['success','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + header=exact(fd,24,deadline);assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_probe.go')],check=True,timeout=90,capture_output=True) + for mode in ['success','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + header=exact(fd,24,deadline);assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + # A live inhibited process is not startup completion. Its writer is held. + try: + premature=os.read(fd,16) + raise AssertionError('unexpected status data/EOF before release: '+repr(premature)) + except BlockingIOError:pass + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('status header') + select.select([fd],[],[],.05) + try:chunk=os.read(fd,n-len(data)) + except BlockingIOError:continue + if chunk:data+=chunk + return data +def snapshot(pid): + base=Path('/proc')/str(pid) + stat=(base/'stat').read_text().rsplit(') ',1)[1].split() + fd=os.open(base/'exe',os.O_PATH|os.O_CLOEXEC) + try:exe=os.fstat(fd) + finally:os.close(fd) + return {'pid':pid,'start':stat[19],'state':stat[0],'exe_device':exe.st_dev,'exe_inode':exe.st_ino,'uid_map':(base/'uid_map').read_text(),'gid_map':(base/'gid_map').read_text()} +try: + binary=WORK/'probe' + run(['go','build','-o',str(binary),str(OUT/'startup_status_reservedfd_probe.go')],timeout=90,env=dict(ENV,CGO_ENABLED='0')) + info=json.loads(run(PODMAN+['info','--format','json']).stdout) + assert info['host']['security']['rootless'] + RESULT['engine']={'version':info['version'],'kernel':info['host']['kernel'],'rootless':True} + image_tar=WORK/'rootfs.tar' + with tarfile.open(image_tar,'w') as archive: + for n in ['tmp','status']: + h=tarfile.TarInfo(n);h.type=tarfile.DIRTYPE;h.mode=0o755;archive.addfile(h) + content=binary.read_bytes();h=tarfile.TarInfo('probe');h.mode=0o555;h.size=len(content);archive.addfile(h,io.BytesIO(content)) + IMAGE=run(PODMAN+['import','--change','LABEL reploy.fg-status-proof='+TOKEN,str(image_tar),'localhost/reploy-fg-status:'+TOKEN]).stdout.strip() + for mode in ['success','success-natural-fd','exec-failure','before-exec-failure','cancel-before-release']: + runtime=WORK/('runtime-'+mode);runtime.mkdir(mode=0o711) + fifo=runtime/'status';os.mkfifo(fifo,0o600) + canary=runtime/'private';canary.write_bytes(b'private-descriptor-canary');canary.chmod(0o600) + fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + run(PODMAN+['unshare','/usr/bin/chown','1:1',str(fifo),str(canary)]) + assert fifo.stat().st_uid not in [0,os.getuid()] + name='reploy-fg-status-'+TOKEN+'-'+mode;NAMES.append(name);binding=secrets.token_hex(8) + command=PODMAN+['run','--interactive','--name',name,'--network','none','--cgroups','disabled','--uidmap','0:1:1','--gidmap','0:1:1','--cap-drop','ALL','--security-opt','no-new-privileges','--read-only','--label','reploy.fg-status-proof='+TOKEN,'--volume',str(runtime)+':/status:rw',IMAGE,'/probe',mode,'/status/status',binding,'/status/private','fixture'] + child=subprocess.Popen(command,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=ENV);CHILDREN.append(child) + deadline=time.monotonic()+15 + assert read_exact(fd,24,deadline)==b'RPLYEX01'+binding.encode() + assert select.select([child.stdout],[],[],5)[0] and child.stdout.readline()==b'ARMED\n' + obj=json.loads(run(PODMAN+['inspect',name]).stdout)[0] + assert obj['Config']['Labels']['reploy.fg-status-proof']==TOKEN + pid=obj['State']['Pid'];assert pid>0 + before=snapshot(pid);assert fifo.stat().st_uid==int(before['uid_map'].split()[1]) + try:os.read(fd,16);raise AssertionError('inhibited waiter acknowledged') + except BlockingIOError:pass + fifo.unlink();canary.unlink() + if mode=='cancel-before-release':run(PODMAN+['kill','--signal','KILL',obj['Id']]) + else:child.stdin.write(b'G');child.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_reservedfd_probe.go')],check=True,timeout=90,capture_output=True) + assembly=subprocess.run(['go','tool','objdump','-s','main.execTail',str(binary)],check=True,timeout=20,capture_output=True,text=True).stdout + calls=[line.strip() for line in assembly.splitlines() if 'CALL' in line] + assert 'TEXT ' in assembly and calls and all('syscall.RawSyscall' in line for line in calls) and 'runtime.' not in assembly + (OUT/'startup-status-reserved-fd-tail.txt').write_text(assembly) + RESULT['compiler']=subprocess.run(['go','version'],check=True,capture_output=True,text=True).stdout.strip() + RESULT['binary_sha256']=hashlib.sha256(binary.read_bytes()).hexdigest() + RESULT['compiled_tail']={'raw_call_count':len(calls),'no_runtime_calls':True,'disassembly_sha256':hashlib.sha256(assembly.encode()).hexdigest()} + for mode in ['success','success-natural-fd','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + try:header=exact(fd,24,deadline) + except Exception: + if p.poll() is not None:RESULT['early_child_failure']={'mode':mode,'exit_code':p.returncode,'stderr':p.stderr.read().decode(),'stdout':p.stdout.read().decode()} + raise + assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + # A live inhibited process is not startup completion. Its writer is held. + try: + premature=os.read(fd,16) + raise AssertionError('unexpected status data/EOF before release: '+repr(premature)) + except BlockingIOError:pass + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_reservedfd_probe.go')],check=True,timeout=90,capture_output=True) + assembly=subprocess.run(['go','tool','objdump','-s','main.execTail',str(binary)],check=True,timeout=20,capture_output=True,text=True).stdout + calls=[line.strip() for line in assembly.splitlines() if 'CALL' in line] + assert 'TEXT ' in assembly and calls and all('syscall.RawSyscall' in line for line in calls) and 'runtime.' not in assembly + (OUT/'startup-status-reserved-fd-tail.txt').write_text(assembly) + RESULT['compiler']=subprocess.run(['go','version'],check=True,capture_output=True,text=True).stdout.strip() + RESULT['binary_sha256']=hashlib.sha256(binary.read_bytes()).hexdigest() + RESULT['compiled_tail']={'raw_call_count':len(calls),'no_runtime_calls':True,'disassembly_sha256':hashlib.sha256(assembly.encode()).hexdigest()} + for mode in ['success','success-low-fd','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + header=exact(fd,24,deadline);assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + # A live inhibited process is not startup completion. Its writer is held. + try: + premature=os.read(fd,16) + raise AssertionError('unexpected status data/EOF before release: '+repr(premature)) + except BlockingIOError:pass + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()deadline:raise TimeoutError('bounded read') + select.select([fd],[],[],min(.1,max(0,deadline-time.monotonic()))) + try:chunk=os.read(fd,n-len(result)) + except BlockingIOError:continue + if chunk:result+=chunk + return result +try: + binary=WORK/'probe' + subprocess.run(['go','build','-o',str(binary),str(OUT/'startup_status_reservedfd_probe.go')],check=True,timeout=90,capture_output=True) + assembly=subprocess.run(['go','tool','objdump','-s','main.execTail',str(binary)],check=True,timeout=20,capture_output=True,text=True).stdout + calls=[line.strip() for line in assembly.splitlines() if 'CALL' in line] + assert 'TEXT ' in assembly and calls and all('syscall.RawSyscall' in line for line in calls) and 'runtime.' not in assembly + (OUT/'startup-status-reserved-fd-tail.txt').write_text(assembly) + RESULT['compiler']=subprocess.run(['go','version'],check=True,capture_output=True,text=True).stdout.strip() + RESULT['binary_sha256']=hashlib.sha256(binary.read_bytes()).hexdigest() + RESULT['compiled_tail']={'raw_call_count':len(calls),'no_runtime_calls':True,'disassembly_sha256':hashlib.sha256(assembly.encode()).hexdigest()} + for mode in ['success','success-low-fd','exec-failure','before-exec-failure','cancel-before-release']: + fifo=WORK/(mode+'.fifo');os.mkfifo(fifo,0o600) + canary=WORK/(mode+'.private');canary.write_bytes(b'not-an-application-descriptor') + token=secrets.token_hex(8);fd=os.open(fifo,os.O_RDONLY|os.O_NONBLOCK) + try: + p=subprocess.Popen([str(binary),mode,str(fifo),token,str(canary),'fixture'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + children.append(p);deadline=time.monotonic()+10 + try:header=exact(fd,24,deadline) + except Exception: + if p.poll() is not None:RESULT['early_child_failure']={'mode':mode,'exit_code':p.returncode,'stderr':p.stderr.read().decode(),'stdout':p.stdout.read().decode()} + raise + assert header==b'RPLYEX01'+token.encode() + assert select.select([p.stdout],[],[],5)[0] and p.stdout.readline()==b'ARMED\n' + fifo.unlink();assert not fifo.exists() + # A live inhibited process is not startup completion. Its writer is held. + try: + premature=os.read(fd,16) + raise AssertionError('unexpected status data/EOF before release: '+repr(premature)) + except BlockingIOError:pass + if mode=='cancel-before-release':p.kill();p.wait(timeout=5) + else:p.stdin.write(b'G');p.stdin.flush() + body=b'';eof=False + while time.monotonic()1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + status,err:=os.OpenFile(fifo,os.O_WRONLY|syscall.O_NONBLOCK,0);must(err) + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(int(status.Fd()),&pipeStat));must(unix.Fstat(211,&fileStat)) + if _,err=status.Write([]byte("RPLYEX01"+token));err!=nil {panic(err)} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + failureBuffer:=new([4]byte) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()),failureBuffer);runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()),failureBuffer) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr,message *[4]byte) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,3,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE|unix.CLOSE_RANGE_CLOEXEC,0,0,0) + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message[0],message[1],message[2],message[3]=byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24) + for { + _,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(message)),4) + if e==syscall.EINTR {continue};break + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt01.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt01.go.txt new file mode 100644 index 00000000..52732c21 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt01.go.txt @@ -0,0 +1,75 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + status,err:=os.OpenFile(fifo,os.O_WRONLY,0);must(err) + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(int(status.Fd()),&pipeStat));must(unix.Fstat(211,&fileStat)) + if _,err=status.Write([]byte("RPLYEX01"+token));err!=nil {panic(err)} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()))}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd())) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,3,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE|unix.CLOSE_RANGE_CLOEXEC,0,0,0) + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message:=[4]byte{byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24)} + for offset:=uintptr(0);offset<4; { + n,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(&message[offset])),4-offset) + if e==syscall.EINTR {continue};if e!=0 || n==0 {break};offset+=n + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt02.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt02.go.txt new file mode 100644 index 00000000..c3903ba5 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt02.go.txt @@ -0,0 +1,75 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + status,err:=os.OpenFile(fifo,os.O_WRONLY,0);must(err) + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(int(status.Fd()),&pipeStat));must(unix.Fstat(211,&fileStat)) + if _,err=status.Write([]byte("RPLYEX01"+token));err!=nil {panic(err)} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()));runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd())) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,3,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE|unix.CLOSE_RANGE_CLOEXEC,0,0,0) + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message:=[4]byte{byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24)} + for offset:=uintptr(0);offset<4; { + n,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(&message[offset])),4-offset) + if e==syscall.EINTR {continue};if e!=0 || n==0 {break};offset+=n + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt03.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt03.go.txt new file mode 100644 index 00000000..08d949c1 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_probe_attempt03.go.txt @@ -0,0 +1,76 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + status,err:=os.OpenFile(fifo,os.O_WRONLY,0);must(err) + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(int(status.Fd()),&pipeStat));must(unix.Fstat(211,&fileStat)) + if _,err=status.Write([]byte("RPLYEX01"+token));err!=nil {panic(err)} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + failureBuffer:=new([4]byte) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()),failureBuffer);runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status.Fd()),failureBuffer) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr,message *[4]byte) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,3,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE|unix.CLOSE_RANGE_CLOEXEC,0,0,0) + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message[0],message[1],message[2],message[3]=byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24) + for offset:=uintptr(0);offset<4; { + n,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(&message[offset])),4-offset) + if e==syscall.EINTR {continue};if e!=0 || n==0 {break};offset+=n + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe.go.txt new file mode 100644 index 00000000..1b2fcd11 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe.go.txt @@ -0,0 +1,87 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + status,err:=unix.Open(fifo,unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC,0);must(err) + if status<3 {panic("status overlaps standard stream")} + if mode!="success-natural-fd" { high,e:=unix.FcntlInt(uintptr(status),unix.F_DUPFD_CLOEXEC,64);must(e);must(unix.Close(status));status=high } + fmt.Fprintf(os.Stderr,"source_status_fd=%d\n",status) + descriptorFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFD,0);must(e) + fileFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFL,0);must(e) + if descriptorFlags&unix.FD_CLOEXEC==0 || fileFlags&unix.O_NONBLOCK==0 {panic("status descriptor flags") } + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(status,&pipeStat));must(unix.Fstat(211,&fileStat)) + if n,e:=unix.Write(status,[]byte("RPLYEX01"+token));e!=nil || n!=24 {panic("status header write")} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + failureBuffer:=new([4]byte) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer);runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr,message *[4]byte) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,status+1,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE,0,0,0) + if failure==0 && status!=3 { + _,_,failure=syscall.RawSyscall(unix.SYS_DUP3,status,3,unix.O_CLOEXEC) + if failure==0 {status=3} + } + if failure==0 {_,_,failure=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,4,uintptr(^uint32(0)),0,0,0,0)} + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message[0],message[1],message[2],message[3]=byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24) + for { + _,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(message)),4) + if e==syscall.EINTR {continue};break + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt01.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt01.go.txt new file mode 100644 index 00000000..66f7e6aa --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt01.go.txt @@ -0,0 +1,86 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + if mode!="success-low-fd" {blocker,e:=unix.Open("/dev/null",unix.O_RDONLY|unix.O_CLOEXEC,0);must(e);if blocker!=3 {panic("fixture requires reserved fd3")}} + status,err:=unix.Open(fifo,unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC,0);must(err) + if mode=="success-low-fd" && status!=3 || mode!="success-low-fd" && status<=3 {panic("wrong fixture descriptor layout")} + descriptorFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFD,0);must(e) + fileFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFL,0);must(e) + if descriptorFlags&unix.FD_CLOEXEC==0 || fileFlags&unix.O_NONBLOCK==0 {panic("status descriptor flags") } + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(status,&pipeStat));must(unix.Fstat(211,&fileStat)) + if n,e:=unix.Write(status,[]byte("RPLYEX01"+token));e!=nil || n!=24 {panic("status header write")} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + failureBuffer:=new([4]byte) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer);runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr,message *[4]byte) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,status+1,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE,0,0,0) + if failure==0 && status!=3 { + _,_,failure=syscall.RawSyscall(unix.SYS_DUP3,status,3,unix.O_CLOEXEC) + if failure==0 {status=3} + } + if failure==0 {_,_,failure=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,4,uintptr(^uint32(0)),0,0,0,0)} + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message[0],message[1],message[2],message[3]=byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24) + for { + _,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(message)),4) + if e==syscall.EINTR {continue};break + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt02.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt02.go.txt new file mode 100644 index 00000000..66f7e6aa --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/startup_status_reservedfd_probe_attempt02.go.txt @@ -0,0 +1,86 @@ +// Disposable Linux design proof. No production startup or credential code changes. +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +func main() { + if len(os.Args)>1 && os.Args[1]=="app" { application(); return } + if len(os.Args)!=6 { panic("fixture arguments") } + mode,fifo,token,canary:=os.Args[1],os.Args[2],os.Args[3],os.Args[4] + if mode!="success-low-fd" {blocker,e:=unix.Open("/dev/null",unix.O_RDONLY|unix.O_CLOEXEC,0);must(e);if blocker!=3 {panic("fixture requires reserved fd3")}} + status,err:=unix.Open(fifo,unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC,0);must(err) + if mode=="success-low-fd" && status!=3 || mode!="success-low-fd" && status<=3 {panic("wrong fixture descriptor layout")} + descriptorFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFD,0);must(e) + fileFlags,e:=unix.FcntlInt(uintptr(status),unix.F_GETFL,0);must(e) + if descriptorFlags&unix.FD_CLOEXEC==0 || fileFlags&unix.O_NONBLOCK==0 {panic("status descriptor flags") } + private,err:=os.Open(canary);must(err) + must(unix.Dup3(int(private.Fd()),211,0)) + var pipeStat,fileStat unix.Stat_t + must(unix.Fstat(status,&pipeStat));must(unix.Fstat(211,&fileStat)) + if n,e:=unix.Write(status,[]byte("RPLYEX01"+token));e!=nil || n!=24 {panic("status header write")} + fmt.Println("ARMED") + var release [1]byte + if _,err=os.Stdin.Read(release[:]);err!=nil || release[0]!='G' {os.Exit(121)} + if mode=="before-exec-failure" { os.Exit(121) } + executable,err:=os.Executable();must(err) + args:=[]string{executable,"app",fifo,strconv.FormatUint(uint64(pipeStat.Dev),10),strconv.FormatUint(pipeStat.Ino,10),strconv.FormatUint(uint64(fileStat.Dev),10),strconv.FormatUint(fileStat.Ino,10)} + if mode=="exec-failure" { executable="/reploy-fg-proof-no-such-program" } + filename,err:=syscall.BytePtrFromString(executable);must(err) + argv,err:=syscall.SlicePtrFromStrings(args);must(err) + env,err:=syscall.SlicePtrFromStrings(os.Environ());must(err) + failureBuffer:=new([4]byte) + runtime.LockOSThread() + // Like E's actual transition, exec from a nonleader thread destroys the + // still-trusted leader and its original descriptor table too. + if unix.Gettid()==os.Getpid() { + go func(){runtime.LockOSThread(); execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer);runtime.KeepAlive(status);runtime.KeepAlive(private);runtime.KeepAlive(argv);runtime.KeepAlive(env)}() + select{} + } + execTail(filename,unsafe.Pointer(&argv[0]),unsafe.Pointer(&env[0]),uintptr(status),failureBuffer) + runtime.KeepAlive(argv);runtime.KeepAlive(env);runtime.KeepAlive(private);runtime.KeepAlive(status) + panic("tail returned") +} + +func must(err error){if err!=nil {panic(err)}} + +//go:nosplit +//go:norace +func execTail(filename *byte,argv,env unsafe.Pointer,status uintptr,message *[4]byte) { + _,_,failure:=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,status+1,uintptr(^uint32(0)),unix.CLOSE_RANGE_UNSHARE,0,0,0) + if failure==0 && status!=3 { + _,_,failure=syscall.RawSyscall(unix.SYS_DUP3,status,3,unix.O_CLOEXEC) + if failure==0 {status=3} + } + if failure==0 {_,_,failure=syscall.RawSyscall6(unix.SYS_CLOSE_RANGE,4,uintptr(^uint32(0)),0,0,0,0)} + if failure==0 { _,_,failure=syscall.RawSyscall(unix.SYS_EXECVE,uintptr(unsafe.Pointer(filename)),uintptr(argv),uintptr(env)) } + // A bounded fixed errno record, written only by this trusted raw tail. + message[0],message[1],message[2],message[3]=byte(failure),byte(failure>>8),byte(failure>>16),byte(failure>>24) + for { + _,_,e:=syscall.RawSyscall(unix.SYS_WRITE,status,uintptr(unsafe.Pointer(message)),4) + if e==syscall.EINTR {continue};break + } + syscall.RawSyscall(unix.SYS_EXIT_GROUP,125,0,0) + for{} +} + +func application(){ + values:=make([]uint64,4) + for i:=range values {v,e:=strconv.ParseUint(os.Args[i+3],10,64);must(e);values[i]=v} + leaked:=[]int{} + for fd:=0;fd<1024;fd++ {var s unix.Stat_t;if unix.Fstat(fd,&s)==nil && (uint64(s.Dev)==values[0]&&s.Ino==values[1] || uint64(s.Dev)==values[2]&&s.Ino==values[3]) {leaked=append(leaked,fd)}} + _,err:=os.OpenFile(os.Args[2],os.O_WRONLY|syscall.O_NONBLOCK,0) + absent:=os.IsNotExist(err) + json.NewEncoder(os.Stdout).Encode(map[string]any{"private_descriptor_leaks":leaked,"status_path_absent":absent,"same_executable_exec":true}) + var finish [1]byte + _,err=os.Stdin.Read(finish[:]);if err!=nil || !absent || len(leaked)!=0 {os.Exit(122)} +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-cleanup-followup.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-cleanup-followup.json new file mode 100644 index 00000000..b988fca9 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-cleanup-followup.json @@ -0,0 +1,9 @@ +{ + "at": "2026-11-01T16:05:42.800104+00:00", + "original_result": "storage-proof-result.json", + "cause": "Podman unshare recreated empty caller-owned storage lock directories while shutting down; original receipt retained unchanged.", + "private_root_removed": true, + "private_proof_processes": [], + "containers_and_image_removed_in_original_receipt": true, + "action": "Removed only the exact caller-owned private proof root after verifying ownership, realpath, empty engine inventory and no matching processes." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-proof-result.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-proof-result.json new file mode 100644 index 00000000..9b20a9ae --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage-proof-result.json @@ -0,0 +1,110 @@ +{ + "scope": "private rootless mount feasibility; not Reploy admission, eligible-engine or full code-closure qualification", + "private_root": "/tmp/reploy-fg-storage-jvpdx_0n", + "attempt": "5bf0be3202ddeafdd803f589", + "cases": [ + { + "mode": "readonly", + "exit_code": 0, + "stderr": "", + "observation": { + "checks": { + "image_code_write_denied": true, + "root_write_denied": true + }, + "details": {}, + "mode": "readonly", + "passed": true, + "uid": 0, + "uid_map": "0 1 1" + } + }, + { + "mode": "shadow-control", + "exit_code": 0, + "stderr": "", + "observation": { + "checks": { + "declared_mount_masks_image_code": true, + "readonly_root_does_not_protect_rw_mount": true + }, + "details": {}, + "mode": "shadow-control", + "passed": true, + "uid": 0, + "uid_map": "0 1 1" + } + }, + { + "mode": "writable-protected", + "exit_code": 0, + "stderr": "", + "observation": { + "checks": { + "explicit_root_write_works": true, + "trusted_binary_rename_denied": true, + "trusted_binary_unlink_denied": true, + "trusted_binary_write_denied": true + }, + "details": {}, + "mode": "writable-protected", + "passed": true, + "uid": 0, + "uid_map": "0 1 1" + } + }, + { + "mode": "ancestor-control", + "exit_code": 0, + "stderr": "", + "observation": { + "checks": { + "ancestor_attack_observed": true + }, + "details": { + "ancestor_rename": "succeeded", + "trusted_path_replaced": "true" + }, + "mode": "ancestor-control", + "passed": true, + "uid": 0, + "uid_map": "0 1 1" + } + } + ], + "cleanup": [ + { + "image": "sha256:d49e3e0517773be2d502d241db5d2411c65144d60a30dde4c86f4c7bf6c93cb0", + "removed": true + } + ], + "passed": true, + "engine": { + "version": { + "APIVersion": "4.9.3", + "Version": "4.9.3", + "GoVersion": "go1.22.2", + "GitCommit": "", + "BuiltTime": "Thu Jan 1 07:30:00 1970", + "Built": 0, + "OsArch": "linux/amd64", + "Os": "linux" + }, + "rootless": true, + "kernel": "6.18.54.1-microsoft-standard-WSL2", + "runtime": { + "name": "crun", + "package": "crun_1.14.1-1_amd64", + "path": "/usr/bin/crun", + "version": "crun version 1.14.1\ncommit: de537a7965bfbe9992e2cfae0baeb56a08128171\nrundir: /tmp/reploy-fg-storage-jvpdx_0n/xdg/crun\nspec: 1.0.0\n+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +WASM:wasmedge +YAJL" + } + }, + "fixture_host_uid": 100000, + "private_inventory_empty": true, + "private_root_removed": false, + "elapsed_seconds": 19.836866600002395, + "source_sha256": { + "run_storage_probe.py": "ab620a85cc6eae15f1defb49e274118ede537662d0055cf0d2215cc16a0801ae", + "storage_probe.go": "7a9c67c92af3bce49eae667f97cf927c06e2ea857cb7b23f095ddecbfdb3022d" + } +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage_probe.go.txt b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage_probe.go.txt new file mode 100644 index 00000000..ef6a72cd --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/mechanism-evidence/storage_probe.go.txt @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "os" + "strings" + "syscall" +) + +func main() { + mode := os.Args[1] + checks := map[string]bool{} + details := map[string]string{} + switch mode { + case "readonly": + err := os.WriteFile("/app/start", []byte("CHANGED"), 0755) + checks["image_code_write_denied"] = err != nil + err = os.WriteFile("/new-root-file", []byte("x"), 0600) + checks["root_write_denied"] = err != nil + case "shadow-control": + data, err := os.ReadFile("/app/start") + checks["declared_mount_masks_image_code"] = err == nil && string(data) == "SHADOW" + err = os.WriteFile("/app/start", []byte("CHANGED"), 0755) + checks["readonly_root_does_not_protect_rw_mount"] = err == nil + case "writable-protected": + err := os.WriteFile("/app/start", []byte("CHANGED"), 0755) + checks["explicit_root_write_works"] = err == nil + file, err := os.OpenFile("/probe", os.O_WRONLY, 0) + checks["trusted_binary_write_denied"] = err != nil + if file != nil { file.Close() } + checks["trusted_binary_unlink_denied"] = os.Remove("/probe") != nil + checks["trusted_binary_rename_denied"] = os.Rename("/probe", "/replaced") != nil + case "ancestor-control": + err := os.Rename("/opt", "/moved-opt") + details["ancestor_rename"] = "succeeded" + if err != nil { details["ancestor_rename"] = err.Error() } + checks["ancestor_attack_observed"] = true + if err == nil { + if err = os.MkdirAll("/opt/reploy", 0755); err == nil { + err = os.WriteFile("/opt/reploy/probe", []byte("SUBSTITUTED"), 0755) + } + details["trusted_path_replaced"] = "false" + if err == nil { details["trusted_path_replaced"] = "true" } + } + } + maps, _ := os.ReadFile("/proc/self/uid_map") + passed := len(checks) != 0 + for _, ok := range checks { passed = passed && ok } + json.NewEncoder(os.Stdout).Encode(map[string]any{"mode":mode,"checks":checks,"details":details,"uid_map":strings.TrimSpace(string(maps)),"uid":syscall.Getuid(),"passed":passed}) + if !passed { os.Exit(1) } +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/startup-summary.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/startup-summary.json new file mode 100644 index 00000000..f0706fa3 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/startup-summary.json @@ -0,0 +1,238 @@ +{ + "status": "bounded startup-input mechanism probes passed; production integration unqualified", + "engines": { + "docker": { + "engine": "docker", + "token": "reploy-startup-proof-05e14736ccaf", + "directory": "/home/omry/dev/reploy/temp/reviews/fg-restart-fence-20261101/reploy-startup-proof-05e14736ccaf-ah859b0w", + "limits": [ + "trusted BusyBox/shell fixture; not the production Go startup transition", + "ordinary disposable containers, not complete private mapped qualification", + "host-side exact-container association and crash publication not implemented", + "no native platform or reboot qualification" + ], + "image": "sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d", + "status": "passed", + "cases": [ + { + "case": "initial startup waits before host approval", + "passed": true + }, + { + "case": "host publishes permission once then closes its writer", + "passed": true + }, + { + "case": "same container retries with no new host grant; record unchanged", + "passed": true + }, + { + "case": "old record does not match a different launch binding", + "passed": true, + "limit": "fresh nonce selection is the trusted host creator responsibility" + }, + { + "case": "writable-mount control confirms file mode alone is insufficient", + "passed": true + } + ], + "cleanup": [ + { + "container": "6a415b9b32d9b545fc6f3fa2a9bba8ee607f73d6997162e6d6ff8f637db22590", + "absent": true + }, + { + "container": "454ad1c932209efb8ebdb845a76b83855d1bc3b54bc0f11d987bc427078c6be0", + "absent": true + }, + { + "container": "44a8e107a9a22ef964c2f1f1ba72c978bf3336744d63be72a7d0421561fe3ffe", + "absent": true + } + ] + }, + "podman": { + "engine": "podman", + "token": "reploy-startup-proof-3a9e0ee1a9a0", + "directory": "/home/omry/dev/reploy/temp/reviews/fg-restart-fence-20261101/reploy-startup-proof-3a9e0ee1a9a0-bphkho3h", + "limits": [ + "trusted BusyBox/shell fixture; not the production Go startup transition", + "ordinary disposable containers, not complete private mapped qualification", + "host-side exact-container association and crash publication not implemented", + "no native platform or reboot qualification" + ], + "image": "sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d", + "status": "passed", + "cases": [ + { + "case": "initial startup waits before host approval", + "passed": true + }, + { + "case": "host publishes permission once then closes its writer", + "passed": true + }, + { + "case": "same container retries with no new host grant; record unchanged", + "passed": true + }, + { + "case": "old record does not match a different launch binding", + "passed": true, + "limit": "fresh nonce selection is the trusted host creator responsibility" + }, + { + "case": "writable-mount control confirms file mode alone is insufficient", + "passed": true + } + ], + "cleanup": [ + { + "container": "0dd4481ed5590a1fa3ea4bbeb35aee18abd4e534a7468fee5898e7dc157e79dd", + "absent": true + }, + { + "container": "23080c66ab5a5f4142e22a6d9c633e9dee4e55de9d8537e4ff24c689f9daca31", + "absent": true + }, + { + "container": "ccb54452feca6a8df5ee559b01726a505a3f5b991f7ae3101b2bdb8c330c7762", + "absent": true + } + ] + } + }, + "filesystem_probe": { + "directory": "/home/omry/dev/reploy/temp/reviews/fg-restart-fence-20261101/mapped-file-q4ob5svf", + "limits": [ + "no application code or real secret", + "filesystem feasibility only, not complete F preparation or sandbox qualification", + "private parent path protects the temporary handoff directory; no storage attachment/race proof" + ], + "status": "passed", + "creation": { + "phase": "create", + "created": true, + "recipient_uid": 200, + "mode": "0400" + }, + "recipient": { + "phase": "recipient", + "read": true, + "digest": "e4392b67d66387f6fe2455f8f5a19ede2fc5d594c48702c5cf6b957994a05a1b" + }, + "other_role": { + "phase": "other-role", + "read": false + }, + "host_owner": [ + 100200, + 100200 + ], + "cleanup": { + "phase": "cleanup", + "cleaned": true + }, + "mapper_operations": [ + { + "tool": "newuidmap", + "phase": "create", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newgidmap", + "phase": "create", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newuidmap", + "phase": "recipient", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newgidmap", + "phase": "recipient", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newuidmap", + "phase": "other-role", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newgidmap", + "phase": "other-role", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newuidmap", + "phase": "cleanup", + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + { + "tool": "newgidmap", + "phase": "cleanup", + "exit_code": 0, + "stdout": "", + "stderr": "" + } + ] + }, + "raw_files": { + "temp/reviews/fg-restart-fence-20261101/docker-startup-probe-source.py": { + "sha256": "a14ff1c529ee753efe0ada1f45835f58b97e7f66dfd8540465c3e620d3a8b133", + "bytes": 6824 + }, + "temp/reviews/fg-restart-fence-20261101/startup_retry_probe.py": { + "sha256": "ea481ae69211b10b4014841a85bcd1146fa3b18333c432e65dc9cc1ab979bad5", + "bytes": 6850 + }, + "temp/reviews/fg-restart-fence-20261101/podman_startup_probe.py": { + "sha256": "62023a8bf8aa56d0c23aa1a3da98dcad65cae62af0522e68f38660d14b19cdb3", + "bytes": 2577 + }, + "temp/reviews/fg-restart-fence-20261101/mapped_file_probe.py": { + "sha256": "c85c6255fca4caf2fcb122cacea0528eca2323e4026ed8696f0e8ed2018e714d", + "bytes": 5691 + }, + "temp/reviews/fg-restart-fence-20261101/docker-startup-result.json": { + "sha256": "e2e919dc95b4907c63d832ad4856ad375a1c264057e39bbb9d62bf98a70bf83f", + "bytes": 1629 + }, + "temp/reviews/fg-restart-fence-20261101/podman-startup-result.json": { + "sha256": "cf472503687b1f5d442f0e0b2840c24f4fd2940b2d758084a052d79d103e76d5", + "bytes": 1629 + }, + "temp/reviews/fg-restart-fence-20261101/docker-startup-commands.json": { + "sha256": "f973a899f40b4801a2684c31168875418e37542b0cbca52ba5838e4fd1fce80d", + "bytes": 358544 + }, + "temp/reviews/fg-restart-fence-20261101/podman-startup-commands.json": { + "sha256": "15ec0c17d118488c17340e91c2829e6e97372133ebfd38e418be25954e223821", + "bytes": 403241 + }, + "temp/reviews/fg-restart-fence-20261101/podman-startup-orchestration.json": { + "sha256": "90ed324011cc707a60b3f673dc632d53452067b6aa08a10cd109a4513b232b08", + "bytes": 1815 + }, + "temp/reviews/fg-restart-fence-20261101/mapped-file-result.json": { + "sha256": "72d52f16f798906830e3ada2530ff93b3948e8327e4a1ff45b6a5990a9a8e5ce", + "bytes": 1863 + } + }, + "source_note": "Docker ran docker-startup-probe-source.py content; Podman ran startup_retry_probe.py with explicit json-file logging. Both are retained." +} diff --git a/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/summary.json b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/summary.json new file mode 100644 index 00000000..f3e7dcd7 --- /dev/null +++ b/docs/supporting/MULTI_IDENTITY_IMPLEMENTATION_PLAN/restart-fence-evidence/summary.json @@ -0,0 +1,147 @@ +{ + "schema": "reploy-restart-fence-investigation-v1", + "source_head": "87899abecced4c5f8b3513ecee66793ecdd2df37", + "platform": { + "host_kernel": "6.18.54.1-microsoft-standard-WSL2", + "docker_substrate": "Docker Desktop (containerized)" + }, + "engines": { + "docker": { + "version": "29.8.1", + "status": "passed", + "image": "sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d", + "cases": [ + { + "case": "automatic restart without Reploy", + "ok": true, + "container": "2c42bff8924cce0194b35416c4f6d83bc730d1a93bfa9558350671889f09933f", + "restart_count": 1 + }, + { + "case": "disable policy then failure stays stopped", + "ok": true + }, + { + "case": "remove automatically restarting container", + "ok": true + }, + { + "case": "explicit stop suppresses always policy during observation", + "ok": true, + "observation_seconds": 3, + "reboot_tested": false + }, + { + "case": "removed exact container remains absent", + "ok": true + }, + { + "case": "container removal preserves separate application data", + "ok": true + } + ], + "cleanup": [ + { + "container": "2c42bff8924cce0194b35416c4f6d83bc730d1a93bfa9558350671889f09933f", + "absent": true + }, + { + "container": "dcf19134c886698b0dcaaf004655dee14476e3ab4ee6f76453b7e27b518bff26", + "absent": true + } + ] + }, + "podman": { + "version": "4.9.3", + "status": "passed", + "image": "sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d", + "cases": [ + { + "case": "automatic restart without Reploy", + "ok": true, + "container": "bc77f9217a2322ad254d7b2568f911d4d400bba7cd7c0880c936ec40ac197b19", + "restart_count": 1 + }, + { + "case": "restart policy update unsupported or ineffective", + "observed": true, + "update_exit": 1, + "policy_after": "always", + "not_a_verified_fence": true + }, + { + "case": "remove automatically restarting container", + "ok": true + }, + { + "case": "explicit stop suppresses always policy during observation", + "ok": true, + "observation_seconds": 3, + "reboot_tested": false + }, + { + "case": "removed exact container remains absent", + "ok": true + }, + { + "case": "container removal preserves separate application data", + "ok": true + } + ], + "cleanup": [ + { + "container": "bc77f9217a2322ad254d7b2568f911d4d400bba7cd7c0880c936ec40ac197b19", + "absent": true + }, + { + "container": "1bcbd35e8ea5f45e24fa28e4b06790dd9cb3ebeccf45b3fe6f81c2a14470d004", + "absent": true + } + ] + } + }, + "raw_files": { + "temp/reviews/fg-restart-fence-20261101/docker_probe.py": { + "sha256": "e88313d879f7d5edaca862672f9006bcc5156079b37322d9af461744ac207751", + "bytes": 6930 + }, + "temp/reviews/fg-restart-fence-20261101/podman_probe.py": { + "sha256": "cc5f58ad05625e392bef71c4b346c03e2be867c7ae801f4d1c88af1abac0b71f", + "bytes": 3350 + }, + "temp/reviews/fg-restart-fence-20261101/docker-probe-result.json": { + "sha256": "4665f37884cc029555468e7fa439ed267e88858ce7c112bfc3780474e7846df8", + "bytes": 623213 + }, + "temp/reviews/fg-restart-fence-20261101/podman-probe-result.json": { + "sha256": "fc0f4d7dbf2d9fc08449663598fc11df0ff85f0f358ff445fa99f573935a9a93", + "bytes": 593887 + }, + "temp/reviews/fg-restart-fence-20261101/docker-probe-commands.json": { + "sha256": "4119ef39d84b51cfea323c109013c6972dd97775d69578df356e0b2ea92f7357", + "bytes": 618186 + }, + "temp/reviews/fg-restart-fence-20261101/podman-probe-commands.json": { + "sha256": "3759f8fc6397fdf98b0493be1f103d4351905828eda9703f89825eae600f6590", + "bytes": 588758 + }, + "temp/reviews/fg-restart-fence-20261101/podman-orchestration-result.json": { + "sha256": "5966c62579e4c2c3bfd1e08524b40ce5a00cd7055be65e295035ce946b9acdbb", + "bytes": 5014 + }, + "temp/reviews/fg-restart-fence-20261101/cleanup-verification.json": { + "sha256": "2e341bc3ad93b94b595772054cc4c6ee4f5f80afed21cc41c675421c27670e9d", + "bytes": 1709 + }, + "temp/reviews/fg-restart-fence-20261101/initial-harness-failure.json": { + "sha256": "c17106b81b94c6cc1a1b5228c567da40b150a07165483fcafeb0d8b98614c113", + "bytes": 277 + } + }, + "not_claimed": [ + "Reploy mapped startup/retry qualification", + "native Windows/macOS/Linux service-manager or reboot qualification", + "safe reusable permission protocol", + "final F/G LLD or estimate approval" + ] +}