From 7a3632ffcc206df354e932d65fb5a66fc5ede875 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 1 Nov 2026 22:16:29 +0100 Subject: [PATCH] docs: preserve netkingdom-target-2026-10-16 session evidence (batch 6) --- .../k8s/user-engine/verify-operability.sh | 5 + .../verify-identity-cutover-dependencies.sh | 137 ++ .../test-root/sso-mfa/k8s/verify-t02.sh | 114 ++ .../test-root/sso-mfa/k8s/verify-t03.sh | 170 +++ .../test-root/sso-mfa/k8s/verify-t04.sh | 213 +++ .../test-root/sso-mfa/k8s/verify-t05.sh | 313 +++++ .../test-root/sso-mfa/k8s/verify-t06.sh | 7 + .../test-root/sso-mfa/k8s/verify-t07.sh | 5 + .../test-root/sso-mfa/k8s/verify-t08.sh | 175 +++ ...lated-keycape-native-fixture-manifest.json | 124 ++ .../run_isolated_keycape_native_fixture.py | 221 ++++ .../tests/test_custody_entrypoints.py | 178 +++ .../test-root/tests/test_identity_bundle.py | 225 ++++ .../test_isolated_keycape_fixture_plan.py | 49 + .../tests/test_isolated_keycape_recovery.py | 234 ++++ .../tests/test_isolated_keycape_session.py | 292 +++++ .../tests/test_prepare_isolated_keycape.py | 108 ++ .../tests/test_private_manifest_binding.py | 98 ++ .../test-root/tests/test_sealed_custody.py | 143 ++ .../tests/test_service_secret_custody.py | 276 ++++ .../test-root/tests/test_sops_private.py | 227 ++++ .../test-root/tests/test_sops_write.py | 244 ++++ .../bootstrap-recovery/capture-identity.py | 296 +++++ .../check-privacyidea-database.py | 156 +++ .../fetch-privacyidea-backup.py | 88 ++ .../identity-backup-profile.json | 158 +++ .../bootstrap-recovery/identity-bundle.py | 211 +++ .../identity-credential-setup.py | 254 ++++ .../bootstrap-recovery/identity-custody.json | 144 ++ .../bootstrap-recovery/identity-files.py | 109 ++ .../bootstrap-recovery/identity-isolation.py | 65 + .../bootstrap-recovery/identity-offsite.py | 124 ++ .../bootstrap-recovery/identity-retention.py | 106 ++ .../bootstrap-recovery/identity-runtime.py | 215 +++ .../install-identity-backup.py | 145 ++ .../bootstrap-recovery/inventory-scaleway.py | 104 ++ .../inventory-storage-share.py | 114 ++ .../isolated-keycape-session.py | 512 ++++++++ .../bootstrap-recovery/isolated-keycape.py | 478 +++++++ .../bootstrap-recovery/isolated-lldap.py | 343 +++++ .../isolated-privacyidea.py | 309 +++++ .../prepare-identity-coverage.py | 94 ++ .../prepare-isolated-keycape.py | 119 ++ .../tools/bootstrap-recovery/recovery-kit.py | 176 +++ .../retrieve-identity-offline.py | 164 +++ .../bootstrap-recovery/retrieve-identity.py | 71 + .../run-isolated-keycape.sh | 43 + .../tools/bootstrap-recovery/verify-bundle.py | 139 ++ .../verify-identity-delivery.py | 93 ++ .../bootstrap-recovery/verify-identity.py | 90 ++ .../workplan-refinement-review.json | 31 + .../workplan-refinement.patch | 209 +++ ...-bootstrap-secret-free-recovery-handoff.md | 1165 +++++++++++++++++ .../coordination-cases.json | 45 + .../current-consumer-source-preflight.json | 17 + .../owner-handoff-review.json | 166 +++ .../railiance-cluster.patch | 278 ++++ .../docs/keycape-tls-recovery-boundary.md | 51 + .../tests/test_public_tls_metadata.py | 34 + .../tools/public-tls-metadata.py | 55 + ...R-WP-0018-keycape-original-tls-recovery.md | 126 ++ .../secrets-engine.patch | 258 ++++ .../keycape-isolated-recovery-selector.md | 38 + .../docs/keycape-recovery-selector.json | 15 + .../src/secrets_engine/service_auth.py | 496 +++++++ .../tests/test_keycape_recovery_selector.py | 31 + .../tools/check-keycape-recovery-selector.py | 64 + ...0015-original-keycape-recovery-selector.md | 95 ++ .../daily-dry-run-state/report.json | 562 ++++++++ .../daily-dry-run-state/run.lock | 0 .../resume-audit-r1/source-snapshot.json | 148 +++ .../resume-coordinator-final/brief.index | 62 + .../brief.index-record | 1 + .../resume-coordinator-final/brief.worktree | 62 + .../resume-coordinator-final/consistency.json | 131 ++ .../consistency.stderr.txt | 1 + .../final-review.json | 16 + .../owner-final-verification.json | 23 + .../progress-receipt.json | 14 + .../apply_reviewed_sources.py | 70 + .../build_verified_results.py | 53 + .../resume-integration-r1/close_custodian.py | 17 + .../collect_publication.py | 70 + .../resume-integration-r1/compile_sources.py | 18 + .../correct_storage_guidance.py | 19 + .../custodian-central-projection.exit.json | 1 + .../custodian-central-projection.stderr.txt | 0 .../custodian-central-projection.stdout.json | 293 +++++ .../custodian-commit-inputs.json | 11 + .../custodian-consistency.exit.json | 1 + .../custodian-consistency.json | 131 ++ .../custodian-consistency.stderr.txt | 1 + .../custodian-final-verification.json | 21 + .../.custodian-brief.md.worktree | 62 + .../WORK-RECORDS.md.worktree | 536 ++++++++ .../originals.json | 1 + .../final-progress-receipt.json | 14 + .../groups-integration.json | 23 + .../groups-preflight.json | 23 + .../integrate_workplans.py | 29 + .../net-kingdom-central-projection.exit.json | 1 + .../net-kingdom-central-projection.stderr.txt | 0 ...net-kingdom-central-projection.stdout.json | 923 +++++++++++++ .../net-kingdom-commit-inputs.json | 64 + .../net-kingdom-commit.json | 30 + .../net-kingdom-followthrough-commit.json | 11 + .../net-kingdom-initial-sync.exit.json | 1 + .../net-kingdom-initial-sync.stderr.txt | 1 + .../net-kingdom-initial-sync.stdout.json | 491 +++++++ .../.custodian-brief.md.worktree | 85 ++ .../WORK-RECORDS.md.worktree | 272 ++++ .../originals.json | 1 + .../net-kingdom-sync.exit.json | 1 + .../net-kingdom-sync.stderr.txt | 1 + .../net-kingdom-sync.stdout.json | 443 +++++++ .../progress-receipt.json | 14 + .../resume-integration-r1/publish_owner.py | 79 ++ .../reconcile_owner_projection.py | 15 + .../record_followthrough.py | 83 ++ .../recovery-integration.json | 25 + .../recovery-preflight.json | 25 + .../repo-manager-commit-inputs.json | 26 + .../repo-manager-commit.json | 11 + .../repo-manager-followthrough-commit.json | 10 + .../repo-manager-initial-sync.exit.json | 1 + .../repo-manager-initial-sync.stderr.txt | 0 .../repo-manager-initial-sync.stdout.json | 91 ++ .../.custodian-brief.md.worktree | 30 + .../WORK-RECORDS.md.worktree | 150 +++ .../originals.json | 1 + .../repo-manager-sync.exit.json | 1 + .../repo-manager-sync.stderr.txt | 0 .../repo-manager-sync.stdout.json | 91 ++ .../scanner-integration.json | 16 + .../scanner-preflight.json | 16 + .../storage-guidance-correction.json | 11 + .../workplan-integration.json | 20 + .../scanner-batch-r1/README.md | 154 +++ .../scanner-batch-r1/RMGR-WP-0021.proposed.md | 187 +++ .../benchmark-driver-result.txt | 1 + .../scanner-batch-r1/candidate.patch | 684 ++++++++++ .../configured-hook-probe-manifest.json | 41 + .../configured-hook-results.json | 53 + .../scanner-batch-r1/manifest.json | 45 + .../scanner-batch-r1/measure_scanner.py | 114 ++ .../scanner-batch-r1/owner-workplan.patch | 51 + .../scanner-batch-r1/parity-driver-result.txt | 1 + .../scanner-batch-r1/parity-results.json | 101 ++ .../scanner-batch-r1/parity_scanner.py | 75 ++ .../probe_configured_hooks.py | 204 +++ .../scanner-batch-r1/publication-plan.md | 57 + .../scanner-batch-r1/run_validation.sh | 10 + .../runtime-routing-readonly.json | 20 + .../scanner-candidate-pre-tmpfs.json | 370 ++++++ .../scanner-batch-r1/scanner-candidate.json | 370 ++++++ .../scanner-batch-r1/secret_scan.before.py | 144 ++ .../scanner-batch-r1/secret_scan.candidate.py | 428 ++++++ .../scanner-batch-r1/test-results.txt | 2 + .../test_secret_scan.candidate.py | 472 +++++++ .../secrets-native-admission-next/README.md | 29 + .../actual-install-work-record.patch | 25 + .../owner-work-record.patch | 36 + .../review_public_preparation.py | 117 ++ .../source-wave10-integration-plan.json | 60 + .../source-wave7-integration-plan.json | 132 ++ .../source-wave8-integration-plan.json | 128 ++ .../sync-session-2439.json | 7 + .../installed-lane-before.json | 13 + .../installed-plan-before.json | 12 + .../warden-admin-routing/manifest.json | 32 + .../warden-admin-routing/ops-warden.patch | 132 ++ .../warden-admin-routing/owner-review.md | 63 + .../source-lane-after.json | 15 + .../source-plan-after.json | 14 + .../source-route-after.json | 15 + .../implementation.patch | 120 ++ .../progress-receipt.json | 1 + .../warden-browser/README.md | 46 + .../warden-browser/implementation.patch | 425 ++++++ .../warden-browser/manifest.json | 46 + .../warden-browser/owner-dirty-baseline.patch | 65 + .../warden-browser/r1/README.md | 42 + .../warden-browser/r1/implementation.patch | 415 ++++++ .../warden-browser/r1/manifest.json | 41 + .../r1/owner-dirty-baseline.patch | 65 + .../warden-browser/r1/validation.json | 9 + .../warden-browser/validation.json | 12 + 187 files changed, 22808 insertions(+) create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/user-engine/verify-operability.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-identity-cutover-dependencies.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t02.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t03.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t04.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t05.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t06.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t07.sh create mode 100755 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t08.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/isolated-keycape-native-fixture-manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/run_isolated_keycape_native_fixture.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_custody_entrypoints.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_identity_bundle.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_fixture_plan.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_recovery.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_session.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_prepare_isolated_keycape.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_private_manifest_binding.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sealed_custody.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_service_secret_custody.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sops_private.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sops_write.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/capture-identity.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/check-privacyidea-database.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/fetch-privacyidea-backup.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-backup-profile.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-bundle.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-credential-setup.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-custody.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-files.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-isolation.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-offsite.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-retention.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-runtime.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/install-identity-backup.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/inventory-scaleway.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/inventory-storage-share.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape-session.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-lldap.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-privacyidea.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-identity-coverage.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-isolated-keycape.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/recovery-kit.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/retrieve-identity-offline.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/retrieve-identity.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/run-isolated-keycape.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-bundle.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-identity-delivery.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-identity.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/coordination-cases.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/current-consumer-source-preflight.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/owner-handoff-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/docs/keycape-tls-recovery-boundary.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tests/test_public_tls_metadata.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tools/public-tls-metadata.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-isolated-recovery-selector.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-recovery-selector.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/src/secrets_engine/service_auth.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tests/test_keycape_recovery_selector.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tools/check-keycape-recovery-selector.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/report.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/run.lock create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/source-snapshot.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index-record create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/final-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/owner-final-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/progress-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/apply_reviewed_sources.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/build_verified_results.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/close_custodian.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/collect_publication.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/compile_sources.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/correct_storage_guidance.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-commit-inputs.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-final-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/.custodian-brief.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/WORK-RECORDS.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/originals.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/final-progress-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-integration.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-preflight.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/integrate_workplans.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit-inputs.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-followthrough-commit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/.custodian-brief.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/WORK-RECORDS.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/originals.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/progress-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publish_owner.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/reconcile_owner_projection.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/record_followthrough.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-integration.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-preflight.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit-inputs.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-followthrough-commit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/.custodian-brief.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/WORK-RECORDS.md.worktree create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/originals.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.exit.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stderr.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stdout.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-integration.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-preflight.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/storage-guidance-correction.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/workplan-integration.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/RMGR-WP-0021.proposed.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/benchmark-driver-result.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/candidate.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-probe-manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-results.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/measure_scanner.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/owner-workplan.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-driver-result.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-results.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity_scanner.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/probe_configured_hooks.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/publication-plan.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/run_validation.sh create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/runtime-routing-readonly.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate-pre-tmpfs.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.before.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.candidate.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test-results.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test_secret_scan.candidate.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/actual-install-work-record.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/owner-work-record.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/review_public_preparation.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/source-wave10-integration-plan.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/source-wave7-integration-plan.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/source-wave8-integration-plan.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/sync-session-2439.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-lane-before.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-plan-before.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/ops-warden.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/owner-review.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-lane-after.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-plan-after.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-route-after.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/implementation.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/progress-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/implementation.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/owner-dirty-baseline.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/implementation.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/owner-dirty-baseline.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/validation.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/warden-browser/validation.json diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/user-engine/verify-operability.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/user-engine/verify-operability.sh new file mode 100755 index 00000000..a9388312 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/user-engine/verify-operability.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Managed verification/rollback belongs to rapp-user-engine, not this migration tree. +set +x +printf '%s\n' 'Retired: use rapp-user-engine make verify-live and its reviewed release/rollback contract. This legacy helper cannot reapply old manifests, exercise rollback or certify historical backup jobs.' >&2 +exit 2 diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-identity-cutover-dependencies.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-identity-cutover-dependencies.sh new file mode 100755 index 00000000..f073c367 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-identity-cutover-dependencies.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Verify that the active identity stack has no runtime dependency on the +# retired/source host. Secret values are decoded only in process memory and +# never printed; failures report resource and key locators only. +set +x +set -euo pipefail +umask 077 + +KUBECTL="${KUBECTL:-kubectl}" +SOURCE_HOST_MARKER="${SOURCE_HOST_MARKER:-coulombcore}" +SOURCE_IP="${SOURCE_IP:-92.205.130.254}" +NAMESPACES="${NAMESPACES:-sso mfa user-engine databases}" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +for namespace in $NAMESPACES; do + for kind in configmap secret deployment statefulset; do + "$KUBECTL" -n "$namespace" get "$kind" -o json \ + >"$tmp/${namespace}-${kind}.json" + done +done + +python3 - "$tmp" "$SOURCE_HOST_MARKER" "$SOURCE_IP" <<'PY' +import base64 +import json +import pathlib +import sys + +root = pathlib.Path(sys.argv[1]) +patterns = { + "source-host": sys.argv[2].lower(), + "source-ip": sys.argv[3].lower(), + "public-lldap": "lldap.coulomb.social", + "public-privacyidea": "pink.coulomb.social", + "public-privacyidea-account": "pink-account.coulomb.social", +} +failures = [] + +for path in sorted(root.glob("*.json")): + namespace, kind = path.stem.rsplit("-", 1) + for item in json.loads(path.read_text()).get("items", []): + name = item["metadata"]["name"] + if kind in {"configmap", "secret"}: + for key, raw in (item.get("data") or {}).items(): + try: + value = ( + base64.b64decode(raw).decode("utf-8", "replace") + if kind == "secret" + else raw + ) + except Exception: + continue + lowered = value.lower() + hits = [label for label, needle in patterns.items() if needle in lowered] + if hits: + failures.append( + f"{kind}/{namespace}/{name}:{key} matches={','.join(hits)}" + ) + else: + lowered = json.dumps(item.get("spec", {}), sort_keys=True).lower() + hits = [label for label, needle in patterns.items() if needle in lowered] + if hits: + failures.append( + f"{kind}/{namespace}/{name} matches={','.join(hits)}" + ) + +if failures: + print("stale_dependencies=found") + for failure in failures: + print(failure) + raise SystemExit(1) +print("stale_dependencies=absent") +PY + +keycape_config="$( + "$KUBECTL" -n sso get secret keycape-config \ + -o jsonpath='{.data.config\.yaml}' | base64 -d +)" + +for expected in \ + lldap.sso.svc.cluster.local \ + authelia.sso.svc.cluster.local \ + privacyidea.mfa.svc.cluster.local \ + user-engine \ + rapp-qonto +do + if [[ "$keycape_config" != *"$expected"* ]]; then + echo "keycape_expected_dependency_missing=$expected" >&2 + exit 1 + fi +done +unset keycape_config +echo "keycape_dependencies=cluster-local-and-complete" + +pg_pod="$( + "$KUBECTL" -n databases get pod \ + -l 'cnpg.io/cluster=net-kingdom-pg,role=primary' \ + -o jsonpath='{.items[0].metadata.name}' +)" +resolver_class="$( + "$KUBECTL" -n databases exec "$pg_pod" -- \ + psql -X -A -t -U postgres -d privacyidea_db -c \ + "select case + when lower(\"Value\") like '%lldap.sso.svc.cluster.local%' + then 'cluster-local' + else 'other' + end + from resolverconfig + where \"Key\"='LDAPURI';" 2>/dev/null +)" +if [[ "$resolver_class" != "cluster-local" ]]; then + echo "privacyidea_lldap_resolver=$resolver_class" >&2 + exit 1 +fi +echo "privacyidea_lldap_resolver=cluster-local" + +for target in \ + sso/lldap \ + sso/authelia \ + sso/keycape \ + mfa/privacyidea \ + user-engine/user-engine +do + namespace="${target%/*}" + name="${target#*/}" + ready="$( + "$KUBECTL" -n "$namespace" get deployment "$name" \ + -o jsonpath='{.status.readyReplicas}' + )" + if [[ "${ready:-0}" -lt 1 ]]; then + echo "deployment_not_ready=$target" >&2 + exit 1 + fi +done +echo "identity_deployments=ready" +echo "identity_cutover_dependencies=PASS" diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t02.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t02.sh new file mode 100755 index 00000000..3d499d72 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t02.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# verify-t02.sh — verify T02 (K8s foundations) completion criteria +# +# Run after applying all T02 manifests. Exits 0 if all checks pass. +# +# Usage: ./verify-t02.sh [--kubeconfig PATH] + +set -euo pipefail + +KUBECTL="kubectl" +if [[ "${1:-}" == "--kubeconfig" ]]; then + KUBECTL="kubectl --kubeconfig $2" +fi + +PASS=0 +FAIL=0 + +check() { + local desc="$1" + shift + if "$@" &>/dev/null; then + echo " PASS $desc" + PASS=$((PASS + 1)) + else + echo " FAIL $desc" + FAIL=$((FAIL + 1)) + fi +} + +check_output() { + local desc="$1" + local expected="$2" + shift 2 + local actual + actual="$("$@" 2>/dev/null || true)" + if echo "$actual" | grep -q "$expected"; then + echo " PASS $desc" + PASS=$((PASS + 1)) + else + echo " FAIL $desc (got: $actual)" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== T02 Verification — K8s Foundations ===" +echo "" + +# ── Namespaces ──────────────────────────────────────────────────────────────── +echo "[ Namespaces ]" +check "namespace sso exists" $KUBECTL get namespace sso +check "namespace mfa exists" $KUBECTL get namespace mfa +check "namespace databases exists" $KUBECTL get namespace databases +check "sso has net-kingdom/component label" \ + $KUBECTL get namespace sso -o jsonpath='{.metadata.labels.net-kingdom/component}' +echo "" + +# ── NetworkPolicies ─────────────────────────────────────────────────────────── +echo "[ NetworkPolicies ]" +for ns in sso mfa databases; do + check "default-deny-all in $ns" \ + $KUBECTL get networkpolicy default-deny-all -n "$ns" + check "allow-egress-dns in $ns" \ + $KUBECTL get networkpolicy allow-egress-dns -n "$ns" +done +check "allow-traefik-to-keycape in sso" $KUBECTL get networkpolicy allow-traefik-to-keycape -n sso +check "allow-user-engine-to-keycape in sso" $KUBECTL get networkpolicy allow-user-engine-to-keycape -n sso +check "allow-keycape-egress-to-privacyidea in sso" $KUBECTL get networkpolicy allow-keycape-egress-to-privacyidea -n sso +check "allow-ingress-from-traefik in mfa" $KUBECTL get networkpolicy allow-ingress-from-traefik -n mfa +check "allow-ingress-from-keycape in mfa" $KUBECTL get networkpolicy allow-ingress-from-keycape -n mfa +check "allow-egress-to-postgres in mfa" $KUBECTL get networkpolicy allow-egress-to-postgres -n mfa +check "allow-ingress-from-keycloak in databases" $KUBECTL get networkpolicy allow-ingress-from-keycloak -n databases +check "allow-ingress-from-privacyidea in databases" $KUBECTL get networkpolicy allow-ingress-from-privacyidea -n databases +echo "" + +# ── cert-manager ───────────────────────────────────────────────────────────── +echo "[ cert-manager ]" +check "cert-manager namespace exists" $KUBECTL get namespace cert-manager +check "selfsigned-issuer exists" $KUBECTL get clusterissuer selfsigned-issuer +check "letsencrypt-prod issuer exists" $KUBECTL get clusterissuer letsencrypt-prod +check_output "selfsigned-issuer is Ready" "True" \ + $KUBECTL get clusterissuer selfsigned-issuer -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' + +# Test certificate (applied separately — only check if present) +if $KUBECTL get namespace cert-manager-test &>/dev/null; then + check_output "test certificate is Ready" "True" \ + $KUBECTL get certificate selfsigned-test -n cert-manager-test \ + -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' +else + echo " SKIP test certificate (namespace cert-manager-test not found — apply test-certificate.yaml)" +fi +echo "" + +# ── StorageClass ────────────────────────────────────────────────────────────── +echo "[ StorageClass ]" +check "a default StorageClass exists" \ + $KUBECTL get storageclass -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}' + +if $KUBECTL get namespace storage-test &>/dev/null; then + check_output "storage-test pod Completed" "Completed\|Succeeded" \ + $KUBECTL get pod storage-test -n storage-test -o jsonpath='{.status.phase}' +else + echo " SKIP StorageClass PVC test (apply storage/verify-pvc.yaml first)" +fi +echo "" + +# ── Summary ─────────────────────────────────────────────────────────────────── +TOTAL=$((PASS + FAIL)) +echo "=== Results: $PASS/$TOTAL passed ===" +if [[ $FAIL -gt 0 ]]; then + echo " $FAIL check(s) failed." + exit 1 +else + echo " T02 COMPLETE — all checks passed." +fi diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t03.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t03.sh new file mode 100755 index 00000000..ff2a91a3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t03.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# verify-t03.sh — verify NK-WP-0001-T03 done-criteria +# +# Checks: +# 1. CloudNativePG operator is installed and running +# 2. Cluster net-kingdom-pg is Ready +# 3. privacyidea_db database exists +# 4. privacyidea role exists +# 5. K8s Secrets are present in the databases namespace +# 6. (Optional) Scheduled backup CR is present when backup is configured +# +# Note: keycloak_db removed — Keycloak replaced by Authelia+LLDAP+KeyCape (T05). +# +# Usage: +# chmod +x verify-t03.sh +# ./verify-t03.sh + +set -euo pipefail + +PASS=0 +FAIL=0 +WARN=0 + +pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); } +fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); } +warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); } + +section() { echo ""; echo "── $1 ──────────────────────────────────────"; } + +# ── 1. CloudNativePG operator ───────────────────────────────────────────────── +section "1. CloudNativePG operator" + +if kubectl get ns cnpg-system &>/dev/null; then + pass "cnpg-system namespace exists" +else + fail "cnpg-system namespace not found — install operator first (see postgresql/README.md)" +fi + +if kubectl get crd clusters.postgresql.cnpg.io &>/dev/null; then + pass "clusters.postgresql.cnpg.io CRD registered" +else + fail "CloudNativePG CRD not found — operator not installed" +fi + +CNPG_READY=$(kubectl get pods -n cnpg-system -l app.kubernetes.io/name=cloudnative-pg \ + --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l || echo 0) +if [[ "$CNPG_READY" -ge 1 ]]; then + pass "CloudNativePG operator pod running ($CNPG_READY pod(s))" +else + fail "No running CloudNativePG operator pods in cnpg-system" +fi + +# ── 2. Cluster readiness ────────────────────────────────────────────────────── +section "2. Cluster net-kingdom-pg" + +CLUSTER_READY=$(kubectl get cluster net-kingdom-pg -n databases \ + -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") +if [[ "$CLUSTER_READY" == "True" ]]; then + pass "Cluster net-kingdom-pg status: Ready" +else + CLUSTER_PHASE=$(kubectl get cluster net-kingdom-pg -n databases \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "not found") + fail "Cluster net-kingdom-pg not Ready (phase: $CLUSTER_PHASE)" +fi + +PRIMARY_POD=$(kubectl get pod -n databases \ + -l "cnpg.io/cluster=net-kingdom-pg,role=primary" \ + --field-selector=status.phase=Running \ + -o name 2>/dev/null | head -1 || echo "") +if [[ -n "$PRIMARY_POD" ]]; then + pass "Primary pod running: $PRIMARY_POD" +else + fail "No running primary pod found for net-kingdom-pg" +fi + +# ── 3. Databases ────────────────────────────────────────────────────────────── +section "3. Databases" + +if [[ -n "$PRIMARY_POD" ]]; then + DB_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \ + psql -U postgres -tAc "SELECT datname FROM pg_database WHERE datname = 'privacyidea_db';" \ + 2>/dev/null || echo "") + + if echo "$DB_LIST" | grep -q "privacyidea_db"; then + pass "privacyidea_db exists" + else + fail "privacyidea_db not found" + fi +else + warn "Skipping database checks — no primary pod available" +fi + +# ── 4. Roles ────────────────────────────────────────────────────────────────── +section "4. Database roles" + +if [[ -n "$PRIMARY_POD" ]]; then + ROLE_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \ + psql -U postgres -tAc "SELECT rolname FROM pg_roles WHERE rolname = 'privacyidea';" \ + 2>/dev/null || echo "") + + if echo "$ROLE_LIST" | grep -q "privacyidea"; then + pass "role privacyidea exists" + else + fail "role privacyidea not found" + fi +else + warn "Skipping role checks — no primary pod available" +fi + +# ── 5. K8s Secrets ──────────────────────────────────────────────────────────── +section "5. K8s Secrets (databases namespace)" + +for secret in net-kingdom-pg-privacyidea-app; do + if kubectl get secret "$secret" -n databases &>/dev/null; then + pass "Secret $secret exists" + else + fail "Secret $secret not found — run create-secrets.sh" + fi +done + +# CNPG auto-creates these +for secret in net-kingdom-pg-superuser; do + if kubectl get secret "$secret" -n databases &>/dev/null; then + pass "Secret $secret exists (CNPG-managed)" + else + warn "Secret $secret not found (CNPG creates this; may appear after cluster init)" + fi +done + +# ── 6. Backup configuration ─────────────────────────────────────────────────── +section "6. Backup (optional until object storage is provisioned)" + +if kubectl get scheduledbackup net-kingdom-pg-daily -n databases &>/dev/null; then + BACKUP_SUSPENDED=$(kubectl get scheduledbackup net-kingdom-pg-daily -n databases \ + -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [[ "$BACKUP_SUSPENDED" == "true" ]]; then + warn "ScheduledBackup net-kingdom-pg-daily is suspended" + else + pass "ScheduledBackup net-kingdom-pg-daily present and active" + fi + + LAST_BACKUP=$(kubectl get backup -n databases \ + -l "cnpg.io/cluster=net-kingdom-pg" \ + --sort-by='.metadata.creationTimestamp' \ + -o name 2>/dev/null | tail -1 || echo "") + if [[ -n "$LAST_BACKUP" ]]; then + pass "At least one backup found: $LAST_BACKUP" + else + warn "No backups yet — trigger a manual backup or wait for schedule" + fi +else + warn "ScheduledBackup not deployed — configure object storage, then apply scheduled-backup.yaml" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo "════════════════════════════════════════════════" +echo " T03 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL" +echo "════════════════════════════════════════════════" + +if [[ "$FAIL" -gt 0 ]]; then + echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T04" + exit 1 +elif [[ "$WARN" -gt 0 ]]; then + echo " Result: PARTIAL — T03 core done; WARN items should be addressed before production" + exit 0 +else + echo " Result: COMPLETE — T03 done-criteria met" + exit 0 +fi diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t04.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t04.sh new file mode 100755 index 00000000..db30b8fc --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t04.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# verify-t04.sh — verify NK-WP-0001-T04 done-criteria +# +# Checks: +# 1. privacyIDEA pod is Running in the mfa namespace +# 2. privacyidea Service exists +# 3. Traefik Middlewares exist +# 4. Ingress resources exist with correct hostnames +# 5. TLS certificates issued by cert-manager +# 6. Required K8s Secrets are present +# 7. PVCs are Bound +# 8. Enckey and auditkey Secrets present (from enckey-bootstrap.sh) +# +# Usage: +# chmod +x verify-t04.sh +# ./verify-t04.sh + +set -euo pipefail + +NAMESPACE="mfa" +PASS=0 +FAIL=0 +WARN=0 + +pass() { echo " [PASS] $1"; ((PASS++)); } +fail() { echo " [FAIL] $1"; ((FAIL++)); } +warn() { echo " [WARN] $1"; ((WARN++)); } + +section() { echo ""; echo "── $1 ──────────────────────────────────────"; } + +# ── 1. privacyIDEA pod ──────────────────────────────────────────────────────── +section "1. privacyIDEA pod (namespace: $NAMESPACE)" + +PI_POD=$(kubectl get pod -n "$NAMESPACE" \ + -l app.kubernetes.io/name=privacyidea \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + +if [[ -n "$PI_POD" ]]; then + pass "Pod Running: $PI_POD" + + READY=$(kubectl get pod -n "$NAMESPACE" "$PI_POD" \ + -o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false") + if [[ "$READY" == "true" ]]; then + pass "Pod readiness probe passing" + else + fail "Pod is Running but not Ready (probe failing — check logs)" + fi +else + PENDING=$(kubectl get pod -n "$NAMESPACE" \ + -l app.kubernetes.io/name=privacyidea \ + -o name 2>/dev/null | wc -l || echo 0) + if [[ "$PENDING" -gt 0 ]]; then + fail "privacyIDEA pod(s) exist but none are Running (check kubectl describe pod -n $NAMESPACE)" + else + fail "No privacyIDEA pods found in namespace $NAMESPACE — apply deployment.yaml" + fi +fi + +# ── 2. Service ──────────────────────────────────────────────────────────────── +section "2. Service" + +if kubectl get service privacyidea -n "$NAMESPACE" &>/dev/null; then + pass "Service privacyidea exists" + PORT=$(kubectl get service privacyidea -n "$NAMESPACE" \ + -o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "") + if [[ "$PORT" == "8080" ]]; then + pass "Service port: 8080" + else + warn "Service port is $PORT (expected 8080 — check container port and netpol)" + fi +else + fail "Service privacyidea not found in namespace $NAMESPACE" +fi + +# ── 3. Traefik Middlewares ──────────────────────────────────────────────────── +section "3. Traefik Middlewares" + +for mw in privacyidea-rate-limit privacyidea-admin-allowlist; do + if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then + pass "Middleware $mw exists" + else + fail "Middleware $mw not found — apply middleware.yaml" + fi +done + +# ── 4. Ingress resources ────────────────────────────────────────────────────── +section "4. Ingress resources" + +for ing in privacyidea privacyidea-admin privacyidea-account; do + if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then + pass "Ingress $ing exists" + else + fail "Ingress $ing not found — apply ingress.yaml" + fi +done + +# Verify hostnames in the main Ingress +PI_HOST=$(kubectl get ingress privacyidea -n "$NAMESPACE" \ + -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "") +if [[ "$PI_HOST" == "pink.coulomb.social" ]]; then + pass "Ingress host: pink.coulomb.social" +else + fail "Ingress host is '$PI_HOST' (expected pink.coulomb.social)" +fi + +ACCOUNT_HOST=$(kubectl get ingress privacyidea-account -n "$NAMESPACE" \ + -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "") +if [[ "$ACCOUNT_HOST" == "pink-account.coulomb.social" ]]; then + pass "Self-service host: pink-account.coulomb.social" +else + fail "Self-service host is '$ACCOUNT_HOST' (expected pink-account.coulomb.social)" +fi + +# ── 5. TLS certificates ─────────────────────────────────────────────────────── +section "5. TLS certificates" + +for cert_secret in pink-tls pink-account-tls; do + if kubectl get secret "$cert_secret" -n "$NAMESPACE" &>/dev/null; then + # Check cert-manager Ready condition + CERT_NAME="${cert_secret%-tls}" + CERT_READY=$(kubectl get certificate "$CERT_NAME" -n "$NAMESPACE" \ + -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") + if [[ "$CERT_READY" == "True" ]]; then + pass "Certificate $cert_name is Ready (TLS secret $cert_secret exists)" + else + warn "TLS secret $cert_secret exists but certificate status is not Ready (DNS propagation pending?)" + fi + else + warn "TLS secret $cert_secret not yet issued (cert-manager pending — check DNS and ACME)" + fi +done + +# ── 6. K8s Secrets ──────────────────────────────────────────────────────────── +section "6. K8s Secrets (namespace: $NAMESPACE)" + +for secret in privacyidea-config; do + if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then + pass "Secret $secret exists" + else + fail "Secret $secret not found — run create-secrets.sh" + fi +done + +# DR secrets — created by enckey-bootstrap.sh (may not exist yet on first run) +for secret in privacyidea-enckey privacyidea-auditkeys; do + if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then + pass "Secret $secret exists" + else + warn "Secret $secret not found — use privacyidea/KEY-RECOVERY.md for private export" + fi +done + +# ── 7. PVCs ─────────────────────────────────────────────────────────────────── +section "7. PersistentVolumeClaims" + +for pvc in privacyidea-data privacyidea-logs; do + STATUS=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "not found") + if [[ "$STATUS" == "Bound" ]]; then + pass "PVC $pvc: Bound" + elif [[ "$STATUS" == "not found" ]]; then + fail "PVC $pvc not found — apply pvc.yaml" + else + fail "PVC $pvc status: $STATUS (expected Bound)" + fi +done + +# ── 8. Key material check ───────────────────────────────────────────────────── +section "8. Key material (inside pod)" + +if [[ -n "$PI_POD" ]]; then + if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/enckey 2>/dev/null; then + pass "enckey present in pod" + else + warn "enckey not found in pod — stop and follow privacyidea/KEY-RECOVERY.md; do not generate a replacement recovery key" + fi + + if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/private.pem 2>/dev/null; then + pass "audit private.pem present in pod" + else + warn "audit private.pem not found — use privacyidea/KEY-RECOVERY.md for private export" + fi + + if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \ + pi-manage admin list 2>/dev/null | grep -q "pi-admin"; then + pass "Admin user pi-admin exists" + else + warn "pi-admin not found — run bootstrap-admin.sh" + fi + + # Legacy trigger-account presence is not a current KeyCape readiness gate. + +else + warn "Skipping key material checks — no running pod" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo "════════════════════════════════════════════════════════════" +echo " T04 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL" +echo "════════════════════════════════════════════════════════════" + +if [[ "$FAIL" -gt 0 ]]; then + echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T05" + exit 1 +elif [[ "$WARN" -gt 0 ]]; then + echo " Result: PARTIAL — T04 core is up; WARN items should be resolved before T05" + exit 0 +else + echo " Result: COMPLETE — T04 done-criteria met; proceed to T05 (SSO core: LLDAP+Authelia+KeyCape)" + exit 0 +fi diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t05.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t05.sh new file mode 100755 index 00000000..da3a833f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t05.sh @@ -0,0 +1,313 @@ +#!/usr/bin/env bash +# verify-t05.sh — verify NK-WP-0001-T05 done-criteria +# +# Checks all three components of the new SSO stack (LLDAP, Authelia, KeyCape). +# +# Sections: +# 1. LLDAP pod Running+Ready +# 2. LLDAP services (ldap :3890, web-ui :17170) +# 3. LLDAP Middleware (lldap-admin-allowlist) +# 4. LLDAP Ingress + hostname (lldap.coulomb.social) +# 5. LLDAP TLS certificate +# 6. LLDAP Secret (lldap-secrets) +# 7. LLDAP PVC Bound +# 8. LLDAP health endpoint +# 9. Authelia pod Running+Ready +# 10. Authelia service +# 11. Authelia Ingress + hostname (auth.coulomb.social) +# 12. Authelia TLS certificate +# 13. Authelia Secrets (authelia-secrets) +# 14. Authelia PVC Bound +# 15. Authelia health endpoint +# 16. KeyCape pod Running+Ready +# 17. KeyCape service +# 18. KeyCape Middlewares (keycape-rate-limit, keycape-hsts) +# 19. KeyCape Ingress + hostname (kc.coulomb.social) +# 20. KeyCape TLS certificate +# 21. KeyCape Secret (keycape-config) +# 22. KeyCape health endpoint (/healthz) +# 23. KeyCape OIDC discovery (/.well-known/openid-configuration) +# +# Usage: +# chmod +x verify-t05.sh +# ./verify-t05.sh + +set -euo pipefail + +NAMESPACE="sso" +LLDAP_HOST="lldap.coulomb.social" +AUTH_HOST="auth.coulomb.social" +KC_HOST="kc.coulomb.social" +PASS=0 +FAIL=0 +WARN=0 + +pass() { echo " [PASS] $1"; ((PASS++)); } +fail() { echo " [FAIL] $1"; ((FAIL++)); } +warn() { echo " [WARN] $1"; ((WARN++)); } + +section() { echo ""; echo "── $1 ──────────────────────────────────────"; } + +check_pod() { + local name="$1" + local label="$2" + local pod="" + pod=$(kubectl get pod -n "$NAMESPACE" \ + -l "app.kubernetes.io/name=${label}" \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + if [[ -n "$pod" ]]; then + pass "Pod Running: $pod" + local ready + ready=$(kubectl get pod -n "$NAMESPACE" "$pod" \ + -o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false") + if [[ "$ready" == "true" ]]; then + pass "Pod readiness probe passing" + else + fail "Pod is Running but not Ready (probe failing — check logs)" + fi + echo "$pod" + else + local count + count=$(kubectl get pod -n "$NAMESPACE" \ + -l "app.kubernetes.io/name=${label}" -o name 2>/dev/null | wc -l || echo 0) + if [[ "$count" -gt 0 ]]; then + fail "${name} pod(s) exist but none Running (kubectl describe pod -n $NAMESPACE -l app.kubernetes.io/name=${label})" + else + fail "No ${name} pods found — apply deployment.yaml" + fi + echo "" + fi +} + +check_service() { + local svc="$1"; shift + local ports=("$@") + if kubectl get service "$svc" -n "$NAMESPACE" &>/dev/null; then + pass "Service $svc exists" + for port in "${ports[@]}"; do + if kubectl get service "$svc" -n "$NAMESPACE" \ + -o jsonpath="{.spec.ports[*].port}" 2>/dev/null | grep -qw "$port"; then + pass "Service $svc has port $port" + else + fail "Service $svc missing port $port" + fi + done + else + fail "Service $svc not found" + fi +} + +check_ingress() { + local ing="$1"; local expected_host="$2" + if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then + pass "Ingress $ing exists" + local host + host=$(kubectl get ingress "$ing" -n "$NAMESPACE" \ + -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "") + if [[ "$host" == "$expected_host" ]]; then + pass "Ingress $ing host: $expected_host" + else + fail "Ingress $ing host is '$host' (expected $expected_host)" + fi + else + fail "Ingress $ing not found — apply ingress.yaml" + fi +} + +check_tls() { + local secret="$1"; local host="$2" + if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then + local cert_name="${secret%-tls}" + local cert_ready + cert_ready=$(kubectl get certificate "$cert_name" -n "$NAMESPACE" \ + -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") + if [[ "$cert_ready" == "True" ]]; then + pass "Certificate $cert_name Ready (TLS secret $secret issued)" + else + warn "TLS secret $secret exists but cert status not Ready (DNS/ACME pending?)" + fi + else + warn "TLS secret $secret not yet issued (cert-manager pending — check DNS and ACME)" + fi +} + +check_secret() { + local secret="$1" + if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then + pass "Secret $secret exists" + else + fail "Secret $secret not found — run create-secrets.sh" + fi +} + +check_pvc() { + local pvc="$1" + local status + status=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "not found") + if [[ "$status" == "Bound" ]]; then + pass "PVC $pvc: Bound" + elif [[ "$status" == "not found" ]]; then + fail "PVC $pvc not found — apply pvc.yaml" + else + fail "PVC $pvc status: $status (expected Bound)" + fi +} + +# ═══════════════════════════════════════════════════════════════════ +# LLDAP +# ═══════════════════════════════════════════════════════════════════ + +section "1. LLDAP pod (namespace: $NAMESPACE)" +LLDAP_POD=$(check_pod "LLDAP" "lldap") + +section "2. LLDAP services" +check_service "lldap" 3890 17170 + +section "3. LLDAP Middleware" +if kubectl get middleware lldap-admin-allowlist -n "$NAMESPACE" &>/dev/null; then + pass "Middleware lldap-admin-allowlist exists" +else + fail "Middleware lldap-admin-allowlist not found — apply middleware.yaml" +fi + +section "4. LLDAP Ingress" +check_ingress "lldap" "$LLDAP_HOST" + +section "5. LLDAP TLS certificate" +check_tls "lldap-tls" "$LLDAP_HOST" + +section "6. LLDAP Secret" +check_secret "lldap-secrets" + +section "7. LLDAP PVC" +check_pvc "lldap-data" + +section "8. LLDAP health endpoint" +if [[ -n "$LLDAP_POD" ]]; then + HTTP=$(kubectl exec -n "$NAMESPACE" "$LLDAP_POD" -- \ + wget -qO- http://localhost:17170/health 2>/dev/null || echo "") + if echo "$HTTP" | grep -qi "ok\|healthy\|true"; then + pass "LLDAP health endpoint responds OK" + else + warn "LLDAP health endpoint response unclear: '$HTTP'" + fi +else + warn "Skipping LLDAP health check — no running pod" +fi + +# ═══════════════════════════════════════════════════════════════════ +# Authelia +# ═══════════════════════════════════════════════════════════════════ + +section "9. Authelia pod (namespace: $NAMESPACE)" +AUTHELIA_POD=$(check_pod "Authelia" "authelia") + +section "10. Authelia service" +check_service "authelia" 9091 + +section "11. Authelia Ingress" +check_ingress "authelia" "$AUTH_HOST" + +section "12. Authelia TLS certificate" +check_tls "auth-tls" "$AUTH_HOST" + +section "13. Authelia Secrets" +check_secret "authelia-secrets" + +section "14. Authelia PVC" +check_pvc "authelia-data" + +section "15. Authelia health endpoint" +if [[ -n "$AUTHELIA_POD" ]]; then + STATUS=$(kubectl exec -n "$NAMESPACE" "$AUTHELIA_POD" -- \ + wget -qO- http://localhost:9091/api/health 2>/dev/null || echo "") + if echo "$STATUS" | grep -qi "ok\|healthy"; then + pass "Authelia /api/health responds OK" + else + warn "Authelia /api/health response unclear: '$STATUS'" + fi +else + warn "Skipping Authelia health check — no running pod" +fi + +# ═══════════════════════════════════════════════════════════════════ +# KeyCape +# ═══════════════════════════════════════════════════════════════════ + +section "16. KeyCape pod (namespace: $NAMESPACE)" +KC_POD=$(check_pod "KeyCape" "keycape") + +section "17. KeyCape service" +check_service "keycape" 8080 + +section "18. KeyCape Middlewares" +for mw in keycape-rate-limit keycape-hsts; do + if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then + pass "Middleware $mw exists" + else + fail "Middleware $mw not found — apply middleware.yaml" + fi +done + +section "19. KeyCape Ingress" +check_ingress "keycape" "$KC_HOST" + +section "20. KeyCape TLS certificate" +check_tls "kc-tls" "$KC_HOST" + +section "21. KeyCape Secrets" +check_secret "keycape-config" + +section "22. KeyCape health endpoint" +if [[ -n "$KC_POD" ]]; then + STATUS=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \ + wget -qO- http://localhost:8080/healthz 2>/dev/null || echo "") + if echo "$STATUS" | grep -qi "ok\|healthy"; then + pass "KeyCape /healthz responds OK" + else + warn "KeyCape /healthz response unclear: '$STATUS'" + fi +else + warn "Skipping KeyCape health check — no running pod" +fi + +section "23. KeyCape OIDC discovery" +if [[ -n "$KC_POD" ]]; then + DISCOVERY=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \ + wget -qO- "http://localhost:8080/.well-known/openid-configuration" 2>/dev/null || echo "") + if echo "$DISCOVERY" | grep -q '"issuer"'; then + pass "OIDC discovery endpoint returns issuer" + ISSUER=$(echo "$DISCOVERY" | python3 -c \ + "import sys,json; d=json.load(sys.stdin); print(d.get('issuer',''))" 2>/dev/null || echo "") + if [[ "$ISSUER" == "https://$KC_HOST" ]]; then + pass "OIDC issuer matches CP-NK-004: $ISSUER" + else + warn "OIDC issuer is '$ISSUER' (expected https://$KC_HOST)" + fi + else + fail "OIDC discovery endpoint did not return expected JSON" + fi +else + warn "Skipping OIDC discovery check — no running pod" +fi + +# ═══════════════════════════════════════════════════════════════════ +# Summary +# ═══════════════════════════════════════════════════════════════════ +echo "" +echo "════════════════════════════════════════════════════════════" +echo " T05 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL" +echo "════════════════════════════════════════════════════════════" + +if [[ "$FAIL" -gt 0 ]]; then + echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T06" + exit 1 +elif [[ "$WARN" -gt 0 ]]; then + echo " Result: PARTIAL — T05 core is up; WARN items should be resolved before T06" + exit 0 +else + echo " Result: COMPLETE — T05 done-criteria met; proceed to T06 (MFA flow integration)" + exit 0 +fi diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t06.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t06.sh new file mode 100755 index 00000000..5b3056a3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t06.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Functional realm/resolver and token-backed MFA proof, NK-WP-0034. +# Usage: ./verify-t06.sh --user platform-root +# Runbook: docs/verify-t06.md (credentials are prompted, not read from old bundles). +set -euo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec python3 "$SCRIPT_DIR/privacyidea/verify_mfa.py" "$@" diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t07.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t07.sh new file mode 100755 index 00000000..3aab32ac --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t07.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Explicit private input and preview/readback adapter; see lldap/GROUP-MAINTENANCE.md. +set +x +set -euo pipefail +exec python3 "$(dirname -- "$0")/lldap/lldap_admin.py" verify "$@" diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t08.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t08.sh new file mode 100755 index 00000000..6a393f6e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/sso-mfa/k8s/verify-t08.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# verify-t08.sh — verify NK-WP-0001-T08 done-criteria +# +# Checks backups, DR readiness, and break-glass account. +# +# Sections: +# 1. Backup CronJobs exist (lldap-backup, authelia-backup, privacyidea-backup) +# 2. backup-sa ServiceAccount and RBAC exist +# 3. lldap-backup has run successfully at least once +# 4. authelia-backup has run successfully at least once +# 5. privacyidea-backup has run successfully at least once +# 6. privacyIDEA enckey backup exists on PVC +# 7. LLDAP SQLite backup exists on PVC +# 8. DR-RUNBOOK.md present in repo +# 9. KeePassXC ops bundle (pack-bundle.sh) — manual confirmation required +# +# Usage: +# chmod +x verify-t08.sh +# ./verify-t08.sh + +set -euo pipefail + +SSO_NAMESPACE="sso" +MFA_NAMESPACE="mfa" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PASS=0 +FAIL=0 +WARN=0 + +pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); } +fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); } +warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); } + +section() { echo ""; echo "── $1 ──────────────────────────────────────"; } + +check_cronjob() { + local name="$1"; local ns="$2" + if kubectl get cronjob "$name" -n "$ns" &>/dev/null; then + pass "CronJob $name exists (namespace: $ns)" + local schedule + schedule=$(kubectl get cronjob "$name" -n "$ns" \ + -o jsonpath='{.spec.schedule}' 2>/dev/null || echo "?") + pass " Schedule: $schedule" + else + fail "CronJob $name not found in namespace $ns — apply backup/cronjob-sqlite-backups.yaml" + fi +} + +check_last_job() { + local cronjob="$1"; local ns="$2" + # Find the most recent Job spawned by this CronJob + LAST_JOB=$(kubectl get job -n "$ns" \ + -l "batch.kubernetes.io/controller-uid" \ + --sort-by=.metadata.creationTimestamp \ + -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || echo "") + # Simpler: look for any completed job with the cronjob name prefix + SUCCEEDED=$(kubectl get job -n "$ns" \ + -o jsonpath="{.items[?(@.metadata.ownerReferences[0].name==\"$cronjob\")].status.succeeded}" \ + 2>/dev/null || echo "") + if [[ "$SUCCEEDED" == *"1"* ]]; then + pass "CronJob $cronjob has at least one successful run" + else + warn "CronJob $cronjob has no successful runs yet — trigger manually to test:" + warn " kubectl create job -n $ns --from=cronjob/$cronjob ${cronjob}-manual-test" + fi +} + +# ── 1. Backup CronJobs ──────────────────────────────────────────────────────── +section "1. Backup CronJobs" +check_cronjob "lldap-backup" "$SSO_NAMESPACE" +check_cronjob "authelia-backup" "$SSO_NAMESPACE" +check_cronjob "privacyidea-backup" "$MFA_NAMESPACE" + +# ── 2. RBAC ─────────────────────────────────────────────────────────────────── +section "2. Backup ServiceAccount and RBAC (namespace: $SSO_NAMESPACE)" +if kubectl get serviceaccount backup-sa -n "$SSO_NAMESPACE" &>/dev/null; then + pass "ServiceAccount backup-sa exists" +else + fail "ServiceAccount backup-sa not found — apply backup/cronjob-sqlite-backups.yaml" +fi +if kubectl get role backup-scaler -n "$SSO_NAMESPACE" &>/dev/null; then + pass "Role backup-scaler exists" +else + fail "Role backup-scaler not found" +fi +if kubectl get rolebinding backup-sa-scaler -n "$SSO_NAMESPACE" &>/dev/null; then + pass "RoleBinding backup-sa-scaler exists" +else + fail "RoleBinding backup-sa-scaler not found" +fi + +# ── 3–5. CronJob run history ────────────────────────────────────────────────── +section "3. lldap-backup run history" +check_last_job "lldap-backup" "$SSO_NAMESPACE" + +section "4. authelia-backup run history" +check_last_job "authelia-backup" "$SSO_NAMESPACE" + +section "5. privacyidea-backup run history" +check_last_job "privacyidea-backup" "$MFA_NAMESPACE" + +# ── 6. privacyIDEA enckey backup on PVC ────────────────────────────────────── +section "6. privacyIDEA enckey backup on PVC" +PI_POD=$(kubectl get pod -n "$MFA_NAMESPACE" \ + -l app.kubernetes.io/name=privacyidea \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") +if [[ -n "$PI_POD" ]]; then + # PI PVC is mounted at /etc/privacyidea (not /data) in the privacyIDEA container + BACKUP_COUNT=$(kubectl exec -n "$MFA_NAMESPACE" "$PI_POD" -- \ + sh -c 'ls /etc/privacyidea/backups/enckey.backup.* 2>/dev/null | wc -l' 2>/dev/null || echo "0") + BACKUP_COUNT="${BACKUP_COUNT// /}" + if [[ "$BACKUP_COUNT" -gt 0 ]]; then + pass "privacyIDEA enckey backups found on PVC ($BACKUP_COUNT file(s))" + else + warn "No enckey backup files on PVC yet — trigger privacyidea-backup CronJob to create one" + warn " kubectl create job -n $MFA_NAMESPACE --from=cronjob/privacyidea-backup pi-backup-test" + fi +else + warn "Skipping enckey backup check — no running privacyIDEA pod" +fi + +# ── 7. LLDAP SQLite backup on PVC ──────────────────────────────────────────── +section "7. LLDAP SQLite backup on PVC" +LLDAP_POD=$(kubectl get pod -n "$SSO_NAMESPACE" \ + -l app.kubernetes.io/name=lldap \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") +if [[ -n "$LLDAP_POD" ]]; then + BACKUP_COUNT=$(kubectl exec -n "$SSO_NAMESPACE" "$LLDAP_POD" -- \ + sh -c 'ls /data/backups/users.backup.* 2>/dev/null | wc -l' 2>/dev/null || echo "0") + BACKUP_COUNT="${BACKUP_COUNT// /}" + if [[ "$BACKUP_COUNT" -gt 0 ]]; then + pass "LLDAP SQLite backups found on PVC ($BACKUP_COUNT file(s))" + else + warn "No LLDAP backup files on PVC yet — trigger lldap-backup CronJob to create one" + warn " kubectl create job -n $SSO_NAMESPACE --from=cronjob/lldap-backup lldap-backup-test" + fi +else + warn "Skipping LLDAP backup check — no running LLDAP pod" +fi + +# ── 8. DR runbook present ───────────────────────────────────────────────────── +section "8. DR runbook" +RUNBOOK="$SCRIPT_DIR/backup/DR-RUNBOOK.md" +if [[ -f "$RUNBOOK" ]]; then + pass "DR-RUNBOOK.md present at $RUNBOOK" +else + fail "DR-RUNBOOK.md not found — it should be at sso-mfa/k8s/backup/DR-RUNBOOK.md" +fi + +# ── 9. Offsite backup (manual confirmation) ─────────────────────────────────── +section "9. Offsite backup (manual)" +warn "Cannot verify offsite backup automatically — confirm manually:" +warn " - pack-bundle.sh has been run with current secrets" +warn " - ops-bundle.tar.age stored in a separate physical location" +warn " - age decryption key stored separately (NOT in the same location as the bundle)" + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo "════════════════════════════════════════════════════════════" +echo " T08 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL" +echo "════════════════════════════════════════════════════════════" + +if [[ "$FAIL" -gt 0 ]]; then + echo " Result: INCOMPLETE — resolve FAIL items before marking T08 done" + exit 1 +elif [[ "$WARN" -gt 0 ]]; then + echo " Result: PARTIAL — structure is in place; resolve WARN items (trigger CronJobs)" + exit 0 +else + echo " Result: COMPLETE — T08 done-criteria met; SSO/MFA platform workplan complete!" + exit 0 +fi diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/isolated-keycape-native-fixture-manifest.json b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/isolated-keycape-native-fixture-manifest.json new file mode 100644 index 00000000..a9254547 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/isolated-keycape-native-fixture-manifest.json @@ -0,0 +1,124 @@ +{ + "schema": "nk49.keycape-native-fixture-proposal.v1", + "status": "prepared_not_executed_not_approved", + "source_sha256": { + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/k8s/privacyidea/key-custody-remote.py": "sha256:27b2b885c81923d85054e3999c5613e104fe627779b2b50398ed9bed5088f47e", + "tests/run_isolated_keycape_native_fixture.py": "sha256:ecbadecc475622eaaf20a726110aebef5f4fe7af141110a98aca1640179b4a03", + "tests/test_identity_bundle.py": "sha256:be5ce74097594cc7f7e77dbccd6cb53aa72f263cd1bb442ce5464038aed1116c", + "tests/test_isolated_keycape_fixture_plan.py": "sha256:5945619f4c5957187763276cc900870a9f08f9475b63c240694688e0c2eca7dd", + "tests/test_isolated_keycape_recovery.py": "sha256:247e9a87f5be2ee9f20f7edbb4a9f612ab7cb0cd940392c3caeca8b046605f55", + "tests/test_isolated_keycape_session.py": "sha256:cbdf51efde26eff337e9303a2c431f80b333b8846d763eb62853f81eb3f13e2c", + "tools/bootstrap-recovery/capture-identity.py": "sha256:47dca7c4dc96fa7eb9fb02c81fca9ca8dc5281aac918979eb7367148168ba705", + "tools/bootstrap-recovery/check-privacyidea-database.py": "sha256:90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4", + "tools/bootstrap-recovery/fetch-privacyidea-backup.py": "sha256:233160e1206643a5cf7ad05141d733f0a75aff651190e27f77515a124ff0a59f", + "tools/bootstrap-recovery/identity-backup-profile.json": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1", + "tools/bootstrap-recovery/identity-bundle.py": "sha256:524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "tools/bootstrap-recovery/identity-credential-setup.py": "sha256:ddd6127fa331c0185523ec3bb22d743a791edd56a2e6af7ad8d7949b7c051141", + "tools/bootstrap-recovery/identity-custody.json": "sha256:f8aee8487ed01d7940b82676bb4a5c8d61f51b6c598521b8b8a73588aaea4d4f", + "tools/bootstrap-recovery/identity-files.py": "sha256:d0063a9eedd3610e3d45bd2d834aae83cff74e4b3504d138b3548f427e89331d", + "tools/bootstrap-recovery/identity-isolation.py": "sha256:88d1e13112a44c79571825af89d3fe5b01fa490cff2653badef4c62a45c844f1", + "tools/bootstrap-recovery/identity-offsite.py": "sha256:e7c31fa9ee081961af873348ee3b4829a645d40c7dc7f1e22c564aa9ed01bc49", + "tools/bootstrap-recovery/identity-retention.py": "sha256:a41ea4759f69c35548149fc8d0d5ace547f16cf9fe4c9dac10b119a4d0becdec", + "tools/bootstrap-recovery/identity-runtime.py": "sha256:a2c0acf567c204705b90be64d561d0fda2285e0fc71a962d6e1b149f571e1940", + "tools/bootstrap-recovery/install-identity-backup.py": "sha256:5c612a2a51c9b59aad8cc01d418c2f563cbd92c62dc1fa8be994e5fb59df90f1", + "tools/bootstrap-recovery/inventory-scaleway.py": "sha256:a219df205708fa4ed44b763a418585d63c820c25434bc0fd14e805eb5c2f79d3", + "tools/bootstrap-recovery/inventory-storage-share.py": "sha256:673f9b17fb2f3fef6fcdb5c50a2bcca69e3df3157a9e5582a2d995c6b87640b5", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-lldap.py": "sha256:35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "tools/bootstrap-recovery/isolated-privacyidea.py": "sha256:4df7196678aac1ad594f32abf45c68dc21a9d2760d9e8a536a215a4a3c5d277f", + "tools/bootstrap-recovery/prepare-identity-coverage.py": "sha256:cd5f3067d28b4c7cc93ed08b8cf3869218bdecf7c54b5dffed22d697de71dbb1", + "tools/bootstrap-recovery/recovery-kit.py": "sha256:6fa0f1494dccaf003b6c6396c5de8b36faa2b276eeb16badd04383f5efb71e66", + "tools/bootstrap-recovery/retrieve-identity-offline.py": "sha256:3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972", + "tools/bootstrap-recovery/retrieve-identity.py": "sha256:ec123d47a07305253570dacab7e103f8c193a23f20c9fc87466ea4a95ae0ec13", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/verify-bundle.py": "sha256:885b05a2011e711228e42e316bda70bbe0d97d481383d61aa8469d0f10452370", + "tools/bootstrap-recovery/verify-identity-delivery.py": "sha256:63658d1976806f8d0ce80ba8f750bbcfdca59da936b6eb6a78a250f0e53710f1", + "tools/bootstrap-recovery/verify-identity.py": "sha256:039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a", + "tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f", + "tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b" + }, + "fixed_endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + "forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010", + "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + ], + "pull_policy": "never", + "synthetic_issuer": "https://nk49-fixture.invalid", + "cases": [ + "positive", + "hard-parent-loss", + "deadline", + "nonzero-after-accept" + ], + "limits": { + "sequential_cases": true, + "max_live_service_containers": 2, + "max_tmpfs_volumes": 4, + "max_public_probe_containers": 1, + "service_memory_bytes_each": 536870912, + "service_cpus_each": 1, + "tmpfs_volume_bytes_each": 33554432, + "host_tmpfs_fixture_limit_expected_bytes": 8388608, + "execution_deadline_seconds": { + "deadline_case": 20, + "other_cases": 120 + }, + "driver_wait_seconds_per_case": 150 + }, + "resources": { + "container_names": "nk49-kc--{transport,issuer,image-probe}", + "volume_names": "nk49-kc--v{0,1,2,3}", + "label": "net-kingdom.recovery-run=", + "host_plaintext": "exact private /dev/shm/nk49-keycape-- registered before writes", + "synthetic_source_root": "private /dev/shm/nk49-keycape-fixture-", + "safe_result": "private /tmp/nk49-keycape-native-fixture-review-/result.json" + }, + "scope": "candidate itself on complete synthetic format/integrity archive fixture; no native PI/LLDAP/Authelia proof, real custody, real factor or real client credential; successor binds sealed-descriptor custody preparation sources; synthetic fixture itself overrides custody with generated fixture key as before", + "mutations": [ + "conditional creation of exact fresh local containers and local-driver tmpfs volumes", + "private synthetic file import to those volumes", + "original-image KeyCape service/client exchange within same loopback-only namespace", + "exact owned quiescent removal and bounded safe receipts" + ], + "forbidden": [ + "Docker execution before independent approval", + "image pulls/builds/pushes", + "production routes/ports/host networking/host directory mounts", + "OpenBao/Kubernetes/provider APIs", + "actual identity archive, age key, TLS key, user password, factor or client secret" + ], + "cleanup": "candidate exact ownership/volume-driver/inode checks; quiesce fresh owner SID/PGID via pidfds first; retained inflight uncertainty never accepted even on absent readback", + "oracles": [ + "positive issuer-native verify-client must establish original-hostname HTTPS trust; helper alone never suffices", + "same original single RS256 key-1 JWKS and exact config/registry/policy/history/lock/deliveries", + "correct service exchange and explicit wrong-secret/excess-scope denial through existing native verifier", + "hard-parent-loss and tightened deadline both quiesce before exact resource/plaintext cleanup", + "after real Docker run accepts a synthetic container, inject returncode1; require failed recovery and sticky daemon uncertainty after cleanup" + ], + "proposed_command": [ + "python3", + "tests/run_isolated_keycape_native_fixture.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "", + "--case", + "positive" + ], + "extended_command": [ + "python3", + "tests/run_isolated_keycape_native_fixture.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "", + "--case", + "all" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/run_isolated_keycape_native_fixture.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/run_isolated_keycape_native_fixture.py new file mode 100644 index 00000000..71463265 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/run_isolated_keycape_native_fixture.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Prepare/run the exact reviewed T18 candidate on disposable synthetic material. + +Default --plan performs no Docker calls and generates no key/material. Execution +requires the reviewed fixture manifest hash and source closure. This runner calls +the candidate itself; hooks only pause or inject a post-response transport fault. +""" +import argparse +import base64 +from datetime import datetime,timedelta,timezone +import hashlib +import importlib.util +import io +import json +import os +import re +from pathlib import Path +import signal +import subprocess +import sys +import tarfile +import tempfile +import time +import uuid + +ROOT=Path(__file__).resolve().parents[1] +MANIFEST=ROOT/'tests/isolated-keycape-native-fixture-manifest.json' +ISSUER='https://nk49-fixture.invalid' +CASES=('positive','hard-parent-loss','deadline','nonzero-after-accept') + + +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path) + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module + +s=load('session',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py') + + +def require(condition): + if not condition:raise RuntimeError('synthetic_fixture_refused') + + +def reviewed_manifest(expected): + raw=MANIFEST.read_bytes();require(hashlib.sha256(raw).hexdigest()==expected) + manifest=json.loads(raw) + for name,digest in manifest['source_sha256'].items(): + require(isinstance(digest,str) and re.fullmatch('sha256:[a-f0-9]{64}',digest)) + require('sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==digest) + return manifest + + +def private_write(path,value): + path.parent.mkdir(mode=0o700,parents=True,exist_ok=True) + fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as out:out.write(value) + + +def materials(root): + """Reuse the existing full archive fixture factory, no real archive accepted.""" + from cryptography import x509 + from cryptography.hazmat.primitives import hashes,serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.x509.oid import NameOID + import yaml + existing=load('existing_bundle_fixture',ROOT/'tests/test_identity_bundle.py') + payload=existing.stage.__wrapped__(root) + config={'issuer':ISSUER,'port':8080,'privateKeyPem':'/etc/keycape/key.pem','environment':'dev','tokenLifetime':'5m', + 'lldap':{'url':'ldap://127.0.0.1:1','bindDN':'cn=fixture','bindPW':'synthetic-only','baseDN':'dc=fixture'}, + 'authelia':{'baseURL':'http://127.0.0.1:1','clientId':'synthetic','clientSecret':'synthetic','redirectURI':ISSUER+'/authorize/callback'}, + 'privacyidea':{'baseURL':'http://127.0.0.1:1'}, + 'clients':[{'clientId':'nk49-fixture-consumer','clientType':'confidential','audience':'nk49-fixture', + 'grantTypes':['client_credentials'],'allowedScopes':['fixture:read'],'secretRef':'env:KEYCAPE_FIXTURE_CLIENT_SECRET', + 'serviceSubject':'service:nk49-fixture','tenant':'tenant:fixture','roles':['fixture-reader']}]+ + [{'clientId':name,'displayName':name,'clientType':'public','grantTypes':['authorization_code'], + 'redirectUris':['https://nk49-fixture.invalid/callback'],'allowedScopes':['openid'],'mfaRequired':True} + for name in ('vergabe-demo-company','user-engine-portal')]} + secret_path=payload/'resources/sso/secret/keycape-config.json';secret=json.loads(secret_path.read_text()) + secret['data']['config.yaml']=base64.b64encode(yaml.safe_dump(config).encode()).decode();secret_path.write_text(json.dumps(secret)) + client_path=payload/'resources/sso/secret/keycape-rapp-qonto-client.json';client=json.loads(client_path.read_text()) + client['data']={'client-secret':base64.b64encode(b'synthetic-fixture-client-secret').decode()};client_path.write_text(json.dumps(client)) + factor_path=payload/'resources/sso/secret/keycape-factor-read.json';factor=json.loads(factor_path.read_text()) + factor['data']={'admin-token':base64.b64encode(b'synthetic-unused-factor-reader').decode()};factor_path.write_text(json.dumps(factor)) + dep_path=payload/'resources/sso/deployment/keycape.json';dep=json.loads(dep_path.read_text()) + dep['spec']={'template':{'spec':{'containers':[{'name':'keycape','image':s.adapter.IMAGE,'env':[ + {'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'}, + {'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'}, + {'name':'KEYCAPE_FIXTURE_CLIENT_SECRET','valueFrom':{'secretKeyRef':{'name':'keycape-rapp-qonto-client','key':'client-secret'}}}]}]}}};dep_path.write_text(json.dumps(dep)) + policy={'Revision':1,'Modes':{k:'mandatory' for k in ('vergabe-demo-company','user-engine-portal')},'History':[ + {'reference':'synthetic-retained-receipt','actor':'service:fixture','client':'user-engine-portal','before':'mandatory','after':'mandatory','revision':1,'at':1}]} + # JSON tags in the owner store are lowercase; retain an existing lock inode + # representation as part of the archived tree, never synthesize it after import. + policy={k.lower():v for k,v in policy.items()};(payload/'data/keycape/policy.json').write_text(json.dumps(policy)) + private_write(payload/'data/keycape/policy.json.lock',b'') + key=rsa.generate_private_key(public_exponent=65537,key_size=2048) + subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'nk49-fixture.invalid')]);now=datetime.now(timezone.utc) + cert=(x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number()) + .not_valid_before(now-timedelta(minutes=5)).not_valid_after(now+timedelta(days=1)) + .add_extension(x509.SubjectAlternativeName([x509.DNSName('nk49-fixture.invalid')]),critical=False) + .add_extension(x509.BasicConstraints(ca=True,path_length=None),critical=True).sign(key,hashes.SHA256())) + tls={'/recovery-tls/server.pem':cert.public_bytes(serialization.Encoding.PEM), + '/recovery-tls/server-key.pem':key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()), + '/recovery-tls/trust.pem':cert.public_bytes(serialization.Encoding.PEM)} + for path,value in tls.items():private_write(root/Path(path).name,value) + age=root/'synthetic-age.key';s.run(['age-keygen','-o',str(age)]) + recipient=s.run(['age-keygen','-y',str(age)]).decode().strip();profile=s.bundle.profile();profile['public_recipient']=recipient + s.bundle.profile=lambda:profile + artifact=root/'net-kingdom-identity-20261016T063159Z-0123456789ab.tar.age';s.bundle.seal(payload,artifact,{'synthetic_native_keycape_fixture':True}) + raw=s.run(['age','-d','-i',str(age),str(artifact)],max_output=s.bundle.LIMIT) + with tarfile.open(fileobj=io.BytesIO(raw)) as archive:manifest=json.load(archive.extractfile('manifest.json')) + transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share', + 'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':artifact.name,'ciphertext_bytes':artifact.stat().st_size, + 'ciphertext_sha256':hashlib.sha256(artifact.read_bytes()).hexdigest(),'etag':'"synthetic"','fetched_path':str(artifact), + 'independent_owner_retrieval_verified':True} + # Derive public executable identity only from the exact cached admitted image. + probe=s.plan_session();name=probe['public_image_probe'] + try: + s.run(['docker','create','--pull=never','--name',name,'--network','none','--read-only','--label',s.adapter.LABEL+'='+probe['run_id'],s.adapter.IMAGE]) + binary=s.adapter.container_file(name,'/keycape') + finally:s.adapter.cleanup({**probe,'containers':[name]}, {}) + admission={'issuer':ISSUER,'ciphertext_sha256':transfer['ciphertext_sha256'], + 'manifest_sha256':hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest(), + 'verifier_sha256':hashlib.sha256(binary).hexdigest(),'trust_sha256':hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest(), + 'tls_cert_sha256':hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest(), + 'consumer':{'client_id':'nk49-fixture-consumer','secret_env':'KEYCAPE_FIXTURE_CLIENT_SECRET','audience':'nk49-fixture','scope':'fixture:read', + 'subject':'service:nk49-fixture','tenant':'tenant:fixture','roles':'fixture-reader','deny_scope':'fixture:write'}} + private_write(root/'fixture.json',json.dumps({'schema':'nk49.generated-keycape-fixture.v1','transfer':transfer,'admission':admission,'recipient':recipient}).encode()) + + +def worker(root,case): + execution=s.isolate_execution();execution['deadline']=time.monotonic()+(20 if case=='deadline' else 120) + data=json.loads((root/'fixture.json').read_text());require(data['schema']=='nk49.generated-keycape-fixture.v1') + profile=s.bundle.profile();profile['public_recipient']=data['recipient'];s.bundle.profile=lambda:profile + s.local.verify.custody.private_key_fd=lambda:os.open(root/'synthetic-age.key',os.O_RDONLY|os.O_NOFOLLOW) + tls={'/recovery-tls/'+name:(root/name).read_bytes() for name in ('server.pem','server-key.pem','trust.pem')} + receipt={};output=root/(case+'.json');checkpoint=lambda r:output.write_text(json.dumps(r)) + provision=s.provision + def paused(*args): + provision(*args) + if case in ('hard-parent-loss','deadline'): + private_write(root/(case+'.barrier'),b'private-import-and-default-launch-complete') + time.sleep(150) + s.provision=paused + if case=='nonzero-after-accept': + original=s.adapter.subprocess.run;injected=False + def fault(argv,**kwargs): + nonlocal injected + result=original(argv,**kwargs) + if argv[:3]==['docker','--host',s.adapter.ENDPOINT] and 'run' in argv and not injected and result.returncode==0: + injected=True + return subprocess.CompletedProcess(argv,1,b'',b'') + return result + s.adapter.subprocess.run=fault + try:s.execute(data['transfer'],data['admission'],tls,receipt,execution,checkpoint=checkpoint) + except BaseException:receipt['status']='failed' + finally:output.write_text(json.dumps(receipt)) + return int(receipt.get('status')!='isolated_keycape_native_registration_checked') + + +def run_cases(root,summary,cases): + for case in cases: + child=subprocess.Popen([sys.executable,str(Path(__file__).resolve()),'--worker-root',str(root),'--worker-case',case, + '--execute-reviewed-fixture','--expected-manifest-sha256',summary['manifest_sha256']],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) + try: + end=time.monotonic()+150 + if case=='hard-parent-loss': + while not (root/(case+'.barrier')).exists() and child.poll() is None and time.monotonic()','exec',optimize=2),{}) + self.assertEqual(json.loads(output.getvalue()),{'original_public_signing_key_in_live_jwks':False}) + +class BoundaryTests(unittest.TestCase): + def test_remote_docker_refused_before_info(self): + session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'} + with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command: + with self.assertRaises(a.Refused):a.inspect_session(session) + self.assertEqual(command.call_count,1) + def test_failed_verifier_cleanup_is_attempted(self): + receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}} + with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup: + with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt) + cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed') + + def test_cleanup_failure_overrides_success(self): + admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}} + result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')} + result['public_signing_spki_sha256']='e'*64;receipt={} + with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused): + with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt) + self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified']) + +class ContainmentTests(unittest.TestCase): + def inspect(self,overrides=None,launch=None,mounts=None): + name='nk49-kc-'+('a'*32)+'-issuer' + session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}} + host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']} + host.update(overrides or {}) + def command(argv,**kwargs): + if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]' + if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}' + if argv[-1]=='{{.Id}}':return b'anchor-id' + if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode() + return json.dumps({'host':host,'mounts':mounts or [],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode() + with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session) + def test_minimal_network_none_admitted(self):self.inspect() + def test_image_declared_anonymous_disk_volume_remains_refused(self): + with self.assertRaises(a.Refused): + self.inspect(mounts=[{'Type':'volume','Name':'anonymous-fixture','Destination':'/privacyidea/etc/persistent','RW':True}]) + + def test_alternate_same_image_command_refused(self): + with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']}) + def test_cap_add_refused(self): + with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']}) + def test_devices_refused(self): + with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]}) + def test_device_requests_refused(self): + with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]}) + def test_host_namespace_refused(self): + for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'): + with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'}) + def test_security_override_refused(self): + with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']}) + +class CleanupTests(unittest.TestCase): + def test_partial_cleanup_attempts_every_owned_container(self): + session={'run_id':'a'*32,'containers':['first','second','third']};receipt={} + removed=[] + def command(argv,**kwargs): + if argv[1:3]==['container','inspect']:return ('a'*32).encode() + if argv[1]=='rm': + removed.append(argv[-1]) + if argv[-1]=='third':raise a.Refused() + return b'' + if argv[1:3]==['container','ls']: + name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$') + return (name+'\n').encode() if name=='third' or name not in removed else b'' + return b'' + with patch.object(a,'command',side_effect=command): + with self.assertRaises(a.Refused):a.cleanup(session,receipt) + self.assertEqual(removed,['third','second','first']) + self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified']) + def test_ownership_mismatch_does_not_remove_and_continues(self): + session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[] + def command(argv,**kwargs): + if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode() + if argv[1]=='rm':removed.append(argv[-1]) + if argv[1:3]==['container','ls']: + name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$') + return (name+'\n').encode() if name not in removed else b'' + return b'' + with patch.object(a,'command',side_effect=command): + with self.assertRaises(a.Refused):a.cleanup(session,receipt) + self.assertEqual(removed,['first']) + self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed') + def test_inspect_failure_retains_uncertain_obligation(self): + receipt={} + with patch.object(a,'command',side_effect=a.Refused): + with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt) + self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}]) + +class JWKSTests(unittest.TestCase): + def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'): + request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'} + def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=') + responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}] + if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2')) + class Response: + def __init__(self,value):self.value=value + def __enter__(self):return self + def __exit__(self,*args):pass + def read(self,*args):return json.dumps(self.value).encode() + class Opener: + def open(self,*args,**kwargs):return Response(responses.pop(0)) + with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()): + exec(compile(a.JWKS_PROBE,'','exec',optimize=2),{}) + def test_exact_original_public_key(self):self.execute() + def test_extra_signing_key_refused_under_optimization(self): + with self.assertRaises(ValueError):self.execute(extra=True) + def test_wrong_kid_refused_under_optimization(self): + with self.assertRaises(ValueError):self.execute(kid='replacement') + def test_wrong_live_signing_key_refused(self): + with self.assertRaises(ValueError):self.execute(n='124') + def test_cross_origin_jwks_refused(self): + with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks') + def test_production_dns_refused_before_fetch(self): + with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1') + +if __name__=='__main__':unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_session.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_session.py new file mode 100644 index 00000000..70cf4079 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_isolated_keycape_session.py @@ -0,0 +1,292 @@ +import base64 +import importlib.util +import json +import io +import os +import tempfile +import signal +import subprocess +import sys +import time +from pathlib import Path +import unittest +from unittest.mock import patch + +SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape-session.py' +spec=importlib.util.spec_from_file_location('session',SOURCE) +s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s) + +class SessionTests(unittest.TestCase): + def test_tmpfs_plan_has_four_exact_original_mounts(self): + session=s.plan_session() + self.assertEqual({v['target'] for v in session['volumes'].values()},set(s.RUNROOTS)) + self.assertTrue(all(v['options']==s.VOLUME_OPTIONS for v in session['volumes'].values())) + self.assertEqual(session['images'][session['issuer']],s.adapter.IMAGE) + def test_non_tmpfs_driver_options_refused(self): + session=s.plan_session();name,spec=next(iter(session['volumes'].items())) + wrong=[{'Name':name,'Driver':'local','Scope':'local','Options':{'type':'none','device':'/host/data'},'Labels':{s.adapter.LABEL:session['run_id']}}] + with patch.object(s,'run',return_value=json.dumps(wrong).encode()): + with self.assertRaises(s.adapter.Refused):s.verify_volume(name,spec,session['run_id']) + def test_owner_secret_is_not_docker_argv_and_import_is_after_inspection(self): + session=s.plan_session();events=[] + payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'original').decode()},'env':{'KEYCAPE_TEST_SECRET':'fixture-sensitive'}} + def run(argv,**kwargs): + events.append(('run',argv,kwargs)) + if argv[-1]=='{{.Id}}':return b'anchor' + return b'' + with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=lambda *args,**kwargs:events.append(('inspect',args,kwargs))): + s.provision(session,payload,{'/recovery-tls/trust.pem':b'original-trust'},'https://issuer.example') + joined=' '.join(' '.join(e[1]) for e in events if e[0]=='run') + self.assertNotIn('fixture-sensitive',joined) + private_index=next(i for i,e in enumerate(events) if e[0]=='run' and s.IMPORT in e[1]) + inspect_index=next(i for i,e in enumerate(events) if e[0]=='inspect') + self.assertLess(inspect_index,private_index) + issuer_call=next(e for e in events if e[0]=='run' and e[1][-1]==s.adapter.IMAGE) + self.assertEqual(issuer_call[2]['env']['KEYCAPE_TEST_SECRET'],'fixture-sensitive') + self.assertIn('KEYCAPE_TEST_SECRET',issuer_call[1]) + self.assertNotIn('--entrypoint',issuer_call[1]) + def test_pinned_helper_image_volume_is_explicitly_masked_with_tmpfs(self): + calls=[] + def run(argv,**kwargs):calls.append(argv);return b'anchor' if argv[-1]=='{{.Id}}' else b'' + with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session'): + s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example') + worker=next(argv for argv in calls if s.WORKER in argv) + mask='/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700' + self.assertIn(mask,worker) + self.assertEqual(worker[worker.index(mask)-1],'--tmpfs') + issuer=next(argv for argv in calls if argv[-1]==s.adapter.IMAGE) + self.assertNotIn(mask,issuer) + def test_public_phase_sequence_reports_containment_before_private_import(self): + session=s.plan_session();phases=[] + def run(argv,**kwargs): + return b'anchor' if argv[-1]=='{{.Id}}' else b'' + with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session'): + s.provision(session,{'files':{},'env':{}},{},'https://issuer.example',phases.append) + self.assertEqual(phases,list(s.PROVISION_PHASES)) + def test_worker_containment_refusal_keeps_exact_phase_and_skips_import(self): + phases=[];calls=[] + def run(argv,**kwargs):calls.append(argv);return b'' + with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=s.adapter.Refused): + with self.assertRaises(s.adapter.Refused): + s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example',phases.append) + self.assertEqual(phases[-1],'worker_containment') + self.assertFalse(any(s.IMPORT in argv for argv in calls)) + def test_issuer_containment_refusal_does_not_claim_provision_complete(self): + phases=[] + def run(argv,**kwargs):return b'anchor' if argv[-1]=='{{.Id}}' else b'' + with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=[None,s.adapter.Refused()]): + with self.assertRaises(s.adapter.Refused): + s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example',phases.append) + self.assertEqual(phases[-1],'issuer_containment') + self.assertNotIn('complete',phases) + def test_non_original_https_port_refused_before_any_mutation(self): + with patch.object(s,'run') as run: + with self.assertRaises(s.adapter.Refused):s.provision(s.plan_session(),{'files':{},'env':{}},{},'http://issuer.example') + run.assert_not_called() + def test_container_failure_still_attempts_each_volume_cleanup(self): + session=s.plan_session();receipt={};removed=[] + def run(argv,**kwargs): + if argv[1:3]==['volume','rm']:removed.append(argv[-1]) + if argv[1:3]==['volume','ls']: + name=next(x for x in argv if x.startswith('name=^')).removeprefix('name=^').removesuffix('$') + return (name+'\n').encode() if name not in removed else b'' + return b'' + with patch.object(s.adapter,'cleanup',side_effect=s.adapter.Refused),patch.object(s,'verify_volume'),patch.object(s,'run',side_effect=run): + with self.assertRaises(s.adapter.Refused):s.cleanup_all(session,receipt) + self.assertEqual(set(removed),set(session['volumes'])) + self.assertFalse(receipt['container_cleanup_verified']) + self.assertEqual(len(receipt['tmpfs_volume_cleanup_obligations']),4) + def test_reaper_inherits_no_custody_or_service_environment(self): + session=s.plan_session();staging={'path':'/dev/shm/registered','run_id':session['run_id']} + with patch.object(s.subprocess,'Popen') as spawn,patch.object(s,'verify_execution'),patch.object(s.os,'pidfd_open',return_value=99),patch.object(s.os,'close'),patch.object(s,'register_workdir',return_value=staging),patch.object(s.select,'select',side_effect=lambda *args:([spawn.return_value.stdout],[],[])): + spawn.return_value.stdout.readline.return_value=b'registered\n' + s.start_reaper(session,staging,{'pid':123}) + kwargs=spawn.call_args.kwargs + self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'}) + self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session']);self.assertEqual(kwargs['pass_fds'],(99,)) + +class TargetBindingTests(unittest.TestCase): + def test_every_direct_docker_command_is_explicitly_bound(self): + with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run',return_value=__import__('subprocess').CompletedProcess([],0,b'ok')) as run: + self.assertEqual(s.run(['docker','info']),b'ok') + argv=run.call_args.args[0] + self.assertEqual(argv[:3],['docker','--host',s.adapter.ENDPOINT]) + self.assertEqual(run.call_args.kwargs['env']['DOCKER_HOST'],s.adapter.ENDPOINT) + def test_nondefault_unix_socket_is_refused_before_any_docker_call(self): + with patch.dict('os.environ',{'DOCKER_HOST':'unix:///tmp/other-daemon.sock'},clear=True),patch.object(s.adapter.subprocess,'run') as run: + with self.assertRaises(s.adapter.Refused):s.run(['docker','info']) + run.assert_not_called() + def test_conflicting_context_is_refused(self): + with patch.dict('os.environ',{'DOCKER_CONTEXT':'production'},clear=True),patch.object(s.adapter.subprocess,'run') as run: + with self.assertRaises(s.adapter.Refused):s.run(['docker','info']) + run.assert_not_called() + def test_conflicting_per_call_host_is_refused(self): + with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run') as run: + with self.assertRaises(s.adapter.Refused):s.run(['docker','info'],env={'DOCKER_HOST':'unix:///tmp/other.sock'}) + run.assert_not_called() + def test_reused_preflight_uses_same_bound_command(self): + self.assertIs(s.local.command,s.adapter.command) + def test_probe_is_registered_before_session_creation(self): + session=s.plan_session() + self.assertEqual(session['public_image_probe'],'nk49-kc-'+session['run_id']+'-image-probe') + self.assertEqual(session['docker_endpoint'],s.adapter.ENDPOINT) + +class PlaintextLifecycleTests(unittest.TestCase): + def setUp(self): + self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup) + self.root=Path(self.tmp.name) + self.override=patch.object(s,'STAGING_ROOT',self.root);self.override.start();self.addCleanup(self.override.stop) + self.run_id='a'*32 + self.work=self.root/('nk49-keycape-'+self.run_id+'-abcdefgh');self.work.mkdir(mode=0o700) + self.staging=s.register_workdir(self.work,self.run_id) + def test_exact_plaintext_directory_removed_and_recorded(self): + (self.work/'archive.tar').write_bytes(b'synthetic-private-archive') + restored=self.work/'restored';restored.mkdir(mode=0o700);(restored/'fixture').write_bytes(b'synthetic') + receipt={};s.cleanup_workdir(self.staging,receipt) + self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified']) + self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'absence_verified') + def test_docker_failure_does_not_skip_plaintext_cleanup(self): + (self.work/'archive.tar').write_bytes(b'synthetic') + receipt={} + with patch.object(s,'cleanup_all',side_effect=s.adapter.Refused): + with self.assertRaises(s.adapter.Refused):s.recover_obligations({},self.staging,receipt) + self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified']) + def test_replacement_inode_is_not_removed(self): + # Preserve original inode while replacing its registered pathname. + old=self.root/'retained-original';self.work.rename(old);self.work.mkdir(mode=0o700) + (self.work/'replacement').write_bytes(b'not-owned-by-this-obligation');receipt={} + with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,receipt) + self.assertTrue((self.work/'replacement').exists());self.assertTrue(old.exists()) + self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'identity_mismatch_not_removed') + def test_symlink_root_is_not_followed(self): + self.work.rmdir();outside=self.root/'outside';outside.mkdir();(outside/'kept').write_bytes(b'keep') + self.work.symlink_to(outside,target_is_directory=True) + with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{}) + self.assertTrue((outside/'kept').exists()) + def test_descendant_symlink_is_not_followed(self): + outside=self.root/'kept';outside.write_bytes(b'keep');(self.work/'link').symlink_to(outside) + with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{}) + self.assertEqual(outside.read_bytes(),b'keep') + def test_wrong_owner_identity_is_not_removed(self): + changed={**self.staging,'owner_uid':os.getuid()+1} + with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(changed,{}) + self.assertTrue(self.work.exists()) + def test_unregistered_path_refused(self): + with self.assertRaises(s.adapter.Refused):s.cleanup_workdir({**self.staging,'path':str(self.root)}, {}) + +@unittest.skipUnless(hasattr(os,'pidfd_open') and hasattr(signal,'pidfd_send_signal'),'Linux pidfd required') +class ExecutionDeadlineTests(unittest.TestCase): + def start_synthetic_owner(self,root): + late=str(root/'late-creation') + code="""import importlib.util,json,subprocess,sys,time +spec=importlib.util.spec_from_file_location('session',sys.argv[1]);s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s) +execution=s.isolate_execution() +child=subprocess.Popen([sys.executable,'-c',"import pathlib,sys,time;time.sleep(2);pathlib.Path(sys.argv[1]).write_text('late')",sys.argv[2]]) +print(json.dumps({'execution':execution,'child_pid':child.pid}),flush=True) +time.sleep(20) +""" + parent=subprocess.Popen([sys.executable,'-c',code,str(SOURCE),late],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL) + message=json.loads(parent.stdout.readline());parent.stdout.close() + def finish(): + if parent.poll() is None:parent.kill() + parent.wait(timeout=5) + try: + before=s.process_facts(message['child_pid']) + if before is None or before['sid']!=message['execution']['sid'] or before['pgid']!=message['execution']['pgid']:return + fd=os.pidfd_open(message['child_pid']) + try: + after=s.process_facts(message['child_pid']) + if after is not None and all(after[k]==before[k] for k in ('pid','sid','pgid','start_ticks','uid')): + signal.pidfd_send_signal(fd,signal.SIGKILL) + except ProcessLookupError:pass + finally:os.close(fd) + except ProcessLookupError:pass + self.addCleanup(finish) + return parent,message['execution'],Path(late) + def test_deadline_quiesces_only_fresh_owner_group_before_cleanup(self): + with tempfile.TemporaryDirectory() as directory: + parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid) + try: + receipt={};s.quiesce_execution(execution,fd,receipt) + self.assertTrue(receipt['execution_quiescence_verified']) + self.assertFalse(s.group_members(execution));self.assertFalse(late.exists()) + finally:os.close(fd) + def test_hard_parent_loss_stops_surviving_marker_creator(self): + with tempfile.TemporaryDirectory() as directory: + parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid) + try: + signal.pidfd_send_signal(fd,signal.SIGKILL) + poll=__import__('select').poll();poll.register(fd,__import__('select').POLLIN);self.assertTrue(poll.poll(2000)) + receipt={};s.quiesce_execution(execution,fd,receipt) + self.assertTrue(receipt['execution_quiescence_verified']);self.assertFalse(late.exists()) + self.assertFalse(s.group_members(execution)) + finally:os.close(fd) + def test_expired_execution_cannot_start_any_docker_command(self): + binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()-1,'staging':'/tmp/not-created'} + with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run') as run: + with self.assertRaises(s.adapter.Refused):s.run(['docker','create','fixed-image']) + run.assert_not_called() + def test_reused_or_foreign_session_refuses_signals(self): + binding={'pid':123,'sid':123,'pgid':123,'start_ticks':10,'uid':os.getuid()} + wrong={**binding,'sid':456,'state':'S'} + with patch.object(s,'process_facts',return_value=wrong),patch.object(s.signal,'pidfd_send_signal') as send: + with self.assertRaises(s.adapter.Refused):s.quiesce_execution(binding,99,{}) + send.assert_not_called() + def test_inflight_uncertainty_survives_plaintext_directory_removal(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'owned-stage';path.mkdir();marker=Path(str(path)+'-docker-inflight');marker.touch() + staging={'path':str(path)};self.assertTrue(s.daemon_uncertain(staging));path.rmdir() + self.assertTrue(s.daemon_uncertain(staging)) + +class ReaperOrderingTests(unittest.TestCase): + def exercise(self,quiescence_fails=False): + events=[];records=[] + def quiet(*args): + events.append('quiesce') + if quiescence_fails:raise s.adapter.Refused() + args[-1]['execution_quiescence_verified']=True + def recover(*args):events.append('cleanup') + with patch.object(s,'verify_execution'),patch.object(s,'pidfd_dead',return_value=False),patch.object(s.select,'poll') as poll,patch.object(s,'quiesce_execution',side_effect=quiet),patch.object(s,'daemon_uncertain',return_value=True),patch.object(s,'recover_obligations',side_effect=recover),patch.object(s.os,'close'),patch.object(s.os,'open',return_value=99),patch.object(s.os,'fdopen'),patch.object(s.json,'dump',side_effect=lambda r,*args:records.append(dict(r))),patch('sys.stdout',io.StringIO()): + poll.return_value.poll.return_value=[] + s.reaper_main({}, {'path':'/dev/shm/exact-staging'}, {'deadline':time.monotonic()-1},77) + return events,records[0] + def test_daemon_uncertainty_survives_verified_resource_readback(self): + events,receipt=self.exercise();self.assertEqual(events,['quiesce','cleanup']) + self.assertTrue(receipt['execution_quiescence_verified']);self.assertTrue(receipt['in_flight_daemon_request_uncertain']) + self.assertEqual(receipt['status'],'bounded_cleanup_uncertain') + def test_unverified_quiescence_prevents_any_cleanup(self): + events,receipt=self.exercise(True);self.assertEqual(events,['quiesce']) + self.assertFalse(receipt['execution_quiescence_verified']);self.assertEqual(receipt['status'],'bounded_cleanup_uncertain') + def test_checkpoint_rewrites_one_valid_json_document(self): + with tempfile.TemporaryFile() as stream: + s.persist_receipt(stream.fileno(),{'status':'registered','long':'x'*100}) + s.persist_receipt(stream.fileno(),{'status':'failed'}) + stream.seek(0);self.assertEqual(json.load(stream),{'status':'failed'}) + def test_nonzero_docker_result_retains_uncertainty_before_inflight_clear(self): + with tempfile.TemporaryDirectory() as directory: + staging=str(Path(directory)/'stage');Path(staging).mkdir() + binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging} + def returned(*args,**kwargs): + self.assertTrue(Path(staging+'-docker-inflight').exists()) + return subprocess.CompletedProcess(args[0],1,b'') + with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=returned): + with self.assertRaises(s.adapter.Refused):s.run(['docker','create','synthetic-already-accepted']) + self.assertFalse(Path(staging+'-docker-inflight').exists());self.assertTrue(s.daemon_uncertain({'path':staging})) + def test_only_policy_mount_is_writable_in_issuer(self): + plan=s.plan_session();args=s.mounts(plan,readonly=True) + mounts=args[1::2] + for value in mounts: + self.assertEqual(not value.endswith(',readonly'),'target=/var/lib/keycape-policy' in value) + def test_docker_timeout_retains_independent_uncertainty_marker(self): + with tempfile.TemporaryDirectory() as directory: + staging=str(Path(directory)/'stage');Path(staging).mkdir() + binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging} + def timeout(*args,**kwargs): + self.assertTrue(Path(staging+'-docker-inflight').exists()) + raise subprocess.TimeoutExpired('synthetic Docker',1) + with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=timeout): + with self.assertRaises(subprocess.TimeoutExpired):s.run(['docker','create','fixed-image']) + self.assertFalse(Path(staging+'-docker-inflight').exists()) + self.assertTrue(Path(staging+'-docker-uncertain').exists());self.assertTrue(s.daemon_uncertain({'path':staging})) + +if __name__=='__main__':unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_prepare_isolated_keycape.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_prepare_isolated_keycape.py new file mode 100644 index 00000000..f486a770 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_prepare_isolated_keycape.py @@ -0,0 +1,108 @@ +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import tempfile +import pty +import subprocess +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).parents[1] +spec=importlib.util.spec_from_file_location('prepare',ROOT/'tools/bootstrap-recovery/prepare-isolated-keycape.py') +p=importlib.util.module_from_spec(spec);spec.loader.exec_module(p) + +class PreparationTests(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) + self.path=Path(self.temp.name)/'ciphertext';self.path.write_bytes(b'synthetic ciphertext');self.path.chmod(0o600) + self.transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share', + 'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':'net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age', + 'ciphertext_sha256':'a'*64,'ciphertext_bytes':len(b'synthetic ciphertext'),'etag':'"synthetic"','fetched_path':str(self.path), + 'independent_owner_retrieval_verified':True} + self.admission={name:'a'*64 for name in ('ciphertext_sha256','manifest_sha256','verifier_sha256','trust_sha256','tls_cert_sha256')} + self.admission.update(issuer='https://issuer.example',consumer=dict(client_id='existing',secret_env='KEYCAPE_EXISTING_SECRET',audience='existing-api',scope='read',subject='svc:existing',tenant='existing',roles='reader',deny_scope='write'), + source_sha256={name:'sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest() for name in p.SOURCE_PATHS}) + def test_complete_public_bindings_admitted_without_reading_ciphertext(self): + original=Path.read_bytes + def read(path): + self.assertNotEqual(path,self.path) + return original(path) + with patch.object(Path,'read_bytes',read):self.assertTrue(p.validate(self.transfer,self.admission,ROOT)) + def test_oversize_metadata_refused_before_any_content_read(self): + self.path.write_bytes(b'x'*(128*1024+1)) + with patch.object(Path,'read_bytes',side_effect=AssertionError('unbounded read')): + with self.assertRaises(p.Refused):p.read_metadata(self.path) + def test_duplicate_json_keys_refused_at_every_level(self): + for raw in (b'{"issuer":"one","issuer":"two"}',b'{"consumer":{"client_id":"one","client_id":"two"}}'): + with self.subTest(raw=raw),self.assertRaises(p.Refused):p.strict_json(raw) + def test_bounded_metadata_reader_accepts_exact_limit(self): + self.path.write_bytes(b'x'*128) + self.assertEqual(p.read_metadata(self.path,128),b'x'*128) + with self.assertRaises(p.Refused):p.read_metadata(self.path,127) + def test_wrong_archive_binding_refused(self): + self.admission['ciphertext_sha256']='b'*64 + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_ciphertext_size_mismatch_refused_without_decryption(self): + self.transfer['ciphertext_bytes']+=1 + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_missing_independent_transfer_refused(self): + self.transfer['independent_owner_retrieval_verified']=False + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_missing_or_drifted_source_refused(self): + for source in p.SOURCE_PATHS: + bad=copy.deepcopy(self.admission);bad['source_sha256'][source]='0'*64 + with self.subTest(source=source),self.assertRaises(p.Refused):p.validate(self.transfer,bad,ROOT) + bad=copy.deepcopy(self.admission);bad['source_sha256'].pop(next(iter(p.SOURCE_PATHS))) + with self.assertRaises(p.Refused):p.validate(self.transfer,bad,ROOT) + def test_unreviewed_extra_dependency_refused(self): + self.admission['source_sha256']['unreviewed.py']='a'*64 + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_ambiguous_or_unsupported_issuer_refused(self): + for issuer in ('http://issuer.example','https://u:p@issuer.example','https://issuer.example:8443','https://issuer.example/path','https://issuer.example?x=1','https://issuer.example#fragment'): + self.admission['issuer']=issuer + with self.subTest(issuer=issuer),self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_missing_consumer_or_overlapping_denial_refused(self): + self.admission['consumer']['deny_scope']='read' + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + self.admission['consumer'].pop('roles') + with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT) + def test_symlink_private_input_refused_without_read(self): + link=self.path.parent/'link';link.symlink_to(self.path) + with self.assertRaises(p.Refused):p.metadata_path(link) + def test_publicly_readable_private_input_refused(self): + self.path.chmod(0o644) + with self.assertRaises(p.Refused):p.metadata_path(self.path) + def test_bad_admission_digest_never_reads_tls_or_starts_custody(self): + admission=self.path.parent/'admission';admission.write_text(json.dumps(self.admission));admission.chmod(0o600) + argv=['prepare','--transfer-receipt',str(self.path),'--admission',str(admission),'--admission-sha256','b'*64, + '--tls-cert','/never/read-cert','--tls-key','/never/read-key','--trust','/never/read-trust'] + with patch('sys.argv',argv),patch('builtins.print') as output: + self.assertEqual(p.main(),1) + self.assertEqual(output.call_args.args,('ERROR: reviewed metadata/source/dependency preflight refused; no key requested.',)) + def test_actual_launcher_bad_digest_stops_before_hidden_prompt(self): + master,slave=pty.openpty() + try: + args=['bash',str(ROOT/'tools/bootstrap-recovery/run-isolated-keycape.sh'),str(self.path),str(self.path),'b'*64,str(self.path),str(self.path),str(self.path)] + process=subprocess.Popen(args,stdin=slave,stdout=slave,stderr=slave) + process.wait(timeout=5);os.close(slave);slave=None + output=b'' + while True: + try: + block=os.read(master,4096) + if not block:break + output+=block + except OSError:break + self.assertEqual(process.returncode,1) + self.assertIn(b'preflight refused; no key requested.',output) + self.assertNotIn(b'Paste custody',output) + self.assertNotIn(b'Enter only',output) + finally: + if slave is not None:os.close(slave) + os.close(master) + def test_launcher_prepares_before_sealed_descriptor_custody(self): + script=(ROOT/'tools/bootstrap-recovery/run-isolated-keycape.sh').read_text() + self.assertLess(script.index('python3 tools/bootstrap-recovery/prepare-isolated-keycape.py'),script.index('bash sso-mfa/bootstrap/sops-custody-unlock.sh')) + self.assertIn('--sealed-fd --prompt -- python3',script) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_private_manifest_binding.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_private_manifest_binding.py new file mode 100644 index 00000000..c8bd1c90 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_private_manifest_binding.py @@ -0,0 +1,98 @@ +"""Generated archive/key only; actual age authentication + archive restore. + +Docker and issuer behavior are intentionally mocked here, not native evidence. +""" +import base64 +from datetime import datetime,timedelta,timezone +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import time +from unittest.mock import MagicMock,patch +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes,serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID +from test_identity_bundle import stage,b + +ROOT=Path(__file__).parents[1] +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m +s=load('session_manifest',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py') +h=load('helper_manifest',ROOT/'sso-mfa/bootstrap/sops-custody-fd.py') + +@pytest.fixture +def envelope(stage,tmp_path,monkeypatch): + raw=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout + key=h.sealed_key(next(line for line in raw.splitlines() if line.startswith(b'AGE-SECRET-KEY-'))+b'\n') + recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{key}'],pass_fds=(key,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip() + profile=b.profile();profile['public_recipient']=recipient;monkeypatch.setattr(b,'profile',lambda:profile);monkeypatch.setattr(s.bundle,'profile',lambda:profile) + artifact=tmp_path/'identity.tar.age';sealed=b.seal(stage,artifact,{'test_only':True}) + signer=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc) + cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(signer.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).sign(signer,hashes.SHA256())).public_bytes(serialization.Encoding.PEM) + tls={'/recovery-tls/server.pem':cert,'/recovery-tls/trust.pem':cert,'/recovery-tls/server-key.pem':signer.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption())} + transfer=dict(schema='netkingdom.identity-offsite.v1',status='readback_verified',provider='Storage Share',folder='Backup/railiance-backups/net-kingdom-identity',artifact='net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age',etag='"synthetic"',ciphertext_sha256=sealed['ciphertext_sha256'],ciphertext_bytes=artifact.stat().st_size,fetched_path=str(artifact),independent_owner_retrieval_verified=True) + admission=dict(ciphertext_sha256=sealed['ciphertext_sha256'],manifest_binding_mode='ciphertext-derived-v1',issuer='https://synthetic.invalid',tls_cert_sha256=hashlib.sha256(cert).hexdigest(),trust_sha256=hashlib.sha256(cert).hexdigest(),verifier_sha256=hashlib.sha256(b'synthetic executable').hexdigest()) + try:yield artifact,key,transfer,admission,tls + finally:os.close(key) + +def execute(envelope,*,wrong_key=False): + artifact,key,transfer,admission,tls=envelope;receipt={};seen={};checkpoints=[] + if wrong_key: + raw=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout + key=h.sealed_key(next(line for line in raw.splitlines() if line.startswith(b'AGE-SECRET-KEY-'))+b'\n') + actualrun=s.run + def run(argv,**kwargs):return actualrun(argv,**kwargs) if argv[0]=='age-keygen' else b'' + def native(root,manifest,checked,receipt,**kwargs):seen.update(checked);receipt['status']='synthetic_flow_checked' + execution={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'uid':os.getuid(),'deadline':time.monotonic()+60} + try: + with patch.object(s,'verify_execution'),patch.object(s.local,'preflight',return_value={}),patch.object(s.local.verify.custody,'private_key_fd',side_effect=lambda:os.dup(key)),patch.object(s,'start_reaper',return_value=MagicMock()),patch.object(s,'run',side_effect=run),patch.object(s.adapter,'container_file',return_value=b'synthetic executable'),patch.object(s.adapter,'private_payload',return_value={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'issuer: https://synthetic.invalid\nport: 8080').decode()},'env':{}}),patch.object(s,'provision'),patch.object(s.adapter,'check_jwks'),patch.object(s.adapter,'native_checks',side_effect=native),patch.object(s,'cleanup_all'): + # Key file must be valid at the later public signer construction. + payload_key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption()) + with patch.object(s.adapter,'private_payload',return_value={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'issuer: https://synthetic.invalid\nport: 8080').decode(),'/etc/keycape/key.pem':base64.b64encode(payload_key).decode()},'env':{}}): + s.execute(transfer,admission,tls,receipt,execution,checkpoint=lambda r:checkpoints.append(json.loads(json.dumps(r)))) + return receipt,seen,checkpoints + finally: + if wrong_key:os.close(key) + +def test_exact_ciphertext_authenticated_restore_derives_private_manifest_without_safe_leak(envelope): + receipt,seen,checkpoints=execute(envelope) + derived=seen['manifest_sha256'];assert len(derived)==64 + safe=json.dumps([receipt,checkpoints]);assert derived not in safe;assert 'manifest_sha256' not in safe + assert receipt['all_manifest_hashes_verified'] is True + assert receipt['temporary_plaintext_cleanup_verified'] is True + +def test_changed_ciphertext_refuses_before_archive_decryption(envelope): + artifact,*_=envelope;artifact.write_bytes(artifact.read_bytes()[:-1]+b'!') + with patch.object(s.adapter,'process') as process,pytest.raises(s.adapter.Refused):execute(envelope) + assert not process.called + +def test_modified_ciphertext_rebound_metadata_still_fails_authenticated_age(envelope): + artifact,key,transfer,admission,tls=envelope + raw=artifact.read_bytes();artifact.write_bytes(raw[:-32]+b'!'*32) + digest=hashlib.sha256(artifact.read_bytes()).hexdigest() + transfer['ciphertext_sha256']=digest;admission['ciphertext_sha256']=digest + with pytest.raises(s.adapter.Refused):execute(envelope) + +def test_wrong_key_refuses_before_decryption_and_never_exposes_manifest(envelope): + with pytest.raises(s.adapter.Refused):execute(envelope,wrong_key=True) + +def test_wrong_explicit_manifest_refuses_after_full_restore(envelope): + admission=envelope[3];admission['manifest_binding_mode']='explicit-digest-v1';admission['manifest_sha256']='0'*64 + with pytest.raises(s.adapter.Refused):execute(envelope) + +@pytest.mark.parametrize('mode',['derived','ciphertext-derived-v2',None,True,{}]) +def test_unknown_binding_mode_refuses_before_effects(envelope,mode): + envelope[3]['manifest_binding_mode']=mode + with patch.object(s.local,'preflight') as preflight,pytest.raises(s.adapter.Refused):execute(envelope) + assert not preflight.called + +def test_conflicting_binding_and_unverified_transfer_refuse(envelope): + admission=envelope[3];transfer=envelope[2];admission['manifest_sha256']='0'*64 + with pytest.raises(s.adapter.Refused):s.adapter.manifest_binding_mode(admission,transfer) + admission.pop('manifest_sha256');transfer['independent_owner_retrieval_verified']=False + with pytest.raises(s.adapter.Refused):s.adapter.manifest_binding_mode(admission,transfer) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sealed_custody.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sealed_custody.py new file mode 100644 index 00000000..e69d5425 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sealed_custody.py @@ -0,0 +1,143 @@ +import fcntl +import importlib.util +import json +import os +from pathlib import Path +import signal +import pty +import select +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).parents[1] +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m +h=load('fdhelper',ROOT/'sso-mfa/bootstrap/sops-custody-fd.py') +c=load('custody',ROOT/'sso-mfa/bootstrap/sops-private.py') + +class SealedCustodyTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Capture generated test key in memory only, never into test output/path. + r=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True) + cls.material=next(x for x in r.stdout.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n' + fd=h.sealed_key(cls.material) + try:cls.recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip() + finally:os.close(fd) + def test_sealed_descriptor_private_duplicate_is_cloexec_and_decrypts(self): + encrypted=subprocess.run(['age','-r',self.recipient],input=b'synthetic recovery material',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout + original=h.sealed_key(self.material) + try: + with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(original)},clear=True):fd=c.private_key_fd() + try: + self.assertFalse(os.get_inheritable(fd));self.assertEqual(os.fstat(fd).st_nlink,0) + r=subprocess.run(['age','-d','-i',f'/proc/self/fd/{fd}'],pass_fds=(fd,),input=encrypted,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL) + self.assertEqual(r.returncode,0);self.assertEqual(r.stdout,b'synthetic recovery material') + finally:os.close(fd) + finally:os.close(original) + def test_incomplete_seals_or_named_file_refused(self): + for named in (False,True): + if named: + file=tempfile.TemporaryFile();fd=file.fileno();file.write(self.material);file.flush() + else:fd=os.memfd_create('synthetic-unsealed',os.MFD_CLOEXEC|os.MFD_ALLOW_SEALING);os.write(fd,self.material) + try: + with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(fd)},clear=True),self.assertRaises((c.Refused,OSError)): + c.private_key_fd() + finally: + if named:file.close() + else:os.close(fd) + def test_ambiguous_sources_refused(self): + with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':'3','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/opened'},clear=True),self.assertRaises(c.Refused):c.private_key_fd() + def test_malformed_descriptor_text_refused_before_dup(self): + for value in ('-1','2','03',' 3','3 ','1000000000','3/4'): + with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':value},clear=True),patch.object(c.os,'dup') as duplicate: + with self.subTest(value=value),self.assertRaises(c.Refused):c.private_key_fd() + duplicate.assert_not_called() + def test_bad_expected_recipient_refused_before_prompt(self): + with patch('sys.argv',['sealed','--expected-recipient','age1bad','--','true']),patch.object(h.os,'isatty',return_value=True),patch.object(h.getpass,'getpass') as prompt,patch('builtins.print'): + self.assertEqual(h.main(),1);prompt.assert_not_called() + def test_existing_file_mode_still_accepts_private_owner_file(self): + with tempfile.TemporaryDirectory() as temp: + path=Path(temp)/'synthetic-key';path.write_bytes(self.material);path.chmod(0o600) + with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(path)},clear=True):fd=c.private_key_fd() + try:self.assertEqual(os.read(fd,16384),self.material) + finally:os.close(fd) + def test_helper_passes_exact_fd_and_clears_ambient_sources(self): + code="""import os,sys +fd=int(os.environ['NETKINGDOM_CUSTODY_AGE_KEY_FD']) +assert os.fstat(fd).st_nlink==0 +assert all(k not in os.environ for k in ('NETKINGDOM_CUSTODY_AGE_KEY_FILE','SOPS_AGE_KEY_FILE','SOPS_AGE_KEY','SOPS_AGE_KEY_CMD')) +""" + with patch.dict(os.environ,{'SOPS_AGE_KEY':'synthetic-competing','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/read'}): + self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',code]),0) + def test_normal_and_error_return_close_parent_fd(self): + actual=h.sealed_key + for status in (0,7): + fds=[] + def capture(material): + fd=actual(material);fds.append(fd);return fd + with patch.object(h,'sealed_key',side_effect=capture):self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',f'raise SystemExit({status})']),status) + with self.assertRaises(OSError):os.fstat(fds[0]) + def test_wrong_recipient_does_not_start_child_and_closes_fd(self): + other=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout + other=next(x for x in other.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n';fd=h.sealed_key(other) + try:recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip() + finally:os.close(fd) + with tempfile.TemporaryDirectory() as temp: + marker=Path(temp)/'child' + with self.assertRaises(h.Refused):h.run(self.material,recipient,[sys.executable,'-c','import pathlib,sys;pathlib.Path(sys.argv[1]).touch()',str(marker)]) + self.assertFalse(marker.exists()) + def test_actual_attended_shell_sealed_mode_hides_key_and_runs_original_fd_consumer(self): + code="import importlib.util,os,sys;spec=importlib.util.spec_from_file_location('c',sys.argv[1]);c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c);fd=c.private_key_fd();assert os.fstat(fd).st_nlink==0;os.close(fd)" + master,slave=pty.openpty() + child=subprocess.Popen(['bash','-x',str(ROOT/'sso-mfa/bootstrap/sops-custody-unlock.sh'),'--expected-recipient',self.recipient,'--sealed-fd','--prompt','--',sys.executable,'-c',code,str(ROOT/'sso-mfa/bootstrap/sops-private.py')],stdin=slave,stdout=slave,stderr=slave) + os.close(slave);captured=b'';sent=False;end=time.monotonic()+5 + try: + while time.monotonic()&2\nexit 23\n') + tool.chmod(0o755) + return {**case[2], 'PATH': str(bin_dir) + ':' + case[2]['PATH']} + + +def test_child_output_and_errors_are_not_exposed_on_failure(case, tmp_path): + result = make(case, env=noisy_tool(case, tmp_path)) + assert result.returncode != 0 + assert 'synthetic-tool-secret' not in result.stdout + result.stderr + assert 'synthetic-error-secret' not in result.stdout + result.stderr + assert not case[4].exists() + assert not list(case[4].parent.glob('.netkingdom-sops-*')) + + +def test_non_age_metadata_refuses_before_tool(case, tmp_path): + data = json.loads(case[3].read_text()); data['sops']['kms'] = [{'arn': 'synthetic'}] + case[3].write_text(json.dumps(data)) + result = make(case, env=noisy_tool(case, tmp_path)) + assert result.returncode != 0 + assert 'age-only metadata' in result.stderr + assert 'synthetic-tool-secret' not in result.stdout + + +def test_filename_is_not_shell_code(case): + ambient_key(case) + odd = case[0] / 'cipher; touch SHELL_EXECUTED; #.json' + shutil.copyfile(case[3], odd) + result = make(case, source=odd) + assert result.returncode == 0 + assert not (case[0] / 'SHELL_EXECUTED').exists() + assert case[4].exists() + + +def test_yaml_roundtrip(case, tmp_path): + public = (case[0] / 'keys/age.pub').read_text().strip() + clear = tmp_path / 'input.yaml'; clear.write_text('credential: synthetic-yaml-secret\n') + cipher = case[0] / 'cipher.yaml' + result = subprocess.run([SOPS, '--encrypt', '--age', public, str(clear)], cwd=tmp_path, + env=case[2], capture_output=True, check=True) + cipher.write_bytes(result.stdout) + result = make(case, source=cipher) + assert result.returncode == 0 + assert 'synthetic-yaml-secret' not in result.stdout + result.stderr + assert 'synthetic-yaml-secret' in case[4].read_text() + + +def test_parser_error_never_echoes_input(case): + case[3].write_text('synthetic-malformed-secret') + result = make(case) + assert result.returncode != 0 + assert 'synthetic-malformed-secret' not in result.stdout + result.stderr + assert not case[4].exists() + + +def test_shared_custody_key_is_refused(case): + case[1].chmod(0o644) + result = make(case) + assert result.returncode != 0 + assert not case[4].exists() + + +def test_creds_init_has_no_permanent_key_requirement(case): + subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True) + hooks = case[0] / '.githooks'; hooks.mkdir() + (hooks / 'pre-commit').write_text('#!/bin/sh\nexit 0\n') + result = make(case, 'creds-init') + assert result.returncode == 0 + assert not (Path(case[2]['HOME']) / '.config/sops').exists() + + +def test_real_sops_does_not_execute_ambient_key_commands(case, tmp_path): + marker = tmp_path / 'ambient-command-executed' + command = tmp_path / 'ambient-key-command' + command.write_text('#!/bin/sh\ntouch ' + str(marker) + '\nexit 1\n') + command.chmod(0o755) + env = {**case[2], 'SOPS_AGE_KEY_CMD': str(command), 'SOPS_AGE_SSH_PRIVATE_KEY_CMD': str(command)} + result = make(case, env=env) + assert result.returncode == 0 + assert not marker.exists() + + +def test_yaml_parser_error_is_sanitized(case): + bad = case[0] / 'broken.yaml' + bad.write_text('secret: [synthetic-malformed-yaml-secret\n') + result = make(case, source=bad) + assert result.returncode != 0 + assert 'synthetic-malformed-yaml-secret' not in result.stdout + result.stderr + assert 'Traceback' not in result.stderr + + +@pytest.mark.parametrize('kind', ['json', 'yaml']) +def test_duplicate_metadata_refused_before_sops(case, kind): + source = case[0] / ('duplicate.' + kind) + source.write_text('{"sops": {}, "sops": {}}' if kind == 'json' else 'sops: {}\nsops: {}\n') + result = make(case, source=source) + assert result.returncode != 0 + assert 'duplicate' in result.stderr + + +@pytest.mark.parametrize('kind', ['bare', 'linked']) +def test_bare_and_linked_git_outputs_are_refused(case, kind): + location = case[4].parent / 'git-output' + if kind == 'bare': + subprocess.run(['git', 'init', '--bare', str(location)], env=case[2], capture_output=True, check=True) + else: + subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True) + subprocess.run(['git', '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', + 'commit', '--allow-empty', '-m', 'fixture'], cwd=case[0], env=case[2], + capture_output=True, check=True) + subprocess.run(['git', 'worktree', 'add', '--detach', str(location)], cwd=case[0], env=case[2], + capture_output=True, check=True) + location.chmod(0o700) + result = make(case, output=location / 'plaintext.json') + assert result.returncode != 0 + assert not (location / 'plaintext.json').exists() diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sops_write.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sops_write.py new file mode 100644 index 00000000..b548fbdb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tests/test_sops_write.py @@ -0,0 +1,244 @@ +"""Real SOPS write-path regressions with disposable keys and plaintext only.""" +import json +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +ROOT=Path(__file__).resolve().parents[1] +SOPS=shutil.which('sops') +pytestmark=pytest.mark.skipif(SOPS is None,reason='real SOPS required') +PLAIN='synthetic-sops-write-value' + + +@pytest.fixture +def case(tmp_path): + repo=tmp_path/'repo';boot=repo/'sso-mfa/bootstrap';boot.mkdir(parents=True) + shutil.copyfile(ROOT/'Makefile',repo/'Makefile') + for name in ['sops-private.py','sops-write.py']: + if (ROOT/'sso-mfa/bootstrap'/name).exists():shutil.copyfile(ROOT/'sso-mfa/bootstrap'/name,boot/name) + (repo/'keys').mkdir() + key=tmp_path/'disposable.key';subprocess.run(['age-keygen','-o',str(key)],capture_output=True,check=True) + public=subprocess.check_output(['age-keygen','-y',str(key)],text=True).strip() + (repo/'keys/age.pub').write_text(public+'\n') + (repo/'.sops.yaml').write_text('creation_rules:\n - age: '+public+'\n') + home=tmp_path/'home';home.mkdir(mode=0o700) + private=tmp_path/'private';private.mkdir(mode=0o700) + replacement=private/'replacement.json';replacement.write_text(json.dumps({'credential':PLAIN+'-edited'}));replacement.chmod(0o600) + cipher=repo/'cipher.json' + env={'HOME':str(home),'PATH':os.environ['PATH'],'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(key),'SOPS_AGE_KEY_FILE':str(key)} + encrypted=subprocess.run([SOPS,'--encrypt','--age',public,'--input-type','json','--output-type','json','/dev/stdin'],input=json.dumps({'credential':PLAIN}).encode(),cwd=tmp_path,env=env,capture_output=True,check=True) + cipher.write_bytes(encrypted.stdout) + return {'repo':repo,'key':key,'public':public,'env':env,'cipher':cipher,'input':replacement,'tmp':tmp_path} + + +def invoke(case,action,*,file=None,input=None,env=None): + return subprocess.run(['make','-s','sops-'+action,'FILE='+str(file or case['cipher']),'INPUT='+str(input or case['input'])],cwd=case['repo'],env=env or case['env'],capture_output=True,text=True,timeout=15) + + +def decode(case,file=None): + result=subprocess.run([SOPS,'--decrypt',str(file or case['cipher'])],env=case['env'],capture_output=True,check=True) + return json.loads(result.stdout) + + +def quiet(result): + assert PLAIN not in result.stdout+result.stderr + assert 'Traceback' not in result.stdout+result.stderr + + +def plaintext(case,name='plain.json'): + path=case['repo']/name;path.write_text(json.dumps({'credential':PLAIN}));path.chmod(0o600);return path + + +def test_public_only_encrypt_roundtrip(case): + path=plaintext(case);env=dict(case['env']);env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE');env.pop('SOPS_AGE_KEY_FILE') + result=invoke(case,'encrypt',file=path,env=env);assert result.returncode==0,result.stderr;quiet(result) + assert decode(case,path)=={'credential':PLAIN} + assert json.loads(path.read_text())['sops']['age'][0]['recipient']==case['public'] + assert path.stat().st_mode & 0o077==0 + + +def test_rotate_changes_ciphertext_not_plaintext_or_recipient(case): + before=case['cipher'].read_bytes();result=invoke(case,'rotate');assert result.returncode==0,result.stderr;quiet(result) + assert case['cipher'].read_bytes()!=before and decode(case)=={'credential':PLAIN} + assert [r['recipient'] for r in json.loads(case['cipher'].read_text())['sops']['age']]==[case['public']] + + +def test_edit_uses_private_replacement_and_never_launches_ambient_editor(case): + marker=case['tmp']/'editor-called';editor=case['tmp']/'editor' + editor.write_text('#!/bin/sh\ntouch '+str(marker)+'\nprintf '+PLAIN+'\\n >&2\n');editor.chmod(0o700) + result=invoke(case,'edit',env={**case['env'],'SOPS_EDITOR':str(editor),'EDITOR':str(editor)}) + assert result.returncode==0,result.stderr;quiet(result) + assert not marker.exists() and decode(case)=={'credential':PLAIN+'-edited'} + assert json.loads(case['input'].read_text())=={'credential':PLAIN+'-edited'} + + +@pytest.mark.parametrize('name',['cipher; touch SHELL_EXECUTED; #.json','$(shell touch MAKE_EXECUTED).json','with spaces.json']) +@pytest.mark.parametrize('action',['encrypt','rotate','edit']) +def test_file_names_are_data_not_shell_or_make_programs(case,name,action): + file=case['repo']/name + if action=='encrypt':file=plaintext(case,name) + else:file.write_bytes(case['cipher'].read_bytes()) + result=invoke(case,action,file=file);assert result.returncode==0,result.stderr;quiet(result) + assert not (case['repo']/'SHELL_EXECUTED').exists() and not (case['repo']/'MAKE_EXECUTED').exists() + assert decode(case,file)=={'credential':PLAIN+('-edited' if action=='edit' else '')} + + +@pytest.mark.parametrize('action',['rotate','edit']) +def test_explicit_key_required_despite_ambient_identity(case,action): + before=case['cipher'].read_bytes();env=dict(case['env']);env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE') + result=invoke(case,action,env=env);assert result.returncode!=0;quiet(result) + assert case['cipher'].read_bytes()==before + + +@pytest.mark.parametrize('action',['rotate','edit']) +def test_unauthorized_recipient_metadata_is_not_a_rotation_grant(case,action): + other=case['tmp']/'other.key';subprocess.run(['age-keygen','-o',str(other)],capture_output=True,check=True) + public=subprocess.check_output(['age-keygen','-y',str(other)],text=True).strip() + data=json.loads(case['cipher'].read_text());data['sops']['age'].append({**data['sops']['age'][0],'recipient':public}) + case['cipher'].write_text(json.dumps(data));before=case['cipher'].read_bytes() + result=invoke(case,action);assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before + + +@pytest.mark.parametrize('defect',['public','symlink','hardlink','git','missing']) +def test_edit_refuses_unsafe_input_without_changing_cipher(case,defect): + file=case['input'];before=case['cipher'].read_bytes() + if defect=='public':file.chmod(0o644) + elif defect=='missing':file.unlink() + elif defect=='symlink': + target=file.with_name('other');file.rename(target);file.symlink_to(target) + elif defect=='hardlink':os.link(file,file.with_name('alias')) + else:subprocess.run(['git','init',str(file.parent)],capture_output=True,check=True) + result=invoke(case,'edit');assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before + + +@pytest.mark.parametrize('defect',['symlink','hardlink','public-write']) +def test_cipher_path_cannot_redirect_or_alias_replacement(case,defect): + file=case['cipher'];before=file.read_bytes() + if defect=='symlink': + target=file.with_name('other.json');file.rename(target);file.symlink_to(target) + elif defect=='hardlink':os.link(file,file.with_name('alias.json')) + else:file.chmod(0o666) + result=invoke(case,'rotate');assert result.returncode!=0;quiet(result);assert file.read_bytes()==before + + +def test_tracked_plaintext_refused_before_encryption(case): + file=plaintext(case) + subprocess.run(['git','init',str(case['repo'])],capture_output=True,check=True) + subprocess.run(['git','add',file.name],cwd=case['repo'],capture_output=True,check=True) + before=file.read_bytes();result=invoke(case,'encrypt',file=file) + assert result.returncode!=0;quiet(result);assert file.read_bytes()==before + + +@pytest.mark.parametrize('action', ['edit', 'rotate']) +def test_tampered_cipher_cannot_be_replaced(case, action): + data=json.loads(case['cipher'].read_text());data['credential']='tampered';case['cipher'].write_text(json.dumps(data));before=case['cipher'].read_bytes() + result=invoke(case,action);assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before + + +def test_encryption_does_not_run_ambient_key_command(case): + path=plaintext(case);marker=case['tmp']/'key-command';command=case['tmp']/'command' + command.write_text('#!/bin/sh\ntouch '+str(marker)+'\n');command.chmod(0o700) + env={**case['env'],'SOPS_AGE_KEY_CMD':str(command),'SOPS_KMS_ARN':'unadmitted','SOPS_CONFIG':str(case['repo']/'.sops.yaml')} + result=invoke(case,'encrypt',file=path,env=env);assert result.returncode==0,result.stderr;quiet(result);assert not marker.exists() + + +def test_malformed_plaintext_is_not_echoed_or_replaced(case): + path=plaintext(case);path.write_text('invalid '+PLAIN);before=path.read_bytes() + result=invoke(case,'encrypt',file=path);assert result.returncode!=0;quiet(result);assert path.read_bytes()==before + + +def test_make_input_is_literal(case): + replacement=case['input'].with_name('$(shell touch MAKE_INPUT_EXECUTED).json');case['input'].rename(replacement) + result=invoke(case,'edit',input=replacement);assert result.returncode==0,result.stderr + assert not (case['repo']/'MAKE_INPUT_EXECUTED').exists() + assert decode(case)=={'credential':PLAIN+'-edited'} + + +@pytest.mark.parametrize('defect',['hardlink','fifo','parent-symlink']) +def test_custody_key_alias_or_nonregular_input_is_refused(case,defect): + key=case['key'];env=dict(case['env']);before=case['cipher'].read_bytes() + if defect=='hardlink':os.link(key,key.with_name('alias.key')) + elif defect=='fifo': + key.unlink();os.mkfifo(key,0o600) + else: + parent=case['tmp']/'key-directory';parent.mkdir(mode=0o700);key.rename(parent/'key');alias=case['tmp']/'alias';alias.symlink_to(parent);env['NETKINGDOM_CUSTODY_AGE_KEY_FILE']=str(alias/'key') + result=invoke(case,'rotate',env=env);assert result.returncode!=0;quiet(result) + assert case['cipher'].read_bytes()==before + + +def test_edit_preserves_declared_encryption_policy(case): + raw=json.dumps({'credential':PLAIN,'visible_unencrypted':'public-label'}).encode() + encrypted=subprocess.run([SOPS,'--encrypt','--age',case['public'],'--input-type','json','--output-type','json','/dev/stdin'],input=raw,cwd=case['tmp'],env=case['env'],capture_output=True,check=True) + case['cipher'].write_bytes(encrypted.stdout) + case['input'].write_text(json.dumps({'credential':PLAIN+'-edited','visible_unencrypted':'public-label'})) + result=invoke(case,'edit');assert result.returncode==0,result.stderr + obj=json.loads(case['cipher'].read_text()) + assert obj['visible_unencrypted']=='public-label' and obj['credential'].startswith('ENC[') + assert decode(case)=={'credential':PLAIN+'-edited','visible_unencrypted':'public-label'} + + +def test_child_diagnostics_and_plaintext_output_are_suppressed(case): + directory=case['tmp']/'bin';directory.mkdir();binary=directory/'sops' + binary.write_text('#!/bin/sh\nprintf "'+PLAIN+'\\n"\nprintf "'+PLAIN+'\\n" >&2\nexit 1\n');binary.chmod(0o700) + file=plaintext(case);before=file.read_bytes() + result=invoke(case,'encrypt',file=file,env={**case['env'],'PATH':str(directory)+':'+case['env']['PATH']}) + assert result.returncode!=0;quiet(result);assert file.read_bytes()==before + assert not list(file.parent.glob('.netkingdom-sops-write-*')) + + +def test_source_change_during_encryption_is_not_overwritten(case): + file=plaintext(case);directory=case['tmp']/'bin';directory.mkdir();binary=directory/'sops' + binary.write_text('#!/usr/bin/python3\nimport subprocess,sys\nfrom pathlib import Path\nr=subprocess.run('+repr([SOPS])+ '+sys.argv[1:],input=sys.stdin.buffer.read(),capture_output=True)\nPath('+repr(str(file))+').write_text("concurrent-update")\nsys.stdout.buffer.write(r.stdout)\nsys.exit(r.returncode)\n');binary.chmod(0o700) + result=invoke(case,'encrypt',file=file,env={**case['env'],'PATH':str(directory)+':'+case['env']['PATH']}) + assert result.returncode!=0;quiet(result);assert file.read_text()=='concurrent-update' + assert not list(file.parent.glob('.netkingdom-sops-write-*')) + + +def test_make_custody_command_cannot_execute_before_handoff(case): + helper=case['repo']/'sso-mfa/bootstrap/sops-custody-unlock.sh' + helper.write_text('#!/bin/bash\npython3 - "$@" <<\'PYTHON\'\nimport json,sys\nfrom pathlib import Path\nPath("handoff.json").write_text(json.dumps(sys.argv[1:]))\nPYTHON\n') + command="printf 'safe'; touch OUTSIDE_CUSTODY; #" + result=subprocess.run(['make','-s','sops-custody-run','COMMAND='+command],cwd=case['repo'],env=case['env'],capture_output=True,text=True,timeout=10) + assert result.returncode==0,result.stderr + assert not (case['repo']/'OUTSIDE_CUSTODY').exists() + assert json.loads((case['repo']/'handoff.json').read_text())[-3:]==['bash','-lc',command] + + +def test_yaml_encrypt_rotate_edit_roundtrip(case): + import yaml + file = case['repo'] / 'plain.yaml' + file.write_text('credential: ' + PLAIN + '\n'); file.chmod(0o600) + replacement = case['input'].with_suffix('.yaml') + replacement.write_text('credential: ' + PLAIN + '-edited\n'); replacement.chmod(0o600) + for action in ('encrypt', 'rotate', 'edit'): + result = invoke(case, action, file=file, input=replacement) + assert result.returncode == 0, result.stderr + quiet(result) + decrypted = subprocess.run([SOPS, '--decrypt', str(file)], env=case['env'], capture_output=True, check=True) + assert yaml.safe_load(decrypted.stdout) == {'credential': PLAIN + ('-edited' if action == 'edit' else '')} + + +def test_make_decrypt_output_is_literal(case): + output = case['input'].with_name('$(shell touch MAKE_OUTPUT_EXECUTED).json') + result = subprocess.run(['make', '-s', 'sops-decrypt', 'FILE=' + str(case['cipher']), 'OUTPUT=' + str(output)], + cwd=case['repo'], env=case['env'], capture_output=True, text=True, timeout=15) + assert result.returncode == 0, result.stderr + quiet(result) + assert not (case['repo'] / 'MAKE_OUTPUT_EXECUTED').exists() + assert json.loads(output.read_text()) == {'credential': PLAIN} + + +def test_make_custody_values_are_not_evaluated(case): + helper = case['repo'] / 'sso-mfa/bootstrap/sops-custody-unlock.sh' + helper.write_text('#!/bin/bash\nexec python3 -c \'import json,sys; from pathlib import Path; Path("handoff.json").write_text(json.dumps(sys.argv[1:]))\' "$@"\n') + command = '$(shell touch MAKE_COMMAND_EXECUTED)' + recipient = '$(shell touch MAKE_RECIPIENT_EXECUTED)' + result = subprocess.run(['make', '-s', 'sops-custody-run', 'COMMAND=' + command, 'OPERATOR_AGE_PUBKEY=' + recipient], + cwd=case['repo'], env=case['env'], capture_output=True, text=True, timeout=15) + assert result.returncode == 0, result.stderr + assert not (case['repo'] / 'MAKE_COMMAND_EXECUTED').exists() + assert not (case['repo'] / 'MAKE_RECIPIENT_EXECUTED').exists() + assert json.loads((case['repo'] / 'handoff.json').read_text()) == ['--expected-recipient', recipient, '--', 'bash', '-lc', command] diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/capture-identity.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/capture-identity.py new file mode 100644 index 00000000..50f61535 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/capture-identity.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +"""Owner-run identity capture: metadata plan first, exact bounded execution later. + +Use the admitted Railiance caller. Flags bind reviewed source/targets; they do +not grant authority. Private child output and provider diagnostics never print. +""" +import argparse +from datetime import datetime,timezone +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import signal +import subprocess +import tempfile +import uuid + +HERE=Path(__file__).resolve().parent + + +def load(name): + spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py')) + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module + + +bundle=load('identity-bundle');prepare=load('prepare-identity-coverage') +IMAGE='ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473' +SOURCES=['capture-identity.py','identity-files.py','identity-bundle.py','prepare-identity-coverage.py','identity-backup-profile.json'] +KUBE=['kubectl'] + + +class Refused(Exception):pass + + +def command(args,payload=None,timeout=60): + result=subprocess.run(KUBE+['--request-timeout=30s']+args,input=payload, + stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=timeout) + if result.returncode:raise Refused() + return result.stdout + + +def get(ns,kind,name): + scope=['-n',ns] if ns else [] + return json.loads(command(scope+['get',kind,name,'-o','json'])) + + +def selects(selector,labels): + if any(labels.get(k)!=v for k,v in selector.get('matchLabels',{}).items()):return False + for expression in selector.get('matchExpressions',[]): + key=expression['key'];op=expression['operator'];values=expression.get('values',[]) + if op=='In' and labels.get(key) not in values:return False + if op=='NotIn' and key in labels and labels[key] in values:return False + if op=='Exists' and key not in labels:return False + if op=='DoesNotExist' and key in labels:return False + if op not in {'In','NotIn','Exists','DoesNotExist'}:raise Refused() + return True + + +def network_review(run_id,actual_labels=None): + rows=[] + for ns in ('sso','mfa'): + policies=json.loads(command(['-n',ns,'get','networkpolicies','-o','json']))['items'] + for obj in policies: + m=obj['metadata'];s=obj['spec'] + labels=(actual_labels or {}).get(ns,{'net-kingdom/capture':run_id}) + if selects(s['podSelector'],labels) and (s.get('ingress') or s.get('egress')): + rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'], + 'spec_sha256':hashlib.sha256(json.dumps(s,sort_keys=True).encode()).hexdigest()}) + return rows + + +def validate_helper(actual,wanted): + a=actual['spec'];w=wanted['spec'] + if (any(a.get(k) for k in ('hostNetwork','hostPID','hostIPC','initContainers','ephemeralContainers')) + or a.get('automountServiceAccountToken') is not False or a.get('activeDeadlineSeconds')!=900 + or len(a['containers'])!=1 or a['volumes']!=w['volumes']):raise Refused() + c=a['containers'][0];expected=w['containers'][0] + if any(c.get(k)!=expected[k] for k in ('name','image','command','volumeMounts')):raise Refused() + if c.get('env') or c.get('envFrom') or c.get('securityContext',{}).get('privileged'):raise Refused() + if any(c.get('securityContext',{}).get(k)!=v for k,v in expected['securityContext'].items()):raise Refused() + if any(a.get('securityContext',{}).get(k)!=v for k,v in w['securityContext'].items()):raise Refused() + + +def manifests(run_id): + result=[] + for ns,claims in [('sso',[('lldap','lldap-data'),('authelia','authelia-data'),('keycape','keycape-authentication-policy')]), + ('mfa',[('privacyidea','privacyidea-data')])]: + label={'net-kingdom/capture':run_id};name='nk49-capture-'+run_id + result.append({'apiVersion':'networking.k8s.io/v1','kind':'NetworkPolicy', + 'metadata':{'name':name,'namespace':ns}, + 'spec':{'podSelector':{'matchLabels':label},'policyTypes':['Ingress','Egress'],'ingress':[],'egress':[]}}) + result.append({'apiVersion':'v1','kind':'Pod','metadata':{'name':name,'namespace':ns,'labels':label}, + 'spec':{'restartPolicy':'Never','activeDeadlineSeconds':900,'automountServiceAccountToken':False, + 'securityContext':{'runAsUser':0,'runAsGroup':0}, + 'containers':[{'name':'capture','image':IMAGE,'imagePullPolicy':'IfNotPresent', + 'command':['python3','-c','import time; time.sleep(900)'], + 'securityContext':{'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True, + 'capabilities':{'drop':['ALL'],'add':['DAC_OVERRIDE']}}, + 'resources':{'requests':{'cpu':'50m','memory':'128Mi'},'limits':{'cpu':'1','memory':'2Gi'}}, + 'volumeMounts':[{'name':key,'mountPath':'/source/'+key,'readOnly':True} for key,_ in claims]+ + [{'name':'scratch','mountPath':'/scratch'}]}], + 'volumes':[{'name':key,'persistentVolumeClaim':{'claimName':claim,'readOnly':True}} for key,claim in claims]+ + [{'name':'scratch','emptyDir':{'medium':'Memory','sizeLimit':'512Mi'}}]}}) + return result + + +def plan(): + p=bundle.profile();binding=prepare.prepare(p,get=get) + cluster=get('databases','cluster','net-kingdom-pg') + primary=cluster['status']['currentPrimary'];pod=get('databases','pod',primary) + if (not any(o.get('uid')==cluster['metadata']['uid'] for o in pod['metadata'].get('ownerReferences',[])) + or not any(c['type']=='Ready' and c['status']=='True' for c in pod['status'].get('conditions',[]))):raise Refused() + run_id=uuid.uuid4().hex[:12] + return {'schema':'netkingdom.identity-capture-plan.v1','status':'requires_owner_execution_admission', + 'created_at':datetime.now(timezone.utc).isoformat(),'run_id':run_id,'binding':binding,'kubeconfig':KUBE[2], + 'sources':[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES], + 'database':{'namespace':'databases','cluster':'net-kingdom-pg','cluster_uid':cluster['metadata']['uid'], + 'pod':primary,'pod_uid':pod['metadata']['uid'],'database':'privacyidea_db'}, + 'create_resources':manifests(run_id),'service_changes':False,'credential_grants':False, + 'matching_network_allow_policies':network_review(run_id), + 'network_policy_enforcement_requires_owner_confirmation':True, + 'offsite_delivery_included':False,'global_atomic_snapshot_claimed':False} + + +def checked_plan(path,expected): + raw=path.read_bytes() + if hashlib.sha256(raw).hexdigest()!=expected:raise Refused() + p=json.loads(raw) + if p.get('schema')!='netkingdom.identity-capture-plan.v1':raise Refused() + if p.get('kubeconfig')!=KUBE[2]:raise Refused() + if network_review(p['run_id']):raise Refused() + if p['sources']!=[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES]:raise Refused() + if p['create_resources']!=manifests(p['run_id']):raise Refused() + now=datetime.now(timezone.utc) + if not 0<=(now-datetime.fromisoformat(p['created_at'])).total_seconds()<3600:raise Refused() + # Recheck the exact immutable source/target bindings before private reads. + current=prepare.prepare(bundle.profile(),get=get) + if current['bindings']!=p['binding']['bindings'] or current['profile_sha256']!=p['binding']['profile_sha256']:raise Refused() + return p + + +def delete_created(obj): + m=obj['metadata'];ns=m['namespace'];name=m['name'];uid=m['uid'] + if obj['kind']=='Pod':path=f'/api/v1/namespaces/{ns}/pods/{name}' + elif obj['kind']=='NetworkPolicy':path=f'/apis/networking.k8s.io/v1/namespaces/{ns}/networkpolicies/{name}' + else:raise Refused() + payload=json.dumps({'apiVersion':'v1','kind':'DeleteOptions','preconditions':{'uid':uid},'gracePeriodSeconds':1}).encode() + command(['delete','--raw',path,'-f','-'],payload) + if obj['kind']=='Pod':command(['-n',ns,'wait','--for=delete','pod/'+name,'--timeout=90s'],timeout=100) + + +def bounded_file(args,path,limit): + # Stream binary dump to private staging, with a byte bound and suppressed + # diagnostics. API command argv contains only public resource/database names. + proc=subprocess.Popen(KUBE+['--request-timeout=600s']+args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL) + try: + count=0 + with path.open('xb') as out: + while block:=proc.stdout.read(1024*1024): + count+=len(block) + if count>limit:raise Refused() + out.write(block) + if proc.wait(timeout=600):raise Refused() + finally: + if proc.poll() is None:proc.kill();proc.wait() + + +def capture(p,output,receipt,decision,checkpoint=lambda:None): + created=[];receipt.update(status='capture_started',decision_ref=decision,plan_run_id=p['run_id'], + created_resources=[],unverified_create_outcomes=[],helper_cleanup_verified=False,temporary_plaintext_cleanup_verified=False) + checkpoint() + with tempfile.TemporaryDirectory(prefix='nk49-identity-',dir='/tmp') as temporary: + receipt['temporary_plaintext_directory']=temporary + checkpoint() + root=Path(temporary)/'payload';root.mkdir(mode=0o700) + held={} + try: + for c in bundle.profile()['components']: + for kind,names in [('deployment',[c['deployment']]),('secret',c['secret_refs']),('configmap',c['configmap_refs'])]: + for name in names: + obj=get(c['namespace'],kind,name) + # Keep reconstructable deployed resources inside encryption, + # without unrelated stale last-applied annotations. + m=obj['metadata'];obj['metadata']={k:m[k] for k in ['name','namespace','uid','resourceVersion']} + obj.pop('status',None) + raw=json.dumps(obj,sort_keys=True).encode();slot=f"resources/{c['namespace']}/{kind}/{name}.json" + held[(c['namespace'],kind,name)]=(m['uid'],m['resourceVersion']) + bundle.save(root,slot,raw) + for proposed in p['create_resources']: + # create, never adopt/apply an existing resource. Unknown create + # outcomes stop; only returned exact UIDs are eligible for cleanup. + pending={'kind':proposed['kind'],**proposed['metadata']} + receipt['unverified_create_outcomes'].append(pending) + checkpoint() + obj=json.loads(command(['create','-f','-','-o','json'],json.dumps(proposed).encode())) + if (obj['kind']!=proposed['kind'] or obj['metadata']['name']!=proposed['metadata']['name'] + or obj['metadata']['namespace']!=proposed['metadata']['namespace'] or not obj['metadata'].get('uid')):raise Refused() + created.append(obj) + receipt['unverified_create_outcomes'].remove(pending) + receipt['created_resources'].append({'namespace':obj['metadata']['namespace'], + 'kind':obj['kind'],'name':obj['metadata']['name'],'uid':obj['metadata']['uid']}) + checkpoint() + for ns in ('sso','mfa'): + name='nk49-capture-'+p['run_id'] + command(['-n',ns,'wait','--for=condition=Ready','pod/'+name,'--timeout=180s'],timeout=190) + live=get(ns,'pod',name) + expected=next(o for o in created if o['kind']=='Pod' and o['metadata']['namespace']==ns) + if live['metadata']['uid']!=expected['metadata']['uid']:raise Refused() + wanted=next(o for o in p['create_resources'] if o['kind']=='Pod' and o['metadata']['namespace']==ns) + validate_helper(live,wanted) + if network_review(p['run_id'],{ns:live['metadata']['labels']}):raise Refused() + raw=command(['-n',ns,'exec','-i',name,'-c','capture','--','python3','-c', + (HERE/'identity-files.py').read_text(),ns],timeout=600) + if len(raw)>bundle.LIMIT+2*1024*1024:raise Refused() + bundle.import_files(raw,root,ns) + if get(ns,'pod',name)['metadata']['uid']!=expected['metadata']['uid']:raise Refused() + db=p['database'];current=get('databases','cluster','net-kingdom-pg') + if current['metadata']['uid']!=db['cluster_uid'] or current['status']['currentPrimary']!=db['pod']:raise Refused() + if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused() + (root/'databases').mkdir(mode=0o700) + bounded_file(['-n','databases','exec',db['pod'],'-c','postgres','--','pg_dump','-U','postgres', + '-d','privacyidea_db','-Fc','--no-owner','--no-acl'],root/'databases/privacyidea.dump',bundle.FILE_LIMIT) + if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused() + for (ns,kind,name),binding in held.items(): + m=get(ns,kind,name)['metadata'] + if (m['uid'],m['resourceVersion'])!=binding:raise Refused() + # Re-read original keys after the database dump. No rotation is + # inferred from file presence; a changed key set refuses publication. + raw=command(['-n','mfa','exec','-i','nk49-capture-'+p['run_id'],'-c','capture','--','python3','-c', + (HERE/'identity-files.py').read_text(),'mfa'],timeout=60) + with tempfile.TemporaryDirectory(prefix='nk49-key-recheck-',dir='/tmp') as keytemp: + again=Path(keytemp);bundle.import_files(raw,again,'mfa') + for name in ('enckey','private.pem','public.pem'): + if (root/'data/privacyidea'/name).read_bytes()!=(again/'data/privacyidea'/name).read_bytes():raise Refused() + receipt.update(bundle.seal(root,output,{'started_at':receipt['started_at'], + 'finished_at':datetime.now(timezone.utc).isoformat(),'database':db,'decision_ref':decision, + 'cluster_uid':prepare.CLUSTER_UID,'global_atomic_snapshot_claimed':False, + 'omissions':['historical local backups','SQLite WAL/SHM/journal represented by native snapshots', + 'Authelia notification.txt','privacyIDEA service logs; audit data retained in database', + 'OpenBao root/unseal/age private key custody remains independently held']})) + finally: + failures=[] + for obj in reversed(created): + try:delete_created(obj) + except Exception:failures.append(obj['metadata']['uid']) + receipt['helper_cleanup_verified']=not failures and not receipt['unverified_create_outcomes'] + checkpoint() + if not receipt['helper_cleanup_verified']:raise Refused() + receipt['temporary_plaintext_cleanup_verified']=not Path(temporary).exists() + checkpoint() + if not receipt['temporary_plaintext_cleanup_verified']:raise Refused() + + +def main(): + global KUBE + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('action',choices=['plan','capture']) + parser.add_argument('--kubeconfig',required=True) + parser.add_argument('--receipt',required=True);parser.add_argument('--plan');parser.add_argument('--plan-sha256') + parser.add_argument('--output');parser.add_argument('--decision-id');a=parser.parse_args();os.umask(0o077) + KUBE=['kubectl','--kubeconfig',str(Path(a.kubeconfig).absolute())] + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + os.close(fd) + r={'status':'failed','started_at':datetime.now(timezone.utc).isoformat(),'secret_values_exported':False} + def checkpoint(): + fd,temp=tempfile.mkstemp(prefix='.capture-receipt-',dir=Path(a.receipt).absolute().parent) + try: + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno()) + os.replace(temp,a.receipt) + finally:Path(temp).unlink(missing_ok=True) + code=1 + try: + if a.action=='plan': + if any([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused() + r=plan();code=0 + else: + if not all([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused() + uuid.UUID(a.decision_id) + p=checked_plan(Path(a.plan),a.plan_sha256) + capture(p,Path(a.output),r,a.decision_id,checkpoint);code=0 + except Exception: + r.update(status='failed',error='capture_refused_or_failed_private_diagnostics_suppressed') + if r.get('temporary_plaintext_directory') and not Path(r['temporary_plaintext_directory']).exists():r['temporary_plaintext_cleanup_verified']=True + finally: + r['finished_at']=datetime.now(timezone.utc).isoformat() + checkpoint() + print('OK: identity '+a.action+' complete; see safe receipt.' if code==0 else 'ERROR: identity operation failed; inspect safe receipt before retry.') + return code + + +def interrupted(*args):raise Refused() + + +if __name__=='__main__': + for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,interrupted) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/check-privacyidea-database.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/check-privacyidea-database.py new file mode 100644 index 00000000..d00361be --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/check-privacyidea-database.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Restore the fetched PI database in an isolated, disposable local PostgreSQL. + +Run only through the attended custody helper. No cluster, production database, +network, ports, host data mounts, IAM login or token decryption is involved. +""" +import argparse +from datetime import datetime, timezone +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import time +import uuid + +ROOT=Path(__file__).resolve().parents[2] +spec=importlib.util.spec_from_file_location('custody',ROOT/'sso-mfa/bootstrap/sops-private.py') +custody=importlib.util.module_from_spec(spec);spec.loader.exec_module(custody) +RECIPIENT='age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz' +IMAGE='postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70' +OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age' +SIZE=89129309 + + +class Refused(Exception):pass + + +def run(args,**kwargs): + result=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=600,**kwargs) + if result.returncode:raise Refused() + return result.stdout + + +def transfer_input(receipt): + r=json.loads(receipt.read_text()) + if (r.get('schema')!='netkingdom.privacyidea-offhost-transfer.v1' or + r.get('status')!='ciphertext_fetched' or r.get('object')!=OBJECT or + r.get('ciphertext_bytes')!=SIZE or r.get('object_unchanged_during_transfer') is not True):raise Refused() + path=Path(r['ciphertext_path']) + if not path.is_absolute() or path.parent.parent!=Path('/tmp') or not path.parent.name.startswith('nk49-offhost-'):raise Refused() + if any(p.is_symlink() for p in (path,*path.parents)):raise Refused() + st=path.stat() + if st.st_uid!=os.getuid() or st.st_mode&0o077 or st.st_nlink!=1 or not path.is_file() or st.st_size!=SIZE:raise Refused() + blob=path.read_bytes() + if len(blob)!=SIZE or hashlib.sha256(blob).hexdigest()!=r['ciphertext_sha256']:raise Refused() + return blob,r['ciphertext_sha256'] + + +def restore(dump,container): + # Require the exact image already present; do not pull through a mutable tag. + image_id=run(['docker','image','inspect','--format','{{.Id}}',IMAGE]).decode().strip() + run(['docker','run','--detach','--pull=never','--name',container,'--network','none', + '--security-opt','no-new-privileges','--memory','2g','--cpus','2', + '--tmpfs','/var/lib/postgresql/data:rw,nosuid,nodev,size=1073741824', + '--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=recovery', + IMAGE,'postgres','-c','listen_addresses=']) + for _ in range(60): + # The image starts a temporary server during initdb. Wait until the + # entrypoint has exec'd the final server before attempting the restore. + pid1=subprocess.run(['docker','exec',container,'cat','/proc/1/comm'], + stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10) + if pid1.returncode or pid1.stdout.strip()!=b'postgres': + time.sleep(1);continue + ready=subprocess.run(['docker','exec',container,'pg_isready','-U','postgres','-d','recovery'], + stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10) + if ready.returncode==0:break + time.sleep(1) + else:raise Refused() + version=run(['docker','exec',container,'pg_restore','--version']).decode().strip() + with dump.open('rb') as source: + run(['docker','exec','-i',container,'pg_restore','-U','postgres','-d','recovery', + '--no-owner','--no-privileges','--exit-on-error'],stdin=source) + # Fixed aggregate queries only. No identity, resolver config or token values. + counts={} + for table in ('token','resolver','realm'): + raw=run(['docker','exec',container,'psql','-X','-U','postgres','-d','recovery','-At', + '-v','ON_ERROR_STOP=1','-c',f'SELECT count(*) FROM public.{table};']).strip() + if not raw.isdigit():raise Refused() + counts[table]=int(raw) + return {'image':IMAGE,'image_id':image_id,'pg_restore_version':version,'aggregate_row_counts':counts} + + +def main(): + p=argparse.ArgumentParser(description=__doc__) + p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True) + a=p.parse_args();os.umask(0o077) + # Reserve the receipt before any recovery action; never overwrite evidence. + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'schema':'netkingdom.privacyidea-database-drill.v1','status':'failed', + 'started_at':datetime.now(timezone.utc).isoformat(),'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'production_contacted':False,'secret_values_exported':False,'native_identity_recovery_proven':False, + 'token_decryption_proven':False,'audit_signature_continuity_proven':False, + 'full_identity_file_coverage_proven':False,'container_cleanup_verified':False, + 'temporary_plaintext_cleanup_verified':False} + container='nk49-pi-recovery-'+uuid.uuid4().hex + keyfd=None;work=None + try: + blob,digest=transfer_input(Path(a.transfer_receipt));r['ciphertext_sha256']=digest + keyfd=custody.private_key_fd() + derived=run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip() + if derived!=RECIPIENT.encode():raise Refused() + r['registered_recipient_match']=True + with tempfile.TemporaryDirectory(prefix='nk49-pi-drill-',dir='/tmp') as temporary: + work=Path(temporary) + env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary} + wrong=work/'wrong.key' + run(['age-keygen','-o',str(wrong)],env=env) + negative=subprocess.run(['age','-d','-i',str(wrong)],input=blob,stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL,timeout=60,env=env) + if negative.returncode==0:raise Refused() + r['wrong_key_refused']=True + dump=work/'privacyidea.dump' + with dump.open('xb') as output: + result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}'],input=blob, + stdout=output,stderr=subprocess.DEVNULL,timeout=120,pass_fds=(keyfd,),env=env) + if result.returncode or not 5200 or not etag.startswith('"') or not etag.endswith('"') or + any(ord(c)<32 for c in etag)): + raise owner.Refused() + return etag + + +def transfer(opener,auth,destination): + url=owner.HOST+owner.BASE+OBJECT + headers={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'} + with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response: + etag=metadata(response,url) + headers['If-Match']=etag + digest=hashlib.sha256();count=0 + with opener.open(urllib.request.Request(url,headers=headers),timeout=60) as response: + if metadata(response,url)!=etag:raise owner.Refused() + fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as out: + while chunk:=response.read(1024*1024): + count+=len(chunk) + if count>SIZE:raise owner.Refused() + digest.update(chunk);out.write(chunk) + out.flush();os.fsync(out.fileno()) + if count!=SIZE:raise owner.Refused() + with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response: + if metadata(response,url)!=etag:raise owner.Refused() + # ETag used for request binding only; it is not asserted to be a content hash. + return {'ciphertext_bytes':count,'ciphertext_sha256':digest.hexdigest(), + 'object_unchanged_during_transfer':True} + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True) + a=p.parse_args();os.umask(0o077) + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'schema':'netkingdom.privacyidea-offhost-transfer.v1','status':'failed', + 'started_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share', + 'object':OBJECT,'age_keys_read':False,'plaintext_created':False, + 'production_writes':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'owner_accessor_sha256':hashlib.sha256((HERE/'inventory-storage-share.py').read_bytes()).hexdigest()} + destination=None + try: + identity,credentials=owner.caller_credentials() + r['caller_auth_path']=identity['path'];r['caller_entity_id']=identity.get('entity_id') + auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode() + destination=Path(tempfile.mkdtemp(prefix='nk49-offhost-',dir='/tmp'))/'privacyidea.dump.age' + opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),owner.NoRedirect()) + r.update(transfer(opener,auth,destination)) + r.update(status='ciphertext_fetched',ciphertext_path=str(destination)) + except Exception: + r['error']='bounded_transfer_failed_diagnostics_suppressed' + if destination is not None: + destination.unlink(missing_ok=True);destination.parent.rmdir() + finally: + r['finished_at']=datetime.now(timezone.utc).isoformat() + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + return int(r['status']=='failed') + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-backup-profile.json b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-backup-profile.json new file mode 100644 index 00000000..8f380f80 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-backup-profile.json @@ -0,0 +1,158 @@ +{ + "schema": "netkingdom.identity-backup-profile.v1", + "status": "implemented_pending_owner_admission", + "owner": "net-kingdom with railiance-platform/rapp-postgres runtime and storage admission", + "public_recipient": "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz", + "archive_profile": "netkingdom-identity-v1; new profile, not the bootstrap secrets-only tar profile", + "components": [ + { + "namespace": "sso", + "deployment": "lldap", + "deployment_uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8", + "pvc_refs": [ + { + "name": "lldap-data", + "uid": "0a06b65a-e7b3-42d3-916e-e74dbe5fc7da" + } + ], + "secret_refs": [ + "lldap-secrets" + ], + "configmap_refs": [], + "required_assets": [ + "SQLite online-backup snapshot of /data/users.db with integrity check", + "remaining non-backup durable /data files, including any private identity material", + "current deployment configuration and exact lldap-secrets fields" + ], + "native_acceptance": [ + "directory user/group/membership preservation", + "existing-user password authentication in isolated environment" + ] + }, + { + "namespace": "sso", + "deployment": "authelia", + "deployment_uid": "dc40a925-2306-4a3b-89c0-e714187503c3", + "pvc_refs": [ + { + "name": "authelia-data", + "uid": "86826554-b014-4df4-8b28-866aa64f6413" + } + ], + "secret_refs": [ + "authelia-secrets", + "authelia-smtp" + ], + "configmap_refs": [ + "authelia-config" + ], + "required_assets": [ + "SQLite online-backup snapshot of /var/authelia/data/db.sqlite3 with integrity check", + "current configuration.yml and all Secret-mounted encryption/OIDC/session fields", + "explicit disposition for notifier credentials and remaining durable files" + ], + "native_acceptance": [ + "storage decryption", + "same OIDC signing identity and preserved client configuration", + "isolated authentication flow with outbound notification disabled" + ] + }, + { + "namespace": "sso", + "deployment": "keycape", + "deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f", + "pvc_refs": [ + { + "name": "keycape-authentication-policy", + "uid": "423f558f-8c11-4edf-956b-243559df4a8e" + } + ], + "secret_refs": [ + "keycape-approval-engine-operator-client", + "keycape-config", + "keycape-factor-read", + "keycape-informed-decision-sitting-requester-client", + "keycape-rapp-qonto-client", + "keycape-secrets-engine-approval-client", + "keycape-secrets-engine-openbao-client", + "keycape-secrets-engine-requester-client" + ], + "configmap_refs": [], + "required_assets": [ + "exact current keycape-config config.yaml and key.pem", + "complete stable policy/receipt PVC snapshot", + "current deployment and owner disposition for every referenced credential: encrypted recovery or independently recoverable provider reissue path" + ], + "native_acceptance": [ + "same issuer/JWKS signing identity", + "registry, authentication policy and receipt continuity", + "current consumer positive/negative authentication under isolated test endpoints" + ] + }, + { + "namespace": "mfa", + "deployment": "privacyidea", + "deployment_uid": "58c7f96d-61cb-4dd4-bca2-54661c0ac375", + "pvc_refs": [ + { + "name": "privacyidea-data", + "uid": "65650fc1-02d8-4c0f-84fc-e0015c8f3ed9" + }, + { + "name": "privacyidea-logs", + "uid": "39fa90d9-bc54-4a03-a0d6-ae85165e0cb9" + } + ], + "secret_refs": [ + "privacyidea-config" + ], + "configmap_refs": [ + "factor-recovery-01119187d90d7b76", + "keycape-onboarding-9465e5e0b98f7986", + "privacyidea-cfg" + ], + "required_assets": [ + "original /etc/privacyidea/enckey, private.pem and public.pem with matching RSA pair validation", + "current privacyidea-config and mounted configuration/scripts", + "bounded native PostgreSQL custom dump with exact database identity and key-generation linkage", + "explicit disposition for service logs; audit database/signatures must be preserved" + ], + "native_acceptance": [ + "decrypt existing retained token material without exporting it", + "verify pre-backup audit signatures and create/verify a new isolated audit entry", + "existing-token MFA and negative-key refusal" + ] + } + ], + "capture_contract": { + "plaintext": "private task staging outside Git; never process output, State Hub or logs; verified cleanup after encryption", + "consistency": "native SQLite online-backup and PostgreSQL logical snapshot; explicit capture interval per asset; no global atomic snapshot claim; before/after source identities/config versions and directory inventories must match", + "key_policy": "preserve original service keys; no age private key in the payload, no replacement generation, no root/unseal collection", + "encryption": "public-recipient-only streaming archive to age; private fresh ciphertext, no overwrite; manifest inside ciphertext with source identities, times, complete required paths, digests and omissions", + "limits": "measure first; proposed 512 MiB encrypted admission ceiling, no silent truncation/exclusions", + "historical_database": "existing verified October15 dump remains protected; new bundle must link its own exact database/key capture generation and recovery receipt" + }, + "proposed_destinations": { + "initial": "Storage Share Backup/railiance-backups/net-kingdom-identity/", + "future_secondary": "s3://railiance-platform-pg-backup/platform-pg/application-archives/net-kingdom/; not implemented/admitted in this rollout", + "admitted": false, + "publisher": "CCR-2026-0048: new folder-only read/create public DAV share, delivered by exact ESO reader; no operator password in runtime; explicitly owner-approved provider-added re-sharing (permissions21)", + "retention": "CCR-2026-0049: separate folder-only read/delete share and process; newest plus seven daily/two weekly and independently recovered pinned generation; unknown/unindexed points never deleted; explicitly owner-approved provider-added re-sharing (permissions25)", + "schedule": "source prepared: daily 03:45 UTC plus up to five minutes jitter, hourly 25-hour freshness check; installation leaves timers disabled until independent archive recovery", + "quota": "512 MiB ceiling, two GiB fresh provider headroom; initial 10 GiB account quota checked; unsupported public-share quota fails closed" + }, + "admission_gates": [ + "Approve exact CCR-2026-0048/0049 grants and node runtime packet; three required owner reviews, confirmed native SA/namespace/audience boundaries before acceptance", + "Exclude helper capture label from existing broad DNS allow policies; validate read-only helper manifests and cluster/PVC/source bindings before private capture", + "Preserve every referenced credential encrypted with original keys; fresh native SQLite/PG snapshots and exact manifest, with documented omissions and no global atomicity claim", + "Create fresh ciphertext and exact Storage Share object, read back ciphertext and receipt by strong ETag/hash, independently retrieve using operator custody", + "Attended independent age-key proof: wrong-key denial, complete archive hashes, SQLite/RSA checks, disposable native PostgreSQL restore and verified cleanup before timer activation", + "Full NK49 closure separately requires native directory/issuer/MFA authentication, retained token decryption, audit signature continuity, current callers and legacy credential dispositions; archive acceptance does not close those gates", + "Host-local health cannot report a dead host; external outage monitoring and second-provider copy remain separate coverage gaps" + ], + "runtime_host": { + "ssh_alias": "railiance01", + "hostname": "239.62.205.92.host.secureserver.net", + "machine_id_sha256": "fe7367a60951922122af7ba18ed2cde65c7e0a232bbedfb045cb575de2094c2f" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-bundle.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-bundle.py new file mode 100644 index 00000000..3a7ea9ef --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-bundle.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Strict data-bearing identity archive format, native checks and private sealing. + +No custody key is used to create a bundle. Its manifest and plaintext digests +stay inside encryption. Public receipts expose only ciphertext provenance. +""" +import base64 +from datetime import datetime,timezone +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path,PurePosixPath +import re +import sqlite3 +import stat +import subprocess +import tarfile +import tempfile + +ROOT=Path(__file__).resolve().parents[2] +PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json' +LIMIT=512*1024*1024 +FILE_LIMIT=256*1024*1024 +NAME=re.compile(r'[A-Za-z0-9_.-]{1,160}\Z') + + +class Refused(Exception):pass + + +def profile():return json.loads(PROFILE.read_text()) + + +def private(path,directory=False,max_bytes=FILE_LIMIT): + if any(p.is_symlink() for p in (path,*path.parents)):raise Refused() + s=path.lstat() + if s.st_uid!=os.getuid() or s.st_mode&0o077:raise Refused() + if directory: + if not stat.S_ISDIR(s.st_mode):raise Refused() + elif not stat.S_ISREG(s.st_mode) or s.st_nlink!=1 or s.st_size>max_bytes:raise Refused() + + +def allowed(name,profile_data=None): + p=PurePosixPath(name) + if p.is_absolute() or any(not NAME.fullmatch(x) or x in {'.','..'} for x in p.parts):return False + if len(p.parts)>=3 and p.parts[:2] in [('data','lldap'),('data','authelia'),('data','keycape'),('data','privacyidea')]:return True + if name=='databases/privacyidea.dump':return True + for c in (profile_data or profile())['components']: + prefix=f"resources/{c['namespace']}/" + if name==prefix+'deployment/'+c['deployment']+'.json':return True + if any(name==prefix+'secret/'+n+'.json' for n in c['secret_refs']):return True + if any(name==prefix+'configmap/'+n+'.json' for n in c['configmap_refs']):return True + return False + + +def save(root,name,value,profile_data=None): + if not allowed(name,profile_data) or len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused() + target=root/name;target.parent.mkdir(mode=0o700,parents=True,exist_ok=True) + fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as out:out.write(value) + + +def import_files(raw,root,mode): + names=set();total=0 + with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive: + for member in archive: + expected=('data/privacyidea/',) if mode=='mfa' else ('data/lldap/','data/authelia/','data/keycape/') + if (not member.isfile() or not member.name.startswith(expected) or member.name in names + or member.size<0 or member.size>FILE_LIMIT or len(names)>=2048):raise Refused() + names.add(member.name);total+=member.size + if total>LIMIT:raise Refused() + data=archive.extractfile(member).read() + if len(data)!=member.size:raise Refused() + save(root,member.name,data) + + +def resource(root,ns,kind,name): + p=root/f'resources/{ns}/{kind}/{name}.json' + data=json.loads(p.read_bytes()) + if data['metadata']['name']!=name or data['metadata']['namespace']!=ns:raise Refused() + return data + + +def checks(root,profile_data=None): + # Checks describe stored formats/integrity, not current login validity. + for c in (profile_data or profile())['components']: + deployment=resource(root,c['namespace'],'deployment',c['deployment']) + if deployment.get('kind')!='Deployment' or deployment['metadata']['uid']!=c['deployment_uid']:raise Refused() + for name in c['secret_refs']: + data=resource(root,c['namespace'],'secret',name) + if data.get('kind')!='Secret' or not isinstance(data.get('data'),dict) or not data['data']:raise Refused() + for value in data['data'].values(): + if b'AGE-SECRET-KEY-' in base64.b64decode(value,validate=True):raise Refused() + for name in c['configmap_refs']: + if resource(root,c['namespace'],'configmap',name).get('kind')!='ConfigMap':raise Refused() + for name,file in [('lldap','users.db'),('authelia','db.sqlite3')]: + db=root/f'data/{name}/{file}' + with sqlite3.connect(db.as_uri()+'?mode=ro',uri=True) as con: + if con.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused() + with (root/'databases/privacyidea.dump').open('rb') as source: + if source.read(5)!=b'PGDMP':raise Refused() + spec=importlib.util.spec_from_file_location('pi_keys',ROOT/'sso-mfa/k8s/privacyidea/key-custody-remote.py') + keys=importlib.util.module_from_spec(spec);spec.loader.exec_module(keys) + keys.validate({name:(root/'data/privacyidea'/name).read_bytes() for name in ('enckey','private.pem','public.pem')}) + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + config=resource(root,'sso','secret','keycape-config')['data'] + if not base64.b64decode(config['config.yaml'],validate=True):raise Refused() + signing=serialization.load_pem_private_key(base64.b64decode(config['key.pem'],validate=True),password=None) + if not isinstance(signing,rsa.RSAPrivateKey) or signing.key_size<2048:raise Refused() + # Policy must exist, even for an empty set of receipts; do not silently + # restore an issuer without its current authentication policy. + policy=json.loads((root/'data/keycape/policy.json').read_bytes()) + if not isinstance(policy,dict):raise Refused() + return {'sqlite_integrity_passed':True,'privacyidea_key_format_and_pair_passed':True, + 'keycape_signing_key_format_passed':True,'policy_json_present':True, + 'database_custom_format_present':True,'native_identity_recovery_proven':False} + + +def inventory(root): + private(root,True);entries=[];total=0 + for path in sorted(root.rglob('*')): + if path.is_dir():private(path,True);continue + private(path);name=path.relative_to(root).as_posix() + if not allowed(name):raise Refused() + with path.open('rb') as source:value=source.read(FILE_LIMIT+1) + if len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused() + total+=len(value) + if total>LIMIT or len(entries)>=4096:raise Refused() + entries.append({'path':name,'bytes':len(value),'sha256':hashlib.sha256(value).hexdigest()}) + if not entries:raise Refused() + return entries + + +def seal(root,output,capture): + output=output.absolute();private(output.parent,True) + if output.exists() or output.is_symlink():raise Refused() + checked=checks(root);entries=inventory(root) + manifest={'schema':'netkingdom.identity-archive.v1','profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(), + 'profile':profile(), + 'captured_at':datetime.now(timezone.utc).isoformat(),'capture':capture,'files':entries,'checks':checked} + with tempfile.TemporaryDirectory(prefix='.identity-seal-',dir=output.parent) as temporary: + ciphertext=Path(temporary)/'ciphertext' + fd=os.open(ciphertext,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) + proc=subprocess.Popen(['age','-r',profile()['public_recipient']],stdin=subprocess.PIPE,stdout=fd,stderr=subprocess.DEVNULL) + os.close(fd) + try: + with tarfile.open(fileobj=proc.stdin,mode='w|') as archive: + raw=json.dumps(manifest,sort_keys=True).encode();m=tarfile.TarInfo('manifest.json');m.size=len(raw);m.mode=0o600 + archive.addfile(m,io.BytesIO(raw)) + for entry in entries: + with (root/entry['path']).open('rb') as source: + value=source.read(FILE_LIMIT+1) + if hashlib.sha256(value).hexdigest()!=entry['sha256']:raise Refused() + m=tarfile.TarInfo(entry['path']);m.size=len(value);m.mode=0o600 + archive.addfile(m,io.BytesIO(value)) + proc.stdin.close() + if proc.wait(timeout=120) or not 04096:raise Refused() + seen=set();total=0 + # Iteration starts with the already-read first member; explicitly skip + # only that member. A later duplicate manifest is never accepted. + for index,member in enumerate(archive): + if index==0 and member is first:continue + name=member.name + if (not member.isfile() or name in seen or name not in expected or not allowed(name,captured) + or not 0<=member.size<=FILE_LIMIT or member.size!=expected[name]['bytes']):raise Refused() + seen.add(name);total+=member.size + if total>LIMIT:raise Refused() + raw=archive.extractfile(member).read() + if len(raw)!=member.size or hashlib.sha256(raw).hexdigest()!=expected[name]['sha256']:raise Refused() + save(root,name,raw,captured) + if seen!=set(expected):raise Refused() + return manifest,checks(root,captured) + + +def require_runtime_host(): + import socket + host=profile()['runtime_host'] + if (os.geteuid()!=0 or socket.gethostname()!=host['hostname'] + or hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()!=host['machine_id_sha256']):raise Refused() diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-credential-setup.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-credential-setup.py new file mode 100644 index 00000000..da49bc02 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-credential-setup.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Attended Platform/Backup owner ceremony for two exact new backup shares. + +Requires approved, hash-bound CCRs. No operator password/age key is delivered to +the runtime; only a folder-scoped share token is stored with initial KV CAS=0. +""" +import argparse +import base64 +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +import uuid + +HERE=Path(__file__).resolve().parent +PLATFORM=HERE.parents[1].parent/'railiance-platform' +sys.path.insert(0,str(PLATFORM/'scripts')) +from state_hub_preflight_lane import bao,data,capabilities,revoke +from repair_eso_kubernetes_auth import role_payload,check_role +from telemetry_smtp_delivery import ensure + + +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m + + +inventory=load('storage_inventory',HERE/'inventory-storage-share.py') +offsite=load('offsite',HERE/'identity-offsite.py') +FOLDER='/railiance-backups/net-kingdom-identity' +PROFILES={'publisher':(48,21),'retention':(49,25)} + + +class Refused(Exception):pass + + +def contract(action,expected): + number,permission=PROFILES[action] + path=PLATFORM/f'credential-change-requests/CCR-2026-{number:04d}-net-kingdom-backup-{action}.yaml' + raw=path.read_bytes() + if hashlib.sha256(raw).hexdigest()!=expected:raise Refused() + module=load('credential_change',PLATFORM/'scripts/credential-change.py') + ccr,errors,_=module.validate_ccr(path) + if errors or ccr['status'] not in {'approved','applied','verified','active'}:raise Refused() + required={'platform-owner','net-kingdom-owner','backup-share-owner'} + approved={r['reviewer'].removeprefix('User (').removesuffix(')') for r in ccr.get('review',{}).get('comments',[]) + if r.get('decision')=='approved' and r.get('source_ref')} + name='net-kingdom-backup-'+action+'-eso' + expected_path='platform/workloads/net-kingdom/identity-backup/'+action + a=ccr['openbao'];provider=ccr['provider'] + if (not required<=approved or a['kv_path']!=expected_path or a['fields']!=['NC_WEBDAV_TOKEN'] + or a['policy_name']!=name or a['auth']['role']!=name + or a['auth']['bound_claims']!={'service_account_names':[name],'service_account_namespaces':['sso']} + or provider['folder']!=FOLDER or provider['share_permissions']!=permission or provider['share_type']!=3 + or provider['label']!='net-kingdom-identity-'+action + or provider.get('provider_added_resharing_approved') is not True):raise Refused() + if (a.get('metadata_read') is not False or a.get('token_self_lifecycle') is not True + or a['auth'].get('audience')!='openbao' or a['auth'].get('token_no_default_policy') is not True + or any(a['auth'].get(k)!='15m' for k in ['ttl','token_max_ttl','token_explicit_max_ttl']) + or a['auth'].get('policies')!=[name] or provider.get('host')!='nx4069.your-storageshare.de'):raise Refused() + lane={'service_account':name,'namespace':'sso','role':name,'policy':name, + 'kv_path':expected_path.replace('platform/','platform/data/',1)} + wanted=('path "'+lane['kv_path']+'" { capabilities = ["read"] }\n' + 'path "auth/token/lookup-self" { capabilities = ["read"] }\n' + 'path "auth/token/revoke-self" { capabilities = ["update"] }\n') + if (PLATFORM/a['policy_file']).read_text()!=wanted:raise Refused() + return ccr,lane + + +def request(client,url,auth,method='GET',body=None,extra=None): + if not url.startswith(offsite.HOST+'/'):raise Refused() + h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode(), + 'X-Requested-With':'XMLHttpRequest'};h.update(extra or {}) + try: + with client.open(urllib.request.Request(url,data=body,method=method,headers=h),timeout=60) as response: + if response.url!=url:raise Refused() + raw=response.read(1024*1024+1) + if len(raw)>1024*1024:raise Refused() + return response.status,raw + except urllib.error.HTTPError as error:return error.code,b'' + + +def ocs(client,auth,suffix='',fields=None): + url=offsite.HOST+'/ocs/v2.php/apps/files_sharing/api/v1/shares'+suffix + body=urllib.parse.urlencode(fields).encode() if fields is not None else None + status,raw=request(client,url,auth,'POST' if fields is not None else 'GET',body, + {'OCS-APIRequest':'true','Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'}) + if status not in (200,201):raise Refused() + parsed=json.loads(raw)['ocs'] + if parsed['meta']['statuscode'] not in (100,200):raise Refused() + return parsed['data'] + + +def share(client,auth,action,receipt): + owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER + status,_=request(client,owner,auth,'MKCOL') + if status not in (201,405):raise Refused() + receipt['folder_created']=status==201 + matches=[s for s in ocs(client,auth,'?path='+urllib.parse.quote(FOLDER,safe='')) + if s.get('label')=='net-kingdom-identity-'+action] + if len(matches)>1:raise Refused() + if matches:result=matches[0];receipt['share_created']=False + else: + result=ocs(client,auth,fields={'path':FOLDER,'shareType':3,'permissions':PROFILES[action][1], + 'label':'net-kingdom-identity-'+action});receipt['share_created']=True + if (result.get('path')!=FOLDER or int(result.get('permissions',-1))!=PROFILES[action][1] + or int(result.get('share_type',-1))!=3 or not result.get('token')):raise Refused() + offsite.headers(result['token']) + receipt.update(share_id=str(result['id']),share_permissions=int(result['permissions']),folder=FOLDER) + return result['token'] + + +def boundary(client,auth,token,action,receipt): + name='nk49-boundary-'+uuid.uuid4().hex+'.age' + payload=subprocess.run(['age','-r',json.loads((HERE/'identity-backup-profile.json').read_text())['public_recipient']], + input=b'NK49 disposable provider boundary fixture',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10) + if payload.returncode:raise Refused() + owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+name + public=offsite.BASE+name;created=False;deleted=False + receipt['provider_test_artifacts']=[name] + receipt['boundary_fixture_cleanup_verified']=False + try: + receipt['boundary_phase']='create_owner_fixture' + status,_=request(client,owner,auth,'PUT',payload.stdout,{'If-None-Match':'*'}) + if status!=201:raise Refused() + created=True + receipt['boundary_phase']='read_public_fixture' + status,raw=request(client,public,(token,'')) + if status!=200 or raw!=payload.stdout:raise Refused() + receipt['boundary_phase']='deny_public_overwrite' + status,_=request(client,public,(token,''),'PUT',payload.stdout) + if status not in (401,403):raise Refused() + denied_name='nk49-absent-'+uuid.uuid4().hex+'.age' + if action=='retention':receipt['provider_test_artifacts'].append(denied_name) + status,_=request(client,offsite.BASE+denied_name,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'}) if action=='retention' else (403,b'') + if status not in (401,403):raise Refused() + receipt['boundary_phase']='public_delete' + status,_=request(client,public,(token,''),'DELETE') + if action=='publisher': + if status not in (401,403):raise Refused() + new=offsite.BASE+'nk49-boundary-'+uuid.uuid4().hex+'.age' + receipt['provider_test_artifacts'].append(new.rsplit('/',1)[-1]) + receipt['boundary_phase']='create_public_fixture' + code,_=request(client,new,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'}) + if code!=201:raise Refused() + # This fixture belongs to the current ceremony, not retained data. + receipt['boundary_phase']='cleanup_public_fixture' + remove_fixture(client,offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+new.rsplit('/',1)[-1],auth,payload.stdout) + else: + if status!=204:raise Refused() + deleted=True + receipt['boundary_phase']='deny_parent_path' + status,_=request(client,offsite.BASE+'%2e%2e/r01-net-kingdom-pg/',(token,'')) + receipt['parent_probe_status']=status + if status not in (400,401,403,404):raise Refused() + receipt['parent_path_rejected']=True + # A malformed-path400 proves request rejection, not authorization. + # Separately probe a known existing account folder with only this share + # token: it must not authenticate as the Backup account. + receipt['boundary_phase']='deny_other_account_folder' + status,_=request(client,offsite.HOST+'/remote.php/dav/files/Backup/r01-net-kingdom-pg/',(token,'')) + receipt['other_account_folder_status']=status + if status not in (401,403,404):raise Refused() + receipt['provider_boundary_verified']=True + finally: + if created and not deleted: + remove_fixture(client,owner,auth,payload.stdout) + receipt['owner_fixture_cleanup_verified']=True + # A rejected operation may have had an unexpected side effect. Only a + # successful complete boundary test claims cleanup; names remain in the + # receipt for owner reconciliation on every failure. + receipt['boundary_fixture_cleanup_verified']=receipt.get('provider_boundary_verified',False) + + +def remove_fixture(client,url,auth,expected): + h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode()} + with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response: + version=offsite.etag(response,url,len(expected)) + code,raw=request(client,url,auth,extra={'If-Match':version}) + if code!=200 or raw!=expected:raise Refused() + code,_=request(client,url,auth,'DELETE',extra={'If-Match':version}) + if code!=204:raise Refused() + + +def run(action,expected,receipt): + ccr,lane=contract(action,expected) + identity,credentials=inventory.caller_credentials() + receipt['caller_entity_id']=identity.get('entity_id');receipt['caller_auth_path']=identity['path'] + client=offsite.opener();auth=(credentials['BACKUP_USERNAME'],credentials['BACKUP_PASSWORD']) + # Use the owner quota accessor only with proxies removed from its environment. + quota=load('owner_quota',PLATFORM/'scripts/migrate_nextcloud_backup_account.py') + quota.request=lambda url,method='GET',body=None,auth=None,headers=None:request(client,url,auth,method,body,headers) + receipt['quota_before']=quota.quota(auth) + if receipt['quota_before']['available_bytes']MAX_FILE:raise Refused() + with os.fdopen(os.dup(fd),'rb') as stream:data=stream.read(MAX_FILE+1) + after=os.stat(path,follow_symlinks=False) + if (len(data)>MAX_FILE or b'AGE-SECRET-KEY-' in data or + any(getattr(before,k)!=getattr(after,k) for k in ('st_dev','st_ino','st_size','st_mtime_ns','st_ctime_ns'))):raise Refused() + return data + finally:os.close(fd) + + +def tree(root,database=None,omit_notification=False,omit_backups=True): + result={} + for directory,dirs,files in os.walk(root,followlinks=False): + path=Path(directory) + if path.is_symlink():raise Refused() + # Historical local snapshots are redundant; do not recursively include + # accumulated backups. WAL/SHM are represented by the native snapshot. + if omit_backups:dirs[:]=[name for name in dirs if name!='backups'] + for name in dirs: + if (path/name).is_symlink():raise Refused() + for name in files: + relative=(path/name).relative_to(root).as_posix() + if database and relative in {database,database+'-wal',database+'-shm',database+'-journal'}:continue + if omit_notification and relative=='notification.txt':continue + result[relative]=read(path/name) + if len(result)>MAX_COUNT or sum(map(len,result.values()))>MAX_TOTAL:raise Refused() + return result + + +def snapshot(source,target): + if source.is_symlink() or not source.is_file():raise Refused() + # A read-only URI intentionally fails if the engine needs a write to its + # production WAL/SHM. Never fall back to a raw copy or mutate the mount. + with sqlite3.connect(source.as_uri()+'?mode=ro',uri=True,timeout=10) as origin: + with sqlite3.connect(target) as dest: + origin.backup(dest,pages=128,sleep=0.05) + if dest.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused() + return read(target) + + +def capture(mode,root=Path('/source'),scratch=Path('/scratch')): + values={} + with tempfile.TemporaryDirectory(prefix='snapshot-',dir=scratch) as temporary: + if mode=='sso': + for name,database in [('lldap','users.db'),('authelia','db.sqlite3'),('keycape',None)]: + source=root/name + if source.is_symlink() or not source.is_dir():raise Refused() + first=tree(source,database,name=='authelia',name!='keycape') + if database:first[database]=snapshot(source/database,Path(temporary)/(name+'.sqlite')) + second=tree(source,database,name=='authelia',name!='keycape') + if {k:v for k,v in first.items() if k!=database}!=second:raise Refused() + values.update({f'data/{name}/{k}':v for k,v in first.items()}) + elif mode=='mfa': + first=tree(root/'privacyidea') + if not {'enckey','private.pem','public.pem'}<=set(first) or first!=tree(root/'privacyidea'):raise Refused() + values={f'data/privacyidea/{name}':value for name,value in first.items()} + else:raise Refused() + if len(values)>MAX_COUNT or sum(map(len,values.values()))>MAX_TOTAL:raise Refused() + return values + + +def emit(values,destination): + with tarfile.open(fileobj=destination,mode='w|') as archive: + for name,value in sorted(values.items()): + member=tarfile.TarInfo(name);member.size=len(value);member.mode=0o600 + archive.addfile(member,io.BytesIO(value)) + + +def interrupted(*args):raise Refused() + + +if __name__=='__main__': + for sig in (signal.SIGTERM,signal.SIGHUP,signal.SIGINT):signal.signal(sig,interrupted) + try: + os.umask(0o077) + if len(sys.argv)!=2:raise Refused() + emit(capture(sys.argv[1]),sys.stdout.buffer) + except Exception: + print('ERROR: private file capture refused; diagnostics suppressed.',file=sys.stderr) + raise SystemExit(1) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-isolation.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-isolation.py new file mode 100644 index 00000000..c19a2723 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-isolation.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Plan two exact DNS-selector exclusions; apply only an owner-admitted hash-bound plan.""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path + +HERE=Path(__file__).resolve().parent +spec=importlib.util.spec_from_file_location('capture',HERE/'capture-identity.py') +capture=importlib.util.module_from_spec(spec);spec.loader.exec_module(capture) +SELECTOR={'matchExpressions':[{'key':'net-kingdom/capture','operator':'DoesNotExist'}]} + + +def plan(): + if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise capture.Refused() + rows=[] + for ns in ('sso','mfa'): + obj=capture.get(ns,'networkpolicy','allow-egress-dns');s=obj['spec'];m=obj['metadata'] + if s['podSelector'] not in ({},SELECTOR):raise capture.Refused() + pods=json.loads(capture.command(['-n',ns,'get','pods','-o','json']))['items'] + if any('net-kingdom/capture' in p['metadata'].get('labels',{}) for p in pods):raise capture.Refused() + # Preserve every peer, port and policy type. Existing unlabelled service + # pods retain their exact selector match. UID/RV/spec tests guard drift. + patch=[{'op':'test','path':'/metadata/uid','value':m['uid']}, + {'op':'test','path':'/metadata/resourceVersion','value':m['resourceVersion']}, + {'op':'test','path':'/spec','value':s}, + {'op':'replace','path':'/spec/podSelector','value':SELECTOR}] + rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'],'patch':patch}) + return {'schema':'netkingdom.identity-isolation.v1','status':'prepared_not_applied', + 'kubeconfig':capture.KUBE[2],'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'policies':rows} + + +def apply(packet,receipt,dry_run=False): + if packet!=plan():raise capture.Refused() + for row in packet['policies']: + args=['-n',row['namespace'],'patch','networkpolicy',row['name'],'--type=json','-p',json.dumps(row['patch']),'-o','json'] + if dry_run:args+=['--dry-run=server'] + receipt['attempted'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']}) + actual=json.loads(capture.command(args));expected=row['patch'][2]['value'].copy();expected['podSelector']=SELECTOR + if actual['metadata']['uid']!=row['uid'] or actual['spec']!=expected:raise capture.Refused() + receipt['verified'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']}) + receipt['status']='server_dry_run_passed' if dry_run else 'capture_dns_exclusions_verified' + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['plan','dry-run','apply']) + p.add_argument('--kubeconfig',required=True);p.add_argument('--plan');p.add_argument('--plan-sha256');p.add_argument('--receipt',required=True) + a=p.parse_args();os.umask(0o077);capture.KUBE=['kubectl','--kubeconfig',a.kubeconfig] + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'status':'failed','attempted':[],'verified':[]} + try: + if a.action=='plan':r=plan() + else: + raw=Path(a.plan).read_bytes() + if hashlib.sha256(raw).hexdigest()!=a.plan_sha256:raise capture.Refused() + apply(json.loads(raw),r,a.action=='dry-run') + except Exception:r.update(status='failed',error='isolation_refused_review_partial_attempts') + finally: + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + return int(r['status']=='failed') + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-offsite.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-offsite.py new file mode 100644 index 00000000..33707e5a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-offsite.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Folder-scoped Storage Share ciphertext publication and verified readback. + +Runtime gets only a dedicated read/create share token. Account administration, +deletion and age private keys have no route in this publisher. +""" +import base64 +from datetime import datetime,timezone +import hashlib +import json +import os +from pathlib import Path +import re +import tempfile +import urllib.error +import urllib.request +import xml.etree.ElementTree as ET + +HOST='https://nx4069.your-storageshare.de' +BASE=HOST+'/public.php/webdav/' +NAME=re.compile(r'net-kingdom-identity-\d{8}T\d{6}Z-[a-f0-9]{12}\.tar\.age\Z') +LIMIT=512*1024*1024 +HEADROOM=2*1024**3 + + +class Refused(Exception):pass + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self,*args,**kwargs):raise Refused() + + +def opener():return urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect()) + + +def headers(token): + if not isinstance(token,str) or not re.fullmatch('[A-Za-z0-9]{10,128}',token):raise Refused() + return {'Authorization':'Basic '+base64.b64encode((token+':').encode()).decode(), + 'X-Requested-With':'XMLHttpRequest','Accept-Encoding':'identity'} + + +def digest(path): + with path.open('rb') as source:return hashlib.file_digest(source,'sha256').hexdigest() + + +def quota(client,token): + body=b'' + h=headers(token);h.update(Depth='0');h['Content-Type']='application/xml' + with client.open(urllib.request.Request(BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response: + if response.status!=207 or response.url!=BASE:raise Refused() + raw=response.read(1024*1024+1) + if len(raw)>1024*1024:raise Refused() + amount=ET.fromstring(raw).findtext('.//{DAV:}quota-available-bytes') or '' + if not amount.isdigit():raise Refused() + return int(amount) + + +def etag(response,url,size): + value=response.headers.get('ETag','') + if (response.status!=200 or response.url!=url or response.headers.get('Content-Length')!=str(size) + or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',value)):raise Refused() + return value + + +def readback(client,token,name,size,expected,target): + valid=NAME.fullmatch(name) or (name.endswith('.receipt.json') and NAME.fullmatch(name.removesuffix('.receipt.json')) and size<=65536) + if not valid or not 0size:raise Refused() + sha.update(block);out.write(block) + out.flush();os.fsync(out.fileno()) + if count!=size or sha.hexdigest()!=expected:raise Refused() + with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response: + if etag(response,url,size)!=version:raise Refused() + return version + + +def publish(client,token,source,capture,target): + if (not NAME.fullmatch(source.name) or source.is_symlink() or source.stat().st_mode&0o077 + or source.stat().st_uid!=os.getuid() or source.stat().st_nlink!=1 or not source.is_file() + or source.stat().st_size>LIMIT or capture.get('status')!='encrypted_bundle_created' + or capture.get('helper_cleanup_verified') is not True or capture.get('temporary_plaintext_cleanup_verified') is not True + or digest(source)!=capture.get('ciphertext_sha256') or source.stat().st_size!=capture.get('ciphertext_bytes')):raise Refused() + with source.open('rb') as stream: + if stream.read(22)!=b'age-encryption.org/v1\n':raise Refused() + available=quota(client,token) + if available' + with client.open(urllib.request.Request(offsite.BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response: + if response.status!=207 or response.url!=offsite.BASE:raise offsite.Refused() + raw=response.read(4*1024*1024+1) + if len(raw)>4*1024*1024:raise offsite.Refused() + result={} + for response in ET.fromstring(raw).findall('{DAV:}response'): + href=urllib.parse.urlsplit(response.findtext('{DAV:}href') or '') + if href.netloc and href.netloc!=urllib.parse.urlsplit(offsite.HOST).netloc:raise offsite.Refused() + name=urllib.parse.unquote(href.path.rstrip('/').rsplit('/',1)[-1]) + for row in response.findall('{DAV:}propstat'): + if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue + p=row.find('{DAV:}prop') + if p is None or p.find('{DAV:}resourcetype/{DAV:}collection') is not None:continue + is_index=name.endswith('.receipt.json') and offsite.NAME.fullmatch(name.removesuffix('.receipt.json')) + if not offsite.NAME.fullmatch(name) and not is_index:continue + size=p.findtext('{DAV:}getcontentlength') or '';version=p.findtext('{DAV:}getetag') or '' + if not size.isdigit() or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',version) or name in result:raise offsite.Refused() + result[name]={'bytes':int(size),'etag':version} + if len(result)>10000:raise offsite.Refused() + return result + + +def generations(client,token,objects): + result=[] + for name,meta in objects.items(): + if not offsite.NAME.fullmatch(name) or name+'.receipt.json' not in objects:continue + index=objects[name+'.receipt.json'] + if not 03600:raise offsite.Refused() + if stamp>datetime.now(timezone.utc):raise offsite.Refused() + result.append({**r,'stamp':stamp,'etag':meta['etag'],'index_etag':index['etag']}) + return sorted(result,key=lambda r:r['stamp'],reverse=True) + + +def select(rows,protected): + if not rows or protected not in {r['artifact'] for r in rows}:raise offsite.Refused() + keep={protected,rows[0]['artifact']};days=set();weeks=set() + for row in rows: + day=row['stamp'].date();week=row['stamp'].isocalendar()[:2] + if day not in days and len(days)<7:keep.add(row['artifact']);days.add(day) + if week not in weeks and len(weeks)<2:keep.add(row['artifact']);weeks.add(week) + return [r for r in rows if r['artifact'] not in keep],keep + + +def run(client,token,accepted,receipt,execute=False,checkpoint=lambda:None): + if (accepted.get('status')!='identity_archive_verified' or accepted.get('container_cleanup_verified') is not True + or accepted.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(accepted.get('artifact',''))):raise offsite.Refused() + objects=listing(client,token);rows=generations(client,token,objects) + candidates,keep=select(rows,accepted['artifact']) + recovered=next(r for r in rows if r['artifact']==accepted['artifact']) + if recovered['ciphertext_sha256']!=accepted['ciphertext_sha256']:raise offsite.Refused() + receipt.update(status='retention_preview',protected=accepted['artifact'],keep=sorted(keep), + candidates=[r['artifact'] for r in candidates],deleted=[],unknown_objects_untouched=True) + checkpoint() + # Validate retained known-good and newest ciphertext before any deletion. + with tempfile.TemporaryDirectory(prefix='nk49-retained-check-',dir='/tmp') as temporary: + for index,row in enumerate({r['artifact']:r for r in [recovered,rows[0]]}.values()): + offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/str(index)) + if not execute:return + for index,row in enumerate(candidates): + version=offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/('expire-'+str(index))) + for name,etag in [(row['artifact'],version),(row['artifact']+'.receipt.json',row['index_etag'])]: + h=offsite.headers(token);h['If-Match']=etag + receipt['deletion_in_progress']=name + checkpoint() + with client.open(urllib.request.Request(offsite.BASE+name,method='DELETE',headers=h),timeout=30) as response: + if response.status!=204 or response.url!=offsite.BASE+name:raise offsite.Refused() + receipt['deleted'].append(name) + checkpoint() + receipt.pop('deletion_in_progress',None) + receipt['status']='retention_applied' diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-runtime.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-runtime.py new file mode 100644 index 00000000..eb0b6014 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/identity-runtime.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Root-owned Railiance backup job. No operator password or age private key. + +Publisher and retention are separate processes with separate scoped delivery. +Scheduled execution is gated by an independently recovered archive receipt. +""" +import argparse +import base64 +from datetime import datetime,timezone +import fcntl +import importlib.util +import json +import os +from pathlib import Path +import signal +import shutil +import socket +import tempfile +import urllib.request + +HERE=Path(__file__).resolve().parent + + +def load(name): + spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py')) + m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m + + +capture=load('capture-identity');offsite=load('identity-offsite');retention=load('identity-retention') + + +def write(path,value): + fd,name=tempfile.mkstemp(prefix='.identity-receipt-',dir=path.parent) + try: + with os.fdopen(fd,'w') as out:json.dump(value,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno()) + os.replace(name,path) + finally:Path(name).unlink(missing_ok=True) + + +def token(action): + name='net-kingdom-backup-'+action + es=capture.get('sso','externalsecret',name) + entries=[] + for entry in es['spec'].get('data',[]): + item=dict(entry);ref=dict(item.get('remoteRef',{})) + for key,default in [('conversionStrategy','Default'),('decodingStrategy','None'),('metadataPolicy','None')]: + if ref.pop(key,default)!=default:raise offsite.Refused() + item['remoteRef']=ref;entries.append(item) + if (es['spec']['target']['name']!=name or entries!=[{'secretKey':'token','remoteRef':{ + 'key':'workloads/net-kingdom/identity-backup/'+action,'property':'NC_WEBDAV_TOKEN'}}] + or es['spec'].get('dataFrom') or es['spec'].get('secretStoreRef')!={'kind':'SecretStore','name':name} + or not any(c['type']=='Ready' and c['status']=='True' for c in es['status'].get('conditions',[]))):raise offsite.Refused() + secret=capture.get('sso','secret',name) + if (set(secret['data'])!={'token'} or not any(o['uid']==es['metadata']['uid'] for o in secret['metadata'].get('ownerReferences',[]))):raise offsite.Refused() + value=base64.b64decode(secret['data']['token'],validate=True).decode();offsite.headers(value) + return value + + +def accepted(path): + capture.bundle.private(path) + r=json.loads(path.read_text()) + if (r.get('schema')!='netkingdom.identity-recovery.v1' or r.get('status')!='identity_archive_verified' + or r.get('all_manifest_hashes_verified') is not True or r.get('container_cleanup_verified') is not True + or r.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(r.get('artifact',''))):raise offsite.Refused() + return r + + +def post_event(state,summary): + # Non-secret durable reporting: replay retained notes at the next run. + directory=state/'event-outbox';directory.mkdir(mode=0o700,exist_ok=True) + import uuid + path=directory/(uuid.uuid4().hex+'.json') + write(path,{'summary':summary,'event_type':'note','author':'net-kingdom-identity-backup', + 'workplan_id':'17d315f0-829e-57c7-85cc-3a7914152804','task_id':'d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac'}) + for pending in sorted(directory.glob('*.json'))[:64]: + request=urllib.request.Request('http://10.43.68.154:8000/progress/',data=pending.read_bytes(), + headers={'Content-Type':'application/json'},method='POST') + try: + with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(request,timeout=10) as response: + if response.status!=201:break + pending.unlink() + except Exception:break + + +def backup(state,decision,first=False): + if not first:accepted(state/'accepted-recovery.json') + if (state/'capture-in-progress.json').exists():raise offsite.Refused() + publisher=token('publisher') + client=offsite.opener() + if offsite.quota(client,publisher)=64:raise Refused() + row=list_prefix(client,prefix);result[prefix]=row + if depth<3: + for child in row['directories']: + # Native database WAL/base trees are not application/key-file + # archives. Record their presence without enumerating every WAL. + if any(x in child.split('/') for x in ['wals','base']):continue + pending.append((child,depth+1)) + return result + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True) + a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed', + 'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True, + 'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False, + 'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'} + try: + if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused() + delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3']) + if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused() + values=delivered['data'] + # Same governed runtime delivery used by Platform's archive verifier; + # explicit credentials avoid SDK ambient/profile/metadata discovery. + import boto3 + from botocore.config import Config + client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams', + aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(), + aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(), + config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30, + retries={'max_attempts':1},s3={'addressing_style':'path'})) + r['prefixes']=inventory(client);r['status']='metadata_inventory_complete' + r['runtime_delivery_uid']=delivered['metadata']['uid'] + except Exception:r['error']='inventory_failed_diagnostics_suppressed' + finally: + r['finished_at']=datetime.now(timezone.utc).isoformat() + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + return int(r['status']=='failed') + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/inventory-storage-share.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/inventory-storage-share.py new file mode 100644 index 00000000..fdcdfb6b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/inventory-storage-share.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Silent read-only inventory in the existing Platform Backup-account procedure. + +Runs only inside the attended platform-admin envelope. Reuses Platform's owner +custody accessor and fixed provider/account contract; no age key is read and no +backup is downloaded, uploaded, deleted, rotated or granted. +""" +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import re +import sys +import urllib.parse +import urllib.request +import xml.etree.ElementTree as ET + +ROOT=Path(__file__).resolve().parents[2] +OWNER=ROOT.parent/'railiance-platform/scripts' +sys.path.insert(0,str(OWNER)) +from state_hub_preflight_lane import bao, data +from migrate_nextcloud_backup_account import HOST, OPERATOR_PATH +BASE='/remote.php/dav/files/Backup/railiance-backups/' +LIMIT=4*1024*1024 +NAME=re.compile(r'[A-Za-z0-9._-]{1,160}\Z') + + +class Refused(Exception):pass + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self,*args,**kwargs):raise Refused() + + +def parse_listing(raw, relative): + if len(raw)>LIMIT:raise Refused() + rows=[] + for item in ET.fromstring(raw).findall('{DAV:}response'): + href=urllib.parse.urlsplit(item.findtext('{DAV:}href') or '') + if href.netloc and href.netloc!=urllib.parse.urlsplit(HOST).netloc:raise Refused() + path=urllib.parse.unquote(href.path) + if not path.startswith(BASE+relative):raise Refused() + name=path[len(BASE+relative):].rstrip('/') + if not name:continue + if not NAME.fullmatch(name) or name in {'.','..'}:raise Refused() + for row in item.findall('{DAV:}propstat'): + if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue + prop=row.find('{DAV:}prop') + if prop is None:continue + folder=prop.find('{DAV:}resourcetype/{DAV:}collection') is not None + size=prop.findtext('{DAV:}getcontentlength') + if size is not None and not size.isdigit():raise Refused() + # Export only known-safe operational names/size, not arbitrary DAV + # properties, URLs, account metadata or raw provider responses. + if folder or name.endswith(('.age','.json','.yaml')): + rows.append({'name':name,'directory':folder,'bytes':int(size) if size else None}) + if len(rows)>10000 or len({r['name'] for r in rows})!=len(rows):raise Refused() + return sorted(rows,key=lambda x:x['name']) + + +def listing(relative,credentials): + if relative and (not relative.endswith('/') or not NAME.fullmatch(relative[:-1])):raise Refused() + auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode() + req=urllib.request.Request(HOST+BASE+relative, + data=b'', + method='PROPFIND',headers={'Authorization':'Basic '+auth,'Depth':'1','Content-Type':'application/xml'}) + opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect()) + with opener.open(req,timeout=30) as response: + if response.status!=207 or response.url!=HOST+BASE+relative:raise Refused() + raw=response.read(LIMIT+1) + return parse_listing(raw,relative) + + +def caller_credentials(): + # Lookup payload contains a token: retain only safe caller/role facts. + identity=data(bao(['token','lookup','-format=json']))['data'] + if 'platform-admin' not in identity.get('policies',[]) or 'root' in identity.get('policies',[]):raise Refused() + if not str(identity.get('path','')).startswith('auth/netkingdom/'):raise Refused() + credentials=data(bao(['read','-format=json',OPERATOR_PATH]))['data']['data'] + if credentials.get('BACKUP_USERNAME')!='Backup' or not isinstance(credentials.get('BACKUP_PASSWORD'),str) or not credentials['BACKUP_PASSWORD']:raise Refused() + return identity,credentials + + +def inventory(): + identity,credentials=caller_credentials() + root=listing('',credentials) + folders={} + for row in root: + if row['directory'] and re.match(r'^(?:r01-|core-|identity|sso|bootstrap|net-kingdom|openbao)',row['name']): + if len(folders)>=32:raise Refused() + folders[row['name']]=listing(row['name']+'/',credentials) + return {'root':root,'folders':folders,'caller_auth_path':identity['path'], + 'caller_entity_id':identity.get('entity_id'),'scope':'Backup/railiance-backups root and bounded identity/database subfolders; other accounts/folders unassessed'} + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True) + a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'schema':'netkingdom.storage-share-inventory.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(), + 'read_only':True,'age_keys_read':False,'backup_contents_downloaded':False,'secret_values_exported':False, + 'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'owner_sources':[{'path':str(OWNER/name),'sha256':hashlib.sha256((OWNER/name).read_bytes()).hexdigest()} for name in ['state_hub_preflight_lane.py','migrate_nextcloud_backup_account.py']]} + try:r.update(inventory());r['status']='metadata_inventory_complete' + except Exception:r['error']='inventory_failed_diagnostics_suppressed' + finally: + r['finished_at']=datetime.now(timezone.utc).isoformat() + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + return int(r['status']=='failed') + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape-session.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape-session.py new file mode 100644 index 00000000..408dd304 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape-session.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +"""Owner-run T18 session assembly using the existing archive/containment pipeline. + +Candidate only: native pinned-image tests and review are required before use. +Shared Docker local-driver tmpfs is required because Docker cp does not support +private tmpfs/mount paths, and the original KeyCape image is distroless. +""" +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import signal +import re +import stat +import select +import subprocess +import sys +import tempfile +import time +import uuid + +HERE=Path(__file__).resolve().parent + +def load(name): + spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py')) + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module + +adapter=load('isolated-keycape') +local=load('isolated-lldap') +bundle=local.bundle +# Bind the reused preflight itself, not only this module's direct Docker calls. +local.command=adapter.command +_original_preflight=local.preflight +def preflight(): + contexts=json.loads(adapter.command(['docker','context','inspect'])) + if len(contexts)!=1 or contexts[0]['Endpoints']['docker']['Host']!=adapter.ENDPOINT:raise adapter.Refused() + result=_original_preflight();result['docker_endpoint']=adapter.ENDPOINT;return result +local.preflight=preflight +WORKER=local.CLIENT +STAGING_ROOT=Path('/dev/shm') +RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls') +VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'} + +IMPORT=r''' +import base64,json,os,sys +from pathlib import Path +os.umask(0o077) +p=json.load(sys.stdin) +allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/') +for name,encoded in p.items(): + f=Path(name) + if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError() + f.parent.mkdir(mode=0o700,parents=True,exist_ok=True) + fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True)) +''' + +# Small transport inside the already pinned helper; it has only loopback routes. +# No token handling, auth bypass, dependency imitation, reset or notifier path. +TLS=r''' +import select,socket,socketserver,ssl,threading +context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +context.minimum_version=ssl.TLSVersion.TLSv1_2 +context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem') +class Handler(socketserver.BaseRequestHandler): + def handle(self): + try: + with context.wrap_socket(self.request,server_side=True) as incoming: + with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing: + incoming.settimeout(15);outgoing.settimeout(15) + while True: + ready,_,_=select.select([incoming,outgoing],[],[],15) + if not ready:return + for source in ready: + data=source.recv(65536) + if not data:return + (outgoing if source is incoming else incoming).sendall(data) + except Exception:pass +class Server(socketserver.ThreadingTCPServer): + allow_reuse_address=True + daemon_threads=True +with Server(('127.0.0.1',443),Handler) as server:server.serve_forever() +''' + + +def run(argv,**kwargs):return adapter.command(argv,**kwargs) + + +def plan_session(): + run_id=uuid.uuid4().hex + stem='nk49-kc-'+run_id + worker=stem+'-transport';issuer=stem+'-issuer' + volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)} + return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer, + 'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes, + 'public_image_probe':stem+'-image-probe','docker_endpoint':adapter.ENDPOINT} + + +def verify_volume(name,spec,run_id): + obj=json.loads(run(['docker','volume','inspect',name])) + if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local' + or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS + or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused() + + +def base_args(session,name,network): + return ['docker','run','--detach','--pull=never','--name',name,'--network',network, + '--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL', + '--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1', + '--pids-limit','64','--log-driver','none','--restart','no', + '--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700'] + + +def mounts(session,readonly=False): + args=[] + for name,spec in session['volumes'].items(): + args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly and spec['target']!='/var/lib/keycape-policy' else '')] + return args + + +PROVISION_PHASES=( + 'volume_creation_and_verification','worker_launch','worker_containment', + 'private_import','issuer_launch','issuer_containment','tls_transport_launch','complete') + + +def provision(session,payload,tls,issuer,phase=None): + """Inspect containment and driver options before the first private import.""" + from urllib.parse import urlsplit + address=urlsplit(issuer) + if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'): + raise adapter.Refused() + if phase is None:phase=lambda value:None + phase('volume_creation_and_verification') + for name,spec in session['volumes'].items(): + run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'], + '--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name]) + verify_volume(name,spec,session['run_id']) + phase('worker_launch') + args=base_args(session,session['worker'],'none')+mounts(session) + # This pinned helper declares an image VOLUME here. Mask it explicitly so + # Docker cannot allocate an unregistered disk-backed anonymous volume. + args+=['--tmpfs','/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700'] + args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1', + '--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)'] + run(args) + # Check helper and every private filesystem before touching the ciphertext's + # private contents. Issuer is created only after originals are present. + phase('worker_containment') + adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True) + phase('private_import') + files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}} + run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode()) + anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip() + phase('issuer_launch') + args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True) + # CLI argv carries environment names only. Original credentials stay in the + # owner subprocess environment and Docker's private container configuration. + env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'], + 'SSL_CERT_FILE':'/recovery-tls/trust.pem'} + for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name] + args+=[adapter.IMAGE] + run(args,env=env) + phase('issuer_containment') + adapter.inspect_session(session) + phase('tls_transport_launch') + run(['docker','exec','--detach',session['worker'],'python3','-c',TLS]) + phase('complete') + + +def cleanup_all(session,receipt): + failed=False + try:adapter.cleanup({**session,'containers':session['containers']+[session['public_image_probe']]},receipt) + except Exception:failed=True + rows=[] + for name,spec in session['volumes'].items(): + row={'volume':name,'status':'unverified'};rows.append(row) + try: + exists=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines() + if not exists: + row['status']='absence_verified' + continue + verify_volume(name,spec,session['run_id']) + run(['docker','volume','rm',name]) + remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines() + if remaining:row['status']='still_present';failed=True + else:row['status']='absence_verified' + except Exception:failed=True + receipt['tmpfs_volume_cleanup_obligations']=rows + receipt['container_cleanup_verified']=not failed + if failed:raise adapter.Refused() + +def register_workdir(work, run_id): + if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+run_id+'-[a-z0-9_]{8}',work.name):raise adapter.Refused() + if any(p.is_symlink() for p in (work,*work.parents)):raise adapter.Refused() + info=work.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid!=os.getuid() or stat.S_IMODE(info.st_mode)!=0o700:raise adapter.Refused() + return {'path':str(work),'run_id':run_id,'device':info.st_dev,'inode':info.st_ino,'owner_uid':info.st_uid} + + +def cleanup_workdir(staging, receipt): + """Delete only the registered inode below actual /dev/shm, never a replacement.""" + row={'path':staging['path'],'device':staging['device'],'inode':staging['inode'],'status':'unverified'} + receipt['plaintext_cleanup_obligation']=row + work=Path(staging['path']) + if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+staging['run_id']+'-[a-z0-9_]{8}',work.name):raise adapter.Refused() + if not any(line.split()[1:3]==['/dev/shm','tmpfs'] for line in Path('/proc/mounts').read_text().splitlines()):raise adapter.Refused() + if any(p.is_symlink() for p in work.parents):raise adapter.Refused() + parentfd=os.open(work.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) + try: + try:info=os.stat(work.name,dir_fd=parentfd,follow_symlinks=False) + except FileNotFoundError: + row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return + if (not stat.S_ISDIR(info.st_mode) or info.st_uid!=staging['owner_uid'] or info.st_uid!=os.getuid() + or info.st_dev!=staging['device'] or info.st_ino!=staging['inode'] or stat.S_IMODE(info.st_mode)!=0o700): + row['status']='identity_mismatch_not_removed';raise adapter.Refused() + def remove(parent,name,expected): + fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent) + try: + current=os.fstat(fd) + if (current.st_dev,current.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused() + for child in os.listdir(fd): + before=os.stat(child,dir_fd=fd,follow_symlinks=False) + if before.st_uid!=staging['owner_uid'] or before.st_dev!=staging['device']:raise adapter.Refused() + if stat.S_ISDIR(before.st_mode):remove(fd,child,before) + elif stat.S_ISREG(before.st_mode) and before.st_nlink==1:os.unlink(child,dir_fd=fd) + else:raise adapter.Refused() # symlinks/special files/hardlinks are uncertain, never followed + finally:os.close(fd) + after=os.stat(name,dir_fd=parent,follow_symlinks=False) + if (after.st_dev,after.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused() + os.rmdir(name,dir_fd=parent) + remove(parentfd,work.name,info) + try:os.stat(work.name,dir_fd=parentfd,follow_symlinks=False) + except FileNotFoundError: + row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return + raise adapter.Refused() + finally:os.close(parentfd) + + +def recover_obligations(session, staging, receipt): + failed=False + try:cleanup_all(session,receipt) + except Exception:failed=True + # The plaintext obligation is attempted even when Docker is unreachable. + try:cleanup_workdir(staging,receipt) + except Exception:failed=True + if failed:raise adapter.Refused() + + +def process_facts(pid): + raw=Path('/proc')/str(pid)/'stat' + try: + fields=raw.read_text().rsplit(')',1)[1].split() + return {'pid':pid,'state':fields[0],'pgid':int(fields[2]),'sid':int(fields[3]), + 'start_ticks':int(fields[19]),'uid':raw.parent.stat().st_uid} + except FileNotFoundError:return None + + +def isolate_execution(): + # Called by main before reading TLS/key inputs. Never reuse an inherited + # shell, custody-helper, user or unrelated process group. + os.setsid() + pid=os.getpid();facts=process_facts(pid) + if os.getsid(0)!=pid or os.getpgrp()!=pid or facts['sid']!=pid or facts['pgid']!=pid:raise adapter.Refused() + return {k:facts[k] for k in ('pid','pgid','sid','start_ticks','uid')}|{'deadline':time.monotonic()+1800} + + +def verify_execution(execution): + if execution['pid']!=execution['sid'] or execution['pid']!=execution['pgid'] or execution['uid']!=os.getuid():raise adapter.Refused() + current=process_facts(execution['pid']) + if current is not None and any(current[k]!=execution[k] for k in ('pid','pgid','sid','start_ticks','uid')): + # Numeric leader PID reuse is not authority to signal a new group. + raise adapter.Refused() + + +def group_members(execution): + verify_execution(execution) + members=[] + for path in Path('/proc').iterdir(): + if not path.name.isdecimal():continue + facts=process_facts(int(path.name)) + if facts is None or facts['pgid']!=execution['pgid']:continue + if facts['sid']!=execution['sid'] or facts['uid']!=execution['uid'] or facts['start_ticks']bundle.LIMIT+4*1024*1024:raise adapter.Refused() + root=work/'restored';root.mkdir(mode=0o700) + manifest,_=bundle.restore(archive,root) + # Data binding comes from exact reviewed ciphertext, never a new grant. + admission=adapter.private_manifest_admission(admission,manifest) + payload=adapter.private_payload(root,manifest,admission['manifest_sha256']) + import yaml + cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml'])) + if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused() + # Public executable readback uses the daemon archive API only on the + # original image rootfs, never a tmpfs or private user-created mount. + probe=session['public_image_probe'] + try: + run(['docker','create','--pull=never','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE]) + binary=adapter.container_file(probe,'/keycape') + if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused() + finally: + receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}] + run(['docker','rm',probe]) + if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused() + receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified' + def phase(value): + # Public enum only: no private payload, exception text or daemon logs. + if value not in PROVISION_PHASES:raise adapter.Refused() + receipt['provision_phase']=value + if checkpoint is not None:checkpoint(receipt) + provision(session,payload,tls,admission['issuer'],phase) + receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files'])) + checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'} + # Native checker consumes no service secret argument or new grant. + # Retry only bounded public readiness; no account/factor attempt. + for attempt in range(40): + try: + numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers() + adapter.check_jwks(session,checked_admission,numbers);break + except Exception: + if attempt==39:raise + time.sleep(.25) + adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False) + finally: + os.close(keyfd) + try: + if session is not None and staging is not None: + uncertain=daemon_uncertain(staging) + receipt['in_flight_daemon_request_uncertain']=uncertain + recover_obligations(session,staging,receipt) + if uncertain:raise adapter.Refused() + if reaper is not None: + reaper.terminate();reaper.wait(timeout=10) + except Exception: + receipt['status']='failed' + raise + finally: + adapter.EXECUTION=None + if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True + + +def persist_receipt(fd,receipt): + raw=(json.dumps(receipt,indent=2)+'\n').encode() + os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0) + with os.fdopen(os.dup(fd),'wb') as output: + output.write(raw);output.flush();os.fsync(output.fileno()) + + +def main(): + p=argparse.ArgumentParser(description=__doc__) + for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True) + args=p.parse_args();os.umask(0o077) + fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False} + try: + execution=isolate_execution() + raw=Path(args.admission).read_bytes() + if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused() + admission=json.loads(raw) + tls={} + for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)): + source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes() + execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt,execution,checkpoint=lambda r:persist_receipt(fd,r)) + except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed') + finally: + receipt['finished_at']=datetime.now(timezone.utc).isoformat() + try:persist_receipt(fd,receipt) + finally:os.close(fd) + print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.') + return int(receipt['status']!='isolated_keycape_native_registration_checked') + +if __name__=='__main__': + for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape.py new file mode 100644 index 00000000..4cc40e65 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-keycape.py @@ -0,0 +1,478 @@ +#!/usr/bin/env python3 +"""T18 native adapter for an owner-admitted, already restored isolated session. + +Called after identity-bundle.restore, never with an unvalidated directory. This +module does not provision TLS, grant credentials, or claim downstream acceptance. +The existing decrypt/tmpfs wrapper remains responsible for archive cleanup. +""" +import base64 +import hashlib +import importlib.util +import json +import io +import os +import secrets +import tarfile +import time +import re +import subprocess +from pathlib import Path + +HERE = Path(__file__).resolve().parent +IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010' +LABEL = 'net-kingdom.recovery-run' + + +class Refused(Exception): + pass + + +def load_bundle(): + spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def manifest_binding_mode(admission,transfer): + """Versioned private-manifest binding; legacy explicit digests stay valid.""" + mode=admission.get('manifest_binding_mode','explicit-digest-v1') + if mode not in ('explicit-digest-v1','ciphertext-derived-v1'):raise Refused() + if (not isinstance(admission.get('ciphertext_sha256'),str) + or not re.fullmatch('[a-f0-9]{64}',admission['ciphertext_sha256']) + or admission['ciphertext_sha256']!=transfer.get('ciphertext_sha256') + or transfer.get('independent_owner_retrieval_verified') is not True):raise Refused() + if mode=='explicit-digest-v1': + if not isinstance(admission.get('manifest_sha256'),str) or not re.fullmatch('[a-f0-9]{64}',admission['manifest_sha256']):raise Refused() + elif 'manifest_sha256' in admission:raise Refused() + return mode + + +def private_manifest_admission(admission,manifest): + # Only called after the fixed ciphertext bytes were hashed, authenticated + # decryption succeeded and bundle.restore checked the complete archive. + # Never return this enriched private object in a safe receipt/checkpoint. + digest=hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest() + mode=admission.get('manifest_binding_mode','explicit-digest-v1') + if mode=='explicit-digest-v1': + if digest!=admission['manifest_sha256']:raise Refused() + elif mode!='ciphertext-derived-v1' or 'manifest_sha256' in admission:raise Refused() + return {**admission,'manifest_sha256':digest} + + +def private_payload(root, manifest, expected_manifest_sha256): + """Bind the complete validated archive before selecting KeyCape material. + + Private hashes never enter the public receipt. The digest is supplied by the + existing private archive admission, not inferred from an untrusted manifest. + """ + encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode() + if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256: + raise Refused() + entries = manifest['files'] + if len(entries) != len({e['path'] for e in entries}): + raise Refused() + values = {} + bundle = load_bundle() + for entry in entries: + if not bundle.allowed(entry['path'], manifest['profile']): + raise Refused() + path = root / entry['path'] + bundle.private(path) + value = path.read_bytes() + if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']: + raise Refused() + values[entry['path']] = value + deployment = json.loads(values['resources/sso/deployment/keycape.json']) + main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape') + if main.get('command') or main.get('args') or main.get('envFrom'): + raise Refused() + if main['image'] != IMAGE: + # Mutable-tag/historical mismatches need a separate explicit owner admission. + raise Refused() + secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data'] + files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True) + for name in ('config.yaml', 'key.pem')} + for path, value in values.items(): + if path.startswith('data/keycape/'): + relative = path.removeprefix('data/keycape/') + files['/var/lib/keycape-policy/' + relative] = value + if '/var/lib/keycape-policy/policy.json' not in files: + raise Refused() + if 'resources/sso/secret/keycape-factor-read.json' in values: + factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data'] + files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True) + # Resolve original delivered environment, including all referenced secrets. + env = {} + for item in main.get('env', []): + if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused() + if item['name'] in env: + raise Refused() + if 'value' in item: + env[item['name']] = item['value'] + else: + ref = item['valueFrom']['secretKeyRef'] + source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data'] + env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode() + if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json': + raise Refused() + return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env} + + +ENDPOINT = 'unix:///var/run/docker.sock' +EXECUTION = None + + +def docker_environment(environment=None): + # Even an admitted daemon exposed by another Unix socket is outside this + # exact session binding. Conflicting ambient routing is refused, not ignored. + for source in (os.environ, environment or {}): + if source.get('DOCKER_HOST') not in (None,'',ENDPOINT):raise Refused() + if source.get('DOCKER_CONTEXT') not in (None,'','default'):raise Refused() + if any(source.get(k) for k in ('DOCKER_TLS_VERIFY','DOCKER_CERT_PATH')):raise Refused() + env=dict(os.environ if environment is None else environment) + for key in ('DOCKER_CONTEXT','DOCKER_TLS_VERIFY','DOCKER_CERT_PATH'):env.pop(key,None) + env['DOCKER_HOST']=ENDPOINT + return env + + +def process(argv, **kwargs): + marker=None + if EXECUTION is not None and (kwargs.get('start_new_session') or kwargs.get('process_group') not in (None,-1)):raise Refused() + if argv[0]=='docker': + argv=['docker','--host',ENDPOINT,*argv[1:]] + kwargs['env']=docker_environment(kwargs.get('env')) + if EXECUTION is not None: + if (os.getpid()!=EXECUTION['pid'] or os.getsid(0)!=EXECUTION['sid'] or os.getpgrp()!=EXECUTION['pgid'] + or time.monotonic()>=EXECUTION['deadline']):raise Refused() + marker=Path(EXECUTION['staging']+'-docker-inflight') + fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + os.close(fd) + try: + result=subprocess.run(argv,**kwargs) + if marker is not None and result.returncode!=0: + fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd) + return result + except BaseException: + # Parent loss leaves .docker-inflight; caught timeout/transport failures + # retain a sticky uncertainty marker even if later cleanup can run. + if marker is not None: + fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd) + raise + finally: + if marker is not None: + try:marker.unlink() + except FileNotFoundError:pass + + +def command(argv, **kwargs): + maximum = kwargs.pop('max_output', 32768) + result = process(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + timeout=kwargs.pop('timeout', 180), **kwargs) + if result.returncode or len(result.stdout) > maximum: + raise Refused() + return result.stdout + + +def inspect_session(session, worker_only=False): + """No credential environment is read by Docker inspect or returned to caller.""" + if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']): + raise Refused() + names = session['containers'] + if not names or len(names) > 12 or len(set(names)) != len(names): + raise Refused() + for name in names: + if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name): + raise Refused() + if session['worker'] not in names or session['issuer'] not in names: + raise Refused() + import os + contexts=json.loads(command(['docker','context','inspect'])) + endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST') or ENDPOINT] + if not all(x==ENDPOINT for x in endpoints):raise Refused() + info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'])) + if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}: + raise Refused() + # The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace. + anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip() + for name,spec in session.get('volumes',{}).items(): + volume=json.loads(command(['docker','volume','inspect',name])) + expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'} + if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused() + for name in names: + obj = json.loads(command(['docker', 'container', 'inspect', name, '--format', + '{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}'])) + host = obj['host'] + mounts=obj['mounts'] + destinations=[m['Destination'] for m in mounts] + if len(destinations)!=len(set(destinations)):raise Refused() + for volume,expected_spec in session.get('volumes',{}).items(): + matches=[m for m in mounts if m['Destination']==expected_spec['target']] + if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker'] or expected_spec['target']=='/var/lib/keycape-policy'):raise Refused() + if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root') + or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings') + or host.get('PidMode') or host.get('IpcMode') == 'host' + or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host' + or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {}) + or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom') + or host.get('CapDrop') != ['ALL'] + or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true']) + or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts']) + or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor) + or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']): + raise Refused() + if worker_only:return + if session['images'][session['issuer']] != IMAGE: + raise Refused() + # Known admitted archive has no command/args override; image default /keycape + # must be the actual issuer executable, not an alternate same-image command. + launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format', + '{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}'])) + if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}: + raise Refused() + + +def container_file(container, path): + # Docker streams one tar member into memory; never materialize private files. + raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024) + with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive: + members = archive.getmembers() + if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024: + raise Refused() + return archive.extractfile(members[0]).read() + + +JWKS_PROBE = r""" +import base64,ipaddress,json,socket,ssl,sys,urllib.request +from urllib.parse import urlsplit +p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer) +def require(condition): + if not condition:raise ValueError('public_jwks_binding_refused') +require(origin.scheme=='https') +resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM) +require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved)) +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self,*args,**kwargs):return None +opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(), + urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust']))) +def get(url): + u=urlsplit(url) + require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443)) + require(not u.username and not u.password and not u.fragment) + with opener.open(url,timeout=10) as response: + raw=response.read(1024*1024+1);require(len(raw)<=1024*1024) + return json.loads(raw) +d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer) +keys=get(d['jwks_uri'])['keys'] +def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big') +# The exact admitted owner release constructs one key, kid key-1. Reject every +# extra key, including irrelevant keys: a native successful exchange can then +# only have used the original signer through the existing verifier. +require(isinstance(keys,list) and len(keys)==1) +k=keys[0] +require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256') +require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e'])) +print('{"original_public_signing_key_in_live_jwks":true}') +""" + +# A failed public readiness probe is closed data, not a failing Docker RPC. +# Transport/CLI nonzero remains uncertain; no deliberately failing exec is +# retried as if daemon acceptance had been established. +JWKS_TRANSPORT = ("import contextlib,io\ntry:\n" + " with contextlib.redirect_stdout(io.StringIO()):\n" + " exec(" + repr(JWKS_PROBE) + ")\n" + "except Exception:\n print('{\"original_public_signing_key_in_live_jwks\":false}')\n" + "else:\n print('{\"original_public_signing_key_in_live_jwks\":true}')\n") + + +def check_jwks(session, admission, public_numbers): + trust=runtime_file(session,admission['trust_path']).decode() + request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)} + result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_TRANSPORT],input=json.dumps(request).encode())) + if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused() + + +RUNTIME_FILES = r""" +import base64,json,sys +from pathlib import Path +p=json.load(sys.stdin) +roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/') +result={} +paths=[] +if p.get('tree'): + paths=list(Path('/var/lib/keycape-policy').rglob('*')) +else:paths=[Path(x) for x in p['paths']] +for f in paths: + if f.is_dir():continue + if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError() + result[str(f)]=base64.b64encode(f.read_bytes()).decode() + if len(result)>4096:raise ValueError() +print(json.dumps(result)) +""" + + +def runtime_files(session, paths=(), tree=False): + raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES], + input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024) + return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()} + + +def runtime_file(session,path): + return runtime_files(session,[path])[path] + + +def run_verifier(session, admission, payload): + issuer = session['issuer'] + if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused() + from urllib.parse import urlsplit + import ipaddress + address = urlsplit(admission['issuer']) + if (address.scheme != 'https' or not address.hostname or address.username or address.password + or address.query or address.fragment): + raise Refused() + if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']: + raise Refused() + if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']: + raise Refused() + # Capture private environment only in owner process memory, never receipt. + environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}'])) + env = dict(item.split('=',1) for item in environment) + if len(env)!=len(environment):raise Refused() + if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused() + if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()): + raise Refused() + if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused() + for name,value in payload['env'].items(): + if env.get(name) != value: + raise Refused() + files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()} + def equal(): + if runtime_files(session,files)!=files:raise Refused() + durable=runtime_files(session,tree=True) + if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}: + raise Refused() + equal() + # Reading YAML is data parsing only; no archived executable is evaluated. + import yaml + cfg = yaml.safe_load(files['/etc/keycape/config.yaml']) + if cfg['issuer'] != admission['issuer']: + raise Refused() + consumer = admission['consumer'] + client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id']) + if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential' + or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])): + raise Refused() + from cryptography.hazmat.primitives import serialization + key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None) + check_jwks(session,admission,key.public_key().public_numbers()) + wrong = secrets.token_urlsafe(48) + if wrong == env[consumer['secret_env']]:raise Refused() + args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client', + '-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET'] + flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope', + 'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'} + for name,flag in flags.items():args+=['-'+flag,consumer[name]] + # Native command reports on stderr. Do not return or print its raw response. + result = process(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150) + raw = (result.stdout+result.stderr).decode() + required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings', + 'principal_type is service','sub matches the registration','tenant matches the registration','roles present', + 'granted scope carries no excess grant','predecessor secret refused'] + required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()] + if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw: + raise Refused() + equal() + from cryptography.hazmat.primitives import serialization + key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None) + check_jwks(session,admission,key.public_key().public_numbers()) + public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo) + return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True, + 'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True, + 'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(), + 'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True} + + +def cleanup(session, receipt=None): + """Attempt every owned cleanup even when one obligation fails. + + Preserve exact safe container names and outcomes so the owner can recover an + uncertain/partial cleanup. An inspect/daemon failure never means absence. + """ + obligations = [] + failed = False + for name in reversed(session['containers']): + row = {'container': name, 'status': 'unverified'} + obligations.append(row) + try: + present=command(['docker','container','ls','--all','--filter','name=^/'+name+'$','--format','{{.Names}}']).decode().splitlines() + if not present: + row['status']='absence_verified' + continue + label = command(['docker', 'container', 'inspect', name, '--format', + '{{index .Config.Labels "' + LABEL + '"}}']).decode().strip() + if label != session['run_id']: + row['status'] = 'ownership_mismatch_not_removed' + failed = True + continue + # A failed remove is followed by a successful explicit inventory; + # interrupted remove may actually have completed. + try: + command(['docker', 'rm', '--force', '--volumes', name]) + except Exception: + pass + remaining = command(['docker','container','ls','--all','--filter', + 'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines() + if remaining: + row['status'] = 'still_present' + failed = True + else: + row['status'] = 'absence_verified' + except Exception: + failed = True + if receipt is not None: + receipt['container_cleanup_obligations'] = obligations + if failed: + raise Refused() + + + +def native_checks(root, manifest, admission, receipt, cleanup_session=True): + """Owner wrapper provides a reviewed session after existing restore pipeline. + + Admission must bind ciphertext, canonical private manifest digest, executable + and trust bytes, exact image/target list, original issuer and selected consumer. + None of these admission fields grant authority or carry a client secret. + """ + session = admission['session'] + # Validate ownership/containment before permitting either exec or cleanup. + inspect_session(session) + receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False, + human_mfa_proven=False, downstream_consumer_reconnect_proven=False, + container_cleanup_verified=False) + try: + for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'): + if not re.fullmatch('[a-f0-9]{64}', admission[field]): + raise Refused() + for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'): + if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip(): + raise Refused() + payload = private_payload(root, manifest, admission['manifest_sha256']) + result = run_verifier(session, admission, payload) + required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused', + 'denied_scopes_refused','original_config_registry_policy_receipts_equal', + 'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'} + if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')): + raise Refused() + # Closed receipt allowlist: never copy arbitrary worker output. + receipt.update({k: True for k in required}) + receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256'] + receipt['status'] = 'isolated_keycape_native_registration_checked' + finally: + try: + if cleanup_session: + cleanup(session, receipt) + receipt['container_cleanup_verified'] = True + except Exception: + receipt['status'] = 'failed' + raise diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-lldap.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-lldap.py new file mode 100644 index 00000000..abe0865c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-lldap.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Owner-run native LLDAP restore from the verified identity bundle. + +Private inputs remain in the owner process/pipe. Decrypted staging uses Linux +tmpfs. Containers have no network, ports, host mounts or production API access. +""" +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import signal +import sqlite3 +import subprocess +import tarfile +import tempfile +import time +import tomllib +import uuid + +HERE = Path(__file__).resolve().parent + + +def load(name): + spec = importlib.util.spec_from_file_location(name, HERE / (name + '.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +verify = load('verify-identity') +offline = load('retrieve-identity-offline') +bundle = verify.bundle +IMAGE = 'docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d' +CLIENT = 'ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473' +ENGINE_ID = '74f93783-6cef-43a8-a3c4-f62586cd3bc1' +ENGINE_NAME = 'bnt-lap001' + + +def command(args, **kwargs): + result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + timeout=kwargs.pop('timeout', 90), **kwargs) + if result.returncode: + raise bundle.Refused() + return result.stdout + + +def preflight(): + contexts = json.loads(command(['docker', 'context', 'inspect'])) + endpoints = [contexts[0]['Endpoints']['docker']['Host']] + if os.environ.get('DOCKER_HOST'): + endpoints.append(os.environ['DOCKER_HOST']) + if not all(url.startswith('unix://') for url in endpoints): + raise bundle.Refused() + facts = json.loads(command(['docker', 'info', '--format', + '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'])) + if facts['ID'] != ENGINE_ID or facts['Name'] != ENGINE_NAME or facts['OSType'] != 'linux': + raise bundle.Refused() + # /dev/shm must actually be tmpfs, not a similarly named disk directory. + mounts = Path('/proc/mounts').read_text().splitlines() + if not any(line.split()[1:3] == ['/dev/shm', 'tmpfs'] for line in mounts): + raise bundle.Refused() + images = [] + for image in (IMAGE, CLIENT): + image_id = command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image]).decode().strip() + images.append({'reference': image, 'image_id': image_id}) + return {'engine_id': facts['ID'], 'engine_name': facts['Name'], 'images': images, + 'production_contacted': False, 'network_mode': 'none', 'host_ports_published': False} + + +def snapshot(path): + # Include all fields of durable identity tables, including stored password + # hashes. Only counts and the equality boolean leave this process. + result = {} + with sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) as db: + if db.execute('PRAGMA integrity_check').fetchall() != [('ok',)]: + raise bundle.Refused() + tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")} + for name in ('users', 'groups', 'memberships', 'user_attributes', 'group_attributes', + 'user_attribute_values', 'group_attribute_values', + 'user_attribute_schema', 'group_attribute_schema'): + if name in tables: + rows = db.execute('SELECT * FROM "' + name + '"').fetchall() + encoded = sorted(repr(row) for row in rows) + result[name] = {'count': len(rows), 'digest': hashlib.sha256('\n'.join(encoded).encode()).digest()} + if not {'users', 'groups', 'memberships'} <= result.keys(): + raise bundle.Refused() + return result + + +def configuration(root): + data = root / 'data/lldap' + existing = data / 'lldap_config.toml' + config = tomllib.loads(existing.read_text()) if existing.is_file() else {} + deployment = bundle.resource(root, 'sso', 'deployment', 'lldap') + main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'lldap') + env = {e['name']: e['value'] for e in main.get('env', []) if 'value' in e} + secret = bundle.resource(root, 'sso', 'secret', 'lldap-secrets')['data'] + jwt = base64.b64decode(secret['LLDAP_JWT_SECRET'], validate=True) + password = base64.b64decode(secret['LLDAP_LDAP_USER_PASS'], validate=True) + if not jwt or not password or b'\n' in jwt or b'\n' in password: + raise bundle.Refused() + base = env.get('LLDAP_LDAP_BASE_DN', config.get('ldap_base_dn')) + user = env.get('LLDAP_LDAP_USER_DN', config.get('ldap_user_dn', 'admin')) + if not isinstance(base, str) or not base or not isinstance(user, str): + raise bundle.Refused() + chosen = {'database_url': 'sqlite:///data/users.db?mode=rwc', + 'ldap_base_dn': base, 'ldap_user_dn': user, + 'ldap_host': '127.0.0.1', 'http_host': '127.0.0.1', + 'ldap_port': 3890, 'http_port': 17170, + 'force_ldap_user_pass_reset': False, 'force_update_private_key': False} + seed = env.get('LLDAP_KEY_SEED', config.get('key_seed')) + if seed: + if not isinstance(seed, str): + raise bundle.Refused() + chosen['key_seed'] = seed + else: + value = env.get('LLDAP_KEY_FILE', config.get('key_file', '/data/private_key')) + key = PurePosixPath(value) + if not key.is_absolute(): + key = PurePosixPath('/data') / key + if key.parent != PurePosixPath('/data') or not (data / key.name).is_file(): + raise bundle.Refused() + chosen['key_file'] = str(key) + # JSON strings are valid TOML basic strings for these service inputs. + raw = '\n'.join(k + ' = ' + json.dumps(v) for k, v in chosen.items()) + raw += '\n[smtp_options]\nenable_password_reset = false\n[ldaps_options]\nenabled = false\n' + return raw.encode(), jwt, password, base + + +def archive_files(files): + out = io.BytesIO() + with tarfile.open(fileobj=out, mode='w') as archive: + for name, value in files.items(): + if PurePosixPath(name).is_absolute() or '..' in PurePosixPath(name).parts: + raise bundle.Refused() + entry = tarfile.TarInfo(name) + entry.size = len(value) + entry.mode = 0o600 + entry.uid = entry.gid = 1000 + archive.addfile(entry, io.BytesIO(value)) + return out.getvalue() + + +PROBE = r''' +import contextlib,io,json,logging,socket,sys,time +logging.disable(logging.CRITICAL) +result={'status':'failed'} +stage='network_isolation' +try: + with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()): + from pathlib import Path + assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'} + assert len(Path('/proc/net/route').read_text().splitlines())==1 + stage='native_client_import' + from ldap3 import Server,Connection,NONE + from ldap3.utils.dn import escape_rdn + values=json.load(sys.stdin) + stage='directory_startup' + for _ in range(60): + try: + with socket.create_connection(('127.0.0.1',3890),timeout=1):pass + break + except OSError:time.sleep(.25) + else:raise RuntimeError() + dn='uid='+escape_rdn(values['user'])+',ou=people,'+values['base'] + server=Server('127.0.0.1',port=3890,get_info=NONE,connect_timeout=3) + stage='existing_user_bind' + connection=Connection(server,user=dn,password=values['password'],auto_bind=True,raise_exceptions=True) + assert connection.bound + connection.unbind() + stage='wrong_password_bind' + wrong=Connection(server,user=dn,password=values['wrong_password'],auto_bind=False,raise_exceptions=False) + refused=not wrong.bind() and wrong.result.get('result')==49 + wrong.unbind() + assert refused + result={'status':'verified','existing_user_native_ldap_bind':True,'wrong_password_refused':True, + 'only_loopback_interface_verified':True,'external_routes_absent':True} +except Exception as error:result={'status':'failed','failed_stage':stage,'failure_type':type(error).__name__} +print(json.dumps(result)) +''' + + +def restore_lldap(root, user, password, receipt): + before = snapshot(root / 'data/lldap/users.db') + config, jwt, admin_password, base = configuration(root) + name = 'nk49-lldap-' + uuid.uuid4().hex + client_name = name + '-client' + try: + command(['docker', 'run', '--detach', '--pull=never', '--name', name, + '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', '1000:1000', + '--memory', '256m', '--cpus', '1', '--pids-limit', '128', + '--tmpfs', '/data:rw,nosuid,nodev,noexec,size=268435456,uid=1000,gid=1000,mode=0700', + '--tmpfs', '/run/recovery:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000', + '--env', 'LLDAP_JWT_SECRET_FILE=/run/recovery/jwt', + '--env', 'LLDAP_LDAP_USER_PASS_FILE=/run/recovery/admin-password', + '--entrypoint', '/bin/sh', IMAGE, '-c', 'sleep 1800']) + guard = json.loads(command(['docker', 'container', 'inspect', name, '--format', + '{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}'])) + host = guard['host'] + if (host['NetworkMode'] != 'none' or not host['ReadonlyRootfs'] or host['Privileged'] + or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', []) + or 'no-new-privileges' not in host.get('SecurityOpt', []) + or guard['user'] != '1000:1000' + or any(mount['Type'] != 'tmpfs' for mount in guard['mounts'])): + raise bundle.Refused() + receipt['container_isolation_verified_before_private_import'] = True + payload = {p.relative_to(root / 'data/lldap').as_posix(): p.read_bytes() + for p in (root / 'data/lldap').rglob('*') if p.is_file()} + command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/data'], input=archive_files(payload)) + command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/recovery'], + input=archive_files({'lldap.toml': config, 'jwt': jwt, 'admin-password': admin_password})) + # No credential values in Docker configuration, environment or argv. + command(['docker', 'exec', '--detach', name, '/bin/sh', '-c', + 'echo $$ > /run/recovery/server.pid; exec /app/lldap run --config-file /run/recovery/lldap.toml > /dev/null 2>&1']) + raw = command(['docker', 'run', '--interactive', '--rm', '--name', client_name, '--pull=never', '--network', 'container:' + name, + '--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', + '--memory', '256m', '--cpus', '1', '--pids-limit', '64', + '--entrypoint', 'python3', CLIENT, '-c', PROBE], + input=json.dumps({'user': user, 'password': password, 'base': base, + 'wrong_password': 'nk49-wrong-' + uuid.uuid4().hex}).encode()) + result = json.loads(raw) + receipt.update(result) + if result.get('status') != 'verified': + raise bundle.Refused() + command(['docker', 'exec', name, '/bin/sh', '-c', 'kill -TERM "$(cat /run/recovery/server.pid)"']) + for _ in range(60): + running = subprocess.run(['docker', 'exec', name, '/bin/sh', '-c', + 'kill -0 "$(cat /run/recovery/server.pid)"'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10) + if running.returncode: + break + time.sleep(.1) + else: + raise bundle.Refused() + raw = command(['docker', 'exec', name, '/bin/sh', '-c', + 'test ! -s /data/users.db-wal && cat /data/users.db']) + after_path = root / 'post-restore-users.db' + after_path.write_bytes(raw) + if snapshot(after_path) != before: + raise bundle.Refused() + receipt.update(directory_identity_rows_preserved=True, + directory_comparison_scope=sorted(before), + directory_table_counts={k: v['count'] for k, v in before.items()}) + finally: + # A failed create can still have created a container (e.g. lost reply). + for owned in (client_name, name): + found = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=15) + if found.returncode == 0: + command(['docker', 'rm', '--force', '--volumes', owned]) + absent = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=15) + command(['docker', 'info', '--format', '{{.ID}}']) + if absent.returncode == 0: + raise bundle.Refused() + receipt['container_cleanup_verified'] = True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--transfer-receipt', required=True) + parser.add_argument('--receipt', required=True) + parser.add_argument('--user', required=True) + args = parser.parse_args() + os.umask(0o077) + fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + result = {'schema': 'netkingdom.isolated-lldap-recovery.v1', 'status': 'failed', + 'production_contacted': False, 'secret_values_exported': False, + 'container_cleanup_verified': False, 'temporary_plaintext_cleanup_verified': False, + 'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()} + keyfd = None + work = None + stage = 'target_preflight' + try: + result['target'] = preflight() + stage = 'input_validation' + if not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}', args.user): + raise bundle.Refused() + transfer = json.loads(Path(args.transfer_receipt).read_text()) + offline.validate(transfer) + if transfer.get('independent_owner_retrieval_verified') is not True: + raise bundle.Refused() + path = Path(transfer['fetched_path']) + bundle.private(path, max_bytes=bundle.LIMIT) + if path.stat().st_size != transfer['ciphertext_bytes'] or offline.offsite.digest(path) != transfer['ciphertext_sha256']: + raise bundle.Refused() + keyfd = verify.custody.private_key_fd() + derived = command(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip() + if derived != bundle.profile()['public_recipient'].encode(): + raise bundle.Refused() + stage = 'private_existing_password_prompt' + password = offline.private_prompt('Existing account password for the isolated LLDAP check: ') + with tempfile.TemporaryDirectory(prefix='nk49-service-recovery-', dir='/dev/shm') as temporary: + work = Path(temporary) + archive = work / 'archive.tar' + stage = 'archive_decryption' + with archive.open('xb') as output: + proc = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}', str(path)], + stdout=output, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), timeout=180) + if proc.returncode or archive.stat().st_size > bundle.LIMIT + 4 * 1024 * 1024: + raise bundle.Refused() + restored = work / 'restored' + restored.mkdir(mode=0o700) + stage = 'archive_validation' + manifest, _ = bundle.restore(archive, restored) + result.update(ciphertext_sha256=transfer['ciphertext_sha256'], + manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True) + stage = 'native_directory_check' + restore_lldap(restored, args.user, password, result) + password = None + if work.exists(): + raise bundle.Refused() + result.update(status='isolated_lldap_verified', temporary_plaintext_cleanup_verified=True) + except (Exception, KeyboardInterrupt): + result.update(status='failed', error='isolated_lldap_check_failed_private_diagnostics_suppressed') + result.setdefault('failed_stage', stage) + finally: + if keyfd is not None: + os.close(keyfd) + if work is not None and not work.exists(): + result['temporary_plaintext_cleanup_verified'] = True + result['finished_at'] = datetime.now(timezone.utc).isoformat() + with os.fdopen(fd, 'w') as output: + json.dump(result, output, indent=2) + output.write('\n') + success = result['status'] == 'isolated_lldap_verified' + print('OK: restored LLDAP existing-user bind, wrong-password refusal, directory preservation and cleanup passed.' + if success else 'ERROR: isolated LLDAP recovery refused; review the safe receipt. No production change.') + return 0 if success else 1 + + +if __name__ == '__main__': + for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP): + signal.signal(sig, offline.interrupted) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-privacyidea.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-privacyidea.py new file mode 100644 index 00000000..a0862290 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/isolated-privacyidea.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Private native privacyIDEA token-material and audit recovery checks. + +This does not authenticate a user's MFA code. It restores only a disposable +database, decrypts token material internally and verifies stored audit rows. +""" +import argparse +import ast +import base64 +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import signal +import subprocess +import tempfile +import time +import uuid + +# Import the already reviewed local target/transport boundary without changing +# the accepted LLDAP executor or its source digest. +import importlib.util +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('isolated_lldap', HERE / 'isolated-lldap.py') +local = importlib.util.module_from_spec(spec) +spec.loader.exec_module(local) +bundle = local.bundle +IMAGE = local.CLIENT +POSTGRES = local.verify.pg.IMAGE + + +def run(args, **kwargs): + return local.command(args, **kwargs) + + +def preflight(): + target = local.preflight() + target['images'].append({'reference': POSTGRES, + 'image_id': run(['docker', 'image', 'inspect', '--format', '{{.Id}}', POSTGRES]).decode().strip()}) + return target + + +def isolation(name, expected_network, user): + info = json.loads(run(['docker', 'container', 'inspect', name, '--format', + '{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}'])) + host = info['host'] + if (host['NetworkMode'] != expected_network or not host['ReadonlyRootfs'] or host['Privileged'] + or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', []) + or 'no-new-privileges' not in host.get('SecurityOpt', []) or info['user'] != user + or any(m['Type'] != 'tmpfs' for m in info['mounts'])): + raise bundle.Refused() + + +def cleanup(names): + for name in names: + present = subprocess.run(['docker', 'container', 'inspect', name], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15) + if present.returncode == 0: + run(['docker', 'rm', '--force', '--volumes', name]) + absent = subprocess.run(['docker', 'container', 'inspect', name], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15) + run(['docker', 'info', '--format', '{{.ID}}']) + if absent.returncode == 0: + raise bundle.Refused() + + +def start_database(name): + run(['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', 'none', + '--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', + '--user', '999:999', '--memory', '2g', '--cpus', '2', '--pids-limit', '128', + '--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=1073741824,uid=999,gid=999,mode=0700', + '--tmpfs', '/var/run/postgresql:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999', + '--env', 'POSTGRES_HOST_AUTH_METHOD=trust', '--env', 'POSTGRES_DB=recovery', + '--entrypoint', '/usr/bin/timeout', POSTGRES, '--kill-after=15s', '1800s', + '/usr/local/bin/docker-entrypoint.sh', 'postgres', '-c', 'listen_addresses=127.0.0.1']) + isolation(name, 'none', '999:999') + for _ in range(80): + # The entrypoint's temporary init server has TCP disabled. Require TCP + # readiness so we cannot mistake it for the final local-only server. + ready = subprocess.run(['docker', 'exec', name, 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'recovery'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10) + if ready.returncode == 0: + return + time.sleep(.25) + raise bundle.Refused() + + +def start_worker(name, database, root): + secret = bundle.resource(root, 'mfa', 'secret', 'privacyidea-config')['data'] + config = {'SQLALCHEMY_DATABASE_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery', + 'PI_AUDIT_SQL_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery', + 'SECRET_KEY': base64.b64decode(secret['PI_SECRET_KEY'], validate=True).decode(), + 'PI_PEPPER': base64.b64decode(secret['PI_PEPPER'], validate=True).decode(), + 'PI_ENCFILE': '/recovery/enckey', 'PI_AUDIT_KEY_PRIVATE': '/recovery/private.pem', + 'PI_AUDIT_KEY_PUBLIC': '/recovery/public.pem', 'PI_AUDIT_NO_SIGN': False, + 'PI_CHECK_OLD_SIGNATURES': legacy_setting(root), 'PI_NO_RESPONSE_SIGN': True, + 'PI_LOGFILE': '/recovery/runtime.log', 'PI_LOGLEVEL': 50, + 'PI_UUID_FILE': '/recovery/node-uuid', + 'PI_TRUSTED_JWT': []} + # This deliberately does not execute archived pi.cfg or reuse its live DB URI. + payload = {'pi.cfg': '\n'.join(k + '=' + repr(v) for k, v in config.items()).encode()} + for key in ('enckey', 'private.pem', 'public.pem'): + payload[key] = (root / 'data/privacyidea' / key).read_bytes() + run(['docker', 'run', '--detach', '--pull=never', '--name', name, + '--network', 'container:' + database, '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', '1000:1000', + '--memory', '1g', '--cpus', '2', '--pids-limit', '128', + '--tmpfs', '/recovery:rw,nosuid,nodev,noexec,size=33554432,uid=1000,gid=1000,mode=0700', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000', + '--tmpfs', '/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700', + '--env', 'PRIVACYIDEA_CONFIGFILE=/recovery/pi.cfg', + '--entrypoint', 'python3', IMAGE, '-c', 'import time;time.sleep(1800)']) + # Docker normalizes container: to container: in HostConfig. + database_id = run(['docker', 'container', 'inspect', database, '--format', '{{.Id}}']).decode().strip() + isolation(name, 'container:' + database_id, '1000:1000') + run(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/recovery'], + input=local.archive_files(payload)) + + +def legacy_setting(root): + path = root / 'resources/mfa/configmap/privacyidea-cfg.json' + if not path.exists(): + return False # Only the lower-level synthetic fixture omits ConfigMaps. + raw = bundle.resource(root, 'mfa', 'configmap', 'privacyidea-cfg')['data']['pi.cfg'] + selected = False + for node in ast.parse(raw).body: + if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'PI_CHECK_OLD_SIGNATURES' for t in node.targets): + if not isinstance(node.value, ast.Constant) or type(node.value.value) is not bool: + raise bundle.Refused() + selected = node.value.value + return selected + + +PROBE = r''' +import contextlib,io,json,logging,os,sys +logging.disable(logging.CRITICAL) +os.umask(0o077) +result={'status':'failed'} +stage='network_isolation' +try: + with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()): + from pathlib import Path + assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'} + assert len(Path('/proc/net/route').read_text().splitlines())==1 + stage='native_application_configuration' + from privacyidea.app import create_app + app=create_app(config_name='testing',config_file='/recovery/pi.cfg',silent=True) + from privacyidea.models import db,Token + from privacyidea.lib.security.default import DefaultSecurityModule + from privacyidea.lib.auditmodules.sqlaudit import Audit,LogEntry + from privacyidea.lib.crypto import Sign + import binascii + with app.app_context(): + assert str(db.engine.url)=='postgresql+psycopg2://postgres@127.0.0.1:5432/recovery' + stage='retained_token_decryption' + security=DefaultSecurityModule({'file':'/recovery/enckey'}) + wrong_path=Path('/recovery/wrong-enckey');wrong_path.write_bytes(os.urandom(96)) + wrong=DefaultSecurityModule({'file':str(wrong_path)}) + total=Token.query.count() + assert 00 + result.update(retained_tokens_total=total,retained_tokens_with_native_decryption=checked, + different_key_cannot_reproduce_material_count=negative, + retained_token_material_decrypted_inside_process=True,existing_token_mfa_proven=False) + stage='historical_audit_verification' + audit=Audit(app.config) + allow_legacy=bool(app.config['PI_CHECK_OLD_SIGNATURES']) + historical=audit.session.query(LogEntry).count() + assert 0 bundle.LIMIT + 4 * 1024 * 1024: + raise bundle.Refused() + restored = work / 'restored';restored.mkdir(mode=0o700) + stage = 'archive_validation' + manifest, _ = bundle.restore(archive, restored) + result.update(ciphertext_sha256=transfer['ciphertext_sha256'], + manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True) + stage = 'native_privacyidea_checks' + native_checks(restored, result) + result['status'] = 'isolated_privacyidea_checked' + except (Exception, KeyboardInterrupt): + result.update(status='failed', error='isolated_privacyidea_check_failed_private_diagnostics_suppressed') + result.setdefault('failed_stage', stage) + finally: + if keyfd is not None:os.close(keyfd) + if work is not None and not work.exists():result['temporary_plaintext_cleanup_verified'] = True + result['finished_at'] = datetime.now(timezone.utc).isoformat() + with os.fdopen(fd, 'w') as output: + json.dump(result, output, indent=2);output.write('\n') + if result['status'] == 'failed': + print('ERROR: isolated privacyIDEA check failed; review safe receipt. No production change.') + return 1 + if not result['audit_signature_continuity_proven']: + print('REVIEW: token-material checks completed; historical audit exceptions require owner review. Cleanup passed; see safe receipt.') + return 2 + print('OK: retained token material decrypted internally; historical/new audit signatures and negative checks passed; cleanup passed. Existing-token MFA remains separate.') + return 0 + + +if __name__ == '__main__': + for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):signal.signal(sig, local.offline.interrupted) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-identity-coverage.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-identity-coverage.py new file mode 100644 index 00000000..4d946eff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-identity-coverage.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Prepare exact identity backup coverage from read-only workload/PVC metadata. + +This produces a proposal, never reads Secrets, executes in pods, changes cluster +resources, captures private files, uploads, schedules or grants credentials. +""" +import argparse +from datetime import datetime,timezone +import hashlib +import json +import os +from pathlib import Path +import subprocess + +ROOT=Path(__file__).resolve().parents[2] +CLUSTER_UID='a553c742-0115-43d4-99a4-a5ca56fe0786' +PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json' + + +class Refused(Exception):pass + + +def query(namespace,kind,name): + # Closed read-only types; Secret/ConfigMap data and arbitrary API paths have + # no transport in this preparation command. + if kind not in {'namespace','deployment','pvc'}:raise Refused() + args=['kubectl','--request-timeout=20s'] + if namespace:args+=['-n',namespace] + r=subprocess.run(args+['get',kind,name,'-o','json'],capture_output=True,timeout=25) + if r.returncode:raise Refused() + return json.loads(r.stdout) + + +def references(spec): + secrets=set();configmaps=set();pvcs=set() + for c in spec.get('containers',[])+spec.get('initContainers',[]): + for e in c.get('env',[]): + v=e.get('valueFrom',{}) + if 'secretKeyRef' in v:secrets.add(v['secretKeyRef']['name']) + if 'configMapKeyRef' in v:configmaps.add(v['configMapKeyRef']['name']) + for e in c.get('envFrom',[]): + if 'secretRef' in e:secrets.add(e['secretRef']['name']) + if 'configMapRef' in e:configmaps.add(e['configMapRef']['name']) + for v in spec.get('volumes',[]): + if 'secret' in v:secrets.add(v['secret']['secretName']) + if 'configMap' in v:configmaps.add(v['configMap']['name']) + if 'persistentVolumeClaim' in v:pvcs.add(v['persistentVolumeClaim']['claimName']) + for p in v.get('projected',{}).get('sources',[]): + if 'secret' in p:secrets.add(p['secret']['name']) + if 'configMap' in p:configmaps.add(p['configMap']['name']) + return secrets,configmaps,pvcs + + +def prepare(profile,get=query): + if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused() + bindings=[] + for component in profile['components']: + ns=component['namespace'];name=component['deployment'] + d=get(ns,'deployment',name);m=d['metadata'] + if (m['uid']!=component['deployment_uid'] or m.get('deletionTimestamp') or + d['status'].get('observedGeneration')!=m['generation'] or d['status'].get('readyReplicas')!=1):raise Refused() + refs=references(d['spec']['template']['spec']) + expected_pvcs={p['name']:p['uid'] for p in component['pvc_refs']} + if refs[0]!=set(component['secret_refs']) or refs[1]!=set(component['configmap_refs']) or refs[2]!=set(expected_pvcs):raise Refused() + pvcs=[] + for pvc,expected in expected_pvcs.items(): + obj=get(ns,'pvc',pvc) + if obj['metadata']['uid']!=expected or obj['metadata'].get('deletionTimestamp') or obj['status'].get('phase')!='Bound':raise Refused() + pvcs.append({'name':pvc,'uid':expected}) + bindings.append({'namespace':ns,'deployment':name,'uid':m['uid'],'generation':m['generation'], + 'pvc_bindings':pvcs,'secret_names_only':sorted(refs[0]),'configmap_names_only':sorted(refs[1])}) + if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused() + return {'schema':'netkingdom.identity-backup-preparation.v1','status':'prepared_not_admitted', + 'observed_at':datetime.now(timezone.utc).isoformat(),'cluster_uid':CLUSTER_UID, + 'profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(), + 'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'bindings':bindings,'secret_values_read':False,'backup_captured':False,'uploaded':False, + 'runtime_changed':False,'credential_grant_created':False, + 'owner_admission_required':profile['admission_gates']} + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True) + a=p.parse_args() + try: + r=prepare(json.loads(PROFILE.read_text())) + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + except Exception: + print('ERROR: coverage preparation refused; private diagnostics suppressed.');return 1 + print('OK: exact coverage proposal prepared; capture, delivery and owner admission remain pending.');return 0 + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-isolated-keycape.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-isolated-keycape.py new file mode 100644 index 00000000..10a7ef48 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/prepare-isolated-keycape.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Validate reviewed public recovery metadata before requesting custody input. + +Never opens TLS material, archive plaintext or any key. This validates bindings, +not owner assent, archive contents or recovery authority. +""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import stat +from urllib.parse import urlsplit + +ROOT=Path(__file__).resolve().parents[2] +# The executable closure extends the synthetic fixture closure with the attended +# custody entry point and this validator. Tests and historical receipts do not run. +SOURCE_PATHS=tuple(sorted([ + 'sso-mfa/bootstrap/sops-custody-unlock.sh','sso-mfa/bootstrap/sops-private.py','sso-mfa/bootstrap/sops-custody-fd.py', + 'sso-mfa/k8s/privacyidea/key-custody-remote.py', + *('tools/bootstrap-recovery/'+name for name in ( + 'isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh', + 'prepare-isolated-keycape.py','isolated-lldap.py','identity-bundle.py', + 'identity-backup-profile.json','retrieve-identity-offline.py','identity-offsite.py', + 'verify-identity.py','identity-custody.json','identity-files.py', + 'check-privacyidea-database.py'))])) + +class Refused(Exception):pass + +def require(value): + if not value:raise Refused() + +def metadata_path(path): + path=Path(os.path.abspath(path)) + require(not any(p.is_symlink() for p in (path,*path.parents))) + st=path.stat() + require(stat.S_ISREG(st.st_mode) and st.st_uid==os.getuid() and st.st_nlink==1 and not st.st_mode&0o077) + return path + +def read_metadata(path,limit=128*1024): + path=metadata_path(path) + require(0 record['ciphertext_bytes']: + raise offsite.Refused() + digest.update(block) + output.write(block) + output.flush() + os.fsync(output.fileno()) + if count != record['ciphertext_bytes'] or digest.hexdigest() != record['ciphertext_sha256']: + raise offsite.Refused() + with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response: + if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']: + raise offsite.Refused() + complete = True + # Closed allowlist: never copy arbitrary source-receipt fields to output. + result = {k: record[k] for k in ('schema', 'provider', 'folder', 'artifact', + 'ciphertext_bytes', 'ciphertext_sha256', 'etag')} + result.update(status='readback_verified', fetched_path=str(target), + independent_owner_retrieval_verified=True, + credential_source='independently_held_provider_access', + provider_auth_mode=mode, openbao_contacted=False, sso_contacted=False, + provider_writes_performed=False, age_private_key_used=False, + secret_values_exported=False, object_unchanged_during_transfer=True, + downloaded_at=datetime.now(timezone.utc).isoformat()) + return result + finally: + if directory is not None and not complete: + shutil.rmtree(directory) + + +def interrupted(signum, frame): + raise InterruptedError() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source-receipt', required=True) + parser.add_argument('--receipt', required=True) + parser.add_argument('--auth', choices=('account', 'folder-share'), default='account') + args = parser.parse_args() + os.umask(0o077) + fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + result = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'} + stage = 'source_receipt_validation' + try: + record = json.loads(Path(args.source_receipt).read_text()) + validate(record) + # getpass must never fall back to echoed stdin (e.g. a captured agent tool). + stage = 'private_provider_prompt' + secret = private_prompt('Storage Share Backup app password or exact folder-share token: ') + try: + stage = 'provider_download' + result = retrieve(record, secret, args.auth) + finally: + secret = None + except (Exception, KeyboardInterrupt) as error: + result.update(failed_stage=stage, **failure_metadata(error)) + finally: + with os.fdopen(fd, 'w') as output: + json.dump(result, output, indent=2) + output.write('\n') + ok = result['status'] == 'readback_verified' + print('OK: ciphertext retrieved without OpenBao or SSO; ETag, size and SHA256 verified.' if ok + else 'ERROR: retrieval refused; private diagnostics suppressed. Use a new receipt path to retry.') + return 0 if ok else 1 + + +if __name__ == '__main__': + for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP): + signal.signal(sig, interrupted) + sys.exit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/retrieve-identity.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/retrieve-identity.py new file mode 100644 index 00000000..ed386d9a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/retrieve-identity.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Silent attended owner retrieval of an existing identity ciphertext. + +Uses Backup-account recovery custody only in memory; no private age key, upload, +share, grant, deletion or production restore operation. +""" +import argparse +import base64 +from datetime import datetime,timezone +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import tempfile +import urllib.request + +HERE=Path(__file__).resolve().parent + + +def load(name): + spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py')) + m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m + + +inventory=load('inventory-storage-share');offsite=load('identity-offsite') + + +def retrieve(record,out): + if (record.get('schema')!='netkingdom.identity-offsite.v1' or record.get('status')!='readback_verified' + or not offsite.NAME.fullmatch(record.get('artifact','')) or not re.fullmatch('[a-f0-9]{64}',record.get('ciphertext_sha256','')) + or not 0record['ciphertext_bytes']:raise offsite.Refused() + digest.update(block);output.write(block) + if count!=record['ciphertext_bytes'] or digest.hexdigest()!=record['ciphertext_sha256']:raise offsite.Refused() + with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response: + if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused() + out.update(record);out.update(status='readback_verified',fetched_path=str(target), + independent_owner_retrieval_verified=True,caller_entity_id=identity.get('entity_id'), + age_private_key_used=False,secret_values_exported=False,downloaded_at=datetime.now(timezone.utc).isoformat()) + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--source-receipt',required=True);p.add_argument('--receipt',required=True) + a=p.parse_args();os.umask(0o077);fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'status':'failed','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()} + try:retrieve(json.loads(Path(a.source_receipt).read_text()),r) + except Exception:r.update(status='failed',error='contained_owner_retrieval_failed') + finally: + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + return int(r['status']=='failed') + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/run-isolated-keycape.sh b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/run-isolated-keycape.sh new file mode 100644 index 00000000..f9b73d4d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/run-isolated-keycape.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Candidate owner launcher. Review and native fixture proof required before use. +set +x +set -euo pipefail +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +cd -- "$ROOT" +umask 077 +[[ $# == 6 ]] || { printf '%s\n' 'Usage: run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST'; exit 1; } +[[ "$3" =~ ^[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact reviewed admission digest required.'; exit 1; } +for index in 1 2 4 5 6; do + [[ -f "${!index}" && ! -L "${!index}" ]] || { printf '%s\n' 'ERROR: provide regular owner-controlled input files.'; exit 1; } +done +[[ -t 0 && -t 1 && -t 2 ]] || { printf '%s\n' 'ERROR: run in your own interactive terminal.'; exit 1; } +for tool in python3 age age-keygen docker; do + command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; } +done +python3 tools/bootstrap-recovery/prepare-isolated-keycape.py \ + --transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \ + --tls-cert "$4" --tls-key "$5" --trust "$6" +python3 - <<'PY' +import importlib.util +from pathlib import Path +p=Path('tools/bootstrap-recovery/isolated-keycape-session.py') +spec=importlib.util.spec_from_file_location('session',p) +m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +try: + m.local.preflight() + m.run(['docker','image','inspect',m.adapter.IMAGE]) +except Exception: + raise SystemExit('ERROR: exact local Docker/images/tmpfs preflight failed; no key requested.') +print('OK: fixed local target and cached pinned images available.') +PY +NK49_KC_RUN="$(mktemp -d /tmp/nk49-isolated-keycape-XXXXXXXX)" +printf 'Safe evidence directory: %s\n' "$NK49_KC_RUN" +printf '%s\n' 'Isolated original issuer/trust and selected registered service-client checks only.' +printf '%s\n' 'No browser, downstream consumer or retained-factor MFA acceptance is claimed.' +printf '%s\n' 'Enter only the independently held backup age key at the hidden prompt.' +bash sso-mfa/bootstrap/sops-custody-unlock.sh \ + --expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \ + --sealed-fd --prompt -- python3 tools/bootstrap-recovery/isolated-keycape-session.py \ + --transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \ + --tls-cert "$4" --tls-key "$5" --trust "$6" --receipt "$NK49_KC_RUN/recovery.json" +printf 'Safe receipt: %s/recovery.json\n' "$NK49_KC_RUN" diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-bundle.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-bundle.py new file mode 100644 index 00000000..e7b3cfb6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-bundle.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Isolated ciphertext recovery check; no cluster, credentials or values emitted. + +This verifies the handoff transport and exact requested file coverage. Current +service validity, independent source custody and native consumer recovery need +separate owner evidence; this tool cannot infer them from a decrypted archive. +""" +import argparse +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import signal +import subprocess +import sys +import tarfile +import tempfile +from datetime import datetime, timezone + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('private_age_recovery', ROOT / 'sso-mfa/bootstrap/sops-private.py') +custody = importlib.util.module_from_spec(spec); spec.loader.exec_module(custody) +LIMIT = 64 * 1024 * 1024 +ALLOWED = {f'{c}/secrets.env' for c in ('privacyidea', 'postgres', 'lldap', 'authelia', 'keycape', 'breakglass')} | { + 'privacyidea/pi.enc', 'privacyidea/private.pem', 'privacyidea/public.pem', + 'keycape/key.pem', 'authelia/oidc_private_key.pem', 'openbao/init.json'} + + +class Refused(Exception): + pass + + +def recover(ciphertext, keyfd, required, work): + # Exclude ambient age keys/plugins and unrelated credential environments. + env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': str(work), 'XDG_CONFIG_HOME': str(work)} + result = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}'], input=ciphertext, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), + timeout=60, env=env, cwd=work) + if result.returncode or not result.stdout or len(result.stdout) > LIMIT: + raise Refused() + restored = set(); names = set(); total = 0 + with tarfile.open(fileobj=io.BytesIO(result.stdout), mode='r:') as archive: + for index, member in enumerate(archive): + if index >= 128 or member.name in names: + raise Refused() + names.add(member.name) + path = PurePosixPath(member.name) + if (path.is_absolute() or '..' in path.parts or '.' in path.parts + or not path.parts or path.parts[0] != 'secrets'): + raise Refused() + if member.isdir(): + if len(path.parts) > 2 or (len(path.parts) == 2 and path.parts[1] not in {p.split('/')[0] for p in ALLOWED}): + raise Refused() + continue + relative = '/'.join(path.parts[1:]) + if not member.isfile() or relative not in ALLOWED or not 0 <= member.size <= 16 * 1024 * 1024: + raise Refused() + total += member.size + if total > LIMIT or member.size == 0: + raise Refused() + content = archive.extractfile(member).read() + if len(content) != member.size or b'AGE-SECRET-KEY-' in content: + raise Refused() + target = work / 'restored' / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'wb') as dest: + dest.write(content); dest.flush(); os.fsync(dest.fileno()) + if target.read_bytes() != content: + raise Refused() + restored.add(relative) + if not required.issubset(restored): + raise Refused() + return sorted(restored) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--bundle', required=True) + parser.add_argument('--bundle-sha256', required=True) + parser.add_argument('--required-file', action='append', required=True) + parser.add_argument('--receipt', required=True) + args = parser.parse_args() + if not re.fullmatch('[a-f0-9]{64}', args.bundle_sha256): raise Refused() + required = set(args.required_file) + if not required or required - ALLOWED or len(required) != len(args.required_file): raise Refused() + path = Path(args.bundle).absolute() + if any(p.is_symlink() for p in (path, *path.parents)): raise Refused() + with path.open('rb') as file: + data = file.read(LIMIT + 1) + if len(data) > LIMIT or hashlib.sha256(data).hexdigest() != args.bundle_sha256: raise Refused() + recipient = custody.public_recipient() + keyfd = custody.private_key_fd() + try: + derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], + pass_fds=(keyfd,), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=10) + if derived.returncode or derived.stdout.strip() != recipient.encode(): raise Refused() + # Check the negative path with a disposable unrelated key. It never + # replaces the registered key, leaves temporary custody, or contacts IAM. + with tempfile.TemporaryDirectory(prefix='netkingdom-recovery-', dir='/tmp') as temporary: + work = Path(temporary) + disposable = work / 'wrong.key' + created = subprocess.run(['age-keygen', '-o', str(disposable)], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=10) + if created.returncode: raise Refused() + wrong = subprocess.run(['age', '-d', '-i', str(disposable)], input=data, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60, + env={'PATH': os.environ.get('PATH', os.defpath), 'HOME': temporary, + 'XDG_CONFIG_HOME': temporary}, cwd=temporary) + if wrong.returncode == 0: raise Refused() + restored = recover(data, keyfd, required, work) + if work.exists(): raise Refused() + finally: + os.close(keyfd) + receipt = {'schema': 'netkingdom.bootstrap-recovery-transport.v1', + 'observed_at': datetime.now(timezone.utc).isoformat(), 'bundle_sha256': args.bundle_sha256, + 'registered_recipient_match': True, 'wrong_key_refused': True, + 'required_files': sorted(required), 'restored_files': restored, + 'isolated_restore_readback_passed': True, 'temporary_plaintext_cleanup_verified': True, + 'secret_values_exported': False, 'cluster_contacted': False, + 'independent_custody_proven': False, 'current_service_validity_proven': False, + 'native_consumer_recovery_proven': False} + # Publish only the closed, safe receipt shape; preserve existing receipts. + fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'w') as dest: + json.dump(receipt, dest, indent=2); dest.write('\n') + print('Ciphertext transport/isolated restore checks passed. Independent custody and current service recovery remain separate owner evidence.') + + +if __name__ == '__main__': + try: + for sig in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT): signal.signal(sig, custody.interrupted) + main() + except (Refused, custody.Refused, OSError, ValueError, TypeError, KeyError, tarfile.TarError, subprocess.SubprocessError): + print('ERROR: recovery transport check refused or failed; no acceptance receipt. Private diagnostics suppressed.', file=sys.stderr) + raise SystemExit(1) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-identity-delivery.py b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-identity-delivery.py new file mode 100644 index 00000000..e4664f42 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/test-root/tools/bootstrap-recovery/verify-identity-delivery.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Attended native reader/ESO verification; only closed booleans leave custody.""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import sys + +HERE=Path(__file__).resolve().parent + + +def load(name): + spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py')) + m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m + + +setup=load('identity-credential-setup');runtime=load('identity-runtime') +from state_hub_preflight_lane import bao,data,capabilities,revoke + + +def verify(action,expected,receipt,kubeconfig): + receipt['phase']='validate_approved_contract' + ccr,lane=setup.contract(action,expected) + receipt['phase']='verify_attended_identity' + ident=data(bao(['token','lookup','-format=json']))['data'] + if 'platform-admin' not in ident.get('policies',[]) or 'root' in ident.get('policies',[]) or not ident.get('path','').startswith('auth/netkingdom/'):raise setup.Refused() + capture=runtime.capture;capture.KUBE=['kubectl','--kubeconfig',kubeconfig] + receipt['phase']='verify_cluster_identity' + if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise setup.Refused() + def login(sa,audience,negative=False,namespace='sso'): + jwt=capture.command(['-n',namespace,'create','token',sa,'--audience='+audience,'--duration=10m']).decode().strip() + result=bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':jwt},allow_failure=negative) + if negative: + if result.returncode==0: + revoke(data(result)['auth']['client_token']);raise setup.Refused() + if b'403' not in result.stderr and b'400' not in result.stderr:raise setup.Refused() + return None + return data(result)['auth'] + receipt['phase']='native_reader_login' + auth=login(lane['service_account'],'openbao');token=auth['client_token'] + receipt['native_reader_metadata']={k:auth.get(k) for k in ['token_policies','identity_policies','policies','lease_duration']} + try: + receipt['phase']='effective_reader_policy_ttl' + if (auth['token_policies']!=[lane['policy']] or auth.get('identity_policies') + or auth.get('policies',auth['token_policies'])!=[lane['policy']] or not 0bundle.LIMIT):raise bundle.Refused() + path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT) + with path.open('rb') as source:digest=hashlib.file_digest(source,'sha256').hexdigest() + if path.stat().st_size!=transfer['ciphertext_bytes'] or digest!=transfer['ciphertext_sha256']:raise bundle.Refused() + derived=pg.run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip() + if derived!=bundle.profile()['public_recipient'].encode():raise bundle.Refused() + receipt.update(ciphertext_sha256=digest,artifact=transfer['artifact'],registered_recipient_match=True) + with tempfile.TemporaryDirectory(prefix='nk49-identity-restore-',dir='/tmp') as temporary: + work=Path(temporary);env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary} + wrong=work/'wrong.key';pg.run(['age-keygen','-o',str(wrong)],env=env) + negative=subprocess.run(['age','-d','-i',str(wrong),str(path)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env=env,timeout=120) + if negative.returncode==0:raise bundle.Refused() + receipt['wrong_key_refused']=True + plain=work/'archive.tar' + with plain.open('xb') as output: + result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output, + stderr=subprocess.DEVNULL,env=env,pass_fds=(keyfd,),timeout=180) + if result.returncode or plain.stat().st_size>bundle.LIMIT+4*1024*1024:raise bundle.Refused() + restored=work/'restored';restored.mkdir(mode=0o700) + manifest,checks=bundle.restore(plain,restored) + receipt.update(checks=checks,manifest_file_count=len(manifest['files']),all_manifest_hashes_verified=True, + profile_sha256=manifest['profile_sha256']) + container='nk49-identity-db-'+uuid.uuid4().hex + try: + receipt['database_restore']=pg.restore(restored/'databases/privacyidea.dump',container) + finally: + pg.run(['docker','rm','--force','--volumes',container]) + absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15) + pg.run(['docker','info','--format','{{.ServerVersion}}']) + if absent.returncode==0:raise bundle.Refused() + receipt['container_cleanup_verified']=True + if work.exists():raise bundle.Refused() + receipt.update(temporary_plaintext_cleanup_verified=True,status='identity_archive_verified', + full_service_authentication_proven=False,existing_token_decryption_proven=False,audit_signature_continuity_proven=False) + + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True) + a=p.parse_args();os.umask(0o077) + fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + r={'schema':'netkingdom.identity-recovery.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(), + 'production_contacted':False,'secret_values_exported':False,'container_cleanup_verified':False, + 'temporary_plaintext_cleanup_verified':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()} + keyfd=None + try: + transfer=json.loads(Path(a.transfer_receipt).read_text());keyfd=custody.private_key_fd() + verify(transfer,keyfd,r) + except Exception:r.update(status='failed',error='identity_recovery_failed_private_diagnostics_suppressed') + finally: + if keyfd is not None:os.close(keyfd) + r['finished_at']=datetime.now(timezone.utc).isoformat() + with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') + if r['status']=='identity_archive_verified': + print('OK: off-host identity bundle decrypted; complete manifest, key pairs, SQLite integrity and PostgreSQL restore passed; disposable data removed. Service authentication remains separate acceptance.') + return 0 + print('ERROR: identity recovery failed; review safe receipt. Private diagnostics suppressed.');return 1 + + +if __name__=='__main__': + for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement-review.json b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement-review.json new file mode 100644 index 00000000..cfa6dd8c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement-review.json @@ -0,0 +1,31 @@ +{ + "status": "proposed_not_integrated_not_projected", + "owner_head": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "source_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md", + "baseline_sha256": "sha256:cfe7ee36c6c4cae677eb9f43121e3910e1274e0f5e3589cd147ede25a991947d", + "proposal_sha256": "sha256:c1798c52aeff150f672fbe2ad097aaadc2bb98107741b2784fb09993d509703c", + "patch_sha256": "sha256:aa11ac3b3cbb36febc436736e7eaa611f8769fbae70b0f026fa9534954aa604c", + "baseline_counts": { + "done": 23, + "progress": 2, + "wait": 6, + "todo": 2 + }, + "proposed_counts": { + "done": 24, + "progress": 2, + "wait": 7, + "todo": 3 + }, + "all_existing_task_ids_and_hub_uuids_preserved": true, + "only_existing_task_status_changed": { + "NK-WP-0049-T33": "todo -> wait" + }, + "new_tasks": { + "NK-WP-0049-T34": "done: source preparation only", + "NK-WP-0049-T35": "todo: original TLS/trust recovery ownership", + "NK-WP-0049-T36": "todo: selected registration/private admission bindings" + }, + "canonical_writes": false, + "cross_lane": "Coordinated group_provenance; that lane owns NK-WP-0048 only." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement.patch b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement.patch new file mode 100644 index 00000000..74c3659c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement.patch @@ -0,0 +1,209 @@ +--- a/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md ++++ b/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md +@@ -8,7 +8,7 @@ + owner: codex + topic_slug: netkingdom + created: "2026-10-12" +-updated: "2026-10-16" ++updated: "2026-10-17" + flavor: implementation + depends_on: [] + state_hub_workstream_id: "17d315f0-829e-57c7-85cc-3a7914152804" +@@ -28,8 +28,8 @@ + or newly granted authority. The earlier dated narrative remains in Git at + `cc2dd01`; its evidence references are carried into the tasks below. + +-**Current position:** 22 tasks done; 2 in progress; 1 ready to start (`todo`); +-6 waiting on explicit prerequisites. These are task counts, not an effort-based ++**Current position, source refinement 2026-10-17:** 24 tasks done; 2 in ++progress; 3 ready to start (`todo`); 7 waiting on explicit prerequisites. These are task counts, not an effort-based + completion percentage. Assignees are also written in task descriptions because + the current Forge projector does not populate the dedicated Hub assignee field. The outage-retrieval tool and restore-packet draft are now prepared; + independent outage retrieval and Storage Share publication are accepted; +@@ -958,27 +958,147 @@ + + ```task + id: NK-WP-0049-T33 ++status: wait ++priority: high ++assignee: codex ++depends_on: [NK-WP-0049-T32, NK-WP-0049-T34, NK-WP-0049-T35, NK-WP-0049-T36] ++blocking_reason: "Source custody/preflight preparation is evidenced in T34; await original TLS/trust recovery disposition T35 and selected original registration/private-admission metadata T36 before final reviewed command and existing owner process." ++state_hub_task_id: "56c8d469-7a10-5d50-a30e-95cbf3cd4756" ++``` ++ ++**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners. ++This umbrella retains final attempt readiness. Completed custody/preflight source ++preparation is extracted into T34; T35 resolves original TLS/trust recovery and ++T36 prepares the selected original registration and private admission. This split ++records distinct outcomes without counting source preparation as recovery. ++ ++After those deliverables, assemble the exact existing six-argument attended ++command and admission digest, bind the integrated source/dependency revision and ++fixed local target, and record execution deadline, safe receipt location, abort, ++rollback and independent receipt verification through the existing owner process. ++Keep private manifest derivation inside custody and original TLS/client/key inputs ++in the owner's own terminal; the current null fields must refuse execution. ++Existing session authorization is retained within scope; this record does not ++supply missing owner assent or approve any actual recovery attempt. ++ ++**Done when:** reviewed executable command and complete metadata-only/private ++admission packet are ready for the existing attended custody path; prerequisites ++and independent receipt verification are explicit. T18 still owns actual ++execution, original consumer continuity and acceptance. T19 retains retained-factor ++MFA. This task neither authorizes a production consumer redirect nor creates a ++new client grant. ++ ++## Deliver attended custody, metadata preflight and private manifest preparation source ++ ++```task ++id: NK-WP-0049-T34 ++status: done ++priority: high ++assignee: codex ++depends_on: [NK-WP-0049-T32] ++``` ++ ++**Accountable worker:** NetKingdom codex; independent Custodian source review. ++Completed 2026-10-17 as source preparation. Existing attended custody entry points ++now have a reviewed candidate sealed anonymous descriptor mode: hidden prompt, ++actual public-recipient verification, strict owned/private/full-seal descriptor ++admission, non-inheritable returned duplicate and explicit descriptor passing. ++The existing file mode stays compatible, with its original hard-loss limitation; ++no improved file-mode cleanup is claimed. Exact parent-loss tests prove descriptor ++lifetime without signalling inherited user shell groups. An admitted child retains ++its descriptor until it exits; parent loss is not child revocation. ++ ++The candidate checks bounded, duplicate-free public JSON, exact admission digest, ++transfer/archive and selected service bindings, typed hashes for all 17 invoked ++local source/config dependencies and required Python imports before requesting a ++key. TLS and ciphertext preparation uses file metadata only. Legacy explicit ++manifest digests remain supported; versioned ciphertext-derived binding computes ++the canonical digest privately only after exact reviewed ciphertext, authenticated ++age decryption and full existing archive validation. The derived private digest ++never enters safe receipts/checkpoints and grants no new consumer authority. ++ ++The coordinator independently passed 111 focused checks. The existing custody ++regression suite passed 94 checks; 61 real-SOPS tests skipped because that trusted ++runtime is unavailable, so that compatibility coverage remains unverified. Tests ++use generated keys/archive material only, including actual age cryptography, ++synthetic hidden-prompt and exact parent-loss processes. Manifest-builder tests ++mock Docker/issuer behavior and confer no new native proof. Frozen R6/R7 proof, ++intermediate test failures and scanner history remain retained. ++ ++Bounded public image readback separately establishes the cached original image's ++13670142-byte `/keycape` executable as ++`sha256:cd89121393e1465aa76009763602a5b8c75f9dddb5679aea6242623f135b51b4`. ++This is executable identity metadata, not recovery or custody acceptance. ++ ++Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/` ++(`candidate.patch`, `review.json`, source closure, attempt/admission metadata and ++`public-verifier-image-readback.json`). Canonical integration/publication and ++projection receipts must be recorded separately when performed; this completed ++source-preparation record does not invent them. T33 remains open for complete ++attempt readiness; actual recovery/consumer acceptance remains T18 and MFA T19. ++ ++## Establish original KeyCape TLS and trust recovery ownership for outage use ++ ++```task ++id: NK-WP-0049-T35 + status: todo + priority: high + assignee: codex +-depends_on: [NK-WP-0049-T32] +-state_hub_task_id: "56c8d469-7a10-5d50-a30e-95cbf3cd4756" +-``` +- +-**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners. +-Prepare the exact archive/manifest, original issuer/TLS/trust and executable +-bindings, selected current registration and dependency graph using public metadata. +-Use the existing independently held archive-key and owner-controlled TLS custody +-paths; never put secret values in the packet. Validate the attended launcher and +-its full dependency closure, execution deadline, recovery obligations and safe +-receipt path before asking for any owner-held input. Bind approved source revision +-and rollback/abort limits through the existing owner process. +- +-**Done when:** reviewed executable command and metadata-only admission packet +-are ready for the existing attended custody path; prerequisites and independent +-receipt verification are explicit. T18 still owns actual execution, original +-consumer continuity and acceptance. T19 retains retained-factor MFA. This task +-neither authorizes a production consumer redirect nor creates a new client grant. ++depends_on: [NK-WP-0049-T34] ++``` ++ ++**Accountable worker:** NetKingdom codex, coordinating with the source owner of ++Ingress/cert-manager/Traefik and the original TLS custody/provider boundary. ++**Next action:** identify and record that actual owner's existing recovery path, ++then obtain its metadata/disposition through the ordinary owner process. A missing ++exact credential-routing catalog match is not evidence that no owner exists. ++ ++Current source declares `kc.coulomb.social`, `sso/kc-tls`, cert-manager ++`ClusterIssuer/letsencrypt-prod` and Traefik TLS termination before KeyCape 8080. ++The identity backup profile excludes `kc-tls`; the verified archive therefore ++establishes no independent recovery of that TLS identity/trust. Determine whether ++the exact original certificate/key/trust is independently recoverable during an ++issuer/cluster outage, or whether a documented reissue path is acceptable for the ++selected continuity claim. Do not read or place the TLS key in work records, use ++a fresh self-signed trust substitute, mutate production or claim owner assent. ++ ++**Done when:** named owner/provider, independent outage prerequisites, original ++public identity/certificate/trust hashes or accepted reissue disposition, private ++custody entry point and verification/abort limits are explicit and reviewed. ++Record any unavailable recovery capability as a live owner work record and bind ++its dependency/unblock condition; a prose-only gap or untested archive is not ++completion. This delivers preparation/disposition, not actual TLS recovery. ++ ++## Prepare selected original service registration and private admission bindings ++ ++```task ++id: NK-WP-0049-T36 ++status: todo ++priority: high ++assignee: codex ++depends_on: [NK-WP-0049-T34] ++``` ++ ++**Accountable worker:** NetKingdom codex with KeyCape and the selected existing ++service consumer's owner. **Next action:** identify that owner and the exact ++original registration's non-secret selector metadata using existing source/owner ++evidence; preserve independent private custody and never retrieve a client secret. ++ ++Bind reviewed ciphertext `7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`, ++its independently verified transfer, source/image/executable closure and an ++explicit versioned manifest-binding mode. For the original service registration, ++record client identifier, secret environment name, audience, granted scope, ++subject, tenant, required roles and denied scope without values or new grants. ++Describe the native service-client dependency graph and distinguish unexercised ++LLDAP/Authelia/privacyIDEA/downstream flows. Keep private manifest material inside ++the existing custody process, never in Git, safe receipts, State Hub or chat. ++ ++**Done when:** the selected existing service owner and exact non-secret registration ++bindings are independently established, the private admission builder validates ++positive/negative source/archive/selector cases, and its metadata is ready to ++combine with T35's original TLS/trust disposition under T33. This task may prepare ++registration metadata while T35 proceeds independently; it does not finalize the ++whole admission, perform actual recovery, redirect a consumer or confer assent. + + ## Accept remaining recovery and caller evidence and close the G08 handoff + +@@ -999,7 +1119,7 @@ + G08 receiving the final evidence. The completed archive/job milestones remain + accepted independently of this final gate. + +-**Done when:** T02 and T15–T31 have their stated completed or explicitly accepted ++**Done when:** T02 and T15–T36 have their stated completed or explicitly accepted + handoff outcomes; service authentication/token/audit proof and current/legacy + caller dispositions are accepted; the source workplan and projected tasks agree; + and G08 has an exact evidence/handoff record. Record actionable residuals as live +@@ -1035,3 +1155,11 @@ + remain retained. Custodian identity-restore-adapter-next/canonical-r7-integration.json + binds source/projection/native-fixture evidence separately. T18 remains waiting + for the actual owner-held attempt and selected consumer acceptance. ++ ++ ++2026-10-17 source/attempt refinement: T34 records completed attended custody, ++preflight/private-manifest source preparation and its bounded tests; T35 and T36 ++make original TLS recovery ownership and selected original admission metadata live ++work. T33 retains final attempt readiness and its original Hub UUID. This adds no ++actual recovery, MFA, native authority or owner assent. Source task counts above ++refer to this file; projection has not been claimed for this proposed refinement. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md new file mode 100644 index 00000000..40bb26e7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/workplan-refinement/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md @@ -0,0 +1,1165 @@ +--- +id: NK-WP-0049 +type: workplan +title: "Remove bootstrap secret output and verify separated recovery custody" +domain: infotech +repo: net-kingdom +status: active +owner: codex +topic_slug: netkingdom +created: "2026-10-12" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +state_hub_workstream_id: "17d315f0-829e-57c7-85cc-3a7914152804" +--- + +Native owner work for CUST-WP-0080 G08. This workplan remains active until the +remaining caller, service-recovery and custody acceptance gates are satisfied. +NK-WP-0045 covers the non-secret root-grant record. Founder-deferred exposure +rotation remains in CUST-WP-0073-T05; this plan does not silently reopen it. + +**Replanned 2026-10-16 at the operator's request:** substantial completed work is +now represented by completed tasks, and remaining work has separate deliverables, +dependencies and acceptance criteria. T01–T03 retain their historical IDs and Hub +UUIDs. Completed parts of the old T02/T03 umbrellas are extracted into T04–T14; +T02 now covers the current-caller ledger and T03 retains final owner acceptance. +These are retrospective task records of evidenced work, not additional delivery +or newly granted authority. The earlier dated narrative remains in Git at +`cc2dd01`; its evidence references are carried into the tasks below. + +**Current position, source refinement 2026-10-17:** 24 tasks done; 2 in +progress; 3 ready to start (`todo`); 7 waiting on explicit prerequisites. These are task counts, not an effort-based +completion percentage. Assignees are also written in task descriptions because +the current Forge projector does not populate the dedicated Hub assignee field. The outage-retrieval tool and restore-packet draft are now prepared; +independent outage retrieval and Storage Share publication are accepted; +service admission remains an explicit gate. + +**Next actions:** finish the read-only caller ledger (T02); admit the drafted +isolated restore packet (T15) and record residual-scope handoffs (T24). +Independent outage retrieval (T23) and guide publication (T26) are done. After owner admission, execute the individual native service and +caller checks. A task's owner is accountable for the result; another repository's +approval or credential grant still requires its own owner path. + +**Closure:** T03 assembles owner acceptance after its named dependencies pass. +Passing archive recovery does not prove existing-user authentication, retained +MFA-token decryption, historical audit signatures or current caller admission. +The verified backup job stays active while those separate tasks are completed. + +## Remove secret-bearing output and ambient root discovery + +```task +id: NK-WP-0049-T01 +status: done +priority: high +assignee: codex +state_hub_task_id: "d7ca8d5f-c5ff-5cba-bd25-038cb53ffe4e" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +2026-10-12: regression reproduced emergency/reprint root export, generation +prefix disclosure, permissive file modes, dry-run side effects and unattended +entry. Replaced emergency export with a non-secret guide (legacy secret-input +arguments refused), suppressed generation prefixes/tracing and set private +permissions. Bootstrap requires the registered public recipient and an attended +terminal; never generates/discovers a permanent private key. Encrypted resume +requires an explicit matching temporary custody key before cluster activity. +Dry-run is a non-mutating plan. The live cluster/apply path was not executed. + +A fresh uniquely named encrypted ops bundle follows apply; old delivery flags +cannot skip current acknowledgment. New acknowledgment state binds its SHA256 +and timestamp. Declining stops before cleanup/completion. This is an operator +statement, not an independent restore receipt. Historical fields remain unchanged. +Make/help/status/console and current credential guidance reflect that distinction. +Evidence and exact source manifest: Custodian +`docs/evidence/2026-10-12-netkingdom-security/bootstrap-handoff.json`. + +## Harden custody, SOPS and rotation source entry points + +```task +id: NK-WP-0049-T04 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T01] +state_hub_task_id: "04c4e8be-5bd8-5d5c-99b1-79d8ae758462" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-12–13. Public-recipient-only encryption, explicit temporary +custody, private no-overwrite output, sanitized diagnostics, stdin-only secret +transport and failed-operation cleanup replace ambient-key and output paths. +SOPS read/edit/data-key rotation preserve recipient/MAC/source-version guards; +component rotations preserve unrelated fields and retain recovery candidates. +This records source and disposable-native proof, not live rotation or revocation. + +Evidence: Custodian `docs/evidence/2026-10-12-netkingdom-security/` receipts +`custody-entrypoints.json`, `sops-make-entrypoints.json`, `rotation-output.json`, +and `admin-bootstrap.json`; October13 `sops-write-callers.json`. + +## Repair component, diagnostic, LDAP and resolver callers + +```task +id: NK-WP-0049-T05 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T04] +state_hub_task_id: "e8e38f42-609e-5c07-a8e6-e73c8eb4ae95" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-13–14. Repaired KeyCape configuration and component Secret +publication, original privacyIDEA key export/restore transport, scoped MFA +counts, LDAP group maintenance and configuration-preserving resolver cutover. +Retired unsafe personal-token mint/refresh and direct LDAP lifecycle shortcuts. +Disposable native PI/LLDAP tests and source regressions passed; estate provider +admission, historical credential validity and service authentication remain T21/T22. + +Evidence: Custodian October13 `keycape-caller-reconciliation.json`, +`service-secret-callers.json`, `privacyidea-key-custody.json`, +`mfa-state-diagnostic.json`, `realm-bootstrap-repair.json`; October14 +`legacy-lldap-retirement.json`, `platform-group-boundary.json`, +`lldap-group-maintenance.json`, `resolver-cutover.json` under the corresponding +`docs/evidence/YYYY-MM-DD-netkingdom-security/` directories. + +## Inventory source callers and retire remaining unsafe delivery paths + +```task +id: NK-WP-0049-T06 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T04, NK-WP-0049-T05] +state_hub_task_id: "a5f50891-c5b2-513b-8670-e1915ee52015" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-15. Enumerated all 87 current non-test shell/Python callers +in the declared bootstrap/k8s scope plus Make/console/delivery adapters. Repaired +streamed encrypted bundle publication and fail-closed bootstrap/lab paths; +retired unsafe delivery copiers, root prompts, registry patchers and stale +rollback recipes. The attended inventory records source dispositions and their +native-proof limits. T02 separately maps these to current live users/owners. + +Evidence: [caller inventory](../docs/evidence/2026-10-15-bootstrap-recovery/caller-source-inventory.json), +[attended procedures](../docs/attended-procedure-inventory.md) and the October15 +recovery evidence directory. No live production bootstrap or lab-init acceptance +is claimed. + +## Discover off-host coverage and recover the existing PI database + +```task +id: NK-WP-0049-T07 +status: done +priority: high +assignee: codex +state_hub_task_id: "2d7302a2-319a-5633-bf58-d51ccd25ebf6" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-15–16. Bounded Storage Share/Scaleway inventories identified +existing database coverage and the missing full identity bundle. The operator +independently matched the backup recipient, then recovered the exact existing +PI dump: correct-key decryption, wrong-key refusal, isolated native PostgreSQL +restore/readback and cleanup passed (5 tokens, 2 resolvers, 2 realms). + +Evidence: October15 recovery `offsite-discovery-summary.json`, +`privacyidea-transfer-r1.json`, `privacyidea-database-drill-r1.json`, +`database-drill-review.json`. This database-only proof led to T08–T14. + +## Implement the full identity capture and archive recovery toolchain + +```task +id: NK-WP-0049-T08 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T06, NK-WP-0049-T07] +state_hub_task_id: "317ca173-c784-5329-9810-5e3d300604b8" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Defined exact LLDAP/Authelia/KeyCape/privacyIDEA assets, +Deployment/PVC/reference bindings, original keys and snapshot consistency limits. +Implemented native SQLite/PG capture, streamed age sealing, strict data-bearing +manifest checks, provider readback, independent retrieval, bounded retention, +root runtime and attended archive verifier. All 56 focused checks passed, +including actual synthetic encrypted/native PostgreSQL recovery. + +Evidence: [profile](../tools/bootstrap-recovery/identity-backup-profile.json), +[implementation review](../docs/evidence/2026-10-16-identity-backup-implementation/review.json), +and the live evidence's tested ESO-default/host-binding corrections. Production +archive recovery is separately evidenced in T13. + +## Admit and verify the two scoped backup credential deliveries + +```task +id: NK-WP-0049-T09 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T08] +state_hub_task_id: "4a47eb2a-7e42-5ae7-9df1-f5ede746eca1" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Platform CCR-2026-0048/0049 are verified. The operator +approved folder-only provider-normalized publisher21/retention25 rights, including +re-sharing, in addition to the original rollout. Coding-agent denials precede KV +publication. Exact 900-second single-policy readers, wrong-SA/namespace/audience +rejection, metadata/sibling/old-escrow/token-create denials, ESO equality and +reader/session revocation passed. Operator passwords and age private keys are +not job credentials. + +Evidence: live `publisher-setup-verified.json`, `retention-setup-verified.json`, +`publisher-native-delivery-verified.json`, `retention-native-delivery-verified.json`; +approval decisions `40bfae40-6108-4e51-8613-073339f5345f` and +`5edd5203-fae7-461b-8775-5d22b7873971`; Platform CCR files. + +## Apply capture isolation and install the guarded node runtime + +```task +id: NK-WP-0049-T10 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T08, NK-WP-0049-T09] +state_hub_task_id: "0519444c-d559-5455-aa27-8c5dc8b832b0" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Applied exact UID/resource-version/spec-guarded DNS +selector exclusions, preserving ordinary service matches. Capture refuses any +remaining helper allow policy. Installed immutable runtime sources bound to the +actual hostname, machine identity and cluster UID. Disabled-unit replacement +checked previous contents and retained rollback bytes. Health unit validation +passed. Activation remained gated until T13. + +Evidence: live `isolation-applied.json`, `capture-ready-plan.json`, +`installed-runtime-packet-r2.json`, `upgrade-disabled-runtime.py`. Actual complete +hardened-job execution is recorded in T14. + +## Connect recovery to Railiance bootstrap and reuse discovery + +```task +id: NK-WP-0049-T11 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T08] +state_hub_task_id: "d69af493-4026-56a8-a526-c267d90dce18" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Added the Railiance bootstrap `identity-recovery` +dispatcher and independent-custody → isolated verification → layer/service-owner +handoff. Updated `capability.security.bootstrap-recovery`, linked bootstrap +sequencing and composed reuse-surface federation. Keyword discovery for identity +backup, restore, privacyIDEA and disaster recovery was verified. The operator +actually used this handoff in T13. No unattended production importer is claimed. + +Evidence: [ceremony](../docs/bootstrap-recovery-ceremony.md), owning capability +entry, `railiance-bootstrap/docs/identity-recovery.md`, bootstrap commit `d1dcfde` +and reuse-surface federation commit `662782e`. + +## Create and verify the first full off-host identity bundle + +```task +id: NK-WP-0049-T12 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T09, NK-WP-0049-T10] +state_hub_task_id: "decb415a-8012-521c-9dbe-d73c6ff0bbfb" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Captured original service keys/current data plus the +linked native PI database dump, sealed and published a unique Storage Share +artifact, then downloaded it and verified size/digest before publishing its index. +Helper and plaintext cleanup passed. Artifact: +`net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age` (109738344 bytes). + +Evidence: live `actual-capture.json`, `actual-offsite.json`; ciphertext SHA256 +`7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`. +No age private key was needed by the capture job. + +## Independently retrieve and recover the complete actual archive + +```task +id: NK-WP-0049-T13 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T11, NK-WP-0049-T12] +state_hub_task_id: "602d4921-cfac-5e5b-9993-4fbf67195c7d" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16. Independently downloaded the exact first generation +through attended Backup-account custody, rehashed it locally and revoked the +session. The operator used the independently held age key through the bootstrap +handoff. Correct/wrong-key cases, all 36 manifest files/hashes, original RSA pairs, +SQLite integrity and isolated native PG16.14 restore/readback passed; plaintext +and containers were removed. No key or restored secret values reached the agent. + +Evidence: live `independent-owner-transfer.json`, `actual-full-archive-recovery.json`, +`actual-full-recovery-review.json`. Existing-user/MFA authentication and old audit +signatures are deliberately separate T16–T20 outcomes. + +## Activate and prove the complete recurring backup and retention job + +```task +id: NK-WP-0049-T14 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T10, NK-WP-0049-T13] +state_hub_task_id: "eaf0708f-b3b7-5531-8172-95edf30c3a19" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16 after T13. Both daily backup/retention and hourly health +timers are enabled/active. The actual hardened systemd service passed retention +before/after a second capture/upload/download (Result=success, ExecMainStatus=0). +Both verified generations are retained, the independently recovered point is +pinned, and nothing was deleted. Helpers/plaintext were cleaned; failure markers +and reporting outbox are empty. Health check succeeded. + +Evidence: live `activation-result.json`, `activation-review.json`, +`recurring-job-review.json`. Cadence: daily 03:45 UTC plus up to 5 minutes jitter, +hourly health, 25-hour freshness bound. Second-provider/host-loss gaps go to T24; +this completed task is not a full-estate recovery claim. + +## Map current callers, owners and native acceptance gaps + +```task +id: NK-WP-0049-T02 +status: progress +priority: high +assignee: codex +depends_on: [NK-WP-0049-T06] +state_hub_task_id: "479d6970-6516-50f5-9436-bb5b56d58aad" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +The source inventory and known legacy objects exist; a complete current-use +ledger is not yet accepted. Next action is read-only reconciliation of the 93 +entries in the latest October15 source inventory (the earlier snapshot had 87) against active Make/bootstrap/operator routes and owner evidence. + +**Done when:** each entry is identified as current, retired or owner-deferred, +with its owner, credential route, current consumer(s), native evidence or exact +missing proof, and next responsible task. Explicitly cover the historical +`sso/keycape-pi-token` and `mfa/privacyidea-trigger-admin` objects, lab init, +factor diagnostic delivery, LDAP group/provider boundary, resolver cutover and +root-output/isolation boundaries. Secret presence is not credential validity. +Send current native checks to T21 and historical revocation/rotation decisions to +T22. Do not reopen completed source repairs without a new finding. + +## Prepare the isolated full-service restore and acceptance packet + +```task +id: NK-WP-0049-T15 +status: progress +priority: high +assignee: codex +depends_on: [NK-WP-0049-T08, NK-WP-0049-T13, NK-WP-0049-T27, NK-WP-0049-T28, NK-WP-0049-T29, NK-WP-0049-T30] +state_hub_task_id: "1d1eb903-a4b0-5bc4-a421-0b842d7f68ae" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +**Prepared draft scope moved to T27.** See +`docs/identity-service-restore-packet.md`: restore order, containment, safe evidence +and per-service positive/negative checks are drafted. Read-only current runtime +image digests were observed for all four services; correspondence to the selected +archive and independent image availability are not yet proved. + +**Next action:** admit a named disposable target/private route, verify and stage +matching images independently, obtain private existing-user/MFA test inputs and +review version-specific native commands with service owners. Incorporate T02's +ledger before submitting current-caller acceptance. + +2026-10-16 checkpoint: T28 completed the exact local image save/load/inspect +and T29 prepared and natively tested the bounded LLDAP stage. Its actual owner +run is T16 and can proceed independently of the remaining multi-service packet. +The other service adapters, matching archive/runtime lineage and private +existing-user/MFA inputs are still required; no blanket T15 completion is claimed. + +**Done when:** service owners accept the exact targets/procedure, existing-user +and MFA test identities/input custody are available without posting secrets, +outbound notification/provider writes are disabled, and every required new +credential or production effect has its own owner decision. The verifier deletes +its plaintext; it is not a production importer. This task prepares authority and +inputs for T16–T20, not an automatic production restore. + +## Prove restored LLDAP directory and existing-user authentication + +```task +id: NK-WP-0049-T16 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T29] +state_hub_task_id: "b64d4d6a-e851-58d1-9929-f03343147de0" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: NetKingdom directory/User Engine owners. Restore the captured +original directory/configuration in the admitted isolated environment. + +**Done when:** user, group and membership identities are preserved; an independently +supplied existing-user credential authenticates; a wrong credential is refused; +provider writes/outbound effects stay confined; cleanup is verified. Native +fixtures from T05 and SQLite integrity from T13 do not satisfy this current-data +acceptance. Record only safe counts, identities and outcomes. + + +Completed 2026-10-16 at 18:37 UTC. The operator privately supplied the +independent age key and an existing backup-era account credential to the exact +local executor. All 36 archive hashes and the known ciphertext/source/target +bindings matched. Native existing-user LDAP bind passed; explicit invalid +credentials were rejected. The original seven users, 17 groups, 13 memberships +and the available attribute/schema rows were preserved across service startup, +authentication and graceful shutdown. The container had only loopback/no +external routes; pre-import isolation and container/tmpfs cleanup passed. +No production endpoint was contacted or secret exported to the agent. + +Evidence: `actual-isolated-lldap-recovery.json` and +`actual-isolated-lldap-review.json` in +`docs/evidence/2026-10-16-identity-outage-recovery/`. This closes the actual +LLDAP stage independently of the remaining T15 packet. It does not prove +Authelia/KeyCape authentication, retained-token MFA or audit continuity. T17, +T18 and T19 retain their remaining adapter/input prerequisites. + +## Prove restored Authelia storage and OIDC continuity + +```task +id: NK-WP-0049-T17 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T15, NK-WP-0049-T16] +blocking_reason: "LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15." +state_hub_task_id: "b47f9975-d6d7-5bb8-93ce-886deb1dfa70" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: SSO/Authelia and directory owners. Restore the original storage, +encryption/session/signing material and configuration in isolation. + +**Done when:** native storage decryption succeeds, issuer/signing identity and +client configuration are preserved, and an isolated authentication flow has +positive/negative results using the restored directory. Disable outbound +notifications and verify cleanup. Archive integrity/RSA parsing alone are not +native Authelia acceptance. + +## Prove retained privacyIDEA token decryption and existing-token MFA + +```task +id: NK-WP-0049-T19 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T31] +blocking_reason: "LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs." +state_hub_task_id: "6f8af1a7-2a3f-5fa8-8202-83103ebd55b2" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: MFA/privacyIDEA and directory owners. Use the exact linked dump +and original enckey from the independently recovered generation. + +**Done when:** retained token material decrypts inside the isolated process, +a selected existing token completes a native MFA check, a wrong key/code is +refused, and cleanup is verified. Obtain any token/user input through attended +private custody; never emit token plaintext, seeds or credentials. Native database +row counts and disposable synthetic-token fixtures do not prove this outcome. + + +2026-10-16 refinement: native token-material readback is separately tracked in +T31 using the prepared T30 executor. T19 retains actual existing-token MFA and +wrong-code/wrong-key authentication refusal. Internal decryption or padding +success does not authenticate a user. No MFA code is needed for T20/T31; the +selected user's private factor input and exact isolated MFA adapter remain T19. + +## Prove historical and new privacyIDEA audit-signature continuity + +```task +id: NK-WP-0049-T20 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T30] +state_hub_task_id: "375fa8f9-b8a5-5048-97a7-ec141aa7c45d" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: MFA/privacyIDEA audit owner. Use the original recovered audit +pair and linked database in the admitted isolated target. + +**Done when:** existing pre-backup audit signatures are verified natively, a new +isolated audit entry is signed and verified with the preserved identity, a wrong +key fails, and cleanup is recorded. Report any historical verification failure +for explicit owner disposition; do not regenerate keys to hide it. Matching RSA +pairs from T13 are a prerequisite, not audit-history verification. + + +Completed 2026-10-16 at 19:10 UTC using the actual independently retrieved +bundle and original audit identity on the fixed isolated local engine. All +279,209 stored rows were signed and verified; zero unsigned, invalid or legacy +signatures. A new isolated entry verified, and tampering/unrelated-public-key +checks were refused. Container/plaintext cleanup passed. Receipt and reviewed +source/ciphertext/target bindings: `actual-isolated-privacyidea-recovery.json` +and `actual-isolated-privacyidea-review.json` in +`docs/evidence/2026-10-16-identity-outage-recovery/`. +Stored-row continuity is accepted; event completeness and existing-token MFA +are separate and are not claimed. + +## Prove restored KeyCape issuer, policy and consumer continuity + +```task +id: NK-WP-0049-T18 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T19, NK-WP-0049-T32, NK-WP-0049-T33] +blocking_reason: "Source and synthetic KeyCape lifecycle proof are delivered in T32; await exact owner-held recovery attempt preparation T33, selected consumer acceptance and retained-token MFA for the selected flow." +state_hub_task_id: "141dd773-c1fa-56c1-a852-bbe2372e5125" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: KeyCape and the selected current consumer owners. Restore the +original signing/configuration, registry, authentication policies and receipts. + +**Done when:** issuer/JWKS signing identity, registry/client configuration and +policy/receipt continuity are preserved; selected current consumers pass native +positive/negative authentication using the restored dependencies; and isolation +and cleanup hold. Confirm the actual dependency graph in T15. Do not route a +production consumer to the isolated issuer or mint new client grants implicitly. + +## Verify current caller and provider admission at their actual boundaries + +```task +id: NK-WP-0049-T21 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T02] +blocking_reason: "Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers." +state_hub_task_id: "77aa42be-7a39-5d93-8042-03d78b4094b4" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: each current caller's service/provider and credential owner. +Use T02's ledger to select the exact current procedures and required owner lanes. + +**Done when:** each current caller has scoped delivery and native positive/negative +acceptance or an explicit accepted retirement/disposition. Include dedicated +factor-diagnostic admission (do not borrow KeyCape's reader), deployed reserved +LDAP group denial, resolver/cutover input provenance and functional proof, and +bootstrap/root-output/isolation callers. Fixtures, an existing tty and historical +bootstrap receipts are not current authority. Propose exact write/cutover actions +before execution; record deferred actions separately rather than faking a pass. + +## Resolve historical credential and configuration-preserving rotation dispositions + +```task +id: NK-WP-0049-T22 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T02, NK-WP-0049-T21] +blocking_reason: "Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged." +state_hub_task_id: "3ea6bf00-a87f-58ea-bdce-328c13a03a55" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: Platform/custody and affected consumer owners. Review historical +personal-admin/trigger credential copies and unsafe predecessor paths against +known current consumers and rollback custody. + +**Done when:** every affected credential has an owner-accepted disposition: +verified revocation/replacement, continued bounded use with acceptance, or an +explicit linked deferral. Signing/client-secret changes require preservation and +native multi-consumer proof if actually approved. CUST-WP-0073-T05's founder +rotation deferral and decision `1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa` remain in force; +record their exact applicability rather than automatically rotating or treating +source retirement as provider revocation. + +## Provide retrieval that works when OpenBao and SSO are unavailable + +```task +id: NK-WP-0049-T23 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T25] +state_hub_task_id: "1ec537ee-dfee-5576-be36-a81bdba417d1" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Completed 2026-10-16 at 18:02 UTC. The operator used independently held Backup +account access in the private prompt. The actual protected 109,738,344-byte +ciphertext was downloaded with unchanged ETag/size/SHA256, with no OpenBao/SSO +contact or provider writes by the retrieval path. The human-held age key matched +the registered recipient; wrong-key refusal, all 36 manifest hashes, SQLite +integrity, original key pairs and native isolated PostgreSQL 16.14 restore passed. +The database retained five token, two resolver and two realm rows. Disposable +container and temporary plaintext cleanup passed. The agent received no secret. + +Evidence: `actual-offline-transfer.json`, `actual-offline-recovery.json` and +`actual-offline-review.json` under +`docs/evidence/2026-10-16-identity-outage-recovery/`. The review rechecked the +local ciphertext digest and verifier source. This proves the independent path +without requiring a deliberate production outage. Service authentication, +retained-token decryption and historical audit verification remain T16–T20. +T26 separately records accepted guide publication. + +## Record owner disposition for second-provider and external monitoring gaps + +```task +id: NK-WP-0049-T24 +status: todo +priority: medium +assignee: codex +depends_on: [NK-WP-0049-T14] +state_hub_task_id: "474f5a78-a643-59c6-bcf9-fc4a908dfcb0" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +**Next action:** record the current single-provider and host-local-health limits, +then obtain explicit owner scope decisions for a second encrypted provider copy, +external host-loss/freshness detection and recurring recovery-drill cadence. + +**Done when:** each gap has either an accepted bounded residual risk or a linked +live intake/workplan with owner and trigger. Existing daily RPO/25-hour health +settings remain the approved baseline. Scaleway full-identity publication and +an external watchdog are not implemented; do not add their grants/jobs implicitly. +This task triages the new insights instead of silently expanding NK49's required +implementation to an entire disaster-recovery programme. + +## Deliver and verify the offline retrieval adapter and recovery tools kit + +```task +id: NK-WP-0049-T25 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13] +state_hub_task_id: "1c5ea7a1-edf7-5072-b36b-8be7f827957a" +``` + +**Accountable worker:** codex. + +Completed 2026-10-16. `retrieve-identity-offline.py` uses privately entered +independent Backup-account access or an exact read-capable identity-folder share. +It performs only fixed-provider HEAD/GET, binds the reviewed generation by strong +ETag/length/SHA256, refuses redirects/proxies/unreviewed scope and removes failed +downloads. No OpenBao/SSO, age key or standing agent credential is required. +`recovery-kit.py` builds a reproducible allowlisted ZIP containing the tools, +public profile, known-good ciphertext receipt and recovery guides, with file +checksums. Its publication path creates only three versioned guide/ZIP/checksum +objects and verifies readback without overwriting existing conflicting objects. +`run-offline-recovery.sh` provides the owner's two-hidden-prompt handoff. + +Evidence: `tests/test_identity_offline_recovery.py` and the bounded test/build +receipt in `docs/evidence/2026-10-16-identity-outage-recovery/`. The focused run +passed 65 tests including real disposable age/PostgreSQL fixtures. ZIP-contained +entry points import without sibling repositories. This is source/fixture proof, +not actual independent-provider retrieval (T23) or publication (T26). + +2026-10-16 correction: the first owner handoff stopped before its credential +prompt because Python text `r+` requires a seekable stream. Reproduced against a +real controlling PTY; replaced both prompts with the shared hidden-input helper, +refusing redirected input and echo fallback. Added real-terminal and both-CLI +regressions plus closed stage/HTTP-code diagnostics (no raw responses). The +focused rerun passed 36 tests. Evidence: `terminal-prompt-fix.json` in the outage +evidence directory. This source correction does not complete T23/T26. + +## Publish and verify the disaster recovery guide on Storage Share + +```task +id: NK-WP-0049-T26 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T25, NK-WP-0049-T27] +state_hub_task_id: "94d054fe-e5d8-5ddf-8e48-80714123d184" +``` + +**Accountable worker:** codex; operator holds provider access privately. + +Requested by the operator on 2026-10-16; decision +`fc3ef8db-0a33-41d5-a0ba-d13169c5f884` records the bounded scope. Guide source: +`docs/disaster-recovery-storage-share.md`. Document scope, independently held +provider/key requirements, exact known-good fallback, copy/download verification, +archive-check commands, service restore order, recurring schedule and remaining +coverage limits. Include the offline tools so failed Forgejo does not prevent +access to the recovery scripts; retain an independent copy and trusted digest. + +Completed 2026-10-16 at 18:01 UTC through the private independent-provider +owner handoff. All three objects were conditionally created and read back with +matching strong ETag, length and content; the recorded digests match a fresh +reproducible local build. No existing object or provider permission was changed. +The failed first attempt and terminal correction remain documented in T25. + +**Published location:** Storage Share account Backup, +`railiance-backups/net-kingdom-identity/`: + +- `net-kingdom-recovery-57d1940947aa25ff-START-HERE.md` +- `net-kingdom-recovery-57d1940947aa25ff.zip` +- `net-kingdom-recovery-57d1940947aa25ff.zip.sha256` + +**Trusted ZIP SHA256:** +`57d1940947aa25ff83d0f25ddf30090eb40595cccaab64225de882a2fd59ea2f`. +Evidence: `actual-offline-publication.json` and `actual-offline-review.json` in +`docs/evidence/2026-10-16-identity-outage-recovery/`. The published guide is the +exact pre-drill revision; this acceptance record records the later successful +drill without implying an unperformed republication. Keep an independent copy +and trusted digest outside the failed service/provider dependency. + +## Draft the isolated service restore packet and observe candidate images + +```task +id: NK-WP-0049-T27 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T08, NK-WP-0049-T13] +state_hub_task_id: "e273e803-bee7-5d90-bc51-ecc08c0c8eff" +``` + +**Accountable worker:** codex. + +Completed 2026-10-16. Draft in `docs/identity-service-restore-packet.md` names the +known recovered input, restore order, original key/data ownership, default-deny +containment, blocked production/provider/notification effects, cleanup and five +native acceptance lanes with positive/negative checks. Read-only metadata from +the pinned primary cluster resolved all four current service image digests, +including the separately observed privacyIDEA main container and LLDAP's mutable +source tag. Evidence: `docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json`. + +This task extracts completed preparation from T15. Named target admission, +archive/runtime image-lineage matching and independent image availability, +private existing-user/MFA inputs and owner-reviewed native commands remain T15; +no service restore, production change or owner acceptance is claimed. + +## Stage and verify the five exact service images for local offline recovery + +```task +id: NK-WP-0049-T28 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T27] +state_hub_task_id: "4829b091-76d2-5b10-bdfc-032800190c0a" +``` + +**Accountable worker:** codex. + +Completed 2026-10-16. All five pinned references (PostgreSQL, LLDAP, Authelia, +KeyCape, privacyIDEA) were inspected on the fixed local Linux Docker engine +`bnt-lap001`, ID `74f93783-6cef-43a8-a3c4-f62586cd3bc1`. Missing LLDAP and +privacyIDEA images were pulled only by their observed immutable digests. Saved a +private 347,521,536-byte image archive, loaded it and re-inspected every exact +reference. Evidence and archive SHA256/location: +`docs/evidence/2026-10-16-identity-outage-recovery/offline-service-images.json`. + +The local drill no longer requires the production registry. This is not off-host +image escrow or proof of historical mutable-tag/backup lineage; those limits +remain in T15. No service custody or age key was supplied to image caching. + +## Prepare and verify the bounded native LLDAP recovery executor + +```task +id: NK-WP-0049-T29 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T27, NK-WP-0049-T28] +state_hub_task_id: "2f8c73b0-1cdd-5b22-99b6-017ccde4454b" +``` + +**Accountable worker:** codex; operator retains actual private recovery inputs. + +Completed 2026-10-16. `isolated-lldap.py` and `run-isolated-lldap.sh` provide the +exact owner-run stage in `docs/identity-service-restore-packet.md`: fixed local +engine/images, no remote Docker endpoint, original key/seed/JWT/directory data, +Linux-tmpfs decryption, memory/stdin-only private inputs, network-none/read-only +containers, isolation inspection before import, actual existing-user bind, +explicit invalid-credential denial, stable directory rows and cleanup checks. +Missing original server keys are refused; reset/replacement/notification paths +are disabled. No live Kubernetes/OpenBao/SSO path or provider grant is used. + +Evidence: `tests/test_isolated_lldap_recovery.py` exercises the actual pinned +image, disposable retained-account directory and a full synthetic encrypted +bundle through the same native executor. The private real-bundle run and its +acceptance remain T16; fixture success is not actual user/MFA/audit recovery. +See `isolated-lldap-preparation.json` in the October16 outage evidence directory +for exact test/source/target provenance and limitations. + +## Prepare the bounded native privacyIDEA token-material and audit executor + +```task +id: NK-WP-0049-T30 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T28] +state_hub_task_id: "6e4a43db-9be2-5662-bdbf-1595fce03052" +``` + +**Accountable worker:** codex; operator retains private recovery inputs. + +Completed 2026-10-16. `isolated-privacyidea.py` and its owner launcher use the +fixed local Docker target and exact cached PostgreSQL/privacyIDEA3.12.2 images. +The linked dump, original enckey/audit pair and required secret fields are +restored only into a loopback-only, nonroot, read-only, capability-free, +no-host-mount/port, tmpfs-backed environment with inspected pre-import isolation. +Decrypted host staging is tmpfs; normal cleanup removes all private data and +owned containers, with a bounded holding lifetime if the parent is interrupted. + +The native provider decrypts token-key material internally, checks every stored +audit row, adds one isolated signed entry and checks wrong-public-key/tampering +negatives. Original policy for legacy signature verification is retained only +from literal archived configuration; archived Python is never executed. Invalid +or unsigned history remains reported, not repaired or sampled away. Native +material readback is not existing-token MFA or proof of event completeness. + +Evidence: `tests/test_isolated_privacyidea_recovery.py` and +`isolated-privacyidea-preparation.json` in the October16 outage evidence folder. +Synthetic pinned-image tests exercise valid/tampered/unsigned history, malformed +keys, encrypted archive to native restore, isolation and cleanup. Real original +material/audit acceptance remains T31/T20; actual MFA remains T19. + +## Verify retained privacyIDEA token material inside the isolated provider + +```task +id: NK-WP-0049-T31 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T13, NK-WP-0049-T30] +state_hub_task_id: "26383841-8076-5c53-bbd1-9db64e32544a" +``` + +**Accountable worker:** codex; operator supplies only the independently held age +key in the hidden helper. This task extracts actual material-readback scope from +T19, preserving T19's existing-token MFA acceptance gate and historical identity. + +Completed 2026-10-16 at 19:10 UTC in the same actual run as T20. All five +retained token materials were decrypted internally with the original enckey; +an unrelated key could not reproduce any of the five. All 36 manifest hashes, +exact executor/ciphertext/target bindings, isolation and cleanup passed. Safe +receipts are the `actual-isolated-privacyidea-{recovery,review}.json` files +referenced under T20. No retained-factor authentication is claimed. +**Done when:** exact ciphertext/source/target/manifest bindings pass; the native +provider reads every nonempty encrypted token-key field using the original +enckey without exporting material; an unrelated key cannot reproduce that +material; aggregate counts and container/plaintext cleanup are recorded. + +Only bounded internal readback is accepted here. Padding/readback success does +not prove the recovered secret authenticates a retained factor; actual positive +MFA and wrong-key/wrong-code authentication refusal remain T19. No token seed, +plaintext, serial or user record belongs in the receipt, Git, State Hub or chat. + +## Deliver isolated KeyCape restore source and synthetic lifecycle proof + +```task +id: NK-WP-0049-T32 +status: done +priority: high +assignee: codex +state_hub_task_id: "cd78dbb7-a8f2-5754-8788-6eb1b7d97741" +``` + +Completed 2026-10-16. This extracts bounded source/fixture delivery from T18; +T18 retains actual original archive, issuer and selected consumer acceptance. +The adapter reuses the existing transfer/archive/custody contracts, binds the +fixed local daemon and cached images, restores private state only to temporary +memory, verifies original issuer/TLS/signing/configuration/policy bytes and runs +the original native service-client checker with wrong-secret/excess-scope denial. +Durable pre-import obligations and exact parent pidfd/session quiescence precede +loss/deadline cleanup. Daemon response uncertainty remains uncertainty after +resource absence; it never authorizes recovery success or replay. + +Frozen R6 fixes a demonstrated image-declared anonymous-volume incompatibility +using bounded helper-only tmpfs, preserving anonymous disk-volume refusal. Safe +phase markers carry no private diagnostics. Independently passed 72 focused +checks, then actual pinned-image synthetic positive, hard-parent-loss, deadline +and nonzero-after-accept cases. Independent fixed-daemon inventories and host +mount-namespace checks prove all exact resources and plaintext absent. Reaper +outcomes are retained; initial sandbox receipt lookup failure is preserved and +superseded by device-bound host verification. R1–R5 failures remain retained. + +Evidence: Custodian docs/changes/netkingdom-target-2026-10-16/ +identity-restore-adapter-next/revisions/r6/, actual-fixture-r6-all.json and +coordinator-r6-all-review.json. Exact source/dependency hashes and candidate +manifest bind the proof. This uses generated test keys only; it proves neither +actual owner-held recovery, human MFA, downstream reconnect nor native authority. + +Publication preparation R7 preserves every R6 recovery adapter byte and digest. +The normal scanner flagged eight public source-file SHA-256 values in the R6 +manifest. Explicit sha256-qualified checksums resolve those false positives; +the fixture reader strictly validates their type/shape before checking the same +bytes. R7 passes 73 focused checks including malformed/untyped digest refusal. +The frozen R6 actual four-case proof remains the runtime evidence; no repeated +native outcome is claimed for publication encoding. Original scanner refusal is +retained, and scanner policy/exemptions are unchanged. + +## Prepare the exact owner-held KeyCape recovery attempt + +```task +id: NK-WP-0049-T33 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T32, NK-WP-0049-T34, NK-WP-0049-T35, NK-WP-0049-T36] +blocking_reason: "Source custody/preflight preparation is evidenced in T34; await original TLS/trust recovery disposition T35 and selected original registration/private-admission metadata T36 before final reviewed command and existing owner process." +state_hub_task_id: "56c8d469-7a10-5d50-a30e-95cbf3cd4756" +``` + +**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners. +This umbrella retains final attempt readiness. Completed custody/preflight source +preparation is extracted into T34; T35 resolves original TLS/trust recovery and +T36 prepares the selected original registration and private admission. This split +records distinct outcomes without counting source preparation as recovery. + +After those deliverables, assemble the exact existing six-argument attended +command and admission digest, bind the integrated source/dependency revision and +fixed local target, and record execution deadline, safe receipt location, abort, +rollback and independent receipt verification through the existing owner process. +Keep private manifest derivation inside custody and original TLS/client/key inputs +in the owner's own terminal; the current null fields must refuse execution. +Existing session authorization is retained within scope; this record does not +supply missing owner assent or approve any actual recovery attempt. + +**Done when:** reviewed executable command and complete metadata-only/private +admission packet are ready for the existing attended custody path; prerequisites +and independent receipt verification are explicit. T18 still owns actual +execution, original consumer continuity and acceptance. T19 retains retained-factor +MFA. This task neither authorizes a production consumer redirect nor creates a +new client grant. + +## Deliver attended custody, metadata preflight and private manifest preparation source + +```task +id: NK-WP-0049-T34 +status: done +priority: high +assignee: codex +depends_on: [NK-WP-0049-T32] +``` + +**Accountable worker:** NetKingdom codex; independent Custodian source review. +Completed 2026-10-17 as source preparation. Existing attended custody entry points +now have a reviewed candidate sealed anonymous descriptor mode: hidden prompt, +actual public-recipient verification, strict owned/private/full-seal descriptor +admission, non-inheritable returned duplicate and explicit descriptor passing. +The existing file mode stays compatible, with its original hard-loss limitation; +no improved file-mode cleanup is claimed. Exact parent-loss tests prove descriptor +lifetime without signalling inherited user shell groups. An admitted child retains +its descriptor until it exits; parent loss is not child revocation. + +The candidate checks bounded, duplicate-free public JSON, exact admission digest, +transfer/archive and selected service bindings, typed hashes for all 17 invoked +local source/config dependencies and required Python imports before requesting a +key. TLS and ciphertext preparation uses file metadata only. Legacy explicit +manifest digests remain supported; versioned ciphertext-derived binding computes +the canonical digest privately only after exact reviewed ciphertext, authenticated +age decryption and full existing archive validation. The derived private digest +never enters safe receipts/checkpoints and grants no new consumer authority. + +The coordinator independently passed 111 focused checks. The existing custody +regression suite passed 94 checks; 61 real-SOPS tests skipped because that trusted +runtime is unavailable, so that compatibility coverage remains unverified. Tests +use generated keys/archive material only, including actual age cryptography, +synthetic hidden-prompt and exact parent-loss processes. Manifest-builder tests +mock Docker/issuer behavior and confer no new native proof. Frozen R6/R7 proof, +intermediate test failures and scanner history remain retained. + +Bounded public image readback separately establishes the cached original image's +13670142-byte `/keycape` executable as +`sha256:cd89121393e1465aa76009763602a5b8c75f9dddb5679aea6242623f135b51b4`. +This is executable identity metadata, not recovery or custody acceptance. + +Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/` +(`candidate.patch`, `review.json`, source closure, attempt/admission metadata and +`public-verifier-image-readback.json`). Canonical integration/publication and +projection receipts must be recorded separately when performed; this completed +source-preparation record does not invent them. T33 remains open for complete +attempt readiness; actual recovery/consumer acceptance remains T18 and MFA T19. + +## Establish original KeyCape TLS and trust recovery ownership for outage use + +```task +id: NK-WP-0049-T35 +status: todo +priority: high +assignee: codex +depends_on: [NK-WP-0049-T34] +``` + +**Accountable worker:** NetKingdom codex, coordinating with the source owner of +Ingress/cert-manager/Traefik and the original TLS custody/provider boundary. +**Next action:** identify and record that actual owner's existing recovery path, +then obtain its metadata/disposition through the ordinary owner process. A missing +exact credential-routing catalog match is not evidence that no owner exists. + +Current source declares `kc.coulomb.social`, `sso/kc-tls`, cert-manager +`ClusterIssuer/letsencrypt-prod` and Traefik TLS termination before KeyCape 8080. +The identity backup profile excludes `kc-tls`; the verified archive therefore +establishes no independent recovery of that TLS identity/trust. Determine whether +the exact original certificate/key/trust is independently recoverable during an +issuer/cluster outage, or whether a documented reissue path is acceptable for the +selected continuity claim. Do not read or place the TLS key in work records, use +a fresh self-signed trust substitute, mutate production or claim owner assent. + +**Done when:** named owner/provider, independent outage prerequisites, original +public identity/certificate/trust hashes or accepted reissue disposition, private +custody entry point and verification/abort limits are explicit and reviewed. +Record any unavailable recovery capability as a live owner work record and bind +its dependency/unblock condition; a prose-only gap or untested archive is not +completion. This delivers preparation/disposition, not actual TLS recovery. + +## Prepare selected original service registration and private admission bindings + +```task +id: NK-WP-0049-T36 +status: todo +priority: high +assignee: codex +depends_on: [NK-WP-0049-T34] +``` + +**Accountable worker:** NetKingdom codex with KeyCape and the selected existing +service consumer's owner. **Next action:** identify that owner and the exact +original registration's non-secret selector metadata using existing source/owner +evidence; preserve independent private custody and never retrieve a client secret. + +Bind reviewed ciphertext `7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`, +its independently verified transfer, source/image/executable closure and an +explicit versioned manifest-binding mode. For the original service registration, +record client identifier, secret environment name, audience, granted scope, +subject, tenant, required roles and denied scope without values or new grants. +Describe the native service-client dependency graph and distinguish unexercised +LLDAP/Authelia/privacyIDEA/downstream flows. Keep private manifest material inside +the existing custody process, never in Git, safe receipts, State Hub or chat. + +**Done when:** the selected existing service owner and exact non-secret registration +bindings are independently established, the private admission builder validates +positive/negative source/archive/selector cases, and its metadata is ready to +combine with T35's original TLS/trust disposition under T33. This task may prepare +registration metadata while T35 proceeds independently; it does not finalize the +whole admission, perform actual recovery, redirect a consumer or confer assent. + +## Accept remaining recovery and caller evidence and close the G08 handoff + +```task +id: NK-WP-0049-T03 +status: wait +priority: high +assignee: codex +depends_on: [NK-WP-0049-T02, NK-WP-0049-T16, NK-WP-0049-T17, NK-WP-0049-T18, NK-WP-0049-T19, NK-WP-0049-T20, NK-WP-0049-T21, NK-WP-0049-T22, NK-WP-0049-T23, NK-WP-0049-T24, NK-WP-0049-T26] +blocking_reason: "Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete." +state_hub_task_id: "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac" +``` + +**Accountable worker:** codex. Service/credential-owner acceptance remains with +the named owner boundary. + +Owner boundary: NetKingdom, Platform and affected service owners, with Custodian +G08 receiving the final evidence. The completed archive/job milestones remain +accepted independently of this final gate. + +**Done when:** T02 and T15–T36 have their stated completed or explicitly accepted +handoff outcomes; service authentication/token/audit proof and current/legacy +caller dispositions are accepted; the source workplan and projected tasks agree; +and G08 has an exact evidence/handoff record. Record actionable residuals as live +work records before workplan closure. Preserve historical IDs and receipts; do +not call a deferred rotation or an untested full-estate restore complete. + +## Evidence locations and refinement history + +“Live” above means `docs/evidence/2026-10-16-identity-backup-live/` in NetKingdom. +Implementation preparation is in the October16 implementation directory; earlier +backup discovery/database recovery is in `docs/evidence/2026-10-15-bootstrap-recovery/`. +The source-repair receipts explicitly labelled Custodian belong to that owning +repository. Use the [recovery ceremony](../docs/bootstrap-recovery-ceremony.md) +for exact procedure and approval history. No secret values belong in task evidence. + +Refinement rule: substantial work and newly understood remaining scope must be +visible in task blocks at a checkpoint, not only in progress prose. This replan +preserves T01–T03 identity and original evidence while making the completed +milestones and each remaining acceptance gate separately manageable. + +Managed identifier assignment also consumed a preexisting staged generated brief; +the follow-up commit restores its prior published baseline while preserving the +operator's working brief and original staged version. This is a projection-tool +side effect, not new recovery evidence. + + +R7 source publication and task bindings verified: +`5fcd5952add1c0167116094298a1d5feaa60cf30` is the observed published source. +All eight canonical additions match the reviewed bytes; all 33 task statuses +match the actual hub projection, including T32 done and T33 todo. Original +staged/working brief content is preserved. Normal synchronization has zero +failures/automation errors; existing warnings and scanner false-positive history +remain retained. Custodian identity-restore-adapter-next/canonical-r7-integration.json +binds source/projection/native-fixture evidence separately. T18 remains waiting +for the actual owner-held attempt and selected consumer acceptance. + + +2026-10-17 source/attempt refinement: T34 records completed attended custody, +preflight/private-manifest source preparation and its bounded tests; T35 and T36 +make original TLS recovery ownership and selected original admission metadata live +work. T33 retains final attempt readiness and its original Hub UUID. This adds no +actual recovery, MFA, native authority or owner assent. Source task counts above +refer to this file; projection has not been claimed for this proposed refinement. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/coordination-cases.json b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/coordination-cases.json new file mode 100644 index 00000000..c0d064f9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/coordination-cases.json @@ -0,0 +1,45 @@ +{ + "status": "derived_local_objects_not_central_publication", + "mode_source": "coordination-engine/spec/cross-owner-wait-mode-v0.1.md", + "new_driver_created": false, + "runtime_worker_injection_enabled": false, + "cases": [ + { + "scenario_type": "cross_owner_wait", + "case_id": "nk49-t35-rcluster18", + "obligor_workplan_id": "RCLUSTER-WP-0018", + "obligor_task_id": "RCLUSTER-WP-0018-T02", + "beneficiary_workplan_id": "NK-WP-0049", + "beneficiary_task_id": "NK-WP-0049-T35", + "flavor": "implementation", + "state": "observed", + "reason": "Owner source handoff prepared; canonical projection pending", + "coordination_context": "CUST-WP-0080" + }, + { + "scenario_type": "cross_owner_wait", + "case_id": "nk49-t36-secrets15", + "obligor_workplan_id": "SECRETS-WP-0015", + "obligor_task_id": "SECRETS-WP-0015-T03", + "beneficiary_workplan_id": "NK-WP-0049", + "beneficiary_task_id": "NK-WP-0049-T36", + "flavor": "implementation", + "state": "observed", + "reason": "Owner source handoff prepared; canonical projection pending", + "coordination_context": "CUST-WP-0080" + }, + { + "scenario_type": "cross_owner_wait", + "case_id": "secrets15-nk49-readiness", + "obligor_workplan_id": "NK-WP-0049", + "obligor_task_id": "NK-WP-0049-T33", + "beneficiary_workplan_id": "SECRETS-WP-0015", + "beneficiary_task_id": "SECRETS-WP-0015-T04", + "flavor": "implementation", + "state": "waiting", + "reason": "Foreign source readiness remains incomplete; beneficiary also has a separate attended human gate", + "coordination_context": "CUST-WP-0080", + "human_gate_never_woken": true + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/current-consumer-source-preflight.json b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/current-consumer-source-preflight.json new file mode 100644 index 00000000..5a68f5ae --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/current-consumer-source-preflight.json @@ -0,0 +1,17 @@ +{ + "schema": "secrets-engine.recovery-selector-preflight.v1", + "status": "source_selector_matches_not_native_assent", + "source_sha256": "sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0", + "consumer": { + "client_id": "secrets-engine-openbao", + "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", + "audience": "secrets-engine-openbao", + "scope": "openbao:login", + "subject": "service:secrets-engine", + "tenant": "tenant:platform", + "roles": "secrets-engine", + "deny_scope": "openbao:admin" + }, + "credential_source_exercised": false, + "network_contacted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/owner-handoff-review.json b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/owner-handoff-review.json new file mode 100644 index 00000000..f6fe290e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/owner-handoff-review.json @@ -0,0 +1,166 @@ +{ + "schema": "nk49.recovery-source-owner-handoffs.v1", + "status": "isolated_patches_ready_for_root_review_not_integrated", + "owners": { + "railiance-cluster": { + "baseline_head": "cac8eabef5a0d0990a7b0de8f5e2c762666ac7f0", + "paths": { + "tools/public-tls-metadata.py": "sha256:87c167e44f0fcf154c9f2afae2187bebd33e5f220c05f579c1233b58bfe4f30b", + "tests/test_public_tls_metadata.py": "sha256:200a5be79dc6a715e22798b63e481431c3afeb2c4d7b71b26375d016a0c45866", + "docs/keycape-tls-recovery-boundary.md": "sha256:f4bcb1d1832c566f9c87793ed2a461c8cd8a00859dc4ae66cd3c160d7ceefa70", + "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md": "sha256:4a0d616ccbea199053531e82276441be21d0f168a63056a456375e27f58fc9af" + }, + "workplan": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md", + "tasks": [ + "RCLUSTER-WP-0018-T01", + "RCLUSTER-WP-0018-T02", + "RCLUSTER-WP-0018-T03", + "RCLUSTER-WP-0018-T04", + "RCLUSTER-WP-0018-T05" + ], + "canonical_additions_all_absent": true + }, + "secrets-engine": { + "baseline_head": "535681052cd55ca613706ecc60c39d299929b3c5", + "paths": { + "tools/check-keycape-recovery-selector.py": "sha256:e3e5e3ee743a212247701d6cb40fcf5b7f19349126fa9b32cbef9a3cc9459150", + "tests/test_keycape_recovery_selector.py": "sha256:221112ba303c58bc4205b577359ca90d8a6f60d09eb0b538990b1e53a156e262", + "docs/keycape-isolated-recovery-selector.md": "sha256:ed23b0df3d4b49e4b76df795455153c805971ccd37886836c960f550709e0739", + "docs/keycape-recovery-selector.json": "sha256:02077af3c18b24f5e4a85992a3d4d0df51a8e458de1e48b48f7eb5f482257012", + "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md": "sha256:22b148f0f403b8a9d471f34b2fae6938066123203d5c6f21ad463c5c262705c5" + }, + "workplan": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md", + "tasks": [ + "SECRETS-WP-0015-T01", + "SECRETS-WP-0015-T02", + "SECRETS-WP-0015-T03", + "SECRETS-WP-0015-T04" + ], + "canonical_additions_all_absent": true + } + }, + "patches": { + "railiance-cluster": "sha256:7a8e4c9620881b18a5752424f5f46e6bd263eba2cc0bbf24a96968b7f9d04287", + "secrets-engine": "sha256:a797d299c719f8c998a7514c8b6685a3f75652e38c12e940569d37bb6bd03da2" + }, + "prior_public_sources_revalidated": [ + { + "repo": "net-kingdom", + "path": "sso-mfa/k8s/keycape/ingress.yaml", + "recorded_sha256": "sha256:bc5c7524a82de5e8b9755e7d7d408b4a66e034424cb2663c1d21352d292d5a72", + "current_bytes_match": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/identity-backup-profile.json", + "recorded_sha256": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1", + "current_bytes_match": true + }, + { + "repo": "net-kingdom", + "path": "sso-mfa/k8s/keycape/secrets-engine-openbao-client-rollout.py", + "recorded_sha256": "sha256:8b834e5f0353a5e11797637420609e06e7bf7de47777c7e78b774f74d4a6afbd", + "current_bytes_match": true + }, + { + "repo": "net-kingdom", + "path": "sso-mfa/k8s/keycape/deployment.yaml", + "recorded_sha256": "sha256:83777942bc922c7d1719b0ad0b09e873105c342dae3b013bb65eeccf6d905626", + "current_bytes_match": true + }, + { + "repo": "net-kingdom", + "path": "workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md", + "recorded_sha256": "sha256:ef38481d26e599802d94f8b06a4d89c7c99a83c81ca5be9bcf7422b885867479", + "current_bytes_match": true + }, + { + "repo": "net-kingdom", + "path": "sso-mfa/k8s/keycape/README.md", + "recorded_sha256": "sha256:e7a002996fd57477f6bcd054264076fe04da17f8685095163c1c80dd57b753f2", + "current_bytes_match": true + }, + { + "repo": "key-cape", + "path": "workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md", + "recorded_sha256": "sha256:3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a", + "current_bytes_match": true + }, + { + "repo": "key-cape", + "path": "docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json", + "recorded_sha256": "sha256:a405ef5289159c525695352ba71da7eca8e897b6f2dc7e5795f9b239361d8068", + "current_bytes_match": true + }, + { + "repo": "secrets-engine", + "path": "docs/evidence/2026-10-10-keycape-openbao-probe.json", + "recorded_sha256": "sha256:bf0b5df9f030499d96770f1d9fb2091a8d37da71422a6d456be7200d3a1a1782", + "current_bytes_match": true + }, + { + "repo": "secrets-engine", + "path": "docs/keycape-openbao-consumer-custody.md", + "recorded_sha256": "sha256:73efdd549f3feaccc53eefd6f1b95c8b9eb7091648f94ad80978ba5381be9124", + "current_bytes_match": true + }, + { + "repo": "railiance-cluster", + "path": "SCOPE.md", + "recorded_sha256": "sha256:e4f6a8dd3d72a1f04f0d94721d57ca2a4c7e98790a8373876a5005b87ec5d8a6", + "current_bytes_match": true + }, + { + "repo": "railiance-cluster", + "path": "docs/rail-kubernetes-substrate-profile.md", + "recorded_sha256": "sha256:75beeca7ef9670efc0b92c1ef8a33e24b72acbcdee06ee8de3c6492cb69d4f6b", + "current_bytes_match": true + }, + { + "repo": "railiance-cluster", + "path": "tools/cmd/railiance-backup-s2", + "recorded_sha256": "sha256:35c60b7ccccd7b4d5a1cf2134c1f2ca78c45040a7ffcc55f2c32c385a0a682a3", + "current_bytes_match": true + }, + { + "repo": "railiance-cluster", + "path": "tools/cmd/railiance-restore-s2", + "recorded_sha256": "sha256:25be58968fee3eeb6069e5af08abe601e8f60f04241e8de4bb917a7c639882e5", + "current_bytes_match": true + }, + { + "repo": "railiance-cluster", + "path": "docs/backup-restore.md", + "recorded_sha256": "sha256:4d0bbaad3d69f10179ba9590d3fa12fba2967202c756ecf1952436d1e27de902", + "current_bytes_match": true + }, + { + "repo": "railiance-master", + "path": "docs/rail-kubernetes-boundary.md", + "recorded_sha256": "sha256:b7a382622e241c174b11a8645aba1801693f22950bb132760b2170fcb828ced1", + "current_bytes_match": true + }, + { + "repo": "railiance-platform", + "path": "docs/backup-provider-coverage.md", + "recorded_sha256": "sha256:f5983aac1e8cc1828e9f73340b1f66c0e67cdcddc1a1e49cbe6f5e39e90c6641", + "current_bytes_match": true + } + ], + "tests": { + "public_tls_and_selector_checks_passed": 18, + "actual_current_consumer_source_preflight": "passed", + "real_certificate_or_key_read": false, + "network_contacted": false + }, + "supervision": "Custodian assignment remains supervised; no owner-specific .kaizen record found, no promotion/scope authority inherited. Source preparation only.", + "existing_records": { + "cluster": "0017 signer only;0007 backlog/HA gated;archived safety-net local backup only; no live original TLS recovery record located", + "secrets": "0012 remains finished;0008T06+0013 retain functional/backend entitlement scope" + }, + "canonical_mutations": false, + "secrets_retrieved": false, + "messages_sent": false, + "native_assent": false, + "publication_next": "Root independent review and authorized canonical apply; normal per-repo consistency projection; verify new IDs/statuses and attach derived case receipts without claiming queued/absent projection." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster.patch b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster.patch new file mode 100644 index 00000000..c10852c8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster.patch @@ -0,0 +1,278 @@ +--- /dev/null ++++ b/tools/public-tls-metadata.py +@@ -0,0 +1,55 @@ ++#!/usr/bin/env python3 ++"""Owner-run public certificate/trust metadata; no private-key argument or network. ++ ++The owner supplies public-only PEM files from the independently established ++original recovery source. This does not identify that source or accept reissue. ++""" ++import argparse ++from datetime import timezone ++import hashlib ++import json ++import os ++from pathlib import Path ++import re ++import stat ++from cryptography import x509 ++from cryptography.hazmat.primitives import serialization ++ ++LIMIT=1024*1024 ++CERT=re.compile(br'-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----') ++ ++def public_bundle(path): ++ # Bound the read before allocation; refuse every non-certificate PEM block. ++ path=Path(os.path.abspath(path)) ++ if any(p.is_symlink() for p in (path,*path.parents)) or not stat.S_ISREG(path.stat().st_mode) or not 0LIMIT:raise ValueError() ++ blocks=CERT.findall(raw) ++ if not blocks or len(blocks)>512 or CERT.sub(b'',raw).strip():raise ValueError() ++ return raw,[x509.load_pem_x509_certificate(block) for block in blocks] ++ ++def describe(cert_path,trust_path): ++ certificate,chain=public_bundle(cert_path);trust,roots=public_bundle(trust_path);leaf=chain[0] ++ try:names=leaf.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName) ++ except x509.ExtensionNotFound:names=[] ++ spki=leaf.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo) ++ def utc_date(name): ++ value=getattr(leaf,name+'_utc',None) ++ if value is None:value=getattr(leaf,name).replace(tzinfo=timezone.utc) ++ return value.isoformat() ++ return {'schema':'nk49.public-tls-metadata.v1','status':'public_metadata_only_not_originality_or_owner_assent', ++ 'tls_cert_sha256':hashlib.sha256(certificate).hexdigest(),'trust_sha256':hashlib.sha256(trust).hexdigest(), ++ 'leaf_public_spki_sha256':'sha256:'+hashlib.sha256(spki).hexdigest(),'leaf_dns_sans':names, ++ 'leaf_not_before':utc_date('not_valid_before'),'leaf_not_after':utc_date('not_valid_after'), ++ 'server_chain_certificates':len(chain),'trust_certificates':len(roots),'network_contacted':False,'private_key_argument_supported':False} ++ ++def main(): ++ p=argparse.ArgumentParser(description=__doc__);p.add_argument('--public-cert',required=True);p.add_argument('--public-trust',required=True);a=p.parse_args() ++ try:result=describe(a.public_cert,a.public_trust) ++ except Exception:print('{"status":"public_metadata_refused","private_diagnostics_suppressed":true}');return 1 ++ print(json.dumps(result,indent=2));return 0 ++ ++if __name__=='__main__':raise SystemExit(main()) +--- /dev/null ++++ b/tests/test_public_tls_metadata.py +@@ -0,0 +1,34 @@ ++import importlib.util ++from pathlib import Path ++from datetime import datetime,timedelta,timezone ++from unittest.mock import patch ++import pytest ++from cryptography import x509 ++from cryptography.hazmat.primitives import hashes,serialization ++from cryptography.hazmat.primitives.asymmetric import rsa ++from cryptography.x509.oid import NameOID ++ ++spec=importlib.util.spec_from_file_location('metadata',Path(__file__).parents[1]/'tools/public-tls-metadata.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) ++@pytest.fixture ++def cert(tmp_path): ++ key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc) ++ cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('synthetic.invalid')]),critical=False).sign(key,hashes.SHA256())) ++ p=tmp_path/'public.pem';p.write_bytes(cert.public_bytes(serialization.Encoding.PEM));return p ++ ++def test_public_only_metadata_and_hashes(cert): ++ result=m.describe(cert,cert) ++ assert result['leaf_dns_sans']==['synthetic.invalid'];assert result['network_contacted'] is False ++ assert '-----BEGIN' not in str(result) ++ ++def test_mixed_or_private_pem_is_refused(cert): ++ cert.write_bytes(cert.read_bytes()+b'\n-----BEGIN PRIVATE KEY-----\nsynthetic\n-----END PRIVATE KEY-----') ++ with pytest.raises(ValueError):m.public_bundle(cert) ++ ++def test_oversize_is_refused_before_read(cert): ++ with patch.object(m,'LIMIT',1),patch.object(m.os,'open') as opened: ++ with pytest.raises(ValueError):m.public_bundle(cert) ++ opened.assert_not_called() ++ ++def test_symlink_refused(cert): ++ link=cert.with_name('link.pem');link.symlink_to(cert) ++ with pytest.raises(ValueError):m.public_bundle(link) +--- /dev/null ++++ b/docs/keycape-tls-recovery-boundary.md +@@ -0,0 +1,51 @@ ++# Original KeyCape TLS recovery boundary ++ ++Source disposition, 2026-10-17, scoped to railiance-cluster INTENT: cluster runtime, ++ingress/certificate plumbing and backup posture. This is source preparation only. ++It is not native acceptance, an issuer/key custody assent, a reissue decision or ++permission to mutate the cluster. ++ ++NetKingdom's ingress source names `kc.coulomb.social`, `sso/kc-tls`, ++`ClusterIssuer/letsencrypt-prod` and Traefik termination before KeyCape8080. ++NetKingdom owns that namespace intent; railiance-cluster owns cert-manager, ++ingress controller and cluster backup posture. No inspected source establishes ++who independently holds the original private TLS recovery material when the ++ordinary issuer/cluster is unavailable. A missing exact routing-catalog entry ++cannot prove absence of an owning subsystem. ++ ++The identity archive excludes `kc-tls`. Current S2 backup source encrypts SQLite ++state, Helm values and kubeconfig to the same host's `/opt/backup/railiance/cluster` ++and retains seven versions. No inspected source proves an actually fresh, ++independent offsite object or original certificate/key/trust recovery. Archived ++RAIL-BS-WP-0004 is historical local-backup delivery; backlog RCLUSTER-WP-0007-T05 ++is gated HA issuance/renewal; active RCLUSTER-WP-0017 is signer runtime/storage. ++None is an existing live task for this original TLS recovery demand. ++ ++`railiance-backup-s2` also stages raw state/Helm output in named `/tmp` paths ++without early hard-loss cleanup; `railiance-restore-s2` prints whole-state restore ++instructions. Do not use either as an isolated TLS extraction path. RCLUSTER-WP-0018 ++tracks exact inventory, safe private staging/extraction preparation where required, ++independent outage proof and future archive-boundary disposition. No original ++ciphertext or historical outcome is rewritten by that work. ++ ++`tools/public-tls-metadata.py` is an offline public-only step. After the source ++owner identifies independent original **public certificate/chain and trust files**: ++ ++```sh ++python3 tools/public-tls-metadata.py --public-cert ORIGINAL_PUBLIC_CERT_CHAIN.pem --public-trust ORIGINAL_PUBLIC_TRUST.pem ++``` ++ ++It accepts no private-key argument, bounds regular-file reads, refuses symlinks and ++non-certificate PEM material, and reports public hashes/SANs/validity/counts only. ++The file names are owner-local placeholders, not a key request. Output establishes ++public metadata, not originality, hostname/chain acceptance, reissue permission or ++custody. Do not supply a Secret object or combined certificate/key PEM. Fail closed ++when original source, trust or exact bindings are missing; no fresh self-signed ++trust workaround is admitted. ++ ++For FUTURE NetKingdom archives, the source owners must explicitly disposition ++original TLS coverage or a reviewed reissue contract: producer/custody owner, ++private artifact boundary, offline recovery independence, original public hashes, ++scoped capture/read permissions, retention and native verification. Merely adding ++`kc-tls` to secret_refs would change capture/permission/custody scope and is not ++authorized by this source review. It cannot repair the current archive retroactively. +--- /dev/null ++++ b/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md +@@ -0,0 +1,126 @@ ++--- ++id: RCLUSTER-WP-0018 ++type: workplan ++title: "Establish original KeyCape TLS recovery and future archive boundary" ++domain: financials ++repo: railiance-cluster ++status: ready ++owner: codex ++topic_slug: railiance ++created: "2026-10-17" ++updated: "2026-10-17" ++flavor: implementation ++depends_on: [] ++related: [CUST-WP-0080, NK-WP-0049, RCLUSTER-WP-0007, RCLUSTER-WP-0017] ++--- ++ ++Required owner source work for NK-WP-0049-T35 and CUST-WP-0080 G08. Reviewed ++against current INTENT/SCOPE, backup sources and existing work records; ready ++means work can proceed, not privileged execute authority. This does not promote ++HA/backlog work, reopen archived local backup delivery or duplicate signer0017. ++Source-owner scope is certificate plumbing/backup posture; NetKingdom owns ++namespace ingress, while exact original independent TLS custody remains to be ++established. No secret read, reissue, live change or native assent occurred. ++ ++## Record source boundary and public-only metadata preparation ++ ++```task ++id: RCLUSTER-WP-0018-T01 ++status: done ++priority: high ++assignee: codex ++``` ++ ++Completed 2026-10-17 as isolated source preparation. Independently reviewed the ++17 public source references against current owner bytes, checked current owner ++instructions/environment/supervision and existing plans, and recorded the exact ++boundary in docs/keycape-tls-recovery-boundary.md. Prepared bounded public-only ++certificate/trust metadata projection with generated-certificate tests; no actual ++certificate/key read. Source integration/publication and task projection remain ++separate receipts, not claimed by this completed preparation task. ++ ++Evidence: the-custodian docs/changes/netkingdom-target-2026-10-16/ ++recovery-owner-handoffs-r1/ and prior recovery-attempt-r1/t35-t36-source-next/. ++ ++## Establish exact independent original TLS artifact and provider custody metadata ++ ++```task ++id: RCLUSTER-WP-0018-T02 ++status: todo ++priority: high ++assignee: codex ++``` ++ ++Next: identify the actual original sso/kc-tls custody owner and independent public ++provenance through existing owner processes. Record artifact/provider, timestamp, ++public certificate/trust hashes, private custody entry point and outage dependencies ++without values. Source review cannot infer a fresh backup, original key coverage ++or independent decryption custody from a local backup script. Use credential ++routing before any later credential need; missing route is not proof of absence. ++Done when exact independent recovery or an explicitly owned unavailable capability ++is evidenced as metadata and a reviewed next action, with no guessed assent. ++ ++## Prepare safe isolated original extraction or explicit reissue alternative ++ ++```task ++id: RCLUSTER-WP-0018-T03 ++status: wait ++priority: high ++assignee: codex ++depends_on: [RCLUSTER-WP-0018-T02] ++blocking_reason: "Exact original artifact/provider/custody disposition is required before selecting extraction or reissue source." ++``` ++ ++From T02, reuse the actual owner path to prepare an exact attended private-only ++recovery command and full dependency/cleanup proof. Do not run whole-state restore ++or expose raw state/Secret output. If existing backup staging is required, correct ++only the needed path with bounded memory-only staging and early exact cleanup, ++including error/parent-loss tests before use. Original recovery and accepted ++reissue are distinct claims; no self-signed trust substitute, new broker or ++implicit production action. Done when the exact source proposal, public metadata ++preflight, negatives, timeout, abort and custody obligations are reviewed. ++ ++## Verify admitted original TLS recovery through the actual owner boundary ++ ++```task ++id: RCLUSTER-WP-0018-T04 ++status: wait ++priority: high ++assignee: codex ++needs_human: true ++depends_on: [RCLUSTER-WP-0018-T02, RCLUSTER-WP-0018-T03] ++blocking_reason: "Await exact reviewed attended owner custody path and its existing scoped authority; no private input or native attempt requested yet." ++``` ++ ++Only the actual admitted owner action can prove recovered certificate/key pairing, ++original hostname/trust and independent outage operation, or the expressly accepted ++reissue result. Retain exact source/artifact/target/identity/negative/cleanup receipts. ++Human custody/authority gates are not worker wakeups. Source and historical local ++backup proof never fill native assent. Return bounded public proof to NK49T35; ++no production mutation, credential retrieval or approval is granted by this task. ++ ++## Disposition original TLS coverage in the future identity archive contract ++ ++```task ++id: RCLUSTER-WP-0018-T05 ++status: todo ++priority: high ++assignee: codex ++``` ++ ++Next: deliver the source-owned requirement in docs/keycape-tls-recovery-boundary.md ++to NetKingdom's existing contract process: original TLS custody or accepted reissue, ++independent provider/key recovery, public identity hashes, scoped capture/read ++permissions and verification/retention. Record named owner dependencies as live ++work before closure. Do not append kc-tls to secret_refs, expand capture credentials ++or rewrite a historical archive without those owner reviews/authority. Done when ++the future contract requirement has an exact tracked disposition/handoff; it is ++not proof of current archive coverage or actual future capture. ++ ++## Coordination and completion ++ ++Beneficiary NK-WP-0049-T35/CUST-WP-0080 remains linked to this obligor record by ++derived cross_owner_wait case nk49-t35-rcluster18 in the packet. No new project ++driver, live worker injection or message was created. Own T03 wait has an explicit ++local prerequisite; T04 is a human gate. Close only after source/native obligations ++are proved or an explicit bounded scope disposition hands off remaining live work. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/docs/keycape-tls-recovery-boundary.md b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/docs/keycape-tls-recovery-boundary.md new file mode 100644 index 00000000..40953976 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/docs/keycape-tls-recovery-boundary.md @@ -0,0 +1,51 @@ +# Original KeyCape TLS recovery boundary + +Source disposition, 2026-10-17, scoped to railiance-cluster INTENT: cluster runtime, +ingress/certificate plumbing and backup posture. This is source preparation only. +It is not native acceptance, an issuer/key custody assent, a reissue decision or +permission to mutate the cluster. + +NetKingdom's ingress source names `kc.coulomb.social`, `sso/kc-tls`, +`ClusterIssuer/letsencrypt-prod` and Traefik termination before KeyCape8080. +NetKingdom owns that namespace intent; railiance-cluster owns cert-manager, +ingress controller and cluster backup posture. No inspected source establishes +who independently holds the original private TLS recovery material when the +ordinary issuer/cluster is unavailable. A missing exact routing-catalog entry +cannot prove absence of an owning subsystem. + +The identity archive excludes `kc-tls`. Current S2 backup source encrypts SQLite +state, Helm values and kubeconfig to the same host's `/opt/backup/railiance/cluster` +and retains seven versions. No inspected source proves an actually fresh, +independent offsite object or original certificate/key/trust recovery. Archived +RAIL-BS-WP-0004 is historical local-backup delivery; backlog RCLUSTER-WP-0007-T05 +is gated HA issuance/renewal; active RCLUSTER-WP-0017 is signer runtime/storage. +None is an existing live task for this original TLS recovery demand. + +`railiance-backup-s2` also stages raw state/Helm output in named `/tmp` paths +without early hard-loss cleanup; `railiance-restore-s2` prints whole-state restore +instructions. Do not use either as an isolated TLS extraction path. RCLUSTER-WP-0018 +tracks exact inventory, safe private staging/extraction preparation where required, +independent outage proof and future archive-boundary disposition. No original +ciphertext or historical outcome is rewritten by that work. + +`tools/public-tls-metadata.py` is an offline public-only step. After the source +owner identifies independent original **public certificate/chain and trust files**: + +```sh +python3 tools/public-tls-metadata.py --public-cert ORIGINAL_PUBLIC_CERT_CHAIN.pem --public-trust ORIGINAL_PUBLIC_TRUST.pem +``` + +It accepts no private-key argument, bounds regular-file reads, refuses symlinks and +non-certificate PEM material, and reports public hashes/SANs/validity/counts only. +The file names are owner-local placeholders, not a key request. Output establishes +public metadata, not originality, hostname/chain acceptance, reissue permission or +custody. Do not supply a Secret object or combined certificate/key PEM. Fail closed +when original source, trust or exact bindings are missing; no fresh self-signed +trust workaround is admitted. + +For FUTURE NetKingdom archives, the source owners must explicitly disposition +original TLS coverage or a reviewed reissue contract: producer/custody owner, +private artifact boundary, offline recovery independence, original public hashes, +scoped capture/read permissions, retention and native verification. Merely adding +`kc-tls` to secret_refs would change capture/permission/custody scope and is not +authorized by this source review. It cannot repair the current archive retroactively. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tests/test_public_tls_metadata.py b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tests/test_public_tls_metadata.py new file mode 100644 index 00000000..53b323ec --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tests/test_public_tls_metadata.py @@ -0,0 +1,34 @@ +import importlib.util +from pathlib import Path +from datetime import datetime,timedelta,timezone +from unittest.mock import patch +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes,serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +spec=importlib.util.spec_from_file_location('metadata',Path(__file__).parents[1]/'tools/public-tls-metadata.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +@pytest.fixture +def cert(tmp_path): + key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc) + cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('synthetic.invalid')]),critical=False).sign(key,hashes.SHA256())) + p=tmp_path/'public.pem';p.write_bytes(cert.public_bytes(serialization.Encoding.PEM));return p + +def test_public_only_metadata_and_hashes(cert): + result=m.describe(cert,cert) + assert result['leaf_dns_sans']==['synthetic.invalid'];assert result['network_contacted'] is False + assert '-----BEGIN' not in str(result) + +def test_mixed_or_private_pem_is_refused(cert): + cert.write_bytes(cert.read_bytes()+b'\n-----BEGIN PRIVATE KEY-----\nsynthetic\n-----END PRIVATE KEY-----') + with pytest.raises(ValueError):m.public_bundle(cert) + +def test_oversize_is_refused_before_read(cert): + with patch.object(m,'LIMIT',1),patch.object(m.os,'open') as opened: + with pytest.raises(ValueError):m.public_bundle(cert) + opened.assert_not_called() + +def test_symlink_refused(cert): + link=cert.with_name('link.pem');link.symlink_to(cert) + with pytest.raises(ValueError):m.public_bundle(link) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tools/public-tls-metadata.py b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tools/public-tls-metadata.py new file mode 100644 index 00000000..b3d429f3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/tools/public-tls-metadata.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Owner-run public certificate/trust metadata; no private-key argument or network. + +The owner supplies public-only PEM files from the independently established +original recovery source. This does not identify that source or accept reissue. +""" +import argparse +from datetime import timezone +import hashlib +import json +import os +from pathlib import Path +import re +import stat +from cryptography import x509 +from cryptography.hazmat.primitives import serialization + +LIMIT=1024*1024 +CERT=re.compile(br'-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----') + +def public_bundle(path): + # Bound the read before allocation; refuse every non-certificate PEM block. + path=Path(os.path.abspath(path)) + if any(p.is_symlink() for p in (path,*path.parents)) or not stat.S_ISREG(path.stat().st_mode) or not 0LIMIT:raise ValueError() + blocks=CERT.findall(raw) + if not blocks or len(blocks)>512 or CERT.sub(b'',raw).strip():raise ValueError() + return raw,[x509.load_pem_x509_certificate(block) for block in blocks] + +def describe(cert_path,trust_path): + certificate,chain=public_bundle(cert_path);trust,roots=public_bundle(trust_path);leaf=chain[0] + try:names=leaf.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName) + except x509.ExtensionNotFound:names=[] + spki=leaf.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo) + def utc_date(name): + value=getattr(leaf,name+'_utc',None) + if value is None:value=getattr(leaf,name).replace(tzinfo=timezone.utc) + return value.isoformat() + return {'schema':'nk49.public-tls-metadata.v1','status':'public_metadata_only_not_originality_or_owner_assent', + 'tls_cert_sha256':hashlib.sha256(certificate).hexdigest(),'trust_sha256':hashlib.sha256(trust).hexdigest(), + 'leaf_public_spki_sha256':'sha256:'+hashlib.sha256(spki).hexdigest(),'leaf_dns_sans':names, + 'leaf_not_before':utc_date('not_valid_before'),'leaf_not_after':utc_date('not_valid_after'), + 'server_chain_certificates':len(chain),'trust_certificates':len(roots),'network_contacted':False,'private_key_argument_supported':False} + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--public-cert',required=True);p.add_argument('--public-trust',required=True);a=p.parse_args() + try:result=describe(a.public_cert,a.public_trust) + except Exception:print('{"status":"public_metadata_refused","private_diagnostics_suppressed":true}');return 1 + print(json.dumps(result,indent=2));return 0 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md new file mode 100644 index 00000000..f16c74de --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/railiance-cluster/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md @@ -0,0 +1,126 @@ +--- +id: RCLUSTER-WP-0018 +type: workplan +title: "Establish original KeyCape TLS recovery and future archive boundary" +domain: financials +repo: railiance-cluster +status: ready +owner: codex +topic_slug: railiance +created: "2026-10-17" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +related: [CUST-WP-0080, NK-WP-0049, RCLUSTER-WP-0007, RCLUSTER-WP-0017] +--- + +Required owner source work for NK-WP-0049-T35 and CUST-WP-0080 G08. Reviewed +against current INTENT/SCOPE, backup sources and existing work records; ready +means work can proceed, not privileged execute authority. This does not promote +HA/backlog work, reopen archived local backup delivery or duplicate signer0017. +Source-owner scope is certificate plumbing/backup posture; NetKingdom owns +namespace ingress, while exact original independent TLS custody remains to be +established. No secret read, reissue, live change or native assent occurred. + +## Record source boundary and public-only metadata preparation + +```task +id: RCLUSTER-WP-0018-T01 +status: done +priority: high +assignee: codex +``` + +Completed 2026-10-17 as isolated source preparation. Independently reviewed the +17 public source references against current owner bytes, checked current owner +instructions/environment/supervision and existing plans, and recorded the exact +boundary in docs/keycape-tls-recovery-boundary.md. Prepared bounded public-only +certificate/trust metadata projection with generated-certificate tests; no actual +certificate/key read. Source integration/publication and task projection remain +separate receipts, not claimed by this completed preparation task. + +Evidence: the-custodian docs/changes/netkingdom-target-2026-10-16/ +recovery-owner-handoffs-r1/ and prior recovery-attempt-r1/t35-t36-source-next/. + +## Establish exact independent original TLS artifact and provider custody metadata + +```task +id: RCLUSTER-WP-0018-T02 +status: todo +priority: high +assignee: codex +``` + +Next: identify the actual original sso/kc-tls custody owner and independent public +provenance through existing owner processes. Record artifact/provider, timestamp, +public certificate/trust hashes, private custody entry point and outage dependencies +without values. Source review cannot infer a fresh backup, original key coverage +or independent decryption custody from a local backup script. Use credential +routing before any later credential need; missing route is not proof of absence. +Done when exact independent recovery or an explicitly owned unavailable capability +is evidenced as metadata and a reviewed next action, with no guessed assent. + +## Prepare safe isolated original extraction or explicit reissue alternative + +```task +id: RCLUSTER-WP-0018-T03 +status: wait +priority: high +assignee: codex +depends_on: [RCLUSTER-WP-0018-T02] +blocking_reason: "Exact original artifact/provider/custody disposition is required before selecting extraction or reissue source." +``` + +From T02, reuse the actual owner path to prepare an exact attended private-only +recovery command and full dependency/cleanup proof. Do not run whole-state restore +or expose raw state/Secret output. If existing backup staging is required, correct +only the needed path with bounded memory-only staging and early exact cleanup, +including error/parent-loss tests before use. Original recovery and accepted +reissue are distinct claims; no self-signed trust substitute, new broker or +implicit production action. Done when the exact source proposal, public metadata +preflight, negatives, timeout, abort and custody obligations are reviewed. + +## Verify admitted original TLS recovery through the actual owner boundary + +```task +id: RCLUSTER-WP-0018-T04 +status: wait +priority: high +assignee: codex +needs_human: true +depends_on: [RCLUSTER-WP-0018-T02, RCLUSTER-WP-0018-T03] +blocking_reason: "Await exact reviewed attended owner custody path and its existing scoped authority; no private input or native attempt requested yet." +``` + +Only the actual admitted owner action can prove recovered certificate/key pairing, +original hostname/trust and independent outage operation, or the expressly accepted +reissue result. Retain exact source/artifact/target/identity/negative/cleanup receipts. +Human custody/authority gates are not worker wakeups. Source and historical local +backup proof never fill native assent. Return bounded public proof to NK49T35; +no production mutation, credential retrieval or approval is granted by this task. + +## Disposition original TLS coverage in the future identity archive contract + +```task +id: RCLUSTER-WP-0018-T05 +status: todo +priority: high +assignee: codex +``` + +Next: deliver the source-owned requirement in docs/keycape-tls-recovery-boundary.md +to NetKingdom's existing contract process: original TLS custody or accepted reissue, +independent provider/key recovery, public identity hashes, scoped capture/read +permissions and verification/retention. Record named owner dependencies as live +work before closure. Do not append kc-tls to secret_refs, expand capture credentials +or rewrite a historical archive without those owner reviews/authority. Done when +the future contract requirement has an exact tracked disposition/handoff; it is +not proof of current archive coverage or actual future capture. + +## Coordination and completion + +Beneficiary NK-WP-0049-T35/CUST-WP-0080 remains linked to this obligor record by +derived cross_owner_wait case nk49-t35-rcluster18 in the packet. No new project +driver, live worker injection or message was created. Own T03 wait has an explicit +local prerequisite; T04 is a human gate. Close only after source/native obligations +are proved or an explicit bounded scope disposition hands off remaining live work. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine.patch b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine.patch new file mode 100644 index 00000000..eac6fc5e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine.patch @@ -0,0 +1,258 @@ +--- /dev/null ++++ b/tools/check-keycape-recovery-selector.py +@@ -0,0 +1,64 @@ ++#!/usr/bin/env python3 ++"""Offline public selector preflight; no imports of credential/backend clients. ++ ++Checks current consumer source constants and the producer's already-deployed ++public env/denial selector. This is source metadata, not runtime/owner assent. ++""" ++import argparse ++import ast ++import hashlib ++import json ++import os ++from pathlib import Path ++import re ++import stat ++ ++ROOT=Path(__file__).resolve().parents[1] ++LIMIT=128*1024 ++ ++def read(path): ++ path=Path(os.path.abspath(path)) ++ if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError() ++ fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) ++ with os.fdopen(fd,'rb') as source: ++ info=os.fstat(source.fileno()) ++ if not stat.S_ISREG(info.st_mode) or not 0LIMIT:raise ValueError() ++ return raw ++ ++def unique(pairs): ++ result={} ++ for key,value in pairs: ++ if key in result:raise ValueError() ++ result[key]=value ++ return result ++ ++def verify(selector,source,source_sha256): ++ if not re.fullmatch('sha256:[a-f0-9]{64}',source_sha256) or 'sha256:'+hashlib.sha256(source).hexdigest()!=source_sha256:raise ValueError() ++ needed={'CLIENT_ID','SUBJECT','TENANT','ROLE','SCOPE'};constants={} ++ for node in ast.parse(source).body: ++ if isinstance(node,ast.Assign): ++ for target in node.targets: ++ if isinstance(target,ast.Name) and target.id in needed: ++ if target.id in constants or not isinstance(node.value,ast.Constant) or not isinstance(node.value.value,str):raise ValueError() ++ constants[target.id]=node.value.value ++ if set(constants)!=needed:raise ValueError() ++ expected={'client_id':constants['CLIENT_ID'],'secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET', ++ 'audience':constants['CLIENT_ID'],'scope':constants['SCOPE'],'subject':constants['SUBJECT'], ++ 'tenant':constants['TENANT'],'roles':constants['ROLE'],'deny_scope':'openbao:admin'} ++ if selector!=expected:raise ValueError() ++ return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent', ++ 'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False} ++ ++def main(): ++ p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True) ++ args=p.parse_args() ++ try: ++ proposal=json.loads(read(args.selector),object_pairs_hook=unique) ++ if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError() ++ result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256) ++ except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1 ++ print(json.dumps(result,indent=2));return 0 ++ ++if __name__=='__main__':raise SystemExit(main()) +--- /dev/null ++++ b/tests/test_keycape_recovery_selector.py +@@ -0,0 +1,31 @@ ++import hashlib ++import importlib.util ++import json ++from pathlib import Path ++import pytest ++ ++spec=importlib.util.spec_from_file_location('check',Path(__file__).parents[1]/'tools/check-keycape-recovery-selector.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) ++SOURCE=b'CLIENT_ID="secrets-engine-openbao"\nSUBJECT="service:secrets-engine"\nTENANT="tenant:platform"\nROLE="secrets-engine"\nSCOPE="openbao:login"\nraise RuntimeError("source is parsed, never executed")\n' ++SELECTOR={'client_id':'secrets-engine-openbao','secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET','audience':'secrets-engine-openbao','scope':'openbao:login','subject':'service:secrets-engine','tenant':'tenant:platform','roles':'secrets-engine','deny_scope':'openbao:admin'} ++DIGEST='sha256:'+hashlib.sha256(SOURCE).hexdigest() ++def test_source_parsed_not_executed(): ++ result=m.verify(SELECTOR,SOURCE,DIGEST) ++ assert result['network_contacted'] is False ++@pytest.mark.parametrize('field',list(SELECTOR)) ++def test_each_selector_binding_is_exact(field): ++ bad={**SELECTOR,field:'different'} ++ with pytest.raises(ValueError):m.verify(bad,SOURCE,DIGEST) ++def test_source_drift_and_untyped_digest_refused(): ++ for source,digest in ((SOURCE+b'\n',DIGEST),(SOURCE,DIGEST.removeprefix('sha256:'))): ++ with pytest.raises(ValueError):m.verify(SELECTOR,source,digest) ++def test_duplicate_constants_refused(): ++ source=SOURCE+b'CLIENT_ID="other"\n' ++ with pytest.raises(ValueError):m.verify(SELECTOR,source,'sha256:'+hashlib.sha256(source).hexdigest()) ++def test_duplicate_public_metadata_keys_refused(): ++ with pytest.raises(ValueError):json.loads('{"consumer":{},"consumer":{}}',object_pairs_hook=m.unique) ++def test_oversize_public_input_refused(tmp_path): ++ p=tmp_path/'selector.json';p.write_bytes(b'x'*(m.LIMIT+1)) ++ with pytest.raises(ValueError):m.read(p) ++def test_symlink_public_input_refused(tmp_path): ++ p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p) ++ with pytest.raises(ValueError):m.read(link) +--- /dev/null ++++ b/docs/keycape-isolated-recovery-selector.md +@@ -0,0 +1,38 @@ ++# KeyCape original service recovery selector ++ ++Source-owner disposition, 2026-10-17, limited to secrets-engine's current consumer ++contract. Current src/secrets_engine/service_auth.py fixes client/audience ++secrets-engine-openbao, subject service:secrets-engine, tenant tenant:platform, ++role secrets-engine and exact openbao:login scope. The NetKingdom producer binds ++KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET; original KeyCape native evidence ++uses openbao:admin as the denied-scope case. This selector matches the current ++consumer source. This disposition is not native assent or a credential grant. ++ ++SECRETS-WP-0012 finished on historical attended custody/probe evidence and stays ++finished. SECRETS-WP-0008-T06 retains functional steady-state adoption and scoped ++backend entitlement. SECRETS-WP-0013 retains task-scoped backend execution. The ++new SECRETS-WP-0015 covers only this original archive/isolated consumer preparation ++and later accepted continuity proof; those existing gates are not duplicated. ++ ++The current consumer profile forbids a client-secret file and requires its ++attended in-memory OpenBao source. That source depends on ordinary KeyCape OIDC ++and OpenBao availability. The isolated issuer's native checker can consume its ++original archive-delivered issuer binding without a provider login, but that does ++not prove the secrets-engine consumer path works during an OpenBao/issuer outage. ++Do not bypass the no-file/source contract, retrieve a live credential, redirect a ++production consumer or invoke probe-openbao to fill this recovery gap. An exact ++owner-reviewed outage-compatible proof/custody plan remains required. ++ ++Offline public preflight, after integration of reviewed source: ++ ++```sh ++python3 tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json --source-sha256 sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0 ++``` ++ ++Use the exact hash in the owner packet, not a digest inferred at invocation from ++an unreviewed file. The tool parses source constants with AST and never imports ++the credential/backend module or calls the network. It requires all eight public ++selector fields, exact source hash and unambiguous bounded JSON. Successful output ++means source correspondence only. Original archived presence/secret binding, ++actual TLS trust, native claim negatives and selected consumer continuity remain ++separately evidenced. Missing/changed bindings refuse before any custody request. +--- /dev/null ++++ b/docs/keycape-recovery-selector.json +@@ -0,0 +1,15 @@ ++{ ++ "schema": "secrets-engine.public-recovery-selector.v1", ++ "status": "source_correspondence_only_not_native_assent", ++ "issuer": "https://kc.coulomb.social", ++ "consumer": { ++ "client_id": "secrets-engine-openbao", ++ "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", ++ "audience": "secrets-engine-openbao", ++ "scope": "openbao:login", ++ "subject": "service:secrets-engine", ++ "tenant": "tenant:platform", ++ "roles": "secrets-engine", ++ "deny_scope": "openbao:admin" ++ } ++} +--- /dev/null ++++ b/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md +@@ -0,0 +1,95 @@ ++--- ++id: SECRETS-WP-0015 ++type: workplan ++title: "Prepare original KeyCape service selector and isolated consumer continuity" ++domain: infotech ++repo: secrets-engine ++status: ready ++owner: codex ++topic_slug: custodian ++created: "2026-10-17" ++updated: "2026-10-17" ++flavor: implementation ++depends_on: [] ++related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013] ++--- ++ ++Needed source-owner work for NK-WP-0049-T36/T18. Reviewed against current ++Lifecycle INTENT, explicit consumer constants/custody source and dated historical ++proof. Ready means work can proceed, not native assent. Finished0012 remains ++finished; functional adoption/backend entitlement stay0008T06/0013. No new grant, ++secret fetch, provider login, production redirect or external message occurred. ++ ++## Establish exact current intended source selector ++ ++```task ++id: SECRETS-WP-0015-T01 ++status: done ++priority: high ++assignee: codex ++``` ++ ++Completed 2026-10-17 as source review. Current consumer constants and config guards ++agree with secrets-engine-openbao client/audience, service:secrets-engine, ++tenant:platform, role secrets-engine and only openbao:login. Producer env selector ++and historical denied openbao:admin case are independently referenced, without ++accepting the producer's native role. Prior native issuance7Oct and consumer ++probe10Oct predate reviewed archive16Oct; they neither prove exact archived bytes ++nor current native assent. Recorded docs/keycape-isolated-recovery-selector.md. ++Evidence: Custodian recovery-owner-handoffs-r1 and prior17-source review. ++ ++## Deliver bounded offline public source selector preflight ++ ++```task ++id: SECRETS-WP-0015-T02 ++status: done ++priority: high ++assignee: codex ++``` ++ ++Completed 2026-10-17 as isolated source preparation. Prepared AST-only source-hash ++and all-eight-selector-field checker with bounded strict JSON, source drift, ++duplicate, oversize and symlink negatives. It imports no backend/credential client ++and never exchanges a token. Tests use public/synthetic metadata only. Canonical ++integration/publication/projection must be recorded separately; successful output ++is source correspondence, not native acceptance or a new secret source. ++ ++## Prepare outage-compatible original consumer proof without custody bypass ++ ++```task ++id: SECRETS-WP-0015-T03 ++status: todo ++priority: high ++assignee: codex ++``` ++ ++Next: map the exact original archive-delivered registration and producer-native ++checker result to the intended consumer, then prepare a source-reviewed isolated ++proof/custody plan. Current consumer source requires attended OpenBao memory read ++and refuses files; ordinary OpenBao/KeyCape outage is therefore a real dependency. ++Do not add a fallback, persistent consumer copy or unreviewed injection source. ++Clarify which continuity claim can be proved offline and which requires an exact ++accepted owner custody/provider path. Bind current original selectors, image/ ++source/TLS metadata, negative cases, timeout/abort and cleanup; retain protected ++backend operation entitlement in existing0008/0013. Done when that exact plan and ++remaining owner dependencies are reviewable, without retrieving any value. ++ ++## Verify the separately admitted actual isolated consumer result ++ ++```task ++id: SECRETS-WP-0015-T04 ++status: wait ++priority: high ++assignee: codex ++needs_human: true ++depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33] ++blocking_reason: "Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required." ++``` ++ ++Only a separately admitted owner action can establish actual archived registration, ++issuer trust, native negative checks and consumer continuity. Source selector and ++historical provider proof do not fill this gate. No production consumer redirect, ++new grant, live OpenBao read, backend write or retained-factor MFA is authorized. ++Return exact bounded evidence to NK49T36/T18; MFA remains NK49T19. Human gate is ++never a worker wakeup. Cross-owner dependency is attached to derived case ++secrets15-nk49-readiness in the packet; do not strand the wait or invent a driver. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-isolated-recovery-selector.md b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-isolated-recovery-selector.md new file mode 100644 index 00000000..790daa2f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-isolated-recovery-selector.md @@ -0,0 +1,38 @@ +# KeyCape original service recovery selector + +Source-owner disposition, 2026-10-17, limited to secrets-engine's current consumer +contract. Current src/secrets_engine/service_auth.py fixes client/audience +secrets-engine-openbao, subject service:secrets-engine, tenant tenant:platform, +role secrets-engine and exact openbao:login scope. The NetKingdom producer binds +KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET; original KeyCape native evidence +uses openbao:admin as the denied-scope case. This selector matches the current +consumer source. This disposition is not native assent or a credential grant. + +SECRETS-WP-0012 finished on historical attended custody/probe evidence and stays +finished. SECRETS-WP-0008-T06 retains functional steady-state adoption and scoped +backend entitlement. SECRETS-WP-0013 retains task-scoped backend execution. The +new SECRETS-WP-0015 covers only this original archive/isolated consumer preparation +and later accepted continuity proof; those existing gates are not duplicated. + +The current consumer profile forbids a client-secret file and requires its +attended in-memory OpenBao source. That source depends on ordinary KeyCape OIDC +and OpenBao availability. The isolated issuer's native checker can consume its +original archive-delivered issuer binding without a provider login, but that does +not prove the secrets-engine consumer path works during an OpenBao/issuer outage. +Do not bypass the no-file/source contract, retrieve a live credential, redirect a +production consumer or invoke probe-openbao to fill this recovery gap. An exact +owner-reviewed outage-compatible proof/custody plan remains required. + +Offline public preflight, after integration of reviewed source: + +```sh +python3 tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json --source-sha256 sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0 +``` + +Use the exact hash in the owner packet, not a digest inferred at invocation from +an unreviewed file. The tool parses source constants with AST and never imports +the credential/backend module or calls the network. It requires all eight public +selector fields, exact source hash and unambiguous bounded JSON. Successful output +means source correspondence only. Original archived presence/secret binding, +actual TLS trust, native claim negatives and selected consumer continuity remain +separately evidenced. Missing/changed bindings refuse before any custody request. diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-recovery-selector.json b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-recovery-selector.json new file mode 100644 index 00000000..5e09253f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/docs/keycape-recovery-selector.json @@ -0,0 +1,15 @@ +{ + "schema": "secrets-engine.public-recovery-selector.v1", + "status": "source_correspondence_only_not_native_assent", + "issuer": "https://kc.coulomb.social", + "consumer": { + "client_id": "secrets-engine-openbao", + "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", + "audience": "secrets-engine-openbao", + "scope": "openbao:login", + "subject": "service:secrets-engine", + "tenant": "tenant:platform", + "roles": "secrets-engine", + "deny_scope": "openbao:admin" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/src/secrets_engine/service_auth.py b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/src/secrets_engine/service_auth.py new file mode 100644 index 00000000..29225e8d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/src/secrets_engine/service_auth.py @@ -0,0 +1,496 @@ +"""KeyCape service-JWT exchange shared by two separately validated profiles. + +The OpenBao login profile and approval consumer profile have distinct clients, +audiences and scopes; both belong to tenant:platform. Provider selection never falls back to another +identity. Token parsing is a claim preflight, not signature verification: the +receiving OpenBao or approval-engine service verifies RS256 before accepting it. +""" +from __future__ import annotations + +import base64 +import binascii +import json +import hashlib +import ipaddress +import os +import stat +import subprocess +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode, urlsplit +from urllib.request import Request, urlopen + +from secrets_engine.errors import BackendError, ProvisioningError +from secrets_engine.safe_paths import containing_git_worktree + +CLIENT_ID = "secrets-engine-openbao" +SUBJECT = "service:secrets-engine" +PRINCIPAL_TYPE = "service" +TENANT = "tenant:platform" +ROLE = "secrets-engine" +SCOPE = "openbao:login" +MAX_TOKEN_SECONDS = 15 * 60 +RENEW_WINDOW_SECONDS = 3 * 60 + + +@dataclass(frozen=True) +class KeyCapeServiceAuthConfig: + token_url: str + issuer: str + # The OpenBao profile takes no file: its secret is read in memory from + # OpenBao per command (OpenBaoOidcClientSecretSource). The approval + # profile subclass still uses a consumer file. + client_secret_file: Path | None = None + client_id: str = CLIENT_ID + subject: str = SUBJECT + audience: str = CLIENT_ID + tenant: str = TENANT + required_role: str = ROLE + scope: str = SCOPE + timeout_seconds: float = 10.0 + max_future_iat_seconds: int = field(default=60, init=False) + + def __post_init__(self) -> None: + if not self.token_url.startswith("https://"): + raise BackendError("KeyCape token URL must use HTTPS") + if not self.issuer.startswith("https://"): + raise BackendError("KeyCape issuer must use HTTPS") + if self.client_id != CLIENT_ID or self.audience != CLIENT_ID: + raise BackendError("KeyCape service client/audience must match accepted contract") + if ( + self.subject != SUBJECT + or self.tenant != TENANT + or self.required_role != ROLE + or self.scope != SCOPE + ): + raise BackendError("KeyCape service identity claims must match accepted contract") + if self.timeout_seconds <= 0: + raise BackendError("KeyCape timeout must be positive") + if type(self) is KeyCapeServiceAuthConfig and self.client_secret_file is not None: + raise BackendError( + "secrets-engine-openbao client secret file source is refused; " + "the secret is read in memory from OpenBao per command" + ) + + +@dataclass(frozen=True) +class ServiceJWT: + token: str = field(repr=False) + issued_at: int + expires_at: int + claims: dict[str, Any] = field(repr=False) + + def needs_renewal(self, now: datetime | None = None) -> bool: + current = int((now or datetime.now(timezone.utc)).timestamp()) + return self.expires_at - current <= RENEW_WINDOW_SECONDS + + +def _decode_segment(value: str, label: str) -> dict[str, Any]: + try: + padded = value + "=" * (-len(value) % 4) + decoded = base64.urlsafe_b64decode(padded.encode("ascii")) + parsed = json.loads(decoded) + except (UnicodeEncodeError, binascii.Error, json.JSONDecodeError) as e: + raise BackendError(f"KeyCape JWT has invalid {label}") from e + if not isinstance(parsed, dict): + raise BackendError(f"KeyCape JWT {label} must be an object") + return parsed + + +def preflight_service_jwt( + token: str, + config: KeyCapeServiceAuthConfig, + *, + now: datetime | None = None, +) -> ServiceJWT: + """Validate non-cryptographic JWT shape/claims before resource-server use.""" + parts = token.split(".") + if len(parts) != 3 or not all(parts): + raise BackendError("KeyCape access token is not a compact JWT") + header = _decode_segment(parts[0], "header") + claims = _decode_segment(parts[1], "payload") + if header.get("alg") != "RS256": + raise BackendError("KeyCape JWT algorithm is not RS256") + + exact_claims = { + "iss": config.issuer, + "sub": config.subject, + "aud": config.audience, + "principal_type": PRINCIPAL_TYPE, + "tenant": config.tenant, + "groups": [], + } + for name, expected in exact_claims.items(): + if claims.get(name) != expected: + raise BackendError(f"KeyCape JWT claim '{name}' does not match contract") + if claims.get("roles") != [config.required_role]: + raise BackendError("KeyCape JWT roles do not match contract") + if claims.get("scope") != config.scope: + raise BackendError("KeyCape JWT scope does not match contract") + + assurance = claims.get("assurance") + if not isinstance(assurance, dict) or ( + assurance.get("level") != "aal1" + or assurance.get("methods") != ["client_secret"] + or assurance.get("mfa") is not False + or assurance.get("source") != "key-cape" + ): + raise BackendError("KeyCape JWT assurance does not match contract") + + issued_at = claims.get("iat") + expires_at = claims.get("exp") + if ( + not isinstance(issued_at, int) + or isinstance(issued_at, bool) + or not isinstance(expires_at, int) + or isinstance(expires_at, bool) + ): + raise BackendError("KeyCape JWT iat/exp must be integer timestamps") + current = int((now or datetime.now(timezone.utc)).timestamp()) + if issued_at > current + config.max_future_iat_seconds: + raise BackendError("KeyCape JWT issue time is in the future") + if expires_at <= current: + raise BackendError("KeyCape JWT has expired") + if expires_at <= issued_at or expires_at - issued_at > MAX_TOKEN_SECONDS: + raise BackendError("KeyCape JWT lifetime exceeds accepted 15-minute bound") + return ServiceJWT( + token=token, + issued_at=issued_at, + expires_at=expires_at, + claims=dict(claims), + ) + + +Transport = Callable[..., Any] + + +@dataclass(frozen=True) +class SecretFileCheck: + """One value-free consumer-file check. Never carries file contents.""" + + name: str + ok: bool + detail: str = "" + + +def check_client_secret_file(path: Path) -> list[SecretFileCheck]: + """Inspect a confidential-client secret file without exposing its value. + + The consumer copy must be an absolute, canonical (no symlink component), + regular, non-empty file owned by the effective user with no group/other + permission bits, in a parent directory owned by the same user with no + group/other permission bits, outside every Git worktree. Stops at the first + structural failure; later checks are then not reported as passed. + """ + checks: list[SecretFileCheck] = [] + + def add(name: str, ok: bool, detail: str = "") -> bool: + checks.append(SecretFileCheck(name, ok, detail)) + return ok + + raw = Path(path) + if not add("absolute", raw.is_absolute(), "" if raw.is_absolute() else "path must be absolute"): + return checks + try: + canonical = raw.resolve(strict=True) + except (OSError, RuntimeError): + add("exists", False, "file not found or unresolvable") + return checks + add("exists", True) + if not add( + "canonical", + canonical == raw, + "" if canonical == raw else "path has a symlink or non-canonical component", + ): + return checks + st = os.lstat(raw) + if not add( + "regular_file", + stat.S_ISREG(st.st_mode), + "" if stat.S_ISREG(st.st_mode) else "not a regular file", + ): + return checks + euid = os.geteuid() + add("file_owner", st.st_uid == euid, "" if st.st_uid == euid else "not owned by the running user") + mode = stat.S_IMODE(st.st_mode) + add("file_mode", not mode & 0o077, "" if not mode & 0o077 else f"group/other-accessible mode {oct(mode)}; must be 0600") + pst = os.lstat(raw.parent) + add("parent_owner", pst.st_uid == euid, "" if pst.st_uid == euid else "parent not owned by the running user") + pmode = stat.S_IMODE(pst.st_mode) + add( + "parent_mode", + not pmode & 0o077, + "" if not pmode & 0o077 else f"group/other-accessible parent mode {oct(pmode)}; must be 0700", + ) + worktree = containing_git_worktree(raw) + add("outside_git_worktree", worktree is None, "" if worktree is None else "inside a Git worktree") + add("non_empty", st.st_size > 0, "" if st.st_size > 0 else "file is empty") + return checks + + +def read_client_secret_file(path: Path, *, purpose: str = "KeyCape client secret") -> str: + """Read the consumer secret only after every value-free check passes.""" + failed = [c for c in check_client_secret_file(path) if not c.ok] + if failed: + reasons = "; ".join(f"{c.name}: {c.detail}" for c in failed) + raise ProvisioningError(f"{purpose} file {path} refused ({reasons})") + value = Path(path).read_text(encoding="utf-8").strip() + if not value or any(ch.isspace() for ch in value): + raise ProvisioningError( + f"{purpose} file {path} refused (must hold exactly one non-empty token)" + ) + return value + + +@dataclass(frozen=True) +class KeyCapeServiceAuthProvider: + config: KeyCapeServiceAuthConfig + transport: Transport = field(default=urlopen, repr=False, compare=False) + # In-memory secret source. Required for the OpenBao profile; the approval + # profile reads its consumer file when this is None. + secret_source: Callable[[], str] | None = field(default=None, repr=False, compare=False) + + def exchange(self, *, now: datetime | None = None) -> ServiceJWT: + """Perform one client-credentials exchange; never retry or fall back.""" + if self.secret_source is not None: + secret = self.secret_source() + elif type(self.config) is KeyCapeServiceAuthConfig or self.config.client_secret_file is None: + raise BackendError( + "secrets-engine-openbao requires the in-memory OpenBao secret source; " + "no file fallback" + ) + else: + secret = read_client_secret_file(self.config.client_secret_file) + basic = base64.b64encode( + f"{self.config.client_id}:{secret}".encode("utf-8") + ).decode("ascii") + del secret + body = urlencode( + {"grant_type": "client_credentials", "scope": self.config.scope} + ).encode("ascii") + request = Request( + self.config.token_url, + data=body, + method="POST", + headers={ + "Authorization": f"Basic {basic}", + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + }, + ) + try: + response = self.transport(request, timeout=self.config.timeout_seconds) + with response: + status = getattr(response, "status", 200) + raw = response.read(65537) + if len(raw) > 65536: + raise BackendError("KeyCape token response exceeds size bound") + except HTTPError as e: + raise BackendError(f"KeyCape token exchange failed with HTTP {e.code}") from e + except (URLError, TimeoutError, OSError) as e: + raise BackendError("KeyCape token exchange failed") from e + if status != 200: + raise BackendError(f"KeyCape token exchange failed with HTTP {status}") + try: + payload = json.loads(raw) + except (UnicodeDecodeError, json.JSONDecodeError) as e: + raise BackendError("KeyCape token response is not valid JSON") from e + if not isinstance(payload, dict): + raise BackendError("KeyCape token response must be an object") + if payload.get("id_token") or payload.get("refresh_token"): + raise BackendError("KeyCape service exchange returned a forbidden extra token") + token = payload.get("access_token") + if not isinstance(token, str) or not token: + raise BackendError("KeyCape token response has no access token") + if str(payload.get("token_type", "")).lower() != "bearer": + raise BackendError("KeyCape token response type is not Bearer") + expires_in = payload.get("expires_in") + if ( + not isinstance(expires_in, int) + or isinstance(expires_in, bool) + or not 0 < expires_in <= MAX_TOKEN_SECONDS + ): + raise BackendError("KeyCape token response lifetime is outside contract") + return preflight_service_jwt(token, self.config, now=now) + + +# -- in-memory OpenBao source for the secrets-engine-openbao profile --------- +# +# ops-mason plan keycape-secrets-engine-openbao-client-custody (658cdbc), +# founder direction 2026-10-06: the value lives only in OpenBao. Per command an +# attended OIDC login on one narrow role yields a 5m/4-use operator token that +# is captured from a pipe, used once in a request header for the KV GET, and +# revoked with revoke-self. Nothing is written to file, env, argv or cache. + +OIDC_MOUNT = "netkingdom" +OIDC_ROLE = "keycape-secrets-engine-openbao-operator-workload-kv-read" +KV_MOUNT = "platform" +KV_PATH = "workloads/keycape/secrets-engine-openbao" +KV_FIELD = "CLIENT_SECRET" +SOURCE_NAME = "openbao-oidc-in-memory" +_AMBIENT_TOKEN_ENV = ("BAO_TOKEN", "VAULT_TOKEN") +_MAX_KV_RESPONSE = 65536 + +Runner = Callable[[list[str], dict[str, str]], "tuple[int, bytes]"] + + +def _run_login(argv: list[str], env: dict[str, str]) -> tuple[int, bytes]: + """Run the attended login. stdout is a pipe into memory; stderr (the + browser prompt, no token) stays on the operator's terminal.""" + proc = subprocess.run( # noqa: S603 - fixed argv, no shell + argv, + env=env, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=None, + check=False, + ) + return proc.returncode, proc.stdout + + +def _require_bao_address(addr: str) -> str: + try: + parts = urlsplit(addr) + host = parts.hostname or "" + port_ok = parts.port != 0 + except ValueError: + raise BackendError("OpenBao address is invalid for the in-memory secret read") from None + if not host or parts.username or parts.password or not port_ok: + raise BackendError("OpenBao address is invalid for the in-memory secret read") + try: + loopback = ipaddress.ip_address(host).is_loopback + except ValueError: + loopback = host == "localhost" + if parts.scheme == "https" or (parts.scheme == "http" and loopback): + return addr.rstrip("/") + raise BackendError("OpenBao address must be HTTPS or a literal loopback forward") + + +@dataclass +class OpenBaoOidcClientSecretSource: + """Read CLIENT_SECRET in memory through one attended OIDC operator login. + + Evidence is value-free: source name, operator accessor fingerprint, and + whether revoke-self was attempted and succeeded. + """ + + bao_addr: str + bao_bin: str = "bao" + oidc_mount: str = OIDC_MOUNT + oidc_role: str = OIDC_ROLE + kv_mount: str = KV_MOUNT + kv_path: str = KV_PATH + kv_field: str = KV_FIELD + timeout_seconds: float = 10.0 + runner: Runner = field(default=_run_login, repr=False, compare=False) + transport: Transport = field(default=urlopen, repr=False, compare=False) + accessor_fingerprint: str = "" + revocation_attempted: bool = False + revocation_succeeded: bool = False + + def __post_init__(self) -> None: + pinned = (OIDC_MOUNT, OIDC_ROLE, KV_MOUNT, KV_PATH, KV_FIELD) + if (self.oidc_mount, self.oidc_role, self.kv_mount, self.kv_path, self.kv_field) != pinned: + raise BackendError("OpenBao secret source coordinates do not match the reviewed plan") + self.bao_addr = _require_bao_address(self.bao_addr) + if not self.bao_bin: + raise BackendError("bao CLI is required for the attended OIDC login") + + def evidence(self) -> dict[str, object]: + return { + "secret_source": SOURCE_NAME, + "operator_session_handle": self.accessor_fingerprint, + "operator_revocation_attempted": self.revocation_attempted, + "operator_revocation_succeeded": self.revocation_succeeded, + } + + def argv(self) -> list[str]: + # Token never appears here; -no-store keeps it out of the token helper. + return [ + self.bao_bin, + "login", + "-method=oidc", + f"-path={self.oidc_mount}", + "-no-store", + "-format=json", + f"role={self.oidc_role}", + ] + + def _login_env(self) -> dict[str, str]: + env = {k: v for k, v in os.environ.items() if k not in _AMBIENT_TOKEN_ENV} + env["BAO_ADDR"] = self.bao_addr + env.pop("VAULT_ADDR", None) + return env + + def _login(self) -> str: + code, out = self.runner(self.argv(), self._login_env()) + try: + if code != 0: + raise BackendError(f"attended OpenBao OIDC login failed (exit {code})") + try: + auth = json.loads(out)["auth"] + token = str(auth["client_token"]) + accessor = str(auth.get("accessor", "")) + except (UnicodeDecodeError, json.JSONDecodeError, KeyError, TypeError) as e: + raise BackendError("malformed OpenBao OIDC login response") from e + finally: + del out + if not token or any(ch.isspace() for ch in token): + raise BackendError("OpenBao OIDC login returned no usable token") + self.accessor_fingerprint = ( + hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12] if accessor else "" + ) + return token + + def _call(self, method: str, path: str, token: str) -> tuple[int, bytes]: + request = Request( + f"{self.bao_addr}/v1/{path}", + data=b"" if method == "POST" else None, + method=method, + headers={"X-Vault-Token": token, "Accept": "application/json"}, + ) + try: + response = self.transport(request, timeout=self.timeout_seconds) + with response: + status = getattr(response, "status", 200) + raw = response.read(_MAX_KV_RESPONSE + 1) + except HTTPError as e: + return e.code, b"" + except (URLError, TimeoutError, OSError) as e: + raise BackendError(f"OpenBao request failed: {method} {path}") from e + if len(raw) > _MAX_KV_RESPONSE: + raise BackendError("OpenBao response exceeds size bound") + return status, raw + + def _read_field(self, token: str) -> str: + status, raw = self._call("GET", f"{self.kv_mount}/data/{self.kv_path}", token) + if status != 200: + raise BackendError(f"OpenBao KV read of the client secret failed with HTTP {status}") + try: + value = json.loads(raw)["data"]["data"][self.kv_field] + except (UnicodeDecodeError, json.JSONDecodeError, KeyError, TypeError) as e: + raise BackendError("OpenBao KV response has no client-secret field") from e + finally: + del raw + if not isinstance(value, str) or not value or any(ch.isspace() for ch in value): + raise BackendError("OpenBao client-secret field must hold exactly one token") + return value + + def _revoke_self(self, token: str) -> None: + self.revocation_attempted = True + try: + status, _ = self._call("POST", "auth/token/revoke-self", token) + except BackendError: + return + self.revocation_succeeded = status in (200, 204) + + def __call__(self) -> str: + token = self._login() + try: + return self._read_field(token) + finally: + self._revoke_self(token) + del token diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tests/test_keycape_recovery_selector.py b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tests/test_keycape_recovery_selector.py new file mode 100644 index 00000000..46667a65 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tests/test_keycape_recovery_selector.py @@ -0,0 +1,31 @@ +import hashlib +import importlib.util +import json +from pathlib import Path +import pytest + +spec=importlib.util.spec_from_file_location('check',Path(__file__).parents[1]/'tools/check-keycape-recovery-selector.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +SOURCE=b'CLIENT_ID="secrets-engine-openbao"\nSUBJECT="service:secrets-engine"\nTENANT="tenant:platform"\nROLE="secrets-engine"\nSCOPE="openbao:login"\nraise RuntimeError("source is parsed, never executed")\n' +SELECTOR={'client_id':'secrets-engine-openbao','secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET','audience':'secrets-engine-openbao','scope':'openbao:login','subject':'service:secrets-engine','tenant':'tenant:platform','roles':'secrets-engine','deny_scope':'openbao:admin'} +DIGEST='sha256:'+hashlib.sha256(SOURCE).hexdigest() +def test_source_parsed_not_executed(): + result=m.verify(SELECTOR,SOURCE,DIGEST) + assert result['network_contacted'] is False +@pytest.mark.parametrize('field',list(SELECTOR)) +def test_each_selector_binding_is_exact(field): + bad={**SELECTOR,field:'different'} + with pytest.raises(ValueError):m.verify(bad,SOURCE,DIGEST) +def test_source_drift_and_untyped_digest_refused(): + for source,digest in ((SOURCE+b'\n',DIGEST),(SOURCE,DIGEST.removeprefix('sha256:'))): + with pytest.raises(ValueError):m.verify(SELECTOR,source,digest) +def test_duplicate_constants_refused(): + source=SOURCE+b'CLIENT_ID="other"\n' + with pytest.raises(ValueError):m.verify(SELECTOR,source,'sha256:'+hashlib.sha256(source).hexdigest()) +def test_duplicate_public_metadata_keys_refused(): + with pytest.raises(ValueError):json.loads('{"consumer":{},"consumer":{}}',object_pairs_hook=m.unique) +def test_oversize_public_input_refused(tmp_path): + p=tmp_path/'selector.json';p.write_bytes(b'x'*(m.LIMIT+1)) + with pytest.raises(ValueError):m.read(p) +def test_symlink_public_input_refused(tmp_path): + p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p) + with pytest.raises(ValueError):m.read(link) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tools/check-keycape-recovery-selector.py b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tools/check-keycape-recovery-selector.py new file mode 100644 index 00000000..88d57546 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/tools/check-keycape-recovery-selector.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Offline public selector preflight; no imports of credential/backend clients. + +Checks current consumer source constants and the producer's already-deployed +public env/denial selector. This is source metadata, not runtime/owner assent. +""" +import argparse +import ast +import hashlib +import json +import os +from pathlib import Path +import re +import stat + +ROOT=Path(__file__).resolve().parents[1] +LIMIT=128*1024 + +def read(path): + path=Path(os.path.abspath(path)) + if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError() + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + with os.fdopen(fd,'rb') as source: + info=os.fstat(source.fileno()) + if not stat.S_ISREG(info.st_mode) or not 0LIMIT:raise ValueError() + return raw + +def unique(pairs): + result={} + for key,value in pairs: + if key in result:raise ValueError() + result[key]=value + return result + +def verify(selector,source,source_sha256): + if not re.fullmatch('sha256:[a-f0-9]{64}',source_sha256) or 'sha256:'+hashlib.sha256(source).hexdigest()!=source_sha256:raise ValueError() + needed={'CLIENT_ID','SUBJECT','TENANT','ROLE','SCOPE'};constants={} + for node in ast.parse(source).body: + if isinstance(node,ast.Assign): + for target in node.targets: + if isinstance(target,ast.Name) and target.id in needed: + if target.id in constants or not isinstance(node.value,ast.Constant) or not isinstance(node.value.value,str):raise ValueError() + constants[target.id]=node.value.value + if set(constants)!=needed:raise ValueError() + expected={'client_id':constants['CLIENT_ID'],'secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET', + 'audience':constants['CLIENT_ID'],'scope':constants['SCOPE'],'subject':constants['SUBJECT'], + 'tenant':constants['TENANT'],'roles':constants['ROLE'],'deny_scope':'openbao:admin'} + if selector!=expected:raise ValueError() + return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent', + 'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False} + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True) + args=p.parse_args() + try: + proposal=json.loads(read(args.selector),object_pairs_hook=unique) + if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError() + result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256) + except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1 + print(json.dumps(result,indent=2));return 0 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md new file mode 100644 index 00000000..b3dc9cb8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/recovery-owner-handoffs-r1/secrets-engine/workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md @@ -0,0 +1,95 @@ +--- +id: SECRETS-WP-0015 +type: workplan +title: "Prepare original KeyCape service selector and isolated consumer continuity" +domain: infotech +repo: secrets-engine +status: ready +owner: codex +topic_slug: custodian +created: "2026-10-17" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013] +--- + +Needed source-owner work for NK-WP-0049-T36/T18. Reviewed against current +Lifecycle INTENT, explicit consumer constants/custody source and dated historical +proof. Ready means work can proceed, not native assent. Finished0012 remains +finished; functional adoption/backend entitlement stay0008T06/0013. No new grant, +secret fetch, provider login, production redirect or external message occurred. + +## Establish exact current intended source selector + +```task +id: SECRETS-WP-0015-T01 +status: done +priority: high +assignee: codex +``` + +Completed 2026-10-17 as source review. Current consumer constants and config guards +agree with secrets-engine-openbao client/audience, service:secrets-engine, +tenant:platform, role secrets-engine and only openbao:login. Producer env selector +and historical denied openbao:admin case are independently referenced, without +accepting the producer's native role. Prior native issuance7Oct and consumer +probe10Oct predate reviewed archive16Oct; they neither prove exact archived bytes +nor current native assent. Recorded docs/keycape-isolated-recovery-selector.md. +Evidence: Custodian recovery-owner-handoffs-r1 and prior17-source review. + +## Deliver bounded offline public source selector preflight + +```task +id: SECRETS-WP-0015-T02 +status: done +priority: high +assignee: codex +``` + +Completed 2026-10-17 as isolated source preparation. Prepared AST-only source-hash +and all-eight-selector-field checker with bounded strict JSON, source drift, +duplicate, oversize and symlink negatives. It imports no backend/credential client +and never exchanges a token. Tests use public/synthetic metadata only. Canonical +integration/publication/projection must be recorded separately; successful output +is source correspondence, not native acceptance or a new secret source. + +## Prepare outage-compatible original consumer proof without custody bypass + +```task +id: SECRETS-WP-0015-T03 +status: todo +priority: high +assignee: codex +``` + +Next: map the exact original archive-delivered registration and producer-native +checker result to the intended consumer, then prepare a source-reviewed isolated +proof/custody plan. Current consumer source requires attended OpenBao memory read +and refuses files; ordinary OpenBao/KeyCape outage is therefore a real dependency. +Do not add a fallback, persistent consumer copy or unreviewed injection source. +Clarify which continuity claim can be proved offline and which requires an exact +accepted owner custody/provider path. Bind current original selectors, image/ +source/TLS metadata, negative cases, timeout/abort and cleanup; retain protected +backend operation entitlement in existing0008/0013. Done when that exact plan and +remaining owner dependencies are reviewable, without retrieving any value. + +## Verify the separately admitted actual isolated consumer result + +```task +id: SECRETS-WP-0015-T04 +status: wait +priority: high +assignee: codex +needs_human: true +depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33] +blocking_reason: "Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required." +``` + +Only a separately admitted owner action can establish actual archived registration, +issuer trust, native negative checks and consumer continuity. Source selector and +historical provider proof do not fill this gate. No production consumer redirect, +new grant, live OpenBao read, backend write or retained-factor MFA is authorized. +Return exact bounded evidence to NK49T36/T18; MFA remains NK49T19. Human gate is +never a worker wakeup. Cross-owner dependency is attached to derived case +secrets15-nk49-readiness in the packet; do not strand the wait or invent a driver. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/report.json b/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/report.json new file mode 100644 index 00000000..4b6aae1e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/report.json @@ -0,0 +1,562 @@ +{ + "READY": [ + { + "id": "CUST-WP-0080-T03", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "the-custodian" + }, + { + "id": "CUST-WP-0080-T06", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "the-custodian" + }, + { + "id": "FLEX-WP-0024-T06", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "access-engine" + }, + { + "id": "KEY-WP-0040-T01", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "key-cape" + }, + { + "id": "NK-WP-0035-T04", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0048-T12", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0048-T14", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0048-T15", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0048-T16", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0049-T02", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0049-T15", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "NK-WP-0050-T02", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "RPF-WP-0035-T12", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0053-T01", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0053-T02", + "status": "todo", + "priority": "high", + "reason": "Platform must admit the exact Scaleway primary archive caller/prefix, reconcile its current custody front door and retention/capacity, and deliver bounded credentials before activation.", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0053-T03", + "status": "todo", + "priority": "high", + "reason": "Needs T01/T02 owner admission and founder go for the exact first quiescence/upload and schedule; independent offsite retrieval and recovery proof remain required.", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0053-T04", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0059-T04", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "SECRETS-WP-0014-T02", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "secrets-engine" + }, + { + "id": "USER-WP-0038-T01", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "user-engine" + }, + { + "id": "GH-WP-0004-T06", + "status": "progress", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "gate-house" + }, + { + "id": "KEY-WP-0040-T02", + "status": "progress", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "key-cape" + }, + { + "id": "NK-WP-0049-T24", + "status": "todo", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "net-kingdom" + }, + { + "id": "RPF-WP-0057-T04", + "status": "progress", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "railiance-platform" + }, + { + "id": "WARDEN-WP-0027-T02", + "status": "todo", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "ops-warden" + } + ], + "UNBLOCKED": [ + { + "id": "FLEX-WP-ADHOC-2026-10-03-T02", + "status": "wait", + "priority": "high", + "reason": "Install accepted retroactively (877c268b); steps 6-7 follow WARDEN-WP-0039-T03 token receipts and fetch URL.", + "blocked_on": "", + "repo": "access-engine" + }, + { + "id": "MASON-WP-0004-T03", + "status": "wait", + "priority": "low", + "reason": "operators/nextcloud base entry removed 2026-10-14 after the founder retired it. Remaining draft gaps still need their owners.", + "blocked_on": "owners", + "repo": "ops-mason" + } + ], + "ATTENDED": [ + { + "id": "ACTIVITY-WP-0042-T08", + "status": "wait", + "priority": "high", + "reason": "KV4 provider repair is delivered; repo-manager historical-prefix acceptance is delivered (receipt 39f2c067, 2026-10-06). Separate approved CNPG hostPath/Forgejo GitOps bundle delivery, ESO/env refresh and native consumer receipt remain. Founder per-change go is required before live promotion.", + "blocked_on": "", + "repo": "activity-core", + "pending_dependencies": [] + }, + { + "id": "CUST-WP-0079-T06", + "status": "wait", + "priority": "high", + "reason": "Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.", + "blocked_on": "", + "repo": "the-custodian", + "pending_dependencies": [] + }, + { + "id": "FLEX-WP-0036-T01", + "status": "wait", + "priority": "high", + "reason": "Exact new runtime principals/naming are included in the single daily red batch with the nine TokenReview grants; no per-item question.", + "blocked_on": "", + "repo": "access-engine", + "pending_dependencies": [] + }, + { + "id": "PNEX-WP-0006-T01", + "status": "wait", + "priority": "high", + "reason": "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request.", + "blocked_on": "", + "repo": "policy-nexus", + "pending_dependencies": [] + }, + { + "id": "PNEX-WP-0006-T02", + "status": "wait", + "priority": "high", + "reason": "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request.", + "blocked_on": "", + "repo": "policy-nexus", + "pending_dependencies": [] + }, + { + "id": "RPF-WP-0044-T04", + "status": "wait", + "priority": "high", + "reason": "Orange attended Platform OpenBao OIDC/MFA login for the already D2-approved exact repository reader; no second source-scope approval.", + "blocked_on": "", + "repo": "railiance-platform", + "pending_dependencies": [] + }, + { + "id": "RPF-WP-0053-T05", + "status": "progress", + "priority": "high", + "reason": "email-connect primary.env is delivered from the accepted reader. Retention, capacity, independent receipts, escrow and the first capture remain open. Coulomb node delivery was not requested.", + "blocked_on": "", + "repo": "railiance-platform", + "pending_dependencies": [] + }, + { + "id": "USER-WP-0026-T03", + "status": "wait", + "priority": "high", + "reason": "Signed-in readback live and correct for the deny path (tegwick: Informed Decision - Not available, founder-confirmed correct; docs/evidence/2026-10-11-signed-in-application-readback.md). Remaining: attended positive check with a net-kingdom-admins account (expect Available), then catalogue-management restoration before customers (FLEX-IN-0003).", + "blocked_on": "", + "repo": "user-engine", + "pending_dependencies": [] + }, + { + "id": "ACTIVITY-WP-0043-T04", + "status": "wait", + "priority": "medium", + "reason": "Founder accepted memo 2 (approval 77993081); trials wait for the first live C-25 owner return", + "blocked_on": "", + "repo": "activity-core", + "pending_dependencies": [] + }, + { + "id": "ACTIVITY-WP-0044-T03", + "status": "wait", + "priority": "medium", + "reason": "Activation follows RMGR-WP-0020-T04 and is a founder action", + "blocked_on": "", + "repo": "activity-core", + "pending_dependencies": [ + "RMGR-WP-0020-T04" + ] + }, + { + "id": "FLEX-WP-0036-T04", + "status": "wait", + "priority": "medium", + "reason": "New public coulomb/access-engine image package publication is included in the single daily red batch. Local source/build checks pass; paired publication has not run.", + "blocked_on": "", + "repo": "access-engine", + "pending_dependencies": [ + "FLEX-WP-0036-T01" + ] + }, + { + "id": "CUST-WP-0073-T05", + "status": "wait", + "priority": "low", + "reason": "Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.", + "blocked_on": "", + "repo": "the-custodian", + "pending_dependencies": [] + } + ], + "STALLED": [ + { + "id": "ACTIVITY-WP-0041-T03", + "status": "wait", + "priority": "high", + "reason": "RPF-IN-0011: return dedicated release principal/custody owner task and native acceptance; D2 scope already accepted", + "blocked_on": "message-from:railiance-platform", + "repo": "activity-core" + }, + { + "id": "ACTIVITY-WP-0042-T05", + "status": "wait", + "priority": "high", + "reason": "Platform owner must accept the candidate request/receipt contract, execution authority checks, limits and credential scope before runtime wiring.", + "blocked_on": "message-from:railiance-platform", + "repo": "activity-core" + }, + { + "id": "FLEX-WP-0020-T11", + "status": "wait", + "priority": "high", + "reason": "Cleanup approved conditionally (e071b34e): runs once the listed soak evidence, SBOM-IN-0001 receipt and residual handoffs exist.", + "blocked_on": "message-from:sbom-nexus", + "repo": "access-engine" + }, + { + "id": "RAPP-POLICY-NEXUS-WP-0002-T02", + "status": "wait", + "priority": "high", + "reason": "Answered railiance-platform's RPF-WP-0043-T02 question on 2026-10-03; waiting on its confirmation and on railiance-apps' values path/revision.", + "blocked_on": "message-from:railiance-platform", + "repo": "rapp-policy-nexus" + }, + { + "id": "RPF-WP-0027-T03", + "status": "wait", + "priority": "high", + "reason": "Owner work first: NetKingdom packages the complete resolver sequence as one receipt-producing command; then it enters the attended-window checklist (D1 standing rule).", + "blocked_on": "message-from:net-kingdom", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0027-T06", + "status": "wait", + "priority": "high", + "reason": "Owner work first: the OpenBao owner confirms mount/path/fields and writer/reader authority and publishes the non-secret routing receipt; the handoff enters the attended window only when a command exists.", + "blocked_on": "message-from:rapp-openbao", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0035-T08", + "status": "wait", + "priority": "high", + "reason": "Approved conditionally (ab85ae8a, standing change rule): executes once independent archive readback and the interim bearer revocation receipt exist; owner work with audit-core.", + "blocked_on": "message-from:audit-core", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0056-T03", + "status": "wait", + "priority": "high", + "reason": "User admitted all identified platform humans and coding agents; their supported Kubernetes authentication/group-mapping route remains unimplemented.", + "blocked_on": "message-from:railiance-cluster", + "repo": "railiance-platform" + }, + { + "id": "SECRETS-WP-0006-T05", + "status": "wait", + "priority": "high", + "reason": "Owner work first: publish a reviewed scoped-apply command per lane. Per-lane approvals are no longer needed (D1 standing rule); each lane with a command enters the attended window.", + "blocked_on": "message-from:railiance-platform", + "repo": "secrets-engine" + }, + { + "id": "SECRETS-WP-0009-T03", + "status": "wait", + "priority": "high", + "reason": "Next owner action: reconcile held parent reservation 6efa9436-6a91-47c0-94e5-b31b5a0e7e3a (EUR 10, observed USD still null) from existing provider records. No corrected runtime digest is admitted; do not retry or replay consumed approvals.", + "blocked_on": "message-from:glas-harness", + "repo": "secrets-engine" + }, + { + "id": "SECRETS-WP-0013-T01", + "status": "wait", + "priority": "high", + "reason": "Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred.", + "blocked_on": "message-from:railiance-platform", + "repo": "secrets-engine" + }, + { + "id": "USER-WP-0035-T02", + "status": "wait", + "priority": "high", + "reason": "Existing mail service lacks an admitted provider password-setup template and recipient-bound opaque issuer handoff. EMAIL-IN-0001 and NET-IN-0005 request exact owner implementation records; no new founder choice or credential request.", + "blocked_on": "message-from:email-connect", + "repo": "user-engine" + }, + { + "id": "USER-WP-0037-T03", + "status": "wait", + "priority": "high", + "reason": "T02 delivered empty no-grant custody integration; real independently authenticated Hub caller credential admission and withdrawal/outage/private integration acceptance remain with state-hub (original f3bcc516; renewed request 350c3e12-284c-43a7-9ab1-116df8bcc007). Hub repository registration is not caller admission.", + "blocked_on": "message-from:state-hub", + "repo": "user-engine" + }, + { + "id": "CUST-WP-0079-T05", + "status": "wait", + "priority": "medium", + "reason": "Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.", + "blocked_on": "message-from:repo-manager", + "repo": "the-custodian" + }, + { + "id": "RPF-WP-0035-T10", + "status": "wait", + "priority": "medium", + "reason": "Blue lane under the standing change rule (removing access, with reviewed order, rollback and receipts): executes once approval-engine confirms there is no presenter. No founder window needed.", + "blocked_on": "message-from:approval-engine", + "repo": "railiance-platform" + }, + { + "id": "RPF-WP-0036-T06", + "status": "wait", + "priority": "medium", + "reason": "Owner-ready handoffs to railiance-forge, rapp-openbao and the ArgoCD bootstrap owner await acceptance; S3 custody contracts stay here until then.", + "blocked_on": "message-from:railiance-forge", + "repo": "railiance-platform" + } + ], + "DEPENDENT": [ + { + "id": "CUST-WP-0080-T04", + "status": "todo", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "the-custodian", + "pending_dependencies": [ + "CUST-WP-0080-T03" + ] + }, + { + "id": "CUST-WP-0080-T05", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "the-custodian", + "pending_dependencies": [ + "RPF-WP-0056", + "RAPP-PLAYWRIGHT-WP-0004" + ] + }, + { + "id": "FLEX-WP-0036-T02", + "status": "progress", + "priority": "high", + "reason": "", + "blocked_on": "", + "repo": "access-engine", + "pending_dependencies": [ + "FLEX-WP-0036-T01" + ] + }, + { + "id": "MASON-WP-0005-T05", + "status": "progress", + "priority": "high", + "reason": "ops-mason checks and the non-member refusal pass. tegwick enrolled TOTP on 2026-10-11 (message 36b443ab). The founder still runs scripts/verify-fluid-telegram-lane.sh member via the 127.0.0.1:18200 tunnel. fluid-telegram runs the salt create-once probe (FT-WP-0002).", + "blocked_on": "human", + "repo": "ops-mason", + "pending_dependencies": [] + }, + { + "id": "MASON-WP-0010-T03", + "status": "progress", + "priority": "high", + "reason": "Typed exact structural evidence request is prepared. Native materialization waits on independently authenticated policy/role/layout/catalog observations and CCR42 consumer binding/application. Mint, delivery and first live verification are follow-through, not structural prerequisites.", + "blocked_on": "message-from:railiance-platform", + "repo": "ops-mason", + "pending_dependencies": [] + }, + { + "id": "WARDEN-WP-0044-T03", + "status": "progress", + "priority": "high", + "reason": "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run.", + "blocked_on": "", + "repo": "ops-warden", + "pending_dependencies": [ + "MASON-WP-0010-T03" + ] + }, + { + "id": "FLEX-WP-0036-T07", + "status": "todo", + "priority": "medium", + "reason": "", + "blocked_on": "", + "repo": "access-engine", + "pending_dependencies": [ + "FLEX-WP-0036-T06" + ] + }, + { + "id": "MASON-WP-0009-T05", + "status": "progress", + "priority": "medium", + "reason": "ops-mason checks complete (policies, roles, invariant ii, login proof). Owner-run: live operator/ESO token capability checks and a non-member refusal; ops-warden activates the proposed pointer.", + "blocked_on": "human", + "repo": "ops-mason", + "pending_dependencies": [] + }, + { + "id": "FLEX-WP-0036-T08", + "status": "todo", + "priority": "low", + "reason": "", + "blocked_on": "", + "repo": "access-engine", + "pending_dependencies": [ + "FLEX-WP-0036-T06" + ] + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/run.lock b/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/daily-dry-run-state/run.lock new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/source-snapshot.json b/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/source-snapshot.json new file mode 100644 index 00000000..85641618 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-audit-r1/source-snapshot.json @@ -0,0 +1,148 @@ +{ + "observed_at": "2026-10-16T21:15:42.219146+00:00", + "scope": "Read-only selected public source/work-record snapshot, not publication or runtime acceptance", + "repos": [ + { + "repo": "the-custodian", + "head": "3607d3e08ffdbc473d19e9baf7225843c4075ae0", + "files": [ + { + "path": "docs/netkingdom-target-security-architecture.md", + "sha256": "3a74cbd71dc22514033906f6029de1d3e113640bc5faab94f8d58c62e0c86203" + }, + { + "path": "docs/netkingdom-target-implementation.md", + "sha256": "a59b906b7fa7dee5f11d4de7fa38c08b974ee2cafa260c44515238ac42e71176" + }, + { + "path": "workplans/CUST-WP-0079-deterministic-security-flow.md", + "sha256": "f4ad3299c90f84f49e5867b3d358582c3faea65db4a2a4f595cf1a7f2b806097" + }, + { + "path": "scripts/security_flow_daily.py", + "sha256": "4a4e84702b3fb38d086695bb6d601430d8914bd79286b4f683794206f110c353" + }, + { + "path": "scripts/sync_resolved_decisions.py", + "sha256": "182433ca7cd31e2b2773246ec1be02569ba6f089ac009add6543e5e48777b5ec" + } + ] + }, + { + "repo": "repo-manager", + "head": "97ea2489257de08911cc5da1e1b84d8e15780dc3", + "files": [ + { + "path": "src/repo_manager/secret_scan.py", + "sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d" + }, + { + "path": "workplans/RMGR-WP-0021-secret-detection-controls.md", + "sha256": "fa649c2f3da205627f98a1dfc87ea6cf44dd78fd34e601f8c0597ec55e179d96" + } + ] + }, + { + "repo": "railiance-forge", + "head": "d322b9c9e8a1caa2b3479b270fff8a0e9f6c1f59", + "files": [ + { + "path": "tools/receive-secret-scan/main.go", + "sha256": "5eee28fbe830ebec7abbdfc93275c34d126f92d1b144df93e72aec0dfb821bc6" + }, + { + "path": "workplans/FORGE-WP-0006-independent-secret-detection.md", + "sha256": "fbcf3f75e884f6c90554c05525d787f61cfc2b915a206e6010bed447e7a720eb" + } + ] + }, + { + "repo": "activity-core", + "head": "f9f8df039258a0d8b6fc29dc1f7c4bcc75fdc8c0", + "files": [ + { + "path": "activity-definitions/daily-security-flow.md", + "sha256": "0281f0cd818b02d1cbd3a732dfb72145e8cb5e9881c4d80c6f6eb3d664da9375" + }, + { + "path": "workplans/ACTIVITY-WP-0047-daily-security-flow-adoption.md", + "sha256": "e0b83b570da688abb405bed165d81db7cf49bc7ff11ef9a8ad86cbee2dc651db" + } + ] + }, + { + "repo": "rein-aharness", + "head": "9b88cadaa817e5abea7950b4a264d4e2739a5ddb", + "files": [ + { + "path": "rein_aharness/claim_loop.py", + "sha256": "5bb6ae75374858d9f515576d0cb8d25e9e555a35f3a89dff9dbe8cd4985f5974" + }, + { + "path": "rein_aharness/approaches.py", + "sha256": "d095ed3bd817fee1fede545342e32f7ddbc9e20dad6f90fd7807906d26a031cd" + }, + { + "path": "rein_aharness/readiness.py", + "sha256": "9d6945a0ef597c84846e378df8109c88c1d33a4f16c3650da2b110b8501729b1" + }, + { + "path": "rein_aharness/glas_execution.py", + "sha256": "860bec6d2225cefb8e6abbab092f2e8521e015fd3a2e5cd76a42569cc8026f6b" + } + ] + }, + { + "repo": "glas-harness", + "head": "7357115c8311e55ee3d495f5266f501a17ec3c89", + "files": [ + { + "path": "profiles/harness.agent-dev-local-1.1.1.yaml", + "sha256": "382c64531a92254c0bcc0d0d6ab059cf190f1dabb7ee2e2ec445974de9ef7edb" + }, + { + "path": "src/glas_harness/gateway.py", + "sha256": "5e998bfa53ade5dafbae799dc04b77031b6414e2af49797f45727d59d7397d42" + }, + { + "path": "src/glas_harness/profiles.py", + "sha256": "40fba829dfb2a9b3acac4d3bf0539025e4cb5315a2fdbedb8e2ec76e16b29b5b" + } + ] + }, + { + "repo": "secrets-engine", + "head": "535681052cd55ca613706ecc60c39d299929b3c5", + "files": [ + { + "path": "workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md", + "sha256": "6cb4ec3500cddd5156235f1a493d7a979227a6b04eb037bacd23073fd1328efe" + }, + { + "path": "src/secrets_engine/lifecycle.py", + "sha256": "c17644356524cd83e96f58a1022638e71fa373cd338c1bf38980ed6cd45a757c" + } + ] + }, + { + "repo": "access-engine", + "head": "d328a2601095686f74376d87791bff01e2a3a2a3", + "files": [ + { + "path": "workplans/FLEX-WP-0024-decision-envelope-authenticity.md", + "sha256": "cc94a493c23c16a3baa718b75ef5d48a596c6386c11534d50fd4839a6f0448f2" + } + ] + }, + { + "repo": "audit-core", + "head": "64509d2c4039ca1249f9e8aabf2bae030e3640cf", + "files": [ + { + "path": "docs/flex-auth-secrets-engine-admission.md", + "sha256": "0825d33b4ed33a0c10eda778a2a7d23764f34086756607a4f5d8a12018ec9a38" + } + ] + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index new file mode 100644 index 00000000..6aad3a57 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index @@ -0,0 +1,62 @@ + +# Custodian Brief — the-custodian + +**Domain:** infotech +**Last synced:** 2026-10-15 23:45 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### NetKingdom target security architecture and SEC-DSM implementation +Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52` + +**Open tasks:** +- ► Close source defects and reconcile uncovered lifecycle gaps `87637402` +- ► Coordinate self-service Playwright identity delivery `ed8b909a` +- ► Establish the target architecture and drive the restructuring `6aca534c` +- · Verify native closure against every requirement `82378b0f` + +### Separate agent credentials and establish supervised privileged execution +Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d` + +**Open tasks:** +- ! Build and hand out the agent identity `4b88b0b7` + *(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)* +- ! Fleet guidance and harness guards `90725511` + *(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)* +- ! Rotate what was exposed `1b41b532` + *(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)* + +### Size Railiance workloads from actual demand and establish a weekly allocation review +Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3` + +**Open tasks:** +- ! Measure the Vergabe prototype under representative pilot use `82192370` + *(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)* +- ! Review ecosystem allocations and reserve operating room `6dc67558` + *(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)* +- ! Apply accepted sizing changes and verify their effects `944f98e2` + *(wait: Await reviewed allocation proposals and applicable workload-owner authority.)* +- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7` + *(wait: Await the repeatable observation and accepted sizing/review procedure.)* + +### Deterministic security flow: standing authorizations, flow report, and agent re-engagement +Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3` + +**Open tasks:** +- ! Schedule the flow report and decision sync `1be0569c` + *(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)* +- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156` + *(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)* +- ! Stop agent-authored index and sync commits `8a3dc219` + *(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)* +- ! Attended window #1 `e3da807f` + *(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)* + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index-record b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index-record new file mode 100644 index 00000000..2ead6464 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.index-record @@ -0,0 +1 @@ +100644 6aad3a5788fad3abd60c0f1270f160062dc0168b 0 .custodian-brief.md diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.worktree new file mode 100644 index 00000000..6aad3a57 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/brief.worktree @@ -0,0 +1,62 @@ + +# Custodian Brief — the-custodian + +**Domain:** infotech +**Last synced:** 2026-10-15 23:45 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### NetKingdom target security architecture and SEC-DSM implementation +Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52` + +**Open tasks:** +- ► Close source defects and reconcile uncovered lifecycle gaps `87637402` +- ► Coordinate self-service Playwright identity delivery `ed8b909a` +- ► Establish the target architecture and drive the restructuring `6aca534c` +- · Verify native closure against every requirement `82378b0f` + +### Separate agent credentials and establish supervised privileged execution +Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d` + +**Open tasks:** +- ! Build and hand out the agent identity `4b88b0b7` + *(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)* +- ! Fleet guidance and harness guards `90725511` + *(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)* +- ! Rotate what was exposed `1b41b532` + *(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)* + +### Size Railiance workloads from actual demand and establish a weekly allocation review +Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3` + +**Open tasks:** +- ! Measure the Vergabe prototype under representative pilot use `82192370` + *(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)* +- ! Review ecosystem allocations and reserve operating room `6dc67558` + *(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)* +- ! Apply accepted sizing changes and verify their effects `944f98e2` + *(wait: Await reviewed allocation proposals and applicable workload-owner authority.)* +- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7` + *(wait: Await the repeatable observation and accepted sizing/review procedure.)* + +### Deterministic security flow: standing authorizations, flow report, and agent re-engagement +Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3` + +**Open tasks:** +- ! Schedule the flow report and decision sync `1be0569c` + *(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)* +- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156` + *(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)* +- ! Stop agent-authored index and sync commits `8a3dc219` + *(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)* +- ! Attended window #1 `e3da807f` + *(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)* + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.json b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.json new file mode 100644 index 00000000..9f7854c8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.json @@ -0,0 +1,131 @@ +{ + "repo_slug": "the-custodian", + "repo_path": "/home/worsch/the-custodian", + "source_revision": "fee99c24bd7dedef98d16035996ba0ea902c624a", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']", + "file_path": "workplans/", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry", + "file_path": "workplans/ADHOC-2026-08-25-retired.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'STATE-WP-0092' is not linked to State Hub", + "file_path": "workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md", + "db_id": "", + "file_value": "STATE-WP-0092", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved", + "file_path": "workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05", + "db_id": "", + "file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \u2014 unrouted human gate", + "file_path": "workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06", + "db_id": "", + "file_value": "needs_human: true", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'CUST-WP-0080-T06': file content differs from DB (file wins)", + "file_path": "workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md#CUST-WP-0080-T06", + "db_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "file_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again", + "db_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again", + "fixable": true + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Closed DB workstream 'cust-wp-0010b' (id=416ff571\u2026, status=finished) has no backing workplan file", + "file_path": "", + "db_id": "416ff571-18cf-5cb2-9c8b-836f68a301b3", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "370c2481-6806-41eb-a917-f8874f03184f", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "3f54ce9c-1f95-42de-894f-0f81a52ba2e8", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "5c53733b-c077-4a93-be66-9c3af8f9b6fd", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \u2014 new plans must conform", + "file_path": "workplans/THE-WP-0001-federation-interface.md", + "db_id": "", + "file_value": "THE-WP-0001", + "db_value": "CUST-WP", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 89/89", + "C-17 cleared: pushed 2 backlogged commit(s)", + "C-22 fixed: task 6aca534c\u2026 description updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 7, + "info": 4 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.stderr.txt new file mode 100644 index 00000000..bbd5a68a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/consistency.stderr.txt @@ -0,0 +1 @@ +WARN: working-memory commit failed: fatal: Unable to create '/home/worsch/the-custodian/.git/index.lock': Read-only file system diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/final-review.json b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/final-review.json new file mode 100644 index 00000000..83809ae0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/final-review.json @@ -0,0 +1,16 @@ +{ + "head": "fee99c24bd7dedef98d16035996ba0ea902c624a", + "remote_main": "fee99c24bd7dedef98d16035996ba0ea902c624a", + "sync_summary": { + "fail": 0, + "automation_error": 0, + "warn": 7, + "info": 4 + }, + "task_count": 6, + "all_statuses_match": true, + "original_staged_and_working_brief_restored": true, + "retained_refinement": "Initial index restore hit sandbox read-only filesystem; exact update-index rerun under required escalation succeeded.", + "goal_status": "active", + "all_16_target_gates_remain_open": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/owner-final-verification.json b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/owner-final-verification.json new file mode 100644 index 00000000..9b5382f3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/owner-final-verification.json @@ -0,0 +1,23 @@ +{ + "owner_head": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "remote_main": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "both_source_commits_in_published_history": true, + "workplans": [ + { + "workplan": "NK-WP-0048", + "task_count": 19, + "all_statuses_match": true + }, + { + "workplan": "NK-WP-0049", + "task_count": 33, + "all_statuses_match": true + } + ], + "final_sync_summary": { + "fail": 0, + "automation_error": 0, + "warn": 27, + "info": 5 + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/progress-receipt.json new file mode 100644 index 00000000..f9112426 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-coordinator-final/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "578371e4-ffc2-4303-a8b6-86b0a0717b6a", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "Resumed NetKingdom target architecture with GPT-6.1-Sol medium delegates. Published browser checker immutable snapshot source8790dd18 and recovery source5fcd5952. Independent49 source+12 real checker scenarios,73 recovery/source checks and four actual synthetic Docker cases passed. Host-bound exact cleanup and three reaper receipts retained; uncertain Docker response remains uncertain. Normal source scanner false-positive public hashes resolved with strictly typed SHA256 metadata, no exemptions. NK-WP0048 has19 projected tasks; NK-WP0049 has33, T32 source proof done/T33 actual-attempt preparation todo/T18 real recovery wait. Scanner baseline and host/rein schedule readiness measured. All16 TSA gates remain open; full goal active. Two delegates reached model usage limit after completed commands; coordinator verified and finished source publication. Custodian commits a3e98b1 and fee99c2 await normal sync.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T21:39:32.450075Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/apply_reviewed_sources.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/apply_reviewed_sources.py new file mode 100644 index 00000000..9a615341 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/apply_reviewed_sources.py @@ -0,0 +1,70 @@ +"""Apply only independently reviewed local source patches; no stage or publish.""" +from pathlib import Path +import argparse +import hashlib +import json +import subprocess +from datetime import datetime, timezone + +BASE = Path(__file__).resolve().parent.parent +LANES = { + 'groups': ('net-kingdom', '878abc93588e12b6da1bac055cfbb3abb8d7adf1', 'group-provenance-r1/source.patch', '1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e'), + 'recovery': ('net-kingdom', '878abc93588e12b6da1bac055cfbb3abb8d7adf1', 'recovery-attempt-r1/candidate.patch', '99cc938d80d64049f927049a5e09a698641e121cf0af2bfa1cbb54ebe7528266'), + 'scanner': ('repo-manager', '97ea2489257de08911cc5da1e1b84d8e15780dc3', 'scanner-batch-r1/candidate.patch', 'd2f65f2af5444b241ddcb85a3e71e66d1bbe071bd9a8a7be415be84b5e2f8fdb'), +} + +def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('lane', choices=LANES) + parser.add_argument('--apply', action='store_true') + args = parser.parse_args() + slug, revision, relative, digest = LANES[args.lane] + owner = Path('/home/worsch') / slug + patch = BASE / relative + def git(*argv): + return subprocess.check_output(['git', '-C', str(owner), *argv], stderr=subprocess.PIPE) + assert git('rev-parse', 'HEAD').decode().strip() == revision, 'owner revision changed' + assert sha(patch) == digest, 'reviewed patch changed' + paths = [line[6:] for line in patch.read_text().splitlines() if line.startswith('+++ b/')] + assert paths and len(paths) == len(set(paths)) + for path in paths: + assert not Path(path).is_absolute() and '..' not in Path(path).parts + assert not git('status', '--porcelain', '--', path).strip(), 'owner source is dirty: ' + path + if args.lane == 'groups': + entries = json.loads((BASE / 'group-provenance-r1/source-manifest.json').read_text())['files'] + expected = {e['path']: e['postimage_sha256'] for e in entries} + for e in entries: + target = owner / e['path'] + assert (sha(target) if target.exists() else None) == e['preimage_sha256'] + elif args.lane == 'scanner': + entries = json.loads((BASE / 'scanner-batch-r1/manifest.json').read_text())['files'] + expected = {e['path']: e['candidate_sha256'] for e in entries} + for e in entries: assert sha(owner / e['path']) == e['base_sha256'] + else: + candidate = BASE / 'recovery-attempt-r1/test-root' + expected = {p: sha(candidate / p) for p in paths} + closure = json.loads((BASE / 'recovery-attempt-r1/source-closure.json').read_text()) + for path, typed in closure.items(): + target = candidate / path if path in paths else owner / path + assert 'sha256:' + sha(target) == typed, 'source closure changed: ' + path + assert set(paths) == set(expected) + git('apply', '--check', str(patch)) + before_index = git('ls-files', '--stage', '--', '.custodian-brief.md') + before_brief = (owner / '.custodian-brief.md').read_bytes() + if args.apply: + git('apply', str(patch)) + for path, wanted in expected.items(): assert sha(owner / path) == wanted, 'postimage changed: ' + path + assert before_index == git('ls-files', '--stage', '--', '.custodian-brief.md') + assert before_brief == (owner / '.custodian-brief.md').read_bytes() + receipt = {'observed_at': datetime.now(timezone.utc).isoformat(), 'lane': args.lane, + 'owner': str(owner), 'base_revision': revision, 'patch_sha256': 'sha256:' + digest, + 'applied': args.apply, 'source_postimages': {p: 'sha256:' + h for p, h in expected.items()}, + 'preexisting_brief_preserved': True, 'staged': False, 'published': False, + 'native_acceptance': False} + out = Path(__file__).resolve().parent / (args.lane + ('-integration.json' if args.apply else '-preflight.json')) + out.write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps({'lane': args.lane, 'applied': args.apply, 'files': len(paths)})) + +if __name__ == '__main__': main() diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/build_verified_results.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/build_verified_results.py new file mode 100644 index 00000000..4153b792 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/build_verified_results.py @@ -0,0 +1,53 @@ +from pathlib import Path +from datetime import datetime, timezone +import hashlib, json, re +base = Path(__file__).resolve().parent +packets = base.parent +def load(path): return json.loads(path.read_text()) +def sha(path): return 'sha256:' + hashlib.sha256(path.read_bytes()).hexdigest() +def typed(value): + if isinstance(value, dict): return {k: typed(v) for k,v in value.items()} + if isinstance(value, list): return [typed(v) for v in value] + if isinstance(value,str) and re.fullmatch('[0-9a-f]{64}',value): return 'sha256:' + value + return value +native_path = packets / 'group-provenance-r1/native-fixture/execution-result.json' +native = load(native_path) +assert native['status'] == 'synthetic_native_compatibility_passed' +assert len(native['checks']) == 14 and all(native['checks'].values()) +assert native['container_cleanup_verified'] and all(x['status']=='absence_verified' for x in native['container_cleanup_obligations']) +hook_path = packets / 'scanner-batch-r1/configured-hook-results.json' +hooks = load(hook_path) +assert hooks['status'] == 'verified' and hooks['fixture_removed'] and hooks['new_scan_storage_absent'] +assert sum(len(x['cases']) for x in hooks['cases']) == 4 +benchmark_path = packets / 'scanner-batch-r1/scanner-candidate.json' +benchmark = load(benchmark_path) +measurements = [{k:v for k,v in item.items() if k != 'samples'} for item in benchmark['observations']] +result = { + 'schema':'custodian.target-source-results.v1','observed_at':datetime.now(timezone.utc).isoformat(), + 'scope':'Reviewed owner source plus generated local/disposable compatibility fixtures; no target gate closed', + 'canonical_tests':{'group_provisioner':{'passed':65,'seconds':8.55}, + 'recovery_preparation':{'passed':111,'seconds':14.50}, + 'staged_scanner':{'passed':42,'seconds':31.66}}, + 'recovery_limitations':{'legacy_custody_tests_passed':94,'legacy_real_sops_skipped':61, + 'actual_owner_held_recovery':False,'private_admission_complete':False, + 'named_file_mode_parent_loss_fixed':False,'sealed_child_fd_lives_until_child_exit':True}, + 'group_native_fixture':native,'original_group_receipt_sha256':sha(native_path), + 'independent_group_cleanup':{'engine':'74f93783-6cef-43a8-a3c4-f62586cd3bc1', + 'exact_container_names_absent':True,'exact_run_label_inventory_empty':True, + 'hard_parent_loss_claim':False}, + 'configured_hooks':hooks,'original_hook_receipt_sha256':sha(hook_path), + 'scanner_parity':{'exact_old_new_cases':13,'fixture_removed':True}, + 'scanner_performance':{'observations':measurements,'original_receipt_sha256':sha(benchmark_path), + 'warm_repetitions':7,'percentiles':'nearest rank; descriptive small sample', + 'baseline_p50_ms':{'clean-1x1KiB':702.358,'clean-16x1KiB':12065.334,'clean-64x1KiB':49171.849,'clean-1x1MiB':2548.887}, + 'single_blob_slowdown_retained':True,'receive_ci_or_workload_claim':False}, + 'review_refinements_retained':['Group exactly-one intent row update and database identity checks', + 'Definite before-write refusal preserves evidence and releases active fence', + 'Scanner batches and reports moved to verified private tmpfs with parent-death cleanup', + 'SQLite-on-PVC production guidance corrected before publication against accepted ADR-0015'], + 'production_deployments':False,'actual_credentials_used':False,'native_target_acceptance':False, + 'all_sixteen_target_gates_remain_open':True, + 'encoding':'Published checksum strings carry explicit sha256: type prefixes; original receipt bytes remain workspace-local and separately hashed.' +} +(base / 'verified-results.json').write_text(json.dumps(typed(result),indent=2)+'\n') +print('Retained typed results for 65/111/42 canonical checks, 14 LLDAP oracles and four actual-hook cases.') diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/close_custodian.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/close_custodian.py new file mode 100644 index 00000000..f2ea3a6b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/close_custodian.py @@ -0,0 +1,17 @@ +from pathlib import Path +import json, subprocess +base=Path(__file__).resolve().parent;root=Path('/home/worsch/the-custodian') +backup=base/'custodian-publication-backup' +try: + result=subprocess.run(['statehub','fix-consistency','--repo','the-custodian','--json'],cwd=root,capture_output=True,text=True,timeout=300) + (base/'custodian-consistency.json').write_text(result.stdout) + (base/'custodian-consistency.stderr.txt').write_text(result.stderr) + (base/'custodian-consistency.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n') +finally: + for name,index in json.loads((backup/'originals.json').read_text()).items(): + (root/name).write_bytes((backup/(name+'.worktree')).read_bytes()) + mode,oid,stage,path=index.split();assert stage=='0' + subprocess.run(['git','-C',str(root),'update-index','--cacheinfo',mode,oid,path],check=True,capture_output=True) + assert subprocess.check_output(['git','-C',str(root),'ls-files','--stage','--',name]).decode()==index +print(json.dumps({'consistency_exit':result.returncode,'original_other_work_restored':True})) +raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/collect_publication.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/collect_publication.py new file mode 100644 index 00000000..65065c7c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/collect_publication.py @@ -0,0 +1,70 @@ +"""Verify owner source, pushed ancestry and exact file-backed task projection.""" +from pathlib import Path +from collections import Counter +from datetime import datetime, timezone +import hashlib, json, re, subprocess, urllib.request +import yaml +base = Path(__file__).resolve().parent +def load(path): return json.loads(path.read_text()) +def get(path): + with urllib.request.urlopen('http://127.0.0.1:8000'+path,timeout=30) as response: return json.load(response) +def git(root,*args): return subprocess.check_output(['git','-C',str(root),*args],stderr=subprocess.PIPE) +out = {'schema':'custodian.owner-publication-verification.v1','observed_at':datetime.now(timezone.utc).isoformat(), + 'native_target_acceptance':False,'all_sixteen_target_gates_remain_open':True,'owners':[]} +health=get('/state/health') +assert health['instance_role']=='primary' and health['instance_label']=='railiance01' +out['projection_instance']={k:health[k] for k in ['instance_role','instance_label']} +for owner in ['net-kingdom','repo-manager']: + root=Path('/home/worsch')/owner + original=load(base/(owner+'-commit.json')); final=load(base/(owner+'-followthrough-commit.json')) + assert original['exit_code']==0 and final['exit_code']==0 + head=git(root,'rev-parse','HEAD').decode().strip() + assert head==final['head'] + remote=git(root,'ls-remote','origin','refs/heads/main').decode().split()[0] + assert head==remote + git(root,'merge-base','--is-ancestor',original['head'],head) + source_count=0 + for path,expected in original['files'].items(): + if path.startswith('workplans/'):continue + assert 'sha256:'+hashlib.sha256((root/path).read_bytes()).hexdigest()==expected + source_count+=1 + backups=base/(owner+'-publication-backup') + for name,index in load(backups/'originals.json').items(): + assert (root/name).read_bytes()==(backups/(name+'.worktree')).read_bytes() + assert git(root,'ls-files','--stage','--',name).decode()==index + assert load(base/(owner+'-sync.exit.json'))['exit_code']==0 + sync=load(base/(owner+'-sync.stdout.json')) + if owner=='repo-manager': + assert sync['ok'] and sync['status']=='applied' and sync['commit']==head + assert sync['receipt']['derived_commit']==head + sync_summary={'status':sync['status'],'commit':head,'refused':sync['receipt']['outcome']['counts']['refused']} + assert sync_summary['refused']==0 + else: + assert sync['source_revision']==head + assert sync['summary']['fail']==0 and sync['summary']['automation_error']==0 + central=load(base/'net-kingdom-central-projection.stdout.json') + assert central['ok'] and central['status']=='applied' and central['commit']==head + assert central['receipt']['derived_commit']==head + assert central['receipt']['outcome']['counts']['refused']==0 + sync_summary={'legacy_consistency':sync['summary'],'central_projection':central['status'], + 'retained_audit_finding':'Legacy sync matched UUID/status but omitted task record IDs after T09; forge-derived reconciliation repaired projection.'} + workplans=[] + for path in [p for p in original['files'] if p.startswith('workplans/')]: + text=(root/path).read_text();front=yaml.safe_load(text.split('---',2)[1]) + source=[yaml.safe_load(block) for block in re.findall(r'```task\s*\n(.*?)```',text,re.S)] + projected=get('/tasks/?workplan_id='+front['state_hub_workstream_id']) + assert isinstance(projected,list) + by_id={item['id']:item for item in projected if item.get('workplan_id')==front['state_hub_workstream_id']} + assert len(by_id)==len(source), (path,len(source),len(by_id)) + for task in source: + found=by_id[task['state_hub_task_id']] + assert found['status']==task['status'], task['id'] + assert found['record_id']==task['id'], task['id'] + workplans.append({'id':front['id'],'tasks':len(source),'source_counts':dict(Counter(t['status'] for t in source)), + 'all_ids_uuids_statuses_match':True}) + out['owners'].append({'repo':owner,'source_commit':original['head'],'published_head':head, + 'remote_main_exact':True,'source_ancestor_verified':True,'source_files_unchanged':source_count, + 'normal_source_and_record_hooks_passed':True,'preexisting_other_work_preserved':True, + 'sync':sync_summary,'workplans':workplans}) +(base/'publication-verification.json').write_text(json.dumps(out,indent=2)+'\n') +print(json.dumps({'verified':True,'owners':out['owners']},indent=2)) diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/compile_sources.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/compile_sources.py new file mode 100644 index 00000000..de5117d7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/compile_sources.py @@ -0,0 +1,18 @@ +from pathlib import Path +import hashlib, json, subprocess +base = Path(__file__).resolve().parent +group = json.loads((base.parent / 'group-provenance-r1/native-storage-proposal-r1/source-evidence.json').read_text())['sources'] +recovery = json.loads((base.parent / 'recovery-attempt-r1/t35-t36-source-next/source-review.json').read_text())['source_refs'] +out = [] +for lane, entries in [('group_storage',group), ('tls_registration',recovery)]: + for entry in entries: + repo, path = entry['repo'], entry['path'] + revision = entry.get('revision', entry.get('head')) + expected = entry.get('head_sha256', entry.get('sha256')).removeprefix('sha256:') + raw = subprocess.check_output(['git','-C','/home/worsch/'+repo,'show',revision+':'+path],stderr=subprocess.PIPE) + assert hashlib.sha256(raw).hexdigest() == expected, 'source changed: ' + repo + '/' + path + out.append({'lane':lane,'repo':repo,'path':path,'revision':revision,'sha256':'sha256:'+expected, + 'source':entry.get('public_source',entry.get('public_source_ref'))}) +assert len(group)==23 and len(recovery)==17 +(base / 'source-references.json').write_text(json.dumps({'scope':'exact public Git sources; no live topology or owner assent','all_40_hashes_independently_verified':True,'references':out},indent=2)+'\n') +print('Independently verified 40 public-source references.') diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/correct_storage_guidance.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/correct_storage_guidance.py new file mode 100644 index 00000000..b59c93ea --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/correct_storage_guidance.py @@ -0,0 +1,19 @@ +from pathlib import Path +import hashlib, json, subprocess +base = Path(__file__).resolve().parent.parent +root = Path('/home/worsch/net-kingdom') +packet = base / 'group-provenance-r1/storage-policy-correction-r1' +manifest = json.loads((packet / 'manifest.json').read_text()) +patch = packet / 'source.patch' +assert hashlib.sha256(patch.read_bytes()).hexdigest() == 'c3e5944a1dba097ddd16d9bfa0021a81002e30242df43129b24746bcdf9f6c2b' +for entry in manifest['files']: + assert hashlib.sha256((root / entry['path']).read_bytes()).hexdigest() == entry['preimage_sha256'] +subprocess.run(['git', '-C', str(root), 'apply', '--check', str(patch)], check=True) +subprocess.run(['git', '-C', str(root), 'apply', str(patch)], check=True) +for entry in manifest['files']: + assert hashlib.sha256((root / entry['path']).read_bytes()).hexdigest() == entry['postimage_sha256'] +receipt = {'patch_sha256': 'sha256:' + manifest['patch_sha256'], 'applied': True, + 'source_code_changed': False, 'production_storage_admitted': False, + 'files': {e['path']: 'sha256:' + e['postimage_sha256'] for e in manifest['files']}} +(base / 'resume-integration-r1/storage-guidance-correction.json').write_text(json.dumps(receipt, indent=2) + '\n') +print('Applied three exact documentation corrections; local source proof retained and native admission open.') diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stdout.json new file mode 100644 index 00000000..4e614ef0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-central-projection.stdout.json @@ -0,0 +1,293 @@ +{ + "ok": true, + "status": "applied", + "repo_slug": "the-custodian", + "commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "requests": 2, + "elapsed_ms": 9184.2, + "git": { + "upstream": "origin/main", + "behind": 0, + "ahead": 0 + }, + "identifiers": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "the-custodian", + "namespace": "helixforge", + "assignments": [], + "historical_invalid_identifiers": [ + { + "kind": "workplan", + "id": "CUST-WP-ADHOC-2026-08-25-RETIRED", + "source": "workplans/ADHOC-2026-08-25-retired.md" + } + ], + "files_changed": [] + }, + "receipt": { + "schema": "state-hub.repository-projection-reconcile.v1", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "derived_commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "outcome": { + "schema": "state-hub.projection-reset.v1", + "repo_slug": "the-custodian", + "commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "status": "applied", + "counts": { + "created": 0, + "updated": 88, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 0, + "updated_tasks": 1, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 0, + "external_edges": 13 + }, + "created": [], + "updated": [ + "CUST-WP-0000", + "CUST-WP-0000b", + "CUST-WP-0000c", + "CUST-WP-0001", + "CUST-WP-0002", + "CUST-WP-0004", + "CUST-WP-0005", + "CUST-WP-0006", + "CUST-WP-0007", + "CUST-WP-0008", + "CUST-WP-0009", + "CUST-WP-0010", + "CUST-WP-0010b", + "CUST-WP-0013", + "CUST-WP-0014", + "CUST-WP-0015", + "CUST-WP-0016", + "CUST-WP-0017", + "CUST-WP-0017-LEGACY", + "CUST-WP-0018", + "CUST-WP-0019", + "CUST-WP-0020", + "CUST-WP-0021", + "CUST-WP-0022", + "CUST-WP-0023", + "CUST-WP-0024", + "CUST-WP-0025", + "CUST-WP-0026", + "CUST-WP-0027", + "CUST-WP-0028", + "CUST-WP-0029", + "CUST-WP-0030", + "CUST-WP-0031", + "CUST-WP-0032", + "CUST-WP-0033", + "CUST-WP-0034", + "CUST-WP-0035", + "CUST-WP-0036", + "CUST-WP-0037", + "CUST-WP-0039", + "CUST-WP-0040", + "CUST-WP-0041", + "CUST-WP-0043", + "CUST-WP-0044", + "CUST-WP-0045", + "CUST-WP-0046", + "CUST-WP-0047", + "CUST-WP-0048", + "CUST-WP-0049", + "CUST-WP-0050", + "CUST-WP-0051", + "CUST-WP-0052", + "CUST-WP-0053", + "CUST-WP-0053-LEGACY", + "CUST-WP-0054", + "CUST-WP-0055", + "CUST-WP-0056", + "CUST-WP-0057", + "CUST-WP-0058", + "CUST-WP-0059", + "CUST-WP-0060", + "CUST-WP-0061", + "CUST-WP-0062", + "CUST-WP-0063", + "CUST-WP-0064", + "CUST-WP-0065", + "CUST-WP-0066", + "CUST-WP-0067", + "CUST-WP-0068", + "CUST-WP-0069", + "CUST-WP-0070", + "CUST-WP-0071", + "CUST-WP-0072", + "CUST-WP-0073", + "CUST-WP-0074", + "CUST-WP-0075", + "CUST-WP-0076", + "CUST-WP-0077", + "CUST-WP-0078", + "CUST-WP-0079", + "CUST-WP-0080", + "CUST-WP-0081", + "CUST-WP-ADHOC-2026-03-29", + "CUST-WP-ADHOC-2026-05-02", + "CUST-WP-ADHOC-2026-07-02", + "CUST-WP-ADHOC-2026-08-25", + "CUST-WP-ADHOC-2026-09-06", + "THE-WP-0001" + ], + "retired": [], + "released": [], + "created_tasks": [], + "updated_tasks": [ + "CUST-WP-0080-T06" + ], + "cancelled_tasks": [], + "created_edges": [], + "removed_edges": [], + "unresolved_edges": [], + "external_edges": [ + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T05", + "target": "CUST-WP-0071-T04", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "20ddd4d7-d85d-5b95-bb0a-7b0ca8af76ca", + "to_workplan_id": null, + "to_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc" + }, + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T03", + "target": "CUST-WP-0071-T01", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495", + "to_workplan_id": null, + "to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a" + }, + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T03", + "target": "CUST-WP-0071-T02", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495", + "to_workplan_id": null, + "to_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68" + }, + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T02", + "target": "CUST-WP-0071-T01", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68", + "to_workplan_id": null, + "to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a" + }, + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T02", + "target": "RAPPS-WP-0014-T03", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68", + "to_workplan_id": null, + "to_task_id": "dd069c6d-fcc1-5bac-b233-976f2f0d5cd1" + }, + { + "workplan": "CUST-WP-0071", + "from": "CUST-WP-0071-T04", + "target": "CUST-WP-0071-T03", + "relationship_type": "starts_after", + "from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3", + "from_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc", + "to_workplan_id": null, + "to_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495" + }, + { + "workplan": "CUST-WP-0073", + "from": "CUST-WP-0073-T02", + "target": "GLAS-WP-0012", + "relationship_type": "blocks", + "from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d", + "from_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35", + "to_workplan_id": "170bf1ae-337f-5553-8d1e-03b07100e08f", + "to_task_id": null + }, + { + "workplan": "CUST-WP-0073", + "from": "CUST-WP-0073-T04", + "target": "CUST-WP-0073-T02", + "relationship_type": "starts_after", + "from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d", + "from_task_id": "90725511-4e31-549f-b567-47feff1a9ca4", + "to_workplan_id": null, + "to_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35" + }, + { + "workplan": "CUST-WP-0079", + "from": "CUST-WP-0079-T03", + "target": "ACTIVITY-WP-0047", + "relationship_type": "blocks", + "from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3", + "from_task_id": "1be0569c-221d-5925-a0ff-fd2af88e52b1", + "to_workplan_id": "ee97869b-dca2-54c9-aa18-0b479c244fb3", + "to_task_id": null + }, + { + "workplan": "CUST-WP-0079", + "from": "CUST-WP-0079-T04", + "target": "PNEX-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3", + "from_task_id": "5dd52156-09a8-5781-b288-99ae822f59a6", + "to_workplan_id": "774c5037-a3d0-5e65-893c-d18970acd66a", + "to_task_id": null + }, + { + "workplan": "CUST-WP-0080", + "from": "CUST-WP-0080-T04", + "target": "CUST-WP-0080-T03", + "relationship_type": "starts_after", + "from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "from_task_id": "82378b0f-7004-5c2f-9683-b356a1227576", + "to_workplan_id": null, + "to_task_id": "87637402-02f1-57c1-8fbc-4141598af8e3" + }, + { + "workplan": "CUST-WP-0080", + "from": "CUST-WP-0080-T05", + "target": "RPF-WP-0056", + "relationship_type": "blocks", + "from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b", + "to_workplan_id": "31933d08-c877-5fda-aff9-6220c3705c92", + "to_task_id": null + }, + { + "workplan": "CUST-WP-0080", + "from": "CUST-WP-0080-T05", + "target": "RAPP-PLAYWRIGHT-WP-0004", + "relationship_type": "blocks", + "from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b", + "to_workplan_id": "f9280c40-201e-59b5-a6f0-1aace8e0e2e2", + "to_task_id": null + } + ], + "refused": [], + "notes": [ + "Preserved bound historical row and task UUIDs: CUST-WP-ADHOC-2026-08-25-RETIRED" + ] + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-commit-inputs.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-commit-inputs.json new file mode 100644 index 00000000..283ca634 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-commit-inputs.json @@ -0,0 +1,11 @@ +{ + "files": { + "docs/netkingdom-target-implementation.md": "sha256:a49de3368eb6f0b5429909797602be392c6e4982559358143180ab1f168e44a5", + "workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md": "sha256:b45d908e47d411e3fb56ec7099b5b646a9fbd6806422bf9a744d960217c9d01c", + "docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/README.md": "sha256:d4710b067289d8fa643beec6dab847c48ad276bd039aa9ad54192ebbe356fe44", + "docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publication-verification.json": "sha256:4893515106b3cf92fdabda701713a3276884cda16c84049d9bebbdf92294d944", + "docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/verified-results.json": "sha256:2e840481872de8784eb0351ecb0aa09ad307339218f05e86b8222e22cd0afd44", + "docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/native-next-steps.md": "sha256:c468533e1bc0970e35409c3c71d30bc31e67c17a99b8db16d2e7046f06faf5d1", + "docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/source-references.json": "sha256:6e12ae4931e41f0ff8162fd8226fc49dfad4a21a2f42677fbd7149f4dd086faa" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.json new file mode 100644 index 00000000..516644ca --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.json @@ -0,0 +1,131 @@ +{ + "repo_slug": "the-custodian", + "repo_path": "/home/worsch/the-custodian", + "source_revision": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']", + "file_path": "workplans/", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry", + "file_path": "workplans/ADHOC-2026-08-25-retired.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'STATE-WP-0092' is not linked to State Hub", + "file_path": "workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md", + "db_id": "", + "file_value": "STATE-WP-0092", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved", + "file_path": "workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05", + "db_id": "", + "file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \u2014 unrouted human gate", + "file_path": "workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06", + "db_id": "", + "file_value": "needs_human: true", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'CUST-WP-0080-T06': file content differs from DB (file wins)", + "file_path": "workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md#CUST-WP-0080-T06", + "db_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "file_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again", + "db_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again", + "fixable": true + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Closed DB workstream 'cust-wp-0010b' (id=416ff571\u2026, status=finished) has no backing workplan file", + "file_path": "", + "db_id": "416ff571-18cf-5cb2-9c8b-836f68a301b3", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "370c2481-6806-41eb-a917-f8874f03184f", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "3f54ce9c-1f95-42de-894f-0f81a52ba2e8", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "5c53733b-c077-4a93-be66-9c3af8f9b6fd", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \u2014 new plans must conform", + "file_path": "workplans/THE-WP-0001-federation-interface.md", + "db_id": "", + "file_value": "THE-WP-0001", + "db_value": "CUST-WP", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 89/89", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-22 fixed: task 6aca534c\u2026 description updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 7, + "info": 4 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.stderr.txt new file mode 100644 index 00000000..047afc64 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-consistency.stderr.txt @@ -0,0 +1 @@ +WARN: working-memory commit failed: Generated-only commits rejected: staged diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-final-verification.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-final-verification.json new file mode 100644 index 00000000..2e2b5664 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-final-verification.json @@ -0,0 +1,21 @@ +{ + "head": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8", + "remote_main_exact": true, + "all_seven_committed_files_unchanged": true, + "all_six_custodian_task_bindings_match": true, + "task_counts": { + "done": 2, + "progress": 3, + "todo": 1 + }, + "sync_summary": { + "fail": 0, + "automation_error": 0, + "warn": 7, + "info": 4 + }, + "central_projection": "applied", + "retained_finding": "Legacy Custodian consistency also omitted some task record IDs; final audit failed until existing forge-derived reconciliation repaired them.", + "preexisting_staged_brief_and_work_records_preserved": true, + "goal_complete": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/.custodian-brief.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/.custodian-brief.md.worktree new file mode 100644 index 00000000..6aad3a57 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/.custodian-brief.md.worktree @@ -0,0 +1,62 @@ + +# Custodian Brief — the-custodian + +**Domain:** infotech +**Last synced:** 2026-10-15 23:45 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### NetKingdom target security architecture and SEC-DSM implementation +Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52` + +**Open tasks:** +- ► Close source defects and reconcile uncovered lifecycle gaps `87637402` +- ► Coordinate self-service Playwright identity delivery `ed8b909a` +- ► Establish the target architecture and drive the restructuring `6aca534c` +- · Verify native closure against every requirement `82378b0f` + +### Separate agent credentials and establish supervised privileged execution +Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d` + +**Open tasks:** +- ! Build and hand out the agent identity `4b88b0b7` + *(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)* +- ! Fleet guidance and harness guards `90725511` + *(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)* +- ! Rotate what was exposed `1b41b532` + *(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)* + +### Size Railiance workloads from actual demand and establish a weekly allocation review +Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3` + +**Open tasks:** +- ! Measure the Vergabe prototype under representative pilot use `82192370` + *(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)* +- ! Review ecosystem allocations and reserve operating room `6dc67558` + *(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)* +- ! Apply accepted sizing changes and verify their effects `944f98e2` + *(wait: Await reviewed allocation proposals and applicable workload-owner authority.)* +- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7` + *(wait: Await the repeatable observation and accepted sizing/review procedure.)* + +### Deterministic security flow: standing authorizations, flow report, and agent re-engagement +Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3` + +**Open tasks:** +- ! Schedule the flow report and decision sync `1be0569c` + *(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)* +- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156` + *(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)* +- ! Stop agent-authored index and sync commits `8a3dc219` + *(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)* +- ! Attended window #1 `e3da807f` + *(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)* + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/WORK-RECORDS.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/WORK-RECORDS.md.worktree new file mode 100644 index 00000000..6dc81634 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/WORK-RECORDS.md.worktree @@ -0,0 +1,536 @@ +# Work Records — the-custodian + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | CUST-WP-ADHOC-2026-05-02 | done | — | workplans/ADHOC-2026-05-02.md | +| workplan | CUST-WP-ADHOC-2026-07-02 | finished | — | workplans/ADHOC-2026-07-02.md | +| workplan | CUST-WP-ADHOC-2026-08-25-RETIRED | finished | — | workplans/ADHOC-2026-08-25-retired.md | +| workplan | CUST-WP-ADHOC-2026-08-25 | finished | — | workplans/ADHOC-2026-08-25.md | +| workplan | CUST-WP-ADHOC-2026-09-06 | finished | — | workplans/ADHOC-2026-09-06.md | +| workplan | CUST-WP-0000 | finished | — | workplans/CUST-WP-0000-state-hub-v0.1-build-deploy.md | +| workplan | CUST-WP-0000b | finished | — | workplans/CUST-WP-0000b-state-hub-v0.2-decisions-dependencies.md | +| workplan | CUST-WP-0001 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md | +| workplan | CUST-WP-0002 | finished | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| workplan | CUST-WP-0004 | finished | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| workplan | CUST-WP-0005 | finished | — | workplans/CUST-WP-0005-dynamic-domains.md | +| workplan | CUST-WP-0006 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| workplan | CUST-WP-0007 | finished | — | workplans/CUST-WP-0007-gems-migration.md | +| workplan | CUST-WP-0008 | done | — | workplans/CUST-WP-0008-consistency-engine.md | +| workplan | CUST-WP-0009 | finished | — | workplans/CUST-WP-0009-human-interventions.md | +| workplan | CUST-WP-0010 | finished | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md | +| workplan | CUST-WP-0010b | finished | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md | +| workplan | CUST-WP-0013 | finished | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| workplan | CUST-WP-0015 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| workplan | CUST-WP-0016 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| workplan | CUST-WP-0017 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| workplan | CUST-WP-0018 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| workplan | CUST-WP-0019 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md | +| workplan | CUST-WP-0020 | done | — | workplans/CUST-WP-0020-test-suite.md | +| workplan | CUST-WP-0021 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| workplan | CUST-WP-0022 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| workplan | CUST-WP-0023 | done | — | workplans/CUST-WP-0023-tpsc.md | +| workplan | CUST-WP-0024 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| workplan | CUST-WP-0026 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| workplan | CUST-WP-0027 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| workplan | CUST-WP-0029 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| workplan | CUST-WP-0030 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| workplan | CUST-WP-0031 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| workplan | CUST-WP-0032 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| workplan | CUST-WP-0033 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| workplan | CUST-WP-0039 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| workplan | CUST-WP-0040 | finished | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| workplan | CUST-WP-0041 | done | — | workplans/CUST-WP-0041-api-performance.md | +| workplan | CUST-WP-0043 | finished | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| workplan | CUST-WP-0044 | finished | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| workplan | CUST-WP-0045 | finished | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| workplan | CUST-WP-0069 | finished | — | workplans/CUST-WP-0045-cutover-runbook.md | +| workplan | CUST-WP-0046 | finished | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| workplan | CUST-WP-0047 | finished | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| workplan | CUST-WP-0048 | finished | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| workplan | CUST-WP-0049 | finished | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| workplan | CUST-WP-0050 | finished | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| workplan | CUST-WP-0051 | finished | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| workplan | CUST-WP-0052 | finished | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| workplan | CUST-WP-0053 | finished | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| workplan | CUST-WP-0055 | finished | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| workplan | CUST-WP-0056 | finished | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md | +| workplan | CUST-WP-0059 | finished | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| workplan | CUST-WP-0060 | finished | — | workplans/CUST-WP-0060-work-record-canonization.md | +| workplan | CUST-WP-0061 | finished | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| workplan | CUST-WP-0062 | finished | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| workplan | CUST-WP-0063 | finished | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| workplan | CUST-WP-0064 | finished | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md | +| workplan | CUST-WP-0065 | finished | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md | +| workplan | CUST-WP-0066 | finished | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md | +| workplan | CUST-WP-0067 | finished | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| workplan | CUST-WP-0068 | finished | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| workplan | CUST-WP-0070 | finished | — | workplans/CUST-WP-0070-publication-repo-category.md | +| workplan | CUST-WP-0071 | blocked | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| workplan | CUST-WP-0072 | finished | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md | +| workplan | CUST-WP-0073 | blocked | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| workplan | CUST-WP-0074 | finished | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| workplan | CUST-WP-0075 | finished | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| workplan | CUST-WP-0076 | finished | — | workplans/CUST-WP-0076-provisioning-window-cadence.md | +| workplan | CUST-WP-0077 | finished | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| workplan | CUST-WP-0078 | finished | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | +| workplan | CUST-WP-0079 | blocked | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| workplan | CUST-WP-0080 | active | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| workplan | CUST-WP-0081 | finished | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md | +| workplan | THE-WP-0001 | finished | — | workplans/THE-WP-0001-federation-interface.md | +| task | CUST-WP-ADHOC-2026-05-02-T01 | done | — | workplans/ADHOC-2026-05-02.md | +| task | CUST-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | +| task | ADHOC-2026-08-25-T01 | done | — | workplans/ADHOC-2026-08-25-retired.md | +| task | CUST-WP-ADHOC-2026-08-25-T01 | done | — | workplans/ADHOC-2026-08-25.md | +| task | CUST-WP-ADHOC-2026-09-06-T01 | done | — | workplans/ADHOC-2026-09-06.md | +| task | CUST-WP-0001-T01 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md | +| task | CUST-WP-0001-T02 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md | +| task | CUST-WP-0002-T01 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T02 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T03 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T04 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T05 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T06 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T07 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T08 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T09 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T10 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T11 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T12 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T13 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T14 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0002-T15 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md | +| task | CUST-WP-0004-T01 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T02 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T03 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T04 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T05 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T06 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T07 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T08 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T09 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0004-T10 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md | +| task | CUST-WP-0005-T01 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T02 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T03 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T04 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T05 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T06 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T07 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T08 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T09 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T10 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | CUST-WP-0005-T11 | done | — | workplans/CUST-WP-0005-dynamic-domains.md | +| task | T01 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| task | T02 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| task | T03 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| task | T04 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| task | T05 | done | — | workplans/CUST-WP-0006-gems-state-hub.md | +| task | T01 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T02 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T03 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T04 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T05 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T06 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T07 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T08 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T09 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T10 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T11 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T12 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T13 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T14 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T15 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T16 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | T17 | done | — | workplans/CUST-WP-0007-gems-migration.md | +| task | CUST-WP-0008-T01 | done | — | workplans/CUST-WP-0008-consistency-engine.md | +| task | CUST-WP-0008-T02 | done | — | workplans/CUST-WP-0008-consistency-engine.md | +| task | CUST-WP-0008-T03 | done | — | workplans/CUST-WP-0008-consistency-engine.md | +| task | CUST-WP-0008-T04 | done | — | workplans/CUST-WP-0008-consistency-engine.md | +| task | CUST-WP-0009-T1 | done | — | workplans/CUST-WP-0009-human-interventions.md | +| task | CUST-WP-0009-T2 | done | — | workplans/CUST-WP-0009-human-interventions.md | +| task | CUST-WP-0009-T3 | done | — | workplans/CUST-WP-0009-human-interventions.md | +| task | CUST-WP-0010-T1 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md | +| task | CUST-WP-0010-T2 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md | +| task | CUST-WP-0010-T3 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md | +| task | CUST-WP-0010b-T01 | done | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md | +| task | CUST-WP-0010b-T02 | done | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md | +| task | CUST-WP-0013-T01 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T02 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T03 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T04 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T05 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T06 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T07 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T08 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T09 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T10 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T11 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T12 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T13 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0013-T14 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md | +| task | CUST-WP-0015-T01 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| task | CUST-WP-0015-T02 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| task | CUST-WP-0015-T03 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| task | CUST-WP-0015-T04 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| task | CUST-WP-0015-T05 | done | — | workplans/CUST-WP-0015-agent-inbox.md | +| task | CUST-WP-0016-T01 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| task | CUST-WP-0016-T02 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| task | CUST-WP-0016-T03 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| task | CUST-WP-0016-T04 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| task | CUST-WP-0016-T05 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md | +| task | CUST-WP-0017-T01 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0017-T02 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0017-T03 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0017-T04 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0017-T05 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0017-T06 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md | +| task | CUST-WP-0018-T01 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T02 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T03 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T04 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T05 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T06 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0018-T07 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md | +| task | CUST-WP-0019-T01 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md | +| task | CUST-WP-0019-T02 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md | +| task | CUST-WP-0019-T03 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md | +| task | CUST-WP-0020-T01 | done | — | workplans/CUST-WP-0020-test-suite.md | +| task | CUST-WP-0020-T02 | done | — | workplans/CUST-WP-0020-test-suite.md | +| task | CUST-WP-0020-T03 | done | — | workplans/CUST-WP-0020-test-suite.md | +| task | CUST-WP-0020-T04 | done | — | workplans/CUST-WP-0020-test-suite.md | +| task | CUST-WP-0020-T05 | done | — | workplans/CUST-WP-0020-test-suite.md | +| task | CUST-WP-0021-T01 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| task | CUST-WP-0021-T02 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| task | CUST-WP-0021-T03 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| task | CUST-WP-0021-T04 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| task | CUST-WP-0021-T05 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md | +| task | CUST-WP-0022-T01 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T02 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T03 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T04 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T05 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T06 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T07 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T08 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T09 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0022-T10 | done | — | workplans/CUST-WP-0022-capability-requests.md | +| task | CUST-WP-0023-T01 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T02 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T03 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T04 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T05 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T06 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T07 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T08 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T09 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0023-T10 | done | — | workplans/CUST-WP-0023-tpsc.md | +| task | CUST-WP-0024-T01 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| task | CUST-WP-0024-T02 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| task | CUST-WP-0024-T03 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| task | CUST-WP-0024-T04 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| task | CUST-WP-0024-T05 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md | +| task | CUST-WP-0026-T01 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| task | CUST-WP-0026-T02 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| task | CUST-WP-0026-T03 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| task | CUST-WP-0026-T04 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| task | CUST-WP-0026-T05 | done | — | workplans/CUST-WP-0026-distributed-consistency.md | +| task | CUST-WP-0027-T01 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| task | CUST-WP-0027-T02 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| task | CUST-WP-0027-T03 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| task | CUST-WP-0027-T04 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| task | CUST-WP-0027-T05 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md | +| task | CUST-WP-0029-T01 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T02 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T03 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T04 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T05 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T06 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T07 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | CUST-WP-0029-T08 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md | +| task | T01 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T02 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T03 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T04 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T05 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T06 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T07 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T08 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T09 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md | +| task | T01 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T02 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T03 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T04 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T05 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T06 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | T07 | done | — | workplans/CUST-WP-0031-capability-registry.md | +| task | CUST-WP-0032-T01 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T02 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T03 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T04 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T05 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T06 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T07 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T08 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T09 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T10 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T11 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T12 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T13 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T14 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0032-T15 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md | +| task | CUST-WP-0033-T01 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0033-T02 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0033-T03 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0033-T04 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0033-T05 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0033-T06 | done | — | workplans/CUST-WP-0033-interface-change-registry.md | +| task | CUST-WP-0039-T1 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | CUST-WP-0039-T2 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | CUST-WP-0039-T3 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | CUST-WP-0039-T4 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | CUST-WP-0039-T5 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | CUST-WP-0039-T6 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md | +| task | T01 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | T02 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | T03 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | T04 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | T05 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | T06 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md | +| task | CUST-WP-0041-T1 | done | — | workplans/CUST-WP-0041-api-performance.md | +| task | CUST-WP-0041-T2 | done | — | workplans/CUST-WP-0041-api-performance.md | +| task | CUST-WP-0041-T3 | done | — | workplans/CUST-WP-0041-api-performance.md | +| task | CUST-WP-0041-T4 | done | — | workplans/CUST-WP-0041-api-performance.md | +| task | CUST-WP-0043-T01 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T02 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T03 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T04 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T05 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T06 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T07 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T08 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0043-T09 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md | +| task | CUST-WP-0044-T01 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0044-T02 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0044-T03 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0044-T04 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0044-T05 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0044-T06 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md | +| task | CUST-WP-0045-T01 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T02 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T03 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T04 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T05 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T06 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T07 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0045-T08 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md | +| task | CUST-WP-0046-T01 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T02 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T03 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T04 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T05 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T06 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0046-T07 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md | +| task | CUST-WP-0047-T01 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T02 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T03 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T04 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T05 | cancel | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T06 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0047-T07 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md | +| task | CUST-WP-0048-T01 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T02 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T03 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T04 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T05 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T06 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T07 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0048-T08 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md | +| task | CUST-WP-0049-T01 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0049-T02 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0049-T03 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0049-T04 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0049-T05 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0049-T06 | cancel | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md | +| task | CUST-WP-0050-T01 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T02 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T03 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T04 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T05 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T06 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T07 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T08 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T09 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T10 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0050-T11 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md | +| task | CUST-WP-0051-T01 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T02 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T03 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T04 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T05 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T06 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T07 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0051-T08 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md | +| task | CUST-WP-0052-T01 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T02 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T03 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T04 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T05 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T06 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0052-T07 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md | +| task | CUST-WP-0053-T01 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| task | CUST-WP-0053-T02 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| task | CUST-WP-0053-T03 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| task | CUST-WP-0053-T04 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| task | CUST-WP-0053-T05 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md | +| task | CUST-WP-0055-T01 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T02 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T03 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T04 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T05 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T06 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T07 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0055-T08 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md | +| task | CUST-WP-0056-T01 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md | +| task | CUST-WP-0056-T02 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md | +| task | CUST-WP-0056-T03 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md | +| task | CUST-WP-0059-T01 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0059-T02 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0059-T03 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0059-T04 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0059-T05 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0059-T06 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md | +| task | CUST-WP-0060-T01 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T02 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T03 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T04 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T05 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T06 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0060-T07 | done | — | workplans/CUST-WP-0060-work-record-canonization.md | +| task | CUST-WP-0061-T01 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0061-T02 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0061-T03 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0061-T04 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0061-T05 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0061-T06 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md | +| task | CUST-WP-0062-T01 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0062-T02 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0062-T03 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0062-T04 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0062-T05 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0062-T06 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md | +| task | CUST-WP-0063-T01 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| task | CUST-WP-0063-T02 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| task | CUST-WP-0063-T03 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| task | CUST-WP-0063-T04 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| task | CUST-WP-0063-T05 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md | +| task | CUST-WP-0064-T01 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md | +| task | CUST-WP-0064-T02 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md | +| task | CUST-WP-0064-T03 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md | +| task | CUST-WP-0064-T04 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md | +| task | CUST-WP-0065-T01 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md | +| task | CUST-WP-0065-T02 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md | +| task | CUST-WP-0065-T03 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md | +| task | CUST-WP-0065-T04 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md | +| task | CUST-WP-0066-T01 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md | +| task | CUST-WP-0066-T02 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md | +| task | CUST-WP-0066-T03 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md | +| task | CUST-WP-0067-T01 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T02 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T03 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T04 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T05 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T06 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T07 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T08 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T09 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0067-T10 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md | +| task | CUST-WP-0068-T01 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T02 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T03 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T04 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T05 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T06 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T07 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T08 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0068-T09 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md | +| task | CUST-WP-0070-T01 | done | — | workplans/CUST-WP-0070-publication-repo-category.md | +| task | CUST-WP-0070-T02 | done | — | workplans/CUST-WP-0070-publication-repo-category.md | +| task | CUST-WP-0070-T03 | done | — | workplans/CUST-WP-0070-publication-repo-category.md | +| task | CUST-WP-0071-T01 | done | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| task | CUST-WP-0071-T02 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| task | CUST-WP-0071-T03 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| task | CUST-WP-0071-T04 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| task | CUST-WP-0071-T05 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md | +| task | CUST-WP-0072-T01 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md | +| task | CUST-WP-0072-T02 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md | +| task | CUST-WP-0072-T03 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md | +| task | CUST-WP-0072-T04 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md | +| task | CUST-WP-0073-T01 | done | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| task | CUST-WP-0073-T02 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| task | CUST-WP-0073-T03 | done | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| task | CUST-WP-0073-T04 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| task | CUST-WP-0073-T05 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md | +| task | CUST-WP-0074-T01 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0074-T02 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0074-T03 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0074-T04 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0074-T05 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0074-T06 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md | +| task | CUST-WP-0075-T01 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0075-T02 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0075-T03 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0075-T04 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0075-T05 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0075-T06 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md | +| task | CUST-WP-0076-T01 | done | — | workplans/CUST-WP-0076-provisioning-window-cadence.md | +| task | CUST-WP-0077-T01 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| task | CUST-WP-0077-T02 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| task | CUST-WP-0077-T03 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| task | CUST-WP-0077-T04 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| task | CUST-WP-0077-T05 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md | +| task | CUST-WP-0078-T01 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | +| task | CUST-WP-0078-T02 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | +| task | CUST-WP-0078-T03 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | +| task | CUST-WP-0078-T04 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | +| task | CUST-WP-0079-T01 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T02 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T03 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T04 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T05 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T06 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T07 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0080-T01 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0080-T02 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0080-T03 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0080-T04 | todo | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0080-T05 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0080-T06 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md | +| task | CUST-WP-0081-T01 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md | +| task | CUST-WP-0081-T02 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md | +| task | CUST-WP-0081-T03 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md | +| task | THE-WP-0001-T01 | done | — | workplans/THE-WP-0001-federation-interface.md | +| task | THE-WP-0001-T02 | done | — | workplans/THE-WP-0001-federation-interface.md | +| intake | CUST-IN-0001 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0002 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0003 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0004 | routed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0005 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0006 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0007 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0008 | closed | blue | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0009 | closed | blue | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0010 | closed | green | intake-legacy-suggestions-migration.md | +| intake | CUST-IN-0011 | routed | red | intakes.md | +| intake | CUST-IN-0012 | closed | green | intakes.md | +| intake | CUST-IN-0013 | closed | blue | intakes.md | +| intake | CUST-IN-0014 | closed | blue | intakes.md | +| intake | CUST-IN-0015 | open | blue | intakes.md | +| intake | CUST-IN-0016 | open | green | intakes.md | +| intake | CUST-IN-0017 | open | green | intakes.md | +| intake | CUST-IN-0018 | open | green | intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/originals.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/originals.json new file mode 100644 index 00000000..82d0dc78 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/custodian-publication-backup/originals.json @@ -0,0 +1 @@ +{".custodian-brief.md": "100644 6aad3a5788fad3abd60c0f1270f160062dc0168b 0\t.custodian-brief.md\n", "WORK-RECORDS.md": "100644 d535e0f2dcc3d454e041a8f1b547b5dea4493380 0\tWORK-RECORDS.md\n"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/final-progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/final-progress-receipt.json new file mode 100644 index 00000000..481bf73a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/final-progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "27a691e0-dab7-41ec-9d3b-b0deebe2535e", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "Final source checkpoint d834d0c is published; all7 reviewed Custodian files match and67 source task ID/UUID/status bindings agree (NK-WP-0048:20, NK-WP-0049:36, RMGR-WP-0021:5, CUST-WP-0080:6). Custodian consistency completed fail0/automation_error0/warn7/info4; existing forge-derived reconciliation repaired missing legacy task record IDs. Original staged brief, working records and unrelated changes preserved. Delivered bounded source: NK-WP-0048-T20 and NK-WP-0049-T34. Live next work: NK-WP-0048-T14, NK-WP-0049-T35/T36, readiness NK-WP-0049-T33, real recovery/MFA NK-WP-0049-T18/T19, scanner runtime gap RMGR-WP-0021-T05. All16 target gates remain open. Native storage, writer isolation and original TLS/trust recovery require their recorded owner boundaries.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T22:49:48.009494Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-integration.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-integration.json new file mode 100644 index 00000000..6df06dde --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-integration.json @@ -0,0 +1,23 @@ +{ + "observed_at": "2026-10-16T22:10:25.662202+00:00", + "lane": "groups", + "owner": "/home/worsch/net-kingdom", + "base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "patch_sha256": "sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e", + "applied": true, + "source_postimages": { + "docs/service-group-provenance.md": "sha256:9cb866c1ba4f92d239ff37ea6be57bafe04cf5e9e811fda5842878ab9677678c", + "identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6", + "identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4", + "identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5", + "identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29", + "identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836", + "identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a", + "identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "sso-mfa/k8s/README.md": "sha256:0aa87dc25bef245eadc184ae58ef1e25b748e2541086d4de2ada15d38b17340f" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-preflight.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-preflight.json new file mode 100644 index 00000000..6f9eb32d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/groups-preflight.json @@ -0,0 +1,23 @@ +{ + "observed_at": "2026-10-16T22:09:04.829846+00:00", + "lane": "groups", + "owner": "/home/worsch/net-kingdom", + "base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "patch_sha256": "sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e", + "applied": false, + "source_postimages": { + "docs/service-group-provenance.md": "sha256:9cb866c1ba4f92d239ff37ea6be57bafe04cf5e9e811fda5842878ab9677678c", + "identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6", + "identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4", + "identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5", + "identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29", + "identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836", + "identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a", + "identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "sso-mfa/k8s/README.md": "sha256:0aa87dc25bef245eadc184ae58ef1e25b748e2541086d4de2ada15d38b17340f" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/integrate_workplans.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/integrate_workplans.py new file mode 100644 index 00000000..12dd82db --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/integrate_workplans.py @@ -0,0 +1,29 @@ +from pathlib import Path +import hashlib, json, subprocess + +base = Path(__file__).resolve().parent.parent +lanes = [ + ('net-kingdom', 'group-provenance-r1/workplan-refinement/source.patch', '8251911aa8d6ebf9be916ece22ae4cd8ddb179110c3f9348aaee67db4c3acf74'), + ('net-kingdom', 'recovery-attempt-r1/workplan-refinement.patch', 'aa11ac3b3cbb36febc436736e7eaa611f8769fbae70b0f026fa9534954aa604c'), + ('repo-manager', 'scanner-batch-r1/owner-workplan.patch', 'e0d08a2e571664a6ec8c8c22b67c3788d248878c1644f9b758ba57c30a321488'), +] +receipts = [] +for slug, rel, expected in lanes: + root = Path('/home/worsch') / slug + patch = base / rel + assert hashlib.sha256(patch.read_bytes()).hexdigest() == expected + def git(*args): return subprocess.check_output(['git', '-C', str(root), *args], stderr=subprocess.PIPE) + paths = [s[6:] for s in patch.read_text().splitlines() if s.startswith('+++ b/')] + assert len(paths) == 1 + assert not git('status', '--porcelain', '--', paths[0]).strip() + git('apply', '--check', str(patch)) + git('apply', str(patch)) + if slug == 'repo-manager': + path = root / paths[0] + text = path.read_text().replace('id: RMGR-WP-0021-T05\nstatus: todo', 'id: RMGR-WP-0021-T05\nstatus: progress') + text += '\n2026-10-17 integration started: exact reviewed source is applied in the owner\ncheckout. Canonical tests, configured-hook fixtures and publication are being\nverified separately; actual-hook scanner-unavailability remains pending.\n' + path.write_text(text) + receipts.append({'owner': slug, 'path': paths[0], 'reviewed_patch_sha256': 'sha256:' + expected, + 'integrated_sha256': 'sha256:' + hashlib.sha256((root / paths[0]).read_bytes()).hexdigest()}) +(base / 'resume-integration-r1/workplan-integration.json').write_text(json.dumps(receipts, indent=2) + '\n') +print(json.dumps(receipts, indent=2)) diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stdout.json new file mode 100644 index 00000000..c43dd706 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-central-projection.stdout.json @@ -0,0 +1,923 @@ +{ + "ok": true, + "status": "applied", + "repo_slug": "net-kingdom", + "commit": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "requests": 2, + "elapsed_ms": 6998.8, + "git": { + "upstream": "origin/main", + "behind": 0, + "ahead": 0 + }, + "identifiers": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "net-kingdom", + "namespace": "helixforge", + "assignments": [], + "historical_invalid_identifiers": [], + "files_changed": [] + }, + "receipt": { + "schema": "state-hub.repository-projection-reconcile.v1", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "derived_commit": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "outcome": { + "schema": "state-hub.projection-reset.v1", + "repo_slug": "net-kingdom", + "commit": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "status": "applied", + "counts": { + "created": 0, + "updated": 64, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 0, + "updated_tasks": 16, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 1, + "external_edges": 77 + }, + "created": [], + "updated": [ + "NET-WP-0015", + "NET-WP-0016", + "NET-WP-0017", + "NET-WP-0018", + "NET-WP-0019", + "NET-WP-0020", + "NK-WP-0001", + "NK-WP-0002", + "NK-WP-0003", + "NK-WP-0004", + "NK-WP-0005", + "NK-WP-0006", + "NK-WP-0007", + "NK-WP-0008", + "NK-WP-0009", + "NK-WP-0010", + "NK-WP-0011", + "NK-WP-0012", + "NK-WP-0013", + "NK-WP-0014", + "NK-WP-0015", + "NK-WP-0016", + "NK-WP-0017", + "NK-WP-0018", + "NK-WP-0019", + "NK-WP-0021", + "NK-WP-0022", + "NK-WP-0023", + "NK-WP-0024", + "NK-WP-0025", + "NK-WP-0026", + "NK-WP-0027", + "NK-WP-0028", + "NK-WP-0029", + "NK-WP-0030", + "NK-WP-0031", + "NK-WP-0032", + "NK-WP-0033", + "NK-WP-0034", + "NK-WP-0035", + "NK-WP-0036", + "NK-WP-0037", + "NK-WP-0038", + "NK-WP-0039", + "NK-WP-0040", + "NK-WP-0041", + "NK-WP-0042", + "NK-WP-0043", + "NK-WP-0044", + "NK-WP-0045", + "NK-WP-0046", + "NK-WP-0047", + "NK-WP-0048", + "NK-WP-0049", + "NK-WP-0050", + "NK-WP-0051", + "NK-WP-ADHOC-2026-06-14", + "NK-WP-ADHOC-2026-07-02", + "NK-WP-ADHOC-2026-08-14", + "NK-WP-ADHOC-2026-08-23", + "NK-WP-ADHOC-2026-10-03", + "NK-WP-ADHOC-2026-10-10", + "NK-WP-ADHOC-2026-10-11", + "NK-WP-ADHOC-2026-10-16" + ], + "retired": [], + "released": [], + "created_tasks": [], + "updated_tasks": [ + "NK-WP-0048-T10", + "NK-WP-0048-T11", + "NK-WP-0048-T12", + "NK-WP-0048-T13", + "NK-WP-0048-T14", + "NK-WP-0048-T15", + "NK-WP-0048-T16", + "NK-WP-0048-T17", + "NK-WP-0048-T18", + "NK-WP-0048-T19", + "NK-WP-0048-T20", + "NK-WP-0049-T32", + "NK-WP-0049-T33", + "NK-WP-0049-T34", + "NK-WP-0049-T35", + "NK-WP-0049-T36" + ], + "cancelled_tasks": [], + "created_edges": [], + "removed_edges": [], + "unresolved_edges": [ + { + "workplan": "NK-WP-0006", + "from": "NK-WP-0006", + "target": "NK-WP-0001", + "reason": "missing" + } + ], + "external_edges": [ + { + "workplan": "NK-WP-0008", + "from": "NK-WP-0008", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NK-WP-0050", + "from": "NK-WP-0050-T03", + "target": "RPF-WP-0056-T03", + "relationship_type": "starts_after", + "from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377", + "from_task_id": "5a3aaf57-de5c-5a03-bc99-9bcf2fc5d95e", + "to_workplan_id": null, + "to_task_id": "a82762eb-bb6d-5600-89ca-a81ada2eba9f" + }, + { + "workplan": "NK-WP-0050", + "from": "NK-WP-0050-T03", + "target": "NK-WP-0050-T02", + "relationship_type": "starts_after", + "from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377", + "from_task_id": "5a3aaf57-de5c-5a03-bc99-9bcf2fc5d95e", + "to_workplan_id": null, + "to_task_id": "a9dadd07-f136-5dc7-9229-7a9dbb941f0d" + }, + { + "workplan": "NK-WP-0050", + "from": "NK-WP-0050-T02", + "target": "NK-WP-0050-T01", + "relationship_type": "starts_after", + "from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377", + "from_task_id": "a9dadd07-f136-5dc7-9229-7a9dbb941f0d", + "to_workplan_id": null, + "to_task_id": "64893d94-9717-5812-b8e3-7cabe095fcdc" + }, + { + "workplan": "NK-WP-0017", + "from": "NK-WP-0017", + "target": "NK-WP-0015", + "relationship_type": "blocks", + "from_workplan_id": "08398d26-cadf-44bc-97ee-67da790040e6", + "from_task_id": null, + "to_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8", + "to_task_id": null + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011", + "target": "NK-WP-0004", + "relationship_type": "blocks", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": null, + "to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e", + "to_task_id": null + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011", + "target": "NK-WP-0003", + "relationship_type": "blocks", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": null, + "to_workplan_id": "f24cefd4-a09b-4fa1-9b25-94bf783b425e", + "to_task_id": null + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T03", + "target": "NK-WP-0011-T02", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908", + "to_workplan_id": null, + "to_task_id": "2fe6f100-3a5d-563e-b033-7d7b846ae487" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T07", + "target": "NK-WP-0011-T03", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833", + "to_workplan_id": null, + "to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T07", + "target": "NK-WP-0011-T04", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833", + "to_workplan_id": null, + "to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T07", + "target": "NK-WP-0011-T05", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833", + "to_workplan_id": null, + "to_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T04", + "target": "NK-WP-0011-T03", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b", + "to_workplan_id": null, + "to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T06", + "target": "NK-WP-0011-T03", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446", + "to_workplan_id": null, + "to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T06", + "target": "NK-WP-0011-T04", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446", + "to_workplan_id": null, + "to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T06", + "target": "NK-WP-0011-T05", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446", + "to_workplan_id": null, + "to_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T05", + "target": "NK-WP-0011-T04", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d", + "to_workplan_id": null, + "to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T09", + "target": "NK-WP-0011-T03", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e", + "to_workplan_id": null, + "to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T09", + "target": "NK-WP-0011-T07", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e", + "to_workplan_id": null, + "to_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T09", + "target": "NK-WP-0011-T08", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e", + "to_workplan_id": null, + "to_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T08", + "target": "NK-WP-0011-T02", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d", + "to_workplan_id": null, + "to_task_id": "2fe6f100-3a5d-563e-b033-7d7b846ae487" + }, + { + "workplan": "NK-WP-0011", + "from": "NK-WP-0011-T08", + "target": "NK-WP-0011-T03", + "relationship_type": "starts_after", + "from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "from_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d", + "to_workplan_id": null, + "to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908" + }, + { + "workplan": "NET-WP-0016", + "from": "NET-WP-0016", + "target": "NET-WP-0015", + "relationship_type": "blocks", + "from_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38", + "from_task_id": null, + "to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd", + "to_task_id": null + }, + { + "workplan": "NET-WP-0016", + "from": "NET-WP-0016", + "target": "NK-WP-0012", + "relationship_type": "blocks", + "from_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38", + "from_task_id": null, + "to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a", + "to_task_id": null + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T18", + "target": "NK-WP-0049-T33", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "141dd773-c1fa-56c1-a852-bbe2372e5125", + "to_workplan_id": null, + "to_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T18", + "target": "NK-WP-0049-T32", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "141dd773-c1fa-56c1-a852-bbe2372e5125", + "to_workplan_id": null, + "to_task_id": "cd78dbb7-a8f2-5754-8788-6eb1b7d97741" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T35", + "target": "NK-WP-0049-T34", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "1bfd1703-589f-5511-bad5-812d860e4a71", + "to_workplan_id": null, + "to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T33", + "target": "NK-WP-0049-T35", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "to_workplan_id": null, + "to_task_id": "1bfd1703-589f-5511-bad5-812d860e4a71" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T33", + "target": "NK-WP-0049-T34", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "to_workplan_id": null, + "to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T33", + "target": "NK-WP-0049-T36", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "to_workplan_id": null, + "to_task_id": "8b72334b-fc27-5707-a189-bad345eaf3bc" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T33", + "target": "NK-WP-0049-T32", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "to_workplan_id": null, + "to_task_id": "cd78dbb7-a8f2-5754-8788-6eb1b7d97741" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T36", + "target": "NK-WP-0049-T34", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "8b72334b-fc27-5707-a189-bad345eaf3bc", + "to_workplan_id": null, + "to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7" + }, + { + "workplan": "NK-WP-0049", + "from": "NK-WP-0049-T03", + "target": "NK-WP-0049-T02", + "relationship_type": "starts_after", + "from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "from_task_id": "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac", + "to_workplan_id": null, + "to_task_id": "479d6970-6516-50f5-9436-bb5b56d58aad" + }, + { + "workplan": "NK-WP-0025", + "from": "NK-WP-0025", + "target": "NK-WP-0024", + "relationship_type": "blocks", + "from_workplan_id": "2412a251-d6b5-5f05-8082-3eb7697c51dc", + "from_task_id": null, + "to_workplan_id": "635a73d4-b3cb-5c38-af22-56a6a66beb0e", + "to_task_id": null + }, + { + "workplan": "NK-WP-0016", + "from": "NK-WP-0016", + "target": "NK-WP-0015", + "relationship_type": "blocks", + "from_workplan_id": "2d592e18-e63d-4856-97a1-f8c3e019e150", + "from_task_id": null, + "to_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8", + "to_task_id": null + }, + { + "workplan": "NK-WP-0006", + "from": "NK-WP-0006", + "target": "NK-WP-0005", + "relationship_type": "blocks", + "from_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "from_task_id": null, + "to_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19", + "to_task_id": null + }, + { + "workplan": "NK-WP-0006", + "from": "NK-WP-0006", + "target": "NK-WP-0004", + "relationship_type": "blocks", + "from_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "from_task_id": null, + "to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e", + "to_task_id": null + }, + { + "workplan": "NK-WP-0013", + "from": "NK-WP-0013", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "32a54d8e-8633-42a6-8ec1-104842c581c1", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NK-WP-0015", + "from": "NK-WP-0015", + "target": "NK-WP-0014", + "relationship_type": "blocks", + "from_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8", + "from_task_id": null, + "to_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c", + "to_task_id": null + }, + { + "workplan": "NET-WP-0017", + "from": "NET-WP-0017", + "target": "NET-WP-0016", + "relationship_type": "blocks", + "from_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea", + "from_task_id": null, + "to_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38", + "to_task_id": null + }, + { + "workplan": "NET-WP-0017", + "from": "NET-WP-0017", + "target": "NET-WP-0015", + "relationship_type": "blocks", + "from_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea", + "from_task_id": null, + "to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd", + "to_task_id": null + }, + { + "workplan": "NK-WP-0007", + "from": "NK-WP-0007", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NK-WP-0007", + "from": "NK-WP-0007", + "target": "NK-WP-0005", + "relationship_type": "blocks", + "from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695", + "from_task_id": null, + "to_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19", + "to_task_id": null + }, + { + "workplan": "NK-WP-0007", + "from": "NK-WP-0007", + "target": "NK-WP-0004", + "relationship_type": "blocks", + "from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695", + "from_task_id": null, + "to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e", + "to_task_id": null + }, + { + "workplan": "NK-WP-0024", + "from": "NK-WP-0024", + "target": "NK-WP-0023", + "relationship_type": "blocks", + "from_workplan_id": "635a73d4-b3cb-5c38-af22-56a6a66beb0e", + "from_task_id": null, + "to_workplan_id": "fdb3deee-06c3-5d12-91ce-9c50b2ac0618", + "to_task_id": null + }, + { + "workplan": "NET-WP-0015", + "from": "NET-WP-0015", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NET-WP-0015", + "from": "NET-WP-0015", + "target": "NK-WP-0012", + "relationship_type": "blocks", + "from_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd", + "from_task_id": null, + "to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a", + "to_task_id": null + }, + { + "workplan": "NK-WP-0018", + "from": "NK-WP-0018", + "target": "NK-WP-0017", + "relationship_type": "blocks", + "from_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87", + "from_task_id": null, + "to_workplan_id": "08398d26-cadf-44bc-97ee-67da790040e6", + "to_task_id": null + }, + { + "workplan": "NK-WP-0018", + "from": "NK-WP-0018", + "target": "NK-WP-0016", + "relationship_type": "blocks", + "from_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87", + "from_task_id": null, + "to_workplan_id": "2d592e18-e63d-4856-97a1-f8c3e019e150", + "to_task_id": null + }, + { + "workplan": "NK-WP-0005", + "from": "NK-WP-0005", + "target": "NK-WP-0004", + "relationship_type": "blocks", + "from_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19", + "from_task_id": null, + "to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e", + "to_task_id": null + }, + { + "workplan": "NET-WP-0019", + "from": "NET-WP-0019", + "target": "NET-WP-0017", + "relationship_type": "blocks", + "from_workplan_id": "75d388b6-7ec1-4e1b-8c87-6ff44f953210", + "from_task_id": null, + "to_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea", + "to_task_id": null + }, + { + "workplan": "NET-WP-0019", + "from": "NET-WP-0019", + "target": "NET-WP-0018", + "relationship_type": "blocks", + "from_workplan_id": "75d388b6-7ec1-4e1b-8c87-6ff44f953210", + "from_task_id": null, + "to_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698", + "to_task_id": null + }, + { + "workplan": "NET-WP-0018", + "from": "NET-WP-0018", + "target": "NET-WP-0017", + "relationship_type": "blocks", + "from_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698", + "from_task_id": null, + "to_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea", + "to_task_id": null + }, + { + "workplan": "NET-WP-0018", + "from": "NET-WP-0018", + "target": "NET-WP-0015", + "relationship_type": "blocks", + "from_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698", + "from_task_id": null, + "to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd", + "to_task_id": null + }, + { + "workplan": "NK-WP-0027", + "from": "NK-WP-0027-T03", + "target": "NK-WP-0027-T02", + "relationship_type": "starts_after", + "from_workplan_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4", + "from_task_id": "e3f0bb0f-0c85-5cb9-a5ab-75f668cd3447", + "to_workplan_id": null, + "to_task_id": "5b35b646-a525-55da-852a-6005613d42ea" + }, + { + "workplan": "NK-WP-0012", + "from": "NK-WP-0012", + "target": "NK-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a", + "from_task_id": null, + "to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862", + "to_task_id": null + }, + { + "workplan": "NK-WP-0014", + "from": "NK-WP-0014", + "target": "NK-WP-0013", + "relationship_type": "blocks", + "from_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c", + "from_task_id": null, + "to_workplan_id": "32a54d8e-8633-42a6-8ec1-104842c581c1", + "to_task_id": null + }, + { + "workplan": "NK-WP-0014", + "from": "NK-WP-0014", + "target": "NK-WP-0012", + "relationship_type": "blocks", + "from_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c", + "from_task_id": null, + "to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a", + "to_task_id": null + }, + { + "workplan": "NK-WP-0019", + "from": "NK-WP-0019", + "target": "NK-WP-0018", + "relationship_type": "blocks", + "from_workplan_id": "d2daa8b4-8ecf-4377-b382-492e653735f7", + "from_task_id": null, + "to_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87", + "to_task_id": null + }, + { + "workplan": "NK-WP-0009", + "from": "NK-WP-0009", + "target": "NK-WP-0008", + "relationship_type": "blocks", + "from_workplan_id": "d4d02dbf-3974-502d-8b87-b776fc63e17e", + "from_task_id": null, + "to_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d", + "to_task_id": null + }, + { + "workplan": "NK-WP-0022", + "from": "NK-WP-0022", + "target": "NK-WP-0023", + "relationship_type": "blocks", + "from_workplan_id": "d76ddccc-00c8-548a-b141-2cd660fa38da", + "from_task_id": null, + "to_workplan_id": "fdb3deee-06c3-5d12-91ce-9c50b2ac0618", + "to_task_id": null + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T07", + "target": "NK-WP-0048-T06", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7", + "to_workplan_id": null, + "to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T09", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "0ee12dfc-8260-5038-bd39-cc7fb4113be0" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T15", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "1b825705-4848-5edc-832f-b1622cd9bdb7" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T16", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T05", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "3d987d3d-0797-538b-86e4-51185a327ae2" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "MASON-WP-0010-T03", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "56cea6be-c613-586c-80cd-125340b374da" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T12", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T20", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T17", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "8c3e4baa-a218-5996-af9e-fe1153a46dac" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T08", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "8e519082-dc8b-5e46-b660-03d03e55dcf6" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T18", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "9c45da71-fb4c-5f20-9065-90f448f37c64" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T13", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "b264c10b-565c-5d29-b788-03cf5fbd2883" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "WARDEN-WP-0044-T03", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T06", + "target": "NK-WP-0048-T14", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "fce95440-a25b-5f27-8ef3-a3d182099113" + }, + { + "workplan": "NK-WP-0048", + "from": "NK-WP-0048-T14", + "target": "NK-WP-0048-T20", + "relationship_type": "starts_after", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "fce95440-a25b-5f27-8ef3-a3d182099113", + "to_workplan_id": null, + "to_task_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8" + }, + { + "workplan": "NK-WP-0010", + "from": "NK-WP-0010", + "target": "NK-WP-0008", + "relationship_type": "blocks", + "from_workplan_id": "f4faf8b4-ae57-40cf-a881-6fe66ca6ad74", + "from_task_id": null, + "to_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d", + "to_task_id": null + } + ], + "refused": [], + "notes": [] + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit-inputs.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit-inputs.json new file mode 100644 index 00000000..0bd79048 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit-inputs.json @@ -0,0 +1,64 @@ +{ + "files": { + "docs/service-group-provenance.md": "sha256:cd6b45640983b11e29a6bc6278bcb45de8fd8a5ddcad53eb77ab1a1f3f1f1280", + "identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6", + "identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4", + "identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5", + "identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29", + "identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836", + "identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a", + "identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "sso-mfa/k8s/README.md": "sha256:2d437634b2d4e90a98a1c6eb508fea4d1ad79ab8dc75a6870b25a8e3755c816a", + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a", + "tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f", + "tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b", + "tests/isolated-keycape-native-fixture-manifest.json": "sha256:1dcefbc7ca7b0c2be33135b1b7977b89ba657223ecbb1310cc691d151b3d916e", + "workplans/NK-WP-0048-security-case-types.md": "sha256:d141c72e80425ecd12008cff65063a1b16f4fc63e8c3068bf88d1bd3f98ac5a3", + "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md": "sha256:9264fb21367b73c5ab51570093c654e04b030f014988ff0a2899103a802fb948" + }, + "assignments": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "net-kingdom", + "namespace": "helixforge", + "assignments": [ + { + "kind": "task", + "record_id": "NK-WP-0048-T20", + "uuid": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8", + "path": "workplans/NK-WP-0048-security-case-types.md" + }, + { + "kind": "task", + "record_id": "NK-WP-0049-T34", + "uuid": "821b3a56-6f36-5812-abc9-5a00e37cb2e7", + "path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md" + }, + { + "kind": "task", + "record_id": "NK-WP-0049-T35", + "uuid": "1bfd1703-589f-5511-bad5-812d860e4a71", + "path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md" + }, + { + "kind": "task", + "record_id": "NK-WP-0049-T36", + "uuid": "8b72334b-fc27-5707-a189-bad345eaf3bc", + "path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md" + } + ], + "historical_invalid_identifiers": [], + "files_changed": [ + "workplans/NK-WP-0048-security-case-types.md", + "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md" + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit.json new file mode 100644 index 00000000..8f17ca01 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-commit.json @@ -0,0 +1,30 @@ +{ + "exit_code": 0, + "stdout": "[main 4f65e41] fix(identity): retain group provenance and prepare sealed recovery custody\n 22 files changed, 1686 insertions(+), 90 deletions(-)\n create mode 100644 docs/service-group-provenance.md\n create mode 100644 identity-provisioner/group_provenance.py\n create mode 100644 identity-provisioner/tests/test_group_provenance.py\n create mode 100644 sso-mfa/bootstrap/sops-custody-fd.py\n create mode 100644 tests/test_prepare_isolated_keycape.py\n create mode 100644 tests/test_private_manifest_binding.py\n create mode 100644 tests/test_sealed_custody.py\n create mode 100644 tools/bootstrap-recovery/prepare-isolated-keycape.py\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 22, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n", + "head": "4f65e41ac55d2bc652122b76312048efdaf6b698", + "files": { + "docs/service-group-provenance.md": "sha256:cd6b45640983b11e29a6bc6278bcb45de8fd8a5ddcad53eb77ab1a1f3f1f1280", + "identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6", + "identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4", + "identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5", + "identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29", + "identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836", + "identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a", + "identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "sso-mfa/k8s/README.md": "sha256:2d437634b2d4e90a98a1c6eb508fea4d1ad79ab8dc75a6870b25a8e3755c816a", + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a", + "tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f", + "tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b", + "tests/isolated-keycape-native-fixture-manifest.json": "sha256:1dcefbc7ca7b0c2be33135b1b7977b89ba657223ecbb1310cc691d151b3d916e", + "workplans/NK-WP-0048-security-case-types.md": "sha256:d141c72e80425ecd12008cff65063a1b16f4fc63e8c3068bf88d1bd3f98ac5a3", + "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md": "sha256:9264fb21367b73c5ab51570093c654e04b030f014988ff0a2899103a802fb948" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-followthrough-commit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-followthrough-commit.json new file mode 100644 index 00000000..57e82d0d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-followthrough-commit.json @@ -0,0 +1,11 @@ +{ + "owner": "net-kingdom", + "exit_code": 0, + "stdout": "[main 6cb43c0] docs(security): record verified source delivery and native preparation\n 2 files changed, 42 insertions(+), 5 deletions(-)\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 2, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n", + "head": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "paths": [ + "workplans/NK-WP-0048-security-case-types.md", + "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stderr.txt new file mode 100644 index 00000000..3345563f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stderr.txt @@ -0,0 +1 @@ +WARN: git commit failed for writeback: Generated-only commits rejected: staged diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stdout.json new file mode 100644 index 00000000..3b5cb49e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-initial-sync.stdout.json @@ -0,0 +1,491 @@ +{ + "repo_slug": "net-kingdom", + "repo_path": "/home/worsch/net-kingdom", + "source_revision": "4f65e41ac55d2bc652122b76312048efdaf6b698", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-24", + "message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['NET-WP', 'NK-WP']", + "file_path": "workplans/", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-33", + "message": "WORK-RECORDS.md is missing or stale \u2014 regenerate via fix-consistency (CUST-WP-0061-T04 generated work-record index)", + "file_path": "WORK-RECORDS.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0011' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0011-enterprise-federation-saml.md", + "db_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub", + "file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md", + "db_id": "", + "file_value": "USER-WP-0020", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md", + "db_id": "", + "file_value": "KEY-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub", + "file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md", + "db_id": "", + "file_value": "USER-WP-0020", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md", + "db_id": "", + "file_value": "KEY-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0021' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "USER-WP-0021", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'FLEX-WP-0009' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "FLEX-WP-0009", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'AUDIT-WP-0003' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "AUDIT-WP-0003", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'EMAIL-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "EMAIL-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0022' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "USER-WP-0022", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0008' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "KEY-WP-0008", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'CSOC-WP-0003' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "CSOC-WP-0003", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0027' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0027-reef-placement-reconciliation.md", + "db_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0031' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0031-deterministic-posture-feedback.md", + "db_id": "9d7b04f9-3803-5613-b7a5-8bd606c77f5a", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0040' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0040-execution-attribution-receipt.md", + "db_id": "e2533f3a-aa43-59b3-bff3-8e64b6149487", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0048-T20' of task 'NK-WP-0048-T06' is not linked to State Hub", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T06", + "db_id": "", + "file_value": "NK-WP-0048-T20", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0048-T20' of task 'NK-WP-0048-T14' is not linked to State Hub", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T14", + "db_id": "", + "file_value": "NK-WP-0048-T20", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'NK-WP-0048-T14': file content differs from DB (file wins)", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T14", + "db_id": "fce95440-a25b-5f27-8ef3-a3d182099113", + "file_value": "Owner: NetKingdom. Next action: specify a CNPG-backed journal through the\nexisting `rapp-postgres` database-per-consumer", + "db_value": "Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner\nensure/revoke contract with independent", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-11", + "message": "Derived task 'NK-WP-0048-T20' is absent from this projection", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T20", + "db_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0049-T34' of task 'NK-WP-0049-T33' is not linked to State Hub", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0049-T35' of task 'NK-WP-0049-T33' is not linked to State Hub", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "", + "file_value": "NK-WP-0049-T35", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0049-T36' of task 'NK-WP-0049-T33' is not linked to State Hub", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "", + "file_value": "NK-WP-0049-T36", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-10", + "message": "Task status drift 'NK-WP-0049-T33': file='wait' db='todo' (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "file_value": "wait", + "db_value": "todo", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'NK-WP-0049-T33': file content differs from DB (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "file_value": "**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners.\nThis umbrella retains final attempt ", + "db_value": "**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners.\nPrepare the exact archive/manifest, ", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-41", + "message": "Task wait-qualifier drift 'NK-WP-0049-T33': file={'needs_human': False, 'blocking_reason': 'Source custody/preflight preparation is evidenced in T34; await original TLS/trust recovery disposition T35 and selected original registration/private-admission metadata T36 before final reviewed command and existing owner process.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} db={'needs_human': False, 'blocking_reason': None, 'decision_id': None, 'blocked_on': None, 'wait_until': None} (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756", + "file_value": "{\"blocked_on\": null, \"blocking_reason\": \"Source custody/preflight preparation is evidenced in T34; await original TLS/trust recovery disposition T35 and selected original registration/private-admission metadata T36 before final reviewed command and existing owner process.\", \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}", + "db_value": "{\"blocked_on\": null, \"blocking_reason\": null, \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-11", + "message": "Derived task 'NK-WP-0049-T34' is absent from this projection", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T34", + "db_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0049-T34' of task 'NK-WP-0049-T35' is not linked to State Hub", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T35", + "db_id": "", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-11", + "message": "Derived task 'NK-WP-0049-T35' is absent from this projection", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T35", + "db_id": "1bfd1703-589f-5511-bad5-812d860e4a71", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Task dependency target 'NK-WP-0049-T34' of task 'NK-WP-0049-T36' is not linked to State Hub", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T36", + "db_id": "", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-11", + "message": "Derived task 'NK-WP-0049-T36' is absent from this projection", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T36", + "db_id": "8b72334b-fc27-5707-a189-bad345eaf3bc", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'NK-WP-0049-T03': file content differs from DB (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T03", + "db_id": "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac", + "file_value": "**Accountable worker:** codex. Service/credential-owner acceptance remains with\nthe named owner boundary.\n\nOwner boundar", + "db_value": "**Accountable worker:** codex. Service/credential-owner acceptance remains with\nthe named owner boundary.\n\nOwner boundar", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAIL-PL-WP-0002' is not linked to State Hub", + "file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md", + "db_id": "", + "file_value": "RAIL-PL-WP-0002", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'nk-wp-0035@retired-20260908' (id=064c5e8b\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "064c5e8b-7852-4d7f-a5fa-e26e406ab8e0", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-08-14@retired-20260827' (id=a5097121\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "a5097121-1461-59f8-81fa-e6ecef527337", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-08-23@retired-20260827' (id=e25ba5fa\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "e25ba5fa-eaae-5039-8d93-a69c6d837fc4", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'net-kingdom-adhoc-2026-06-14@retired-20260827' (id=c6f3d6bf\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "c6f3d6bf-4916-490d-96ce-092d2776d7e7", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'net-kingdom-adhoc-2026-07-02@retired-20260827' (id=67c1c7ac\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "67c1c7ac-d8b1-41dc-a81a-4e43f1afd068", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0020' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md", + "db_id": "", + "file_value": "NET-WP-0020", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0015' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md", + "db_id": "", + "file_value": "NET-WP-0015", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0016' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md", + "db_id": "", + "file_value": "NET-WP-0016", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0018' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md", + "db_id": "", + "file_value": "NET-WP-0018", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0017' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md", + "db_id": "", + "file_value": "NET-WP-0017", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0019' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md", + "db_id": "", + "file_value": "NET-WP-0019", + "db_value": "NK-WP", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 64/64", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-33 fixed: regenerated WORK-RECORDS.md", + "C-22 fixed: task fce95440\u2026 description updated", + "C-11 fixed: task 'NK-WP-0048-T20' \u2192 863fcbdb\u2026", + "C-10 fixed: task 56c8d469\u2026 status \u2192 'wait'", + "C-22 fixed: task 56c8d469\u2026 description updated", + "C-41 fixed: task 56c8d469\u2026 wait qualifiers synced", + "C-11 fixed: task 'NK-WP-0049-T34' \u2192 821b3a56\u2026", + "C-11 fixed: task 'NK-WP-0049-T35' \u2192 1bfd1703\u2026", + "C-11 fixed: task 'NK-WP-0049-T36' \u2192 8b72334b\u2026", + "C-22 fixed: task d68cd4d3\u2026 description updated", + "brief: .custodian-brief.md updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 41, + "info": 5 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/.custodian-brief.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/.custodian-brief.md.worktree new file mode 100644 index 00000000..18bd5dff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/.custodian-brief.md.worktree @@ -0,0 +1,85 @@ + +# Custodian Brief — net-kingdom + +**Domain:** infotech +**Last synced:** 2026-10-16 19:19 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Publish the NetKingdom emission-cadence security profile +Progress: 5/6 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc` + +**Open tasks:** +- ► Bind and hand off the published contract `e3fbc8b8` + +### Register Playwright human and agent consumers through governed identity delivery +Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377` + +**Open tasks:** +- ! Verify issuer and real consumer acceptance `5a3aaf57` + *(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)* +- ► Deliver custody integration and guarded registration activation `a9dadd07` + +### Remove bootstrap secret output and verify separated recovery custody +Progress: 22/31 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804` + +**Open tasks:** +- ! Accept remaining recovery and caller evidence and close the G08 handoff `d68cd4d3` + *(wait: Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete.)* +- ! Resolve historical credential and configuration-preserving rotation dispositions `3ea6bf00` + *(wait: Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged.)* +- ! Verify current caller and provider admission at their actual boundaries `77aa42be` + *(wait: Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers.)* +- ! Prove retained privacyIDEA token decryption and existing-token MFA `6f8af1a7` + *(wait: LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs.)* +- ! Prove restored KeyCape issuer, policy and consumer continuity `141dd773` + *(wait: LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.)* +- ! Prove restored Authelia storage and OIDC continuity `b47f9975` + *(wait: LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15.)* +- ► Map current callers, owners and native acceptance gaps `479d6970` +- … and 2 more open tasks + +### Establish SecurityCaseTypes so that known security cases are light +Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb` + +**Open tasks:** +- ! T05 — Acknowledge the type through informed-decision `d9b26bcf` + *(wait: Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status.)* +- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae` + *(wait: The next types follow once the first one is proven end to end.)* + +### Define an execution-attribution receipt for Railiance runs +Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487` + +**Open tasks:** +- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1` + *(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)* + +### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker +Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07` + +**Open tasks:** +- ! Admit the database consumer `2fe6f100` + *(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)* +- ! Package the broker deployment `32d1411b` + *(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)* +- ! Integrate an upstream identity provider `6697a994` + *(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)* +- ! Define federated assurance `d845380d` + *(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)* +- ! Verify IAM conformance and coexistence `9cb7fd93` + *(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)* +- ! Enforce tenant and platform boundaries `38998c68` + *(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)* +- ! Prove recovery and audit delivery `e5f44ddc` + *(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)* +- … and 1 more open tasks + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/WORK-RECORDS.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/WORK-RECORDS.md.worktree new file mode 100644 index 00000000..e6f84275 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/WORK-RECORDS.md.worktree @@ -0,0 +1,272 @@ +# Work Records — net-kingdom + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | NK-WP-ADHOC-2026-07-02 | finished | — | workplans/ADHOC-2026-07-02.md | +| workplan | NK-WP-ADHOC-2026-08-14 | finished | — | workplans/ADHOC-2026-08-14.md | +| workplan | NK-WP-ADHOC-2026-08-23 | finished | — | workplans/ADHOC-2026-08-23.md | +| workplan | NK-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | NK-WP-ADHOC-2026-10-10 | finished | — | workplans/ADHOC-2026-10-10.md | +| workplan | NK-WP-ADHOC-2026-10-11 | finished | — | workplans/ADHOC-2026-10-11.md | +| workplan | NK-WP-ADHOC-2026-10-16 | finished | — | workplans/ADHOC-2026-10-16.md | +| workplan | NET-WP-0020 | finished | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| workplan | NK-WP-0009 | finished | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| workplan | NK-WP-0011 | active | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| workplan | NK-WP-0021 | finished | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| workplan | NK-WP-0022 | finished | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| workplan | NK-WP-0023 | finished | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| workplan | NK-WP-0024 | finished | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| workplan | NK-WP-0025 | finished | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| workplan | NK-WP-0026 | finished | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| workplan | NK-WP-0027 | backlog | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| workplan | NK-WP-0028 | finished | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| workplan | NK-WP-0029 | finished | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| workplan | NK-WP-0030 | finished | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| workplan | NK-WP-0031 | finished | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| workplan | NK-WP-0032 | finished | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| workplan | NK-WP-0033 | finished | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| workplan | NK-WP-0034 | finished | — | workplans/NK-WP-0034-verification-that-verifies.md | +| workplan | NK-WP-0035 | active | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| workplan | NK-WP-0036 | finished | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| workplan | NK-WP-0037 | finished | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| workplan | NK-WP-0038 | finished | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| workplan | NK-WP-0039 | finished | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| workplan | NK-WP-0040 | active | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| workplan | NK-WP-0041 | finished | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| workplan | NK-WP-0042 | finished | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| workplan | NK-WP-0043 | finished | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| workplan | NK-WP-0044 | finished | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| workplan | NK-WP-0045 | finished | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| workplan | NK-WP-0046 | finished | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| workplan | NK-WP-0047 | finished | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| workplan | NK-WP-0048 | active | — | workplans/NK-WP-0048-security-case-types.md | +| workplan | NK-WP-0049 | active | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| workplan | NK-WP-0050 | active | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| workplan | NK-WP-0051 | finished | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | +| task | NK-WP-ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md | +| task | NK-WP-ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-14-T02 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-14-T03 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md | +| task | NK-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | NK-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | +| task | NK-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T03 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T04 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T05 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T06 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T07 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-16-T01 | done | — | workplans/ADHOC-2026-10-16.md | +| task | NET-WP-0020-T01 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T02 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T03 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T04 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T05 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NK-WP-0009-T01 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T02 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T03 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T04 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T05 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T06 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0011-T01 | done | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T02 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T03 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T04 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T05 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T06 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T07 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T08 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T09 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0021-T01 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T02 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T03 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T04 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T05 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0022-T01 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T02 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T03 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T04 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T05 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T06 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T07 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T08 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0023-T01 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T02 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T03 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T04 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T05 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T06 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T07 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T08 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0024-T01 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T02 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T03 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T04 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0025-T01 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T02 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T03 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T04 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T05 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0026-T01 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| task | NK-WP-0026-T02 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| task | NK-WP-0027-T01 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T02 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T03 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T04 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T05 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T06 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0028-T01 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T02 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T03 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T04 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0029-T01 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T02 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T03 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T04 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0030-T01 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T02 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T03 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T04 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T05 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0031-T01 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T02 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T03 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T04 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T05 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0032-T01 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T02 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T03 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T04 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0033-T01 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T02 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T03 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T04 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T05 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0034-T01 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T02 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T03 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T04 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0035-T01 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T02 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T03 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T04 | progress | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T06 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0036-T01 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T02 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T04 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T05 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T06 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0037-T01 | done | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| task | NK-WP-0037-T02 | done | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| task | NK-WP-0038-T01 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| task | NK-WP-0038-T02 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| task | NK-WP-0039-T01 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T02 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T03 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T04 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0040-T01 | done | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| task | NK-WP-0040-T02 | wait | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| task | NK-WP-0041-T01 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0041-T02 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0041-T03 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0042-T01 | done | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| task | NK-WP-0042-T02 | cancel | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| task | NK-WP-0043-T01 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0043-T02 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0043-T03 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0044-T01 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T02 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T03 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T04 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T05 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T06 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T07 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0045-T01 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T02 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T03 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T04 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T05 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T06 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0046-T01 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0046-T02 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0046-T03 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0047-T01 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0047-T02 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0047-T03 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0048-T01 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T02 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T03 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T04 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T05 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T06 | wait | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T07 | wait | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T08 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T09 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T10 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T11 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T12 | todo | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T13 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T14 | todo | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T15 | todo | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T16 | todo | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T17 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T18 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T19 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0049-T01 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T02 | progress | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T03 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T04 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T05 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T06 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T07 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T08 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T09 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T10 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T11 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T12 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T13 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T14 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T15 | progress | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T16 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T17 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T18 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T19 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T20 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T21 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T22 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T23 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T24 | todo | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T25 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T26 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T27 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T28 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T29 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T30 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T31 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T32 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T33 | todo | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0050-T01 | done | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0050-T02 | progress | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0050-T03 | wait | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0051-T01 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T02 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T03 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T04 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T05 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T06 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NET-IN-0001 | open | — | intakes/intakes.md | +| intake | NET-IN-0002 | open | — | intakes/intakes.md | +| intake | NET-IN-0003 | open | — | intakes/intakes.md | +| intake | NET-IN-0004 | open | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/originals.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/originals.json new file mode 100644 index 00000000..1be7eb91 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-publication-backup/originals.json @@ -0,0 +1 @@ +{".custodian-brief.md": "100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0\t.custodian-brief.md\n", "WORK-RECORDS.md": "100644 76a6b7fc304c508f1b50010cb72d892826c6a831 0\tWORK-RECORDS.md\n"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stderr.txt new file mode 100644 index 00000000..3345563f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stderr.txt @@ -0,0 +1 @@ +WARN: git commit failed for writeback: Generated-only commits rejected: staged diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stdout.json new file mode 100644 index 00000000..a9bee10d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/net-kingdom-sync.stdout.json @@ -0,0 +1,443 @@ +{ + "repo_slug": "net-kingdom", + "repo_path": "/home/worsch/net-kingdom", + "source_revision": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-24", + "message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['NET-WP', 'NK-WP']", + "file_path": "workplans/", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-33", + "message": "WORK-RECORDS.md is missing or stale \u2014 regenerate via fix-consistency (CUST-WP-0061-T04 generated work-record index)", + "file_path": "WORK-RECORDS.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0011' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0011-enterprise-federation-saml.md", + "db_id": "1075448f-d533-5f9e-94b7-c3adfe151a07", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub", + "file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md", + "db_id": "", + "file_value": "USER-WP-0020", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md", + "db_id": "", + "file_value": "KEY-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub", + "file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md", + "db_id": "", + "file_value": "USER-WP-0020", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md", + "db_id": "", + "file_value": "KEY-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0021' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "USER-WP-0021", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'FLEX-WP-0009' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "FLEX-WP-0009", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'AUDIT-WP-0003' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "AUDIT-WP-0003", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'EMAIL-WP-0004' is not linked to State Hub", + "file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md", + "db_id": "", + "file_value": "EMAIL-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0022' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "USER-WP-0022", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'KEY-WP-0008' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "KEY-WP-0008", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'CSOC-WP-0003' is not linked to State Hub", + "file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md", + "db_id": "", + "file_value": "CSOC-WP-0003", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0027' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0027-reef-placement-reconciliation.md", + "db_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0031' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0031-deterministic-posture-feedback.md", + "db_id": "9d7b04f9-3803-5613-b7a5-8bd606c77f5a", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'nk-wp-0040' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/NK-WP-0040-execution-attribution-receipt.md", + "db_id": "e2533f3a-aa43-59b3-bff3-8e64b6149487", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0048-T06' starts after task NK-WP-0048-T20", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T06", + "db_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "file_value": "NK-WP-0048-T20", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0048-T14' starts after task NK-WP-0048-T20", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T14", + "db_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "file_value": "NK-WP-0048-T20", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-10", + "message": "Task status drift 'NK-WP-0048-T14': file='progress' db='todo' (file wins)", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T14", + "db_id": "fce95440-a25b-5f27-8ef3-a3d182099113", + "file_value": "progress", + "db_value": "todo", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'NK-WP-0048-T20': file content differs from DB (file wins)", + "file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T20", + "db_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8", + "file_value": "Completed 2026-10-17, source candidate preparation and bounded synthetic\ncompatibility proof only. This extracts evidenc", + "db_value": "Completed 2026-10-17, source candidate preparation and bounded synthetic\ncompatibility proof only. This extracts evidenc", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0049-T33' starts after task NK-WP-0049-T34", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0049-T33' starts after task NK-WP-0049-T35", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "file_value": "NK-WP-0049-T35", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0049-T33' starts after task NK-WP-0049-T36", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33", + "db_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "file_value": "NK-WP-0049-T36", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0049-T35' starts after task NK-WP-0049-T34", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T35", + "db_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-10", + "message": "Task status drift 'NK-WP-0049-T35': file='progress' db='todo' (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T35", + "db_id": "1bfd1703-589f-5511-bad5-812d860e4a71", + "file_value": "progress", + "db_value": "todo", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Missing DB dependency edge: task 'NK-WP-0049-T36' starts after task NK-WP-0049-T34", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T36", + "db_id": "17d315f0-829e-57c7-85cc-3a7914152804", + "file_value": "NK-WP-0049-T34", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-10", + "message": "Task status drift 'NK-WP-0049-T36': file='progress' db='todo' (file wins)", + "file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T36", + "db_id": "8b72334b-fc27-5707-a189-bad345eaf3bc", + "file_value": "progress", + "db_value": "todo", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAIL-PL-WP-0002' is not linked to State Hub", + "file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md", + "db_id": "", + "file_value": "RAIL-PL-WP-0002", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'nk-wp-0035@retired-20260908' (id=064c5e8b\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "064c5e8b-7852-4d7f-a5fa-e26e406ab8e0", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-08-14@retired-20260827' (id=a5097121\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "a5097121-1461-59f8-81fa-e6ecef527337", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'adhoc-2026-08-23@retired-20260827' (id=e25ba5fa\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "e25ba5fa-eaae-5039-8d93-a69c6d837fc4", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'net-kingdom-adhoc-2026-06-14@retired-20260827' (id=c6f3d6bf\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "c6f3d6bf-4916-490d-96ce-092d2776d7e7", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'net-kingdom-adhoc-2026-07-02@retired-20260827' (id=67c1c7ac\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "67c1c7ac-d8b1-41dc-a81a-4e43f1afd068", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0020' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md", + "db_id": "", + "file_value": "NET-WP-0020", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0015' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md", + "db_id": "", + "file_value": "NET-WP-0015", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0016' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md", + "db_id": "", + "file_value": "NET-WP-0016", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0018' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md", + "db_id": "", + "file_value": "NET-WP-0018", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0017' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md", + "db_id": "", + "file_value": "NET-WP-0017", + "db_value": "NK-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'NET-WP-0019' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md", + "db_id": "", + "file_value": "NET-WP-0019", + "db_value": "NK-WP", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 64/64", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-33 fixed: regenerated WORK-RECORDS.md", + "C-20 fixed: dependency e31f3f9d\u2026 starts_after \u2192 863fcbdb\u2026", + "C-20 fixed: dependency e31f3f9d\u2026 starts_after \u2192 863fcbdb\u2026", + "C-10 fixed: task fce95440\u2026 status \u2192 'progress'", + "C-22 fixed: task 863fcbdb\u2026 description updated", + "C-20 fixed: dependency 17d315f0\u2026 starts_after \u2192 821b3a56\u2026", + "C-20 fixed: dependency 17d315f0\u2026 starts_after \u2192 1bfd1703\u2026", + "C-20 fixed: dependency 17d315f0\u2026 starts_after \u2192 8b72334b\u2026", + "C-20 fixed: dependency 17d315f0\u2026 starts_after \u2192 821b3a56\u2026", + "C-10 fixed: task 1bfd1703\u2026 status \u2192 'progress'", + "C-20 fixed: dependency 17d315f0\u2026 starts_after \u2192 821b3a56\u2026", + "C-10 fixed: task 8b72334b\u2026 status \u2192 'progress'", + "brief: .custodian-brief.md updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 36, + "info": 5 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/progress-receipt.json new file mode 100644 index 00000000..87c2a350 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52", + "id": "c7ca9e24-1e21-4423-824b-dfd13065ec72", + "topic_id": null, + "task_id": "6aca534c-5c7a-556f-b6b6-406433670c29", + "decision_id": null, + "event_type": "note", + "summary": "Published NetKingdom source4f65e41/followthrough6cb43c0 and Repo Manager source83c521b/followthroughb02e1ce after independent65/111/42 canonical checks,14 actual disposable LLDAP oracles,13 scanner parity cases and4 actual configured-hook generated cases. Verified22 source files unchanged, exact remote ancestry and all61 owner task IDs/UUIDs/statuses. Corrected SQLite-on-PVC production advice against ADR0015 before publication; SQLite stays local/disposable. Independently checked40 public source references. NK-WP0048-T20 and NK-WP0049-T34 retain completed bounded source preparation; T14/T35/T36 progress native storage/TLS/registration work, T33 waits readiness, T18/T19 retain real recovery/MFA. RMGR-WP0021-T05 remains progress with actual-hook unavailability pending. Legacy consistency omitted task record IDs; normal forge-derived reconciliation repaired cache. All16 target gates remain open; no live deployment, actual credential recovery, owner assent or rotation. Evidence: docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T22:46:45.862033Z", + "workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publish_owner.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publish_owner.py new file mode 100644 index 00000000..f9244fc4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publish_owner.py @@ -0,0 +1,79 @@ +"""Publish explicit reviewed source paths with normal hooks and preserve other work.""" +from pathlib import Path +import argparse, hashlib, json, os, subprocess, sys + +BASE = Path(__file__).resolve().parent +PACKETS = BASE.parent +sys.path.insert(0, '/home/worsch/repo-manager/src') +from repo_manager.identifiers import ensure_missing_work_record_identifiers + +def main(): + p = argparse.ArgumentParser() + p.add_argument('owner', choices=['net-kingdom', 'repo-manager']) + p.add_argument('action', choices=['commit', 'sync']) + args = p.parse_args() + root = Path('/home/worsch') / args.owner + def git(*argv): return subprocess.check_output(['git', '-C', str(root), *argv], stderr=subprocess.PIPE) + def sha(path): return 'sha256:' + hashlib.sha256(path.read_bytes()).hexdigest() + if args.owner == 'net-kingdom': + paths = list(json.loads((BASE / 'groups-integration.json').read_text())['source_postimages']) + paths += list(json.loads((BASE / 'recovery-integration.json').read_text())['source_postimages']) + paths += ['workplans/NK-WP-0048-security-case-types.md', 'workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md'] + message = 'fix(identity): retain group provenance and prepare sealed recovery custody' + else: + paths = ['src/repo_manager/secret_scan.py', 'tests/test_secret_scan.py', 'workplans/RMGR-WP-0021-secret-detection-controls.md'] + message = 'perf(security): batch small staged blobs with bounded volatile storage' + backup = BASE / (args.owner + '-publication-backup') + if args.action == 'commit': + if args.owner == 'net-kingdom': + expected = json.loads((BASE / 'groups-integration.json').read_text())['source_postimages'] + expected.update(json.loads((BASE / 'recovery-integration.json').read_text())['source_postimages']) + expected.update(json.loads((BASE / 'storage-guidance-correction.json').read_text())['files']) + else: + expected = {e['path']: 'sha256:' + e['candidate_sha256'] for e in json.loads((PACKETS / 'scanner-batch-r1/manifest.json').read_text())['files']} + for path, digest in expected.items(): assert sha(root / path) == digest, 'source drift: ' + path + assert not backup.exists(), 'preserve earlier publication backup' + backup.mkdir() + originals = {} + for name in ['.custodian-brief.md', 'WORK-RECORDS.md']: + file = root / name + if file.exists(): + (backup / (name + '.worktree')).write_bytes(file.read_bytes()) + originals[name] = git('ls-files', '--stage', '--', name).decode() + (backup / 'originals.json').write_text(json.dumps(originals)) + planned_assignments = ensure_missing_work_record_identifiers(root, execute=False) + assert set(planned_assignments['files_changed']) <= set(paths), 'unexpected identifier writeback; stop before commit' + assignments = ensure_missing_work_record_identifiers(root) + manifest = {path: sha(root / path) for path in paths} + (BASE / (args.owner + '-commit-inputs.json')).write_text(json.dumps({'files': manifest, 'assignments': assignments}, indent=2) + '\n') + git('add', '--', *paths) + result = subprocess.run(['git', '-C', str(root), 'commit', '--only', '-m', message, '--', *paths], capture_output=True, text=True, timeout=180) + receipt = {'exit_code': result.returncode, 'stdout': result.stdout, 'stderr': result.stderr, + 'head': git('rev-parse', 'HEAD').decode().strip(), 'files': manifest} + (BASE / (args.owner + '-commit.json')).write_text(json.dumps(receipt, indent=2) + '\n') + for name, index in originals.items(): + assert git('ls-files', '--stage', '--', name).decode() == index + assert (root / name).read_bytes() == (backup / (name + '.worktree')).read_bytes() + print(json.dumps({'owner': args.owner, 'commit_exit': result.returncode, 'head': receipt['head']})) + if result.returncode: raise SystemExit('Normal source commit refused; exact receipt retained.') + else: + originals = json.loads((backup / 'originals.json').read_text()) + command = (['/home/worsch/repo-manager/.venv/bin/rmgr', 'sync', '--path', str(root), '--push'] + if args.owner == 'repo-manager' else ['statehub', 'fix-consistency', '--repo', args.owner, '--json']) + try: + result = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=300) + (BASE / (args.owner + '-sync.stdout.json')).write_text(result.stdout) + (BASE / (args.owner + '-sync.stderr.txt')).write_text(result.stderr) + (BASE / (args.owner + '-sync.exit.json')).write_text(json.dumps({'exit_code': result.returncode}) + '\n') + finally: + for name, index in originals.items(): + (root / name).write_bytes((backup / (name + '.worktree')).read_bytes()) + if index: + mode, oid, stage, filename = index.split() + assert stage == '0' + git('update-index', '--cacheinfo', mode, oid, filename) + assert git('ls-files', '--stage', '--', name).decode() == index + print(json.dumps({'owner': args.owner, 'sync_exit': result.returncode, 'original_other_work_restored': True})) + if result.returncode: raise SystemExit(result.returncode) + +if __name__ == '__main__': main() diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/reconcile_owner_projection.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/reconcile_owner_projection.py new file mode 100644 index 00000000..9cb4d8a2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/reconcile_owner_projection.py @@ -0,0 +1,15 @@ +"""Use existing forge-derived reconciliation to repair missing task record IDs.""" +from pathlib import Path +import json, subprocess +base=Path(__file__).resolve().parent +root=Path('/home/worsch/net-kingdom') +result=subprocess.run(['/home/worsch/repo-manager/.venv/bin/rmgr','sync','--path',str(root),'--push'],cwd=root,capture_output=True,text=True,timeout=180) +(base/'net-kingdom-central-projection.stdout.json').write_text(result.stdout) +(base/'net-kingdom-central-projection.stderr.txt').write_text(result.stderr) +(base/'net-kingdom-central-projection.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n') +backups=base/'net-kingdom-publication-backup' +for name,index in json.loads((backups/'originals.json').read_text()).items(): + assert (root/name).read_bytes()==(backups/(name+'.worktree')).read_bytes() + assert subprocess.check_output(['git','-C',str(root),'ls-files','--stage','--',name]).decode()==index +print(json.dumps({'projection_exit':result.returncode,'preexisting_other_work_preserved':True})) +raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/record_followthrough.py b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/record_followthrough.py new file mode 100644 index 00000000..d54a4a21 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/record_followthrough.py @@ -0,0 +1,83 @@ +"""Record verified source delivery and ongoing native preparation, without closing gates.""" +from pathlib import Path +import argparse, hashlib, json, subprocess +base = Path(__file__).resolve().parent +p = argparse.ArgumentParser(); p.add_argument('owner', choices=['net-kingdom','repo-manager']); args = p.parse_args() +root = Path('/home/worsch') / args.owner +initial = json.loads((base / (args.owner + '-commit.json')).read_text()) +def git(*argv): return subprocess.check_output(['git','-C',str(root),*argv], stderr=subprocess.PIPE) +assert git('rev-parse','HEAD').decode().strip() == initial['head'] +paths = [path for path in initial['files'] if path.startswith('workplans/')] +for path in paths: + target = root / path + assert 'sha256:' + hashlib.sha256(target.read_bytes()).hexdigest() == initial['files'][path] + assert not git('status','--porcelain','--',path).strip() + text = target.read_text() + if path.endswith('NK-WP-0048-security-case-types.md'): + text = text.replace('id: NK-WP-0048-T14\nstatus: todo', 'id: NK-WP-0048-T14\nstatus: progress') + text += ''' +2026-10-17 verified source follow-through: source commit `4f65e41` passed normal +local scanning and all 65 canonical provisioner tests. The actual disposable +LLDAP receipt passed all 14 oracles; independent verification on the pinned +daemon found both exact names and the run label absent. Source integration +includes the ADR-0015 documentation correction; the journal remains local-only. + +T14 source preparation is underway. Twenty-three pinned public sources identify +NetKingdom implementation/reconciliation, Platform custody/database governance +and rapp-postgres consumer provisioning; the inspected runtime binding does not +assign identity-provisioner a package. The existing User Engine runtime token +and pod route are not a protected-case caller lane. The native proposal preserves +the same journal seam on a dedicated CNPG consumer and requires authoritative +scope binding, admitted role/lease/placement/recovery and all LDAP writers fenced. +The declared exclusive maintenance window is not mechanical exclusion of WebUI, +helper or lifecycle writers. Capacity placement and EUR cost/risk remain owner +decisions. No package, database, credential, rollout or owner assent was created. +Custodian evidence: group-provenance-r1/native-storage-proposal-r1; final source, +projection and cleanup review: resume-integration-r1/publication-verification.json. +''' + elif path.endswith('NK-WP-0049-bootstrap-secret-free-recovery-handoff.md'): + for task in ['T35','T36']: + text = text.replace('id: NK-WP-0049-' + task + '\nstatus: todo', 'id: NK-WP-0049-' + task + '\nstatus: progress') + text = text.replace('24 tasks done; 2 in\nprogress; 3 ready to start (`todo`); 7 waiting', '24 tasks done; 4 in\nprogress; 1 ready to start (`todo`); 7 waiting') + text += ''' +2026-10-17 verified source follow-through: source commit `4f65e41` passed normal +local scanning and 111 canonical focused checks. The initial T34 source-only +preparation now has an exact integrated source revision; existing real-SOPS +compatibility remains partly skipped and actual owner-held execution remains T18. + +T35/T36 source preparation is underway, with 17 pinned public-source references. +railiance-cluster owns the ingress/cert-manager/cluster-backup substrate; the +inspected same-host encrypted backup source establishes no independent original +kc-tls/trust recovery or accepted reissue. Draft owner questions remain unsent; +no private TLS input was requested. The historically deployed +secrets-engine-openbao registration is a defensible proposed consumer: its +7 October issuer and 10 October consumer proofs predate the reviewed archive. +The exact selector proposal is retained without current assent or private archive +validation. No token exchange, OpenBao login, client secret retrieval, new grant +or production redirect occurred. Keep incomplete admission refused before custody. +Custodian evidence: recovery-attempt-r1/t35-t36-source-next; final source/projection +review: resume-integration-r1/publication-verification.json. +''' + else: + text += ''' +2026-10-17 verified source follow-through: source commit `83c521b` passed normal +local scanning and all 42 canonical scanner tests. The exact final tmpfs candidate +also passed 13 old/new parity cases. The actual configured shared and Custodian +hooks each accepted a clean generated fixture and blocked a generated private key, +using alternate indexes/object stores in private tmpfs; the real indexes were not +opened and their stat identities and HEADs stayed unchanged. All new fixture and +scan storage was removed. Scanner/hook/source bytes and routing stayed exact. +No chmod or unavailable-scanner injection touched the real scanner. Source tests +cover unavailable/wrong-digest refusal, but actual configured-hook unavailability +is still pending, so T05 stays progress. The explicit repo-local CLI published +through rmgr sync --push and central primary/railiance01 accepted source projection; +no global installed CLI is claimed. T03/T04 and whole target gates remain open. +Final coordinator evidence: resume-integration-r1/publication-verification.json. +''' + target.write_text(text) +result = subprocess.run(['git','-C',str(root),'commit','--only','-m','docs(security): record verified source delivery and native preparation','--',*paths],capture_output=True,text=True,timeout=180) +receipt = {'owner': args.owner,'exit_code':result.returncode,'stdout':result.stdout,'stderr':result.stderr, + 'head':git('rev-parse','HEAD').decode().strip(),'paths':paths} +(base / (args.owner + '-followthrough-commit.json')).write_text(json.dumps(receipt,indent=2)+'\n') +print(json.dumps({'owner':args.owner,'commit_exit':result.returncode,'head':receipt['head']})) +raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-integration.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-integration.json new file mode 100644 index 00000000..abab82b5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-integration.json @@ -0,0 +1,25 @@ +{ + "observed_at": "2026-10-16T22:10:25.790564+00:00", + "lane": "recovery", + "owner": "/home/worsch/net-kingdom", + "base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "patch_sha256": "sha256:99cc938d80d64049f927049a5e09a698641e121cf0af2bfa1cbb54ebe7528266", + "applied": true, + "source_postimages": { + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a", + "tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f", + "tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b", + "tests/isolated-keycape-native-fixture-manifest.json": "sha256:1dcefbc7ca7b0c2be33135b1b7977b89ba657223ecbb1310cc691d151b3d916e" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-preflight.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-preflight.json new file mode 100644 index 00000000..aff36576 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/recovery-preflight.json @@ -0,0 +1,25 @@ +{ + "observed_at": "2026-10-16T22:09:04.964272+00:00", + "lane": "recovery", + "owner": "/home/worsch/net-kingdom", + "base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1", + "patch_sha256": "sha256:99cc938d80d64049f927049a5e09a698641e121cf0af2bfa1cbb54ebe7528266", + "applied": false, + "source_postimages": { + "sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780", + "sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e", + "sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f", + "tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a", + "tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f", + "tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b", + "tests/isolated-keycape-native-fixture-manifest.json": "sha256:1dcefbc7ca7b0c2be33135b1b7977b89ba657223ecbb1310cc691d151b3d916e" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit-inputs.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit-inputs.json new file mode 100644 index 00000000..66308db4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit-inputs.json @@ -0,0 +1,26 @@ +{ + "files": { + "src/repo_manager/secret_scan.py": "sha256:e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "tests/test_secret_scan.py": "sha256:827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876", + "workplans/RMGR-WP-0021-secret-detection-controls.md": "sha256:a8f365375189bcf478804eaa7a354323c20ed447b8d50ffb7e974bf4bff7c08a" + }, + "assignments": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "repo-manager", + "namespace": "helixforge", + "assignments": [ + { + "kind": "task", + "record_id": "RMGR-WP-0021-T05", + "uuid": "25fbe694-41d8-5232-bce0-4ed362387d5a", + "path": "workplans/RMGR-WP-0021-secret-detection-controls.md" + } + ], + "historical_invalid_identifiers": [], + "files_changed": [ + "workplans/RMGR-WP-0021-secret-detection-controls.md" + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit.json new file mode 100644 index 00000000..364ea324 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-commit.json @@ -0,0 +1,11 @@ +{ + "exit_code": 0, + "stdout": "[main 83c521b] perf(security): batch small staged blobs with bounded volatile storage\n 3 files changed, 615 insertions(+), 30 deletions(-)\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 3, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n", + "head": "83c521bf2fa351d730a90bf7f7e13dfd4b0cff2a", + "files": { + "src/repo_manager/secret_scan.py": "sha256:e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "tests/test_secret_scan.py": "sha256:827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876", + "workplans/RMGR-WP-0021-secret-detection-controls.md": "sha256:a8f365375189bcf478804eaa7a354323c20ed447b8d50ffb7e974bf4bff7c08a" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-followthrough-commit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-followthrough-commit.json new file mode 100644 index 00000000..e2723c56 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-followthrough-commit.json @@ -0,0 +1,10 @@ +{ + "owner": "repo-manager", + "exit_code": 0, + "stdout": "[main b02e1ce] docs(security): record verified source delivery and native preparation\n 1 file changed, 14 insertions(+)\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 1, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n", + "head": "b02e1cefb0e07fe246dc9fe9c5a989fe06116587", + "paths": [ + "workplans/RMGR-WP-0021-secret-detection-controls.md" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stdout.json new file mode 100644 index 00000000..acf28745 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-initial-sync.stdout.json @@ -0,0 +1,91 @@ +{ + "ok": true, + "status": "applied", + "repo_slug": "repo-manager", + "commit": "83c521bf2fa351d730a90bf7f7e13dfd4b0cff2a", + "requests": 2, + "elapsed_ms": 11265.1, + "git": { + "upstream": "origin/main", + "behind": 0, + "ahead": 0 + }, + "identifiers": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "repo-manager", + "namespace": "helixforge", + "assignments": [], + "historical_invalid_identifiers": [], + "files_changed": [] + }, + "receipt": { + "schema": "state-hub.repository-projection-reconcile.v1", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "83c521bf2fa351d730a90bf7f7e13dfd4b0cff2a", + "derived_commit": "83c521bf2fa351d730a90bf7f7e13dfd4b0cff2a", + "outcome": { + "schema": "state-hub.projection-reset.v1", + "repo_slug": "repo-manager", + "commit": "83c521bf2fa351d730a90bf7f7e13dfd4b0cff2a", + "status": "applied", + "counts": { + "created": 0, + "updated": 25, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 1, + "updated_tasks": 0, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 0, + "external_edges": 0 + }, + "created": [], + "updated": [ + "RMGR-WP-0001", + "RMGR-WP-0002", + "RMGR-WP-0003", + "RMGR-WP-0004", + "RMGR-WP-0005", + "RMGR-WP-0006", + "RMGR-WP-0007", + "RMGR-WP-0008", + "RMGR-WP-0009", + "RMGR-WP-0010", + "RMGR-WP-0011", + "RMGR-WP-0012", + "RMGR-WP-0013", + "RMGR-WP-0014", + "RMGR-WP-0015", + "RMGR-WP-0016", + "RMGR-WP-0017", + "RMGR-WP-0018", + "RMGR-WP-0019", + "RMGR-WP-0020", + "RMGR-WP-0021", + "RMGR-WP-ADHOC-2026-09-06", + "RMGR-WP-ADHOC-2026-09-09", + "RMGR-WP-ADHOC-2026-09-11", + "RMGR-WP-ADHOC-2026-10-03" + ], + "retired": [], + "released": [], + "created_tasks": [ + "RMGR-WP-0021-T05" + ], + "updated_tasks": [], + "cancelled_tasks": [], + "created_edges": [], + "removed_edges": [], + "unresolved_edges": [], + "external_edges": [], + "refused": [], + "notes": [] + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/.custodian-brief.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/.custodian-brief.md.worktree new file mode 100644 index 00000000..9a4d7f70 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/.custodian-brief.md.worktree @@ -0,0 +1,30 @@ + +# Custodian Brief — repo-manager + +**Domain:** infotech +**Last synced:** 2026-10-06 09:17 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Bounded Forge-sourced freshness runner via Activity Core +Progress: 3/5 done | workplan_id: `cda2296b-9fe0-5e9e-b630-7e143a3c2796` + +**Open tasks:** +- ! Activation and rollback plan (founder review) `8941af6b` + *(wait: Runner deployed in the Activity Core worker (Argo a86a143); apply trials need the read-only Forgejo token (draft CCR-2026-0034))* +- ! Proof and handback to State Hub `f01fb241` + *(wait: Proof follows the T04 trials and the memo 4 schedule)* + +## Inbox Hygiene + +**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start. +**Missing thread_id:** 2 unread message(s) lack supersession chains. + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/WORK-RECORDS.md.worktree b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/WORK-RECORDS.md.worktree new file mode 100644 index 00000000..e8cd41d7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/WORK-RECORDS.md.worktree @@ -0,0 +1,150 @@ +# Work Records — repo-manager + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | RMGR-WP-ADHOC-2026-09-06 | finished | — | workplans/ADHOC-2026-09-06.md | +| workplan | RMGR-WP-ADHOC-2026-09-09 | finished | — | workplans/ADHOC-2026-09-09.md | +| workplan | RMGR-WP-ADHOC-2026-09-11 | finished | — | workplans/ADHOC-2026-09-11.md | +| workplan | RMGR-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | RMGR-WP-0001 | finished | — | workplans/RMGR-WP-0001-foundation.md | +| workplan | RMGR-WP-0002 | finished | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| workplan | RMGR-WP-0003 | finished | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | +| workplan | RMGR-WP-0004 | finished | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| workplan | RMGR-WP-0005 | finished | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| workplan | RMGR-WP-0006 | finished | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| workplan | RMGR-WP-0007 | finished | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| workplan | RMGR-WP-0008 | finished | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| workplan | RMGR-WP-0009 | finished | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| workplan | RMGR-WP-0010 | finished | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| workplan | RMGR-WP-0011 | finished | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | +| workplan | RMGR-WP-0012 | finished | — | workplans/RMGR-WP-0012-fast-work-record-mutation-and-projection.md | +| workplan | RMGR-WP-0013 | finished | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| workplan | RMGR-WP-0014 | finished | — | workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md | +| workplan | RMGR-WP-0015 | finished | — | workplans/RMGR-WP-0015-historical-sync-validation.md | +| workplan | RMGR-WP-0016 | finished | — | workplans/RMGR-WP-0016-interface-canon-consumer-review.md | +| workplan | RMGR-WP-0017 | finished | — | workplans/RMGR-WP-0017-projection-retry-convergence.md | +| workplan | RMGR-WP-0018 | finished | — | workplans/RMGR-WP-0018-publisher-registry-image-refresh.md | +| workplan | RMGR-WP-0019 | finished | — | workplans/RMGR-WP-0019-historical-source-admission.md | +| workplan | RMGR-WP-0020 | blocked | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| task | RMGR-WP-ADHOC-2026-09-06-T01 | done | — | workplans/ADHOC-2026-09-06.md | +| task | RMGR-WP-ADHOC-2026-09-09-T01 | done | — | workplans/ADHOC-2026-09-09.md | +| task | RMGR-WP-ADHOC-2026-09-11-T01 | done | — | workplans/ADHOC-2026-09-11.md | +| task | RMGR-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md | +| task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md | +| task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md | +| task | RMGR-WP-0001-T04 | done | — | workplans/RMGR-WP-0001-foundation.md | +| task | RMGR-WP-0001-T05 | done | — | workplans/RMGR-WP-0001-foundation.md | +| task | RMGR-WP-0002-T01 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0002-T02 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0002-T03 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0002-T04 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0002-T05 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0002-T06 | done | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | +| task | RMGR-WP-0003-T01 | done | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | +| task | RMGR-WP-0003-T02 | done | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | +| task | RMGR-WP-0003-T03 | done | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | +| task | RMGR-WP-0003-T04 | done | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | +| task | RMGR-WP-0004-T01 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T02 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T03 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T04 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T05 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T06 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T07 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T08 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T09 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0005-T01 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T02 | cancel | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T03 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T04 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T05 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T06 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T07 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T08 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T09 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T10 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T11 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T12 | cancel | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0006-T01 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T02 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T03 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T04 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T05 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0007-T01 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T02 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T03 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T04 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T05 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T06 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0007-T07 | done | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | +| task | RMGR-WP-0008-T01 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T02 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T03 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T04 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T05 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T06 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0008-T07 | done | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | +| task | RMGR-WP-0009-T01 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T02 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T03 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T04 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T05 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T06 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0009-T07 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0010-T01 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T02 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T03 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T04 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T05 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T06 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T07 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0011-T01 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | +| task | RMGR-WP-0011-T02 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | +| task | RMGR-WP-0011-T03 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | +| task | RMGR-WP-0011-T04 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | +| task | RMGR-WP-0012-T01 | done | — | workplans/RMGR-WP-0012-fast-work-record-mutation-and-projection.md | +| task | RMGR-WP-0012-T02 | done | — | workplans/RMGR-WP-0012-fast-work-record-mutation-and-projection.md | +| task | RMGR-WP-0012-T03 | done | — | workplans/RMGR-WP-0012-fast-work-record-mutation-and-projection.md | +| task | RMGR-WP-0012-T04 | done | — | workplans/RMGR-WP-0012-fast-work-record-mutation-and-projection.md | +| task | RMGR-WP-0013-T01 | done | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| task | RMGR-WP-0013-T02 | done | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| task | RMGR-WP-0013-T03 | done | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| task | RMGR-WP-0013-T04 | done | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| task | RMGR-WP-0013-T05 | done | — | workplans/RMGR-WP-0013-repository-classification-publisher.md | +| task | RMGR-WP-0014-T01 | done | — | workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md | +| task | RMGR-WP-0014-T02 | done | — | workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md | +| task | RMGR-WP-0014-T03 | done | — | workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md | +| task | RMGR-WP-0015-T01 | done | — | workplans/RMGR-WP-0015-historical-sync-validation.md | +| task | RMGR-WP-0015-T02 | done | — | workplans/RMGR-WP-0015-historical-sync-validation.md | +| task | RMGR-WP-0016-T01 | done | — | workplans/RMGR-WP-0016-interface-canon-consumer-review.md | +| task | RMGR-WP-0016-T02 | done | — | workplans/RMGR-WP-0016-interface-canon-consumer-review.md | +| task | RMGR-WP-0017-T01 | done | — | workplans/RMGR-WP-0017-projection-retry-convergence.md | +| task | RMGR-WP-0017-T02 | done | — | workplans/RMGR-WP-0017-projection-retry-convergence.md | +| task | RMGR-WP-0017-T03 | done | — | workplans/RMGR-WP-0017-projection-retry-convergence.md | +| task | RMGR-WP-0018-T01 | done | — | workplans/RMGR-WP-0018-publisher-registry-image-refresh.md | +| task | RMGR-WP-0018-T02 | done | — | workplans/RMGR-WP-0018-publisher-registry-image-refresh.md | +| task | RMGR-WP-0018-T03 | done | — | workplans/RMGR-WP-0018-publisher-registry-image-refresh.md | +| task | RMGR-WP-0019-T01 | done | — | workplans/RMGR-WP-0019-historical-source-admission.md | +| task | RMGR-WP-0019-T02 | done | — | workplans/RMGR-WP-0019-historical-source-admission.md | +| task | RMGR-WP-0019-T03 | done | — | workplans/RMGR-WP-0019-historical-source-admission.md | +| task | RMGR-WP-0020-T01 | done | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| task | RMGR-WP-0020-T02 | done | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| task | RMGR-WP-0020-T03 | done | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| task | RMGR-WP-0020-T04 | wait | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| task | RMGR-WP-0020-T05 | wait | — | workplans/RMGR-WP-0020-bounded-freshness-runner.md | +| intake | RMGR-IN-0001 | closed | — | intakes/intakes.md | +| intake | RMGR-IN-0002 | closed | — | intakes/intakes.md | +| intake | RMGR-IN-0004 | closed | — | intakes/intakes.md | +| decision | RMGR-DEC-2026-001 | resolved | — | decisions/RMGR-DEC-2026-001-whynot-identifier-batch.md | +| decision | RMGR-DEC-2026-002 | resolved | — | decisions/RMGR-DEC-2026-002-markitect-identifier-batch.md | +| decision | RMGR-DEC-2026-003 | resolved | — | decisions/RMGR-DEC-2026-003-railiance-cluster-identifier-batch.md | +| decision | RMGR-DEC-2026-005 | resolved | — | decisions/RMGR-DEC-2026-005-reef-railiance-identifier-batch.md | +| decision | RMGR-DEC-2026-006 | resolved | — | decisions/RMGR-DEC-2026-006-fleet-completion-identifier-batch.md | +| decision | RMGR-DEC-2026-004 | resolved | — | decisions/decisions.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/originals.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/originals.json new file mode 100644 index 00000000..1de0a874 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-publication-backup/originals.json @@ -0,0 +1 @@ +{".custodian-brief.md": "100644 9a4d7f705afdcad6277a476bbcad545578187229 0\t.custodian-brief.md\n", "WORK-RECORDS.md": "100644 e8cd41d7623f5df9257d6611bb5eee0d1622a249 0\tWORK-RECORDS.md\n"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.exit.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.exit.json new file mode 100644 index 00000000..43623d14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.exit.json @@ -0,0 +1 @@ +{"exit_code": 0} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stdout.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stdout.json new file mode 100644 index 00000000..de2a1667 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/repo-manager-sync.stdout.json @@ -0,0 +1,91 @@ +{ + "ok": true, + "status": "applied", + "repo_slug": "repo-manager", + "commit": "b02e1cefb0e07fe246dc9fe9c5a989fe06116587", + "requests": 2, + "elapsed_ms": 11453.7, + "git": { + "upstream": "origin/main", + "behind": 0, + "ahead": 0 + }, + "identifiers": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "repo-manager", + "namespace": "helixforge", + "assignments": [], + "historical_invalid_identifiers": [], + "files_changed": [] + }, + "receipt": { + "schema": "state-hub.repository-projection-reconcile.v1", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "b02e1cefb0e07fe246dc9fe9c5a989fe06116587", + "derived_commit": "b02e1cefb0e07fe246dc9fe9c5a989fe06116587", + "outcome": { + "schema": "state-hub.projection-reset.v1", + "repo_slug": "repo-manager", + "commit": "b02e1cefb0e07fe246dc9fe9c5a989fe06116587", + "status": "applied", + "counts": { + "created": 0, + "updated": 25, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 0, + "updated_tasks": 1, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 0, + "external_edges": 0 + }, + "created": [], + "updated": [ + "RMGR-WP-0001", + "RMGR-WP-0002", + "RMGR-WP-0003", + "RMGR-WP-0004", + "RMGR-WP-0005", + "RMGR-WP-0006", + "RMGR-WP-0007", + "RMGR-WP-0008", + "RMGR-WP-0009", + "RMGR-WP-0010", + "RMGR-WP-0011", + "RMGR-WP-0012", + "RMGR-WP-0013", + "RMGR-WP-0014", + "RMGR-WP-0015", + "RMGR-WP-0016", + "RMGR-WP-0017", + "RMGR-WP-0018", + "RMGR-WP-0019", + "RMGR-WP-0020", + "RMGR-WP-0021", + "RMGR-WP-ADHOC-2026-09-06", + "RMGR-WP-ADHOC-2026-09-09", + "RMGR-WP-ADHOC-2026-09-11", + "RMGR-WP-ADHOC-2026-10-03" + ], + "retired": [], + "released": [], + "created_tasks": [], + "updated_tasks": [ + "RMGR-WP-0021-T05" + ], + "cancelled_tasks": [], + "created_edges": [], + "removed_edges": [], + "unresolved_edges": [], + "external_edges": [], + "refused": [], + "notes": [] + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-integration.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-integration.json new file mode 100644 index 00000000..bd5ec811 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-integration.json @@ -0,0 +1,16 @@ +{ + "observed_at": "2026-10-16T22:10:25.877369+00:00", + "lane": "scanner", + "owner": "/home/worsch/repo-manager", + "base_revision": "97ea2489257de08911cc5da1e1b84d8e15780dc3", + "patch_sha256": "sha256:d2f65f2af5444b241ddcb85a3e71e66d1bbe071bd9a8a7be415be84b5e2f8fdb", + "applied": true, + "source_postimages": { + "src/repo_manager/secret_scan.py": "sha256:e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "tests/test_secret_scan.py": "sha256:827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-preflight.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-preflight.json new file mode 100644 index 00000000..d5a988e5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/scanner-preflight.json @@ -0,0 +1,16 @@ +{ + "observed_at": "2026-10-16T22:09:05.046911+00:00", + "lane": "scanner", + "owner": "/home/worsch/repo-manager", + "base_revision": "97ea2489257de08911cc5da1e1b84d8e15780dc3", + "patch_sha256": "sha256:d2f65f2af5444b241ddcb85a3e71e66d1bbe071bd9a8a7be415be84b5e2f8fdb", + "applied": false, + "source_postimages": { + "src/repo_manager/secret_scan.py": "sha256:e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "tests/test_secret_scan.py": "sha256:827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876" + }, + "preexisting_brief_preserved": true, + "staged": false, + "published": false, + "native_acceptance": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/storage-guidance-correction.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/storage-guidance-correction.json new file mode 100644 index 00000000..5e40a427 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/storage-guidance-correction.json @@ -0,0 +1,11 @@ +{ + "patch_sha256": "sha256:c3e5944a1dba097ddd16d9bfa0021a81002e30242df43129b24746bcdf9f6c2b", + "applied": true, + "source_code_changed": false, + "production_storage_admitted": false, + "files": { + "docs/service-group-provenance.md": "sha256:cd6b45640983b11e29a6bc6278bcb45de8fd8a5ddcad53eb77ab1a1f3f1f1280", + "sso-mfa/k8s/README.md": "sha256:2d437634b2d4e90a98a1c6eb508fea4d1ad79ab8dc75a6870b25a8e3755c816a", + "workplans/NK-WP-0048-security-case-types.md": "sha256:eaf9be204e7b6f481d498d918d3b24b3f50b0591027f92959e40513b1e79347a" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/workplan-integration.json b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/workplan-integration.json new file mode 100644 index 00000000..90350c54 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/workplan-integration.json @@ -0,0 +1,20 @@ +[ + { + "owner": "net-kingdom", + "path": "workplans/NK-WP-0048-security-case-types.md", + "reviewed_patch_sha256": "sha256:8251911aa8d6ebf9be916ece22ae4cd8ddb179110c3f9348aaee67db4c3acf74", + "integrated_sha256": "sha256:d8158d64006c404fcd017f5947f37424150a17ccc5089b7f636cd09655bd0075" + }, + { + "owner": "net-kingdom", + "path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md", + "reviewed_patch_sha256": "sha256:aa11ac3b3cbb36febc436736e7eaa611f8769fbae70b0f026fa9534954aa604c", + "integrated_sha256": "sha256:c1798c52aeff150f672fbe2ad097aaadc2bb98107741b2784fb09993d509703c" + }, + { + "owner": "repo-manager", + "path": "workplans/RMGR-WP-0021-secret-detection-controls.md", + "reviewed_patch_sha256": "sha256:e0d08a2e571664a6ec8c8c22b67c3788d248878c1644f9b758ba57c30a321488", + "integrated_sha256": "sha256:ef2be43b2ebdc18ad1a5522a099c2109dd1963170e148e2c6eb50344dc4de0e0" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/README.md b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/README.md new file mode 100644 index 00000000..bc7fb180 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/README.md @@ -0,0 +1,154 @@ +# TSA15 scanner batching candidate, revision 1 + +This is an isolated review candidate against repo-manager commit +`97ea2489257de08911cc5da1e1b84d8e15780dc3`. The canonical owner checkout is +unchanged. `manifest.json` fixes the base, candidate and patch hashes; +`candidate.patch` applies without changing repository control ownership. +This package does not admit or install the candidate and does not change hook, +receive-path, CI or independent source-control coverage. + +## Behavior and attribution + +One metadata query checks every unique staged blob, then bounded `cat-file +--batch` reads replace per-object size/content subprocess pairs. Every returned +object is checked for exact framing, type, size, requested OID and recomputed Git +blob hash. Index snapshots still bracket the scan. Existing stage/submodule, +byte/count limits, alternate-index support, no-replace behavior and content-silent +refusal remain in force. + +Blobs of at most 4096 bytes use one directory invocation per batch of at most 64 +objects. Each object remains a separate regular file with a neutral OID filename; +content is never concatenated and original filenames cannot activate ignore +patterns. Larger blobs retain the original individual `stdin` route. At the +small-file cutoff, Python's stdin writer uses one atomic PIPE_BUF write and the +scanner gets one fragment in both routes. This avoids changing larger inputs' +pipe-dependent fragmentation. The fixed template emits only neutral file paths; +unknown paths, missing/oversized reports, exit/report disagreement, warnings, +timeouts and failures all refuse. The pinned scanner's single fixed successful +`WRN leaks found: N` summary is the only accepted diagnostic. No finding content +is emitted or retained in reports. + +Both routes use Gitleaks 8.30.1 and the original exact SHA256, default-rule extend +config, explicit empty ignore file, `--ignore-gitleaks-allow`, full redaction and +an explicit environment. The executable is now copied into a sealed memfd while +hashing and executed from that immutable snapshot. Path replacement or in-place +modification of the original after verification cannot substitute executable +bytes. + +## Storage and process lifecycle + +All new source-bearing temporary files are restricted to a private 0700 +directory on fixed `/dev/shm`, verified with `fstatfs` against TMPFS_MAGIC. There +is no disk-backed fallback. Scanner reports and diagnostics use the same +volatile directory. Tests use generated disposable fixtures only. + +Before reading any blob content, a separate isolated Python watchdog registers +an exact parent pidfd and confirms readiness. Normal cleanup happens after all +synchronous source children exit. Hard parent death also triggers guarded cleanup; +every Git/scanner child installs Linux parent-death SIGKILL and closes the +fork/registration race. At the exceptional 180-second watchdog deadline, the +watchdog sends TERM to the exact parent pidfd, allows five seconds, sends KILL +if needed and waits for proven death before cleanup. If kernel quiescence stalls, +the cleanup observer retains its duty until death is established. It never +signals a shell, user process group or tool harness. The shortened deadline test +targets only a newly created disposable Python process. + +Deletion verifies directory device, inode, owner and 0700 mode through a directory +fd, then uses symlink-safe `rmtree`. Normal cleanup verifies watcher completion; +watchdog loss/failure refuses. This is process and memory lifecycle handling, +not a new service or authority boundary. + +## Bounds + +- Unique staged objects: 500; individual source blob: 8 MiB; total source: 64 MiB. +- Git content batches: at most 64 objects and 8 MiB, plus at most 128 framing bytes + per object. Git output is capped before capture with RLIMIT_FSIZE. +- Materialized small-file payload: at most 64 × 4096 bytes per batch. Large blobs + remain individual stdin inputs. An anonymous content-batch file can hold 8 MiB + plus framing, and in-memory batch contents are bounded by 8 MiB. +- Each report and diagnostic file: at most 1 MiB. Exceeding a limit refuses. +- Child virtual address space: 8 GiB (a virtual address bound, not a resident + memory guarantee); CPU: 20 seconds per Git/scanner child; scanner Go parallelism + fixed at two. The pinned WASM regex engine reserves a 4 GiB virtual region. +- Each subprocess has a 20-second caller timeout; scanner internal timeout is + 15 seconds. Clean/blocked acceptance requires the entire scan to finish within + 120 seconds. Cleanup uses five-second waits; the separate exceptional watchdog + deadline is 180 seconds plus five seconds TERM grace and proven-death wait. +- The scanner snapshot is capped at 64 MiB and sealed before execution. Existing + index-enumeration output bounds are unchanged; staged paths are not file content. + +## Validation evidence + +`run_validation.sh` is the exact generated-fixture driver. Its three stages run +42 tests, 13 old/new exact-summary parity cases, then the same five benchmark +fixture families and repetition/percentile method as the retained baseline. +The coordinator executed the exact reviewed runner successfully: **42 tests passed +in 32.95 seconds**, all 13 old/new exact-summary parity cases matched, all +benchmark cases and missing/wrong scanner refusals passed, and generated +fixtures were removed. The final source and pinned scanner hashes were unchanged. +The hard-loss and shortened-watchdog-deadline probes affected only disposable +Python processes. See `test-results.txt`, `parity-results.json` and +`scanner-candidate.json`. + +Final warm p50 results, using the same retained fixture families and seven warm +samples per case: + +| Generated case | Earlier baseline | Final candidate | Comparison | +|---|---:|---:|---:| +| 1 distinct 1 KiB blob | 702.358 ms | 835.286 ms | 18.9% slower | +| 16 distinct 1 KiB blobs | 12065.334 ms | 884.239 ms | 13.6× faster | +| 64 distinct 1 KiB blobs | 49171.849 ms | 904.024 ms | 54.4× faster | +| 1 × 1 MiB blob | 2548.887 ms | 1902.956 ms | 1.34× faster | +| Generated private key | 854.193 ms | 882.714 ms | 3.3% slower | + +These are separate descriptive local fixture samples rather than a controlled +interleaved trial or production tail estimate. They establish bounded local +multi-object startup amortization, with explicit single-object regressions; +there is no aggregate performance, remote receive, CI or whole-TSA15 claim. + +`scanner-candidate-pre-tmpfs.json` is retained **superseded** evidence from the +initial disk-temporary revision, not proof of the final storage/lifecycle design. +Its p50 for 1/16/64 distinct 1 KiB blobs was 1.058/1.048/1.136 seconds, compared +with the earlier baseline's 0.702/12.065/49.172 seconds. The initial one-blob case +regressed; multi-object amortization improved. Those samples are descriptive +local fixtures, not workload or production tail estimates. + +Failed preparation experiments are preserved here in prose: a 4 GiB virtual +address limit caused the pinned WASM allocator to refuse startup; the candidate +therefore uses 8 GiB. The pinned CLI required explicit `--report-format template` +despite help claiming `--report-template` implied it. Successful blocked scans +emit a warning-level fixed summary, so blanket rejection of all nonempty stderr +was refined to accept only that exact summary. An initial test discovery run +resolved the installed package instead of the isolated namespace copy; it was +excluded and rerun after copying `__init__.py` and printing the candidate import. +The subsequent 17 original and 34 expanded tests passed on the pre-tmpfs candidate; +none of those runs establishes the final revision's lifecycle result. + +## Pinned upstream behavior reviewed + +Gitleaks source at tag v8.30.1: + +- [stdin command](https://github.com/gitleaks/gitleaks/blob/v8.30.1/cmd/stdin.go) +- [directory command](https://github.com/gitleaks/gitleaks/blob/v8.30.1/cmd/directory.go) +- [shared file fragment reader](https://github.com/gitleaks/gitleaks/blob/v8.30.1/sources/file.go) +- [directory traversal and skip diagnostics](https://github.com/gitleaks/gitleaks/blob/v8.30.1/sources/files.go) +- [default config and path allowlists](https://github.com/gitleaks/gitleaks/blob/v8.30.1/config/gitleaks.toml) +- [template reporter](https://github.com/gitleaks/gitleaks/blob/v8.30.1/report/template.go) + +Both commands use the same file-fragment scanner. Directory mode adds path-based +allowlists and warning/error skip behavior; neutral relative OID paths, restricted +small-file batching and refusal on unexpected diagnostics address those additional +conditions. The generated parity matrix tests representative binary, encoded, +path-collision and fragment-boundary cases; it is not an exhaustive proof over +all possible scanner inputs or a promise that the scanner detects every secret. + + +## Owner refinement and publication preparation + +`owner-workplan.patch` proposes RMGR-WP-0021-T05 without changing the existing +done tasks or claiming source publication. `RMGR-WP-0021.proposed.md` is the exact +reviewable owner-file result. `runtime-routing-readonly.json` records the actual +hook and CLI source paths; no hook was executed against a real user index. +`publication-plan.md` distinguishes source publication from configured-hook and +CLI acceptance. The globally advertised `~/.local/bin/rmgr` executable was absent; +the verified repo-local `.venv/bin/rmgr` imports the canonical source. diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/RMGR-WP-0021.proposed.md b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/RMGR-WP-0021.proposed.md new file mode 100644 index 00000000..551dce29 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/RMGR-WP-0021.proposed.md @@ -0,0 +1,187 @@ +--- +id: RMGR-WP-0021 +type: workplan +title: "Secret-detection adapter and repository enforcement acceptance" +domain: infotech +repo: repo-manager +status: active +owner: codex +topic_slug: repo-manager +created: "2026-10-12" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +state_hub_workstream_id: "5aa6f84a-c682-5996-ae26-96daef670833" +--- + +Native repository-control work for CUST-WP-0080 G07 / DSM-DETECT. Existing +RMGR-WP-0009 provenance remains separate and nonblocking by its contract. +Detection is an additional security check; this plan does not redefine provenance, +credential custody, PDP authority, Forge ownership or incident authorization. + +## Provide a bounded content-silent staged-object scanner adapter + +```task +id: RMGR-WP-0021-T01 +status: done +priority: high +state_hub_task_id: "d3d20c18-5a5f-5f8a-b5c4-a22b4b662ebe" +``` + +2026-10-12: all 21 reviewed estate checkouts resolve their pre-commit hook to the +same generated-only guard. A disposable valid private key was committed through +that inherited hook, proving no local prevention for that fixture. No real +credential was used. The new `rmgr secret-scan` adapter pins Gitleaks 8.30.1 Linux +x64, reads complete changed index blobs, isolates configuration, suppresses raw +output, verifies index stability and refuses unknown binaries, oversized inputs, +changed gitlinks and other unverified cases. Real scanner tests prove refusal of +a disposable private-key commit, partial/alternate-index correctness, local +configuration/inline suppression refusal and Git replacement-object resistance. +Evidence: Custodian `docs/evidence/2026-10-12-netkingdom-security/secret-detection.json`. +This is source/tool behavior, not workstation or forge activation. + +## Admit the tool and activate local prevention for the reviewed repositories + +```task +id: RMGR-WP-0021-T02 +status: done +priority: high +state_hub_task_id: "e27817e6-b298-581f-b99a-05ec459be749" +``` + +Review durable executable installation/version custody and exact repository +coverage. Preserve existing shared generated-only and provenance hooks and the +Custodian's separate local hook. Do not replace unknown hooks, alter global +routing or silently affect unreviewed repositories. Test actual configured hooks +with clean/disposable-secret/scanner-failure cases without changing production +source or committing a credential. Record observed coverage and explicit +uncovered checkouts. Installer rollback restores exact prior content/routing. + +2026-10-12: activated the reviewed composition in the existing shared hook and +Custodian local hook, with Gitleaks 8.30.1 admitted at +`/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks` under the pinned +SHA256. No Git routing or other hook changed. All 22 exact workstation roots in +the proposal passed actual configured-hook checks: clean exit 0, disposable +private key exit 1, generated-only exit 1, non-executable scanner exit 2. Scanner +mode was restored; a checkout outside the exact scope retained its prior behavior. +Probes used temporary alternate indexes/object stores, without changing user +source, staged content or branches. Eleven activation/rollback/guard tests pass. +Custodian evidence: `secret-hook-activation.json`; reviewed proposal and rollback +executor: `docs/changes/CUST-WP-0080/`. This covers these workstation checkouts, +not other machines/clones, existing history, remote enforcement or real findings. +T03/T04 remain open; local hooks remain bypassable. + +## Establish independent forge and CI detection coverage + +```task +id: RMGR-WP-0021-T03 +status: progress +priority: high +state_hub_task_id: "0ab44bd9-00d8-5470-ad45-7350e7868d0a" +``` + +Under CUST-WP-0080 coordination with railiance-forge, inspect existing server-side +and required-check controls; define the production repository denominator and +owner-native enforcement record before activating changes. Cover introduced +history/merge cases and bypasses, missing scanner, malformed input and scanner +failure. Record actual rejected/accepted push or required-check receipts. Local +hook success is insufficient and this staged adapter alone does not scan history. + +2026-10-12 read-only native baseline: railiance01 runs Forgejo 11.0.3. +All 22 reviewed repositories have only the upstream-generated receive-hook files, +matched by SHA256 to the v11.0.3 templates; no custom scanner hook is installed at +that boundary. Storage contains 165 repository directories, which still need +classification before being an admitted production denominator. Independent +Actions/branch-protection coverage and push acceptance remain unverified. Do not +replace generated dispatchers or enable arbitrary user hook editing to add the +control. Receipt: Custodian `forge-secret-detection-baseline.json`. No live +server mutation, credential access or synthetic push was performed. + +2026-10-12 implementation handoff: FORGE-WP-0006 owns the independent receive +boundary. Its introduced-history adapter passes 33 leaf cases with actual +disposable pushes, including added-then-deleted/merge/message/tag secrets and +scanner failures. Source is not installed server enforcement. Native inventory, +exact-artifact custody, SSH/HTTP/frontend/restart/new-repository acceptance and +CI authority remain open in FORGE-WP-0006-T02/T03. Custodian receipt: +`docs/evidence/2026-10-12-netkingdom-security/receive-secret-detection.json`. + +2026-10-13 receive lifecycle acceptance now exists in FORGE-WP-0006: 22 server +profiles, template inheritance and repaired scanner custody across two actual +Kubernetes replacements. The original failed rollout is preserved in Custodian's +`forge-custody-recovery-summary.json`; 144 of 166 repositories and the remaining +HTTP/SSO/frontend/CI/response acceptance stay open. The server scanner's 90-second +budget exceeded State Hub's 30-second and Repo Manager's 60-second push deadlines. +Both clients now allow 120 seconds; a timeout reports unverified remote outcome +and requires ref inspection before retry. No force push or scanner exception is +introduced. State Hub owner record: STATE-WP-ADHOC-2026-10-13-T01. Dedicated timeout +regressions and real delayed-hook acceptance are recorded in Custodian's +`push-deadline.json`. This transport correction does not close T03. + +2026-10-13 expansion completed: FORGE6-EXPAND-001 revision 1 exited 0 after +all 143 targets passed actual SSH clean/scanner-refusal probes under an exact-ref +veto. The independent final read-only audit reconciles 167 repositories: 166 +admitted profiles, one builtin-only (numeric ID 55), zero unknown profiles. All +23 baseline profiles remain exact; no probe refs, temporary vetoes or ownership +markers remain. Each rejected synthetic commit was absent after its probe. +Repository 55 has four external submodules and remains required compatibility +work. Ordinary source acceptance is distinct from vetoed scanner checks. +Production HTTP/API/frontend acceptance remains unverified: the protected API +credential route failed before child handoff and the metadata-only capability +check denied access. Prepare one complete acceptance command for the existing +attended login window. CI, import/mirror, historical/LFS/log/artifact detection +and private response gates remain open; this is no whole-estate closure. +Evidence: Custodian `docs/evidence/2026-10-13-netkingdom-security/forge-expansion-final.json` +and `forge-api-route-readiness.json`. + +## Verify private finding-to-response handling + +```task +id: RMGR-WP-0021-T04 +status: todo +priority: high +state_hub_task_id: "44272818-db49-58a2-83b1-d2f9e9d5976c" +``` + +Exercise a synthetic finding through metadata-only owner routing and approved +private review. Define exact false-positive exception governance; require actual +approval, scope, expiry/review and regression coverage. For genuine exposed +credentials, retain incident owner and revoke/replace/adopt/validate gates. Do not +execute founder-deferred rotations or treat suppressed output as remediation. +Close only after native coverage and response evidence are adequate for the +applicable DSM-DETECT requirements; retain residuals as live work records. + + +## Publish measured staged-scanner batching and verify runtime routing + +```task +id: RMGR-WP-0021-T05 +status: todo +priority: medium +``` + +2026-10-17 prepared refinement for the local staged-object adapter. Independent +review and a coordinator-executed generated-fixture run cover the exact source +SHA256 `e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400`: +42 tests, 13 exact old/new summary parity cases, immutable scanner snapshot, +volatile-only bounded batches, watchdog and parent-loss refusal/cleanup. Warm +p50 for 1/16/64 distinct 1 KiB blobs is 0.835/0.884/0.904 seconds, compared with +0.702/12.065/49.172 seconds in the retained baseline. The one-blob case regressed; +this is descriptive local fixture evidence, not aggregate workload or tail +acceptance. Larger blobs retain the original stdin route. + +Publish the reviewed patch against the exact owner base, verify the configured +shared and Custodian hooks still resolve to the admitted owner source, then run +clean/private-key/scanner-unavailable generated alternate-index probes without +reading or changing user source/indexes. Verify actual CLI import routing; the +repo-local `.venv/bin/rmgr` imports owner source, but the previously advertised +`/home/worsch/.local/bin/rmgr` is absent in the read-only observation and must not +be described as installed operational coverage. Reconcile that exact CLI route +before claiming CLI publication. Preserve existing hook guards and routing; +retain rollback to the exact prior adapter bytes. Source publication and runtime +acceptance must have separate receipts. + +Review packet: Custodian +`docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/`. +No canonical owner mutation or runtime admission has occurred in the packet. +This task does not close T03/T04, independent receive/CI coverage, source-control +bypass limits or whole TSA15. diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/benchmark-driver-result.txt b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/benchmark-driver-result.txt new file mode 100644 index 00000000..f0bc1436 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/benchmark-driver-result.txt @@ -0,0 +1 @@ +{"ok": true, "cases": 5, "negative_cases": 2, "fixture_removed": true} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/candidate.patch b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/candidate.patch new file mode 100644 index 00000000..1e3c1b2f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/candidate.patch @@ -0,0 +1,684 @@ +--- a/src/repo_manager/secret_scan.py ++++ b/src/repo_manager/secret_scan.py +@@ -7,20 +7,42 @@ + from __future__ import annotations + + import argparse ++import contextlib ++import ctypes ++import fcntl ++import functools + import hashlib + import json + import os + import re ++import shutil ++import signal ++import resource + import stat ++import sys + import subprocess + import tempfile ++import time + from pathlib import Path ++ ++# Resolve libc and prctl before fork; child_limits performs no library loading. ++_LIBC = ctypes.CDLL(None, use_errno=True) ++_PRCTL = _LIBC.prctl ++_PRCTL.argtypes = [ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong] ++_PRCTL.restype = ctypes.c_int + + VERSION = '8.30.1' + BINARY_SHA256 = '88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509' + MAX_BLOB_BYTES = 8 * 1024 * 1024 + MAX_TOTAL_BYTES = 64 * 1024 * 1024 + MAX_BLOBS = 500 ++MAX_BATCH_BLOBS = 64 ++MAX_BATCH_BYTES = 8 * 1024 * 1024 ++# PIPE_BUF bytes fit the original stdin writer's single atomic write. Larger ++# blobs retain the stdin path, avoiding a change to fragment boundaries. ++DIRECTORY_BLOB_BYTES = 4096 ++MAX_REPORT_BYTES = 1024 * 1024 ++SCAN_SECONDS = 120 + CONFIG = '[extend]\nuseDefault = true\n' + + +@@ -31,7 +53,8 @@ + def git(repo: Path, *args: str) -> bytes: + env = {**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'} + result = subprocess.run(['git', '-C', str(repo), *args], stdout=subprocess.PIPE, +- stderr=subprocess.DEVNULL, env=env, timeout=20, check=False) ++ stderr=subprocess.DEVNULL, env=env, timeout=20, check=False, ++ preexec_fn=functools.partial(child_limits, MAX_TOTAL_BYTES, os.getpid())) + if result.returncode: + raise Unverified('git-read-failed') + return result.stdout +@@ -63,10 +86,167 @@ + return snapshot, sorted(objects) + + ++# Source bytes must never be copied onto persistent storage. The watchdog owns ++# no source data; its pipe EOF detects parent death without a service or hook. ++_CLEANUP = """ ++import os, select, shutil, signal, stat, sys ++path, dev, ino, uid, parent = sys.argv[1:] ++root = os.open('/dev/shm', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) ++if os.getppid() != int(parent): ++ sys.exit(4) ++pid = os.pidfd_open(int(parent)) ++if os.getppid() != int(parent): ++ sys.exit(4) ++os.write(1, b'R') ++ready, _, _ = select.select([0, pid], [], [], 180) ++# Exceptional abort targets only the exact parent, never a process group. ++# If kernel quiescence stalls, retain this observer/duty until death is proven. ++if not ready: ++ try: ++ signal.pidfd_send_signal(pid, signal.SIGTERM) ++ except ProcessLookupError: ++ pass ++ dead, _, _ = select.select([pid], [], [], 5) ++ if not dead: ++ try: ++ signal.pidfd_send_signal(pid, signal.SIGKILL) ++ except ProcessLookupError: ++ pass ++ select.select([pid], [], []) ++name = os.path.basename(path) ++try: ++ info = os.stat(name, dir_fd=root, follow_symlinks=False) ++except FileNotFoundError: ++ sys.exit(0) ++if (info.st_dev, info.st_ino, info.st_uid) != (int(dev), int(ino), int(uid)): ++ sys.exit(3) ++if not stat.S_ISDIR(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o700: ++ sys.exit(3) ++shutil.rmtree(name, dir_fd=root) ++""" ++ ++ ++def is_tmpfs(directory: Path) -> bool: ++ descriptor = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) ++ try: ++ result = ctypes.create_string_buffer(256) ++ if _LIBC.fstatfs(descriptor, ctypes.byref(result)): ++ raise OSError(ctypes.get_errno(), 'statfs failed') ++ return ctypes.c_long.from_buffer(result).value == 0x01021994 ++ finally: ++ os.close(descriptor) ++ ++ ++def remove_storage(directory: Path, identity: os.stat_result) -> None: ++ root = os.open('/dev/shm', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) ++ try: ++ try: ++ current = os.stat(directory.name, dir_fd=root, follow_symlinks=False) ++ except FileNotFoundError: ++ return ++ if ((current.st_dev, current.st_ino, current.st_uid) != ++ (identity.st_dev, identity.st_ino, identity.st_uid) ++ or not stat.S_ISDIR(current.st_mode) ++ or stat.S_IMODE(current.st_mode) != 0o700): ++ raise Unverified('volatile-storage-identity-changed') ++ shutil.rmtree(directory.name, dir_fd=root) ++ finally: ++ os.close(root) ++ ++ ++@contextlib.contextmanager ++def private_storage(): ++ if not is_tmpfs(Path('/dev/shm')): ++ raise Unverified('volatile-storage-unavailable') ++ temporary = tempfile.mkdtemp(prefix='rmgr-secret-scan-', dir='/dev/shm') ++ directory = Path(temporary) ++ identity = directory.lstat() ++ watcher = None ++ try: ++ if not is_tmpfs(directory): ++ raise Unverified('volatile-storage-unavailable') ++ watcher = subprocess.Popen([sys.executable, '-I', '-c', _CLEANUP, temporary, ++ str(identity.st_dev), str(identity.st_ino), str(identity.st_uid), str(os.getpid())], ++ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ++ env={'PATH': '/usr/bin:/bin', 'LANG': 'C'}, close_fds=True) ++ import select ++ ready, _, _ = select.select([watcher.stdout], [], [], 5) ++ if not ready or watcher.stdout.read(1) != b'R' or watcher.poll() is not None: ++ raise Unverified('cleanup-watchdog-unavailable') ++ watcher.stdout.close() ++ yield directory, watcher ++ if watcher.poll() is not None: ++ raise Unverified('cleanup-watchdog-unavailable') ++ finally: ++ pending_error = sys.exc_info()[0] is not None ++ # All synchronous Git/scanner children have exited before yield unwinds. ++ try: ++ remove_storage(directory, identity) ++ finally: ++ if watcher is not None: ++ watcher.stdin.close() ++ watcher.stdout.close() ++ try: ++ code = watcher.wait(timeout=5) ++ except subprocess.TimeoutExpired: ++ watcher.kill() ++ code = watcher.wait(timeout=5) ++ if code and not pending_error: ++ raise Unverified('cleanup-watchdog-failed') ++ ++ ++def child_limits(file_bytes: int, parent_pid: int) -> None: ++ # Parent loss must quiesce every source-reading/writing child. Install the ++ # signal before any exec and close the fork/prctl race using the exact PID. ++ if _PRCTL(1, signal.SIGKILL, 0, 0, 0) or os.getppid() != parent_pid: ++ os._exit(127) ++ # Linux adapter already requires /proc/self/fd. Limits apply only to children. ++ resource.setrlimit(resource.RLIMIT_FSIZE, (file_bytes, file_bytes)) ++ resource.setrlimit(resource.RLIMIT_CPU, (20, 20)) ++ resource.setrlimit(resource.RLIMIT_AS, (8 * 1024**3, 8 * 1024**3)) ++ ++ ++def object_batch(repo: Path, objects: list[str], directory: Path) -> list[tuple[str, bytes]]: ++ """Bound Git's output before capture, then validate the complete framing.""" ++ request = ''.join(oid + '\n' for oid in objects).encode('ascii') ++ env = {**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'} ++ with tempfile.TemporaryFile(dir=directory) as output: ++ result = subprocess.run(['git', '-C', str(repo), 'cat-file', '--batch'], ++ input=request, stdout=output, stderr=subprocess.DEVNULL, ++ env=env, timeout=20, check=False, ++ preexec_fn=functools.partial(child_limits, ++ MAX_BATCH_BYTES + len(objects) * 128, os.getpid())) ++ if result.returncode: ++ raise Unverified('git-read-failed') ++ output.seek(0) ++ blobs = [] ++ for oid in objects: ++ header = output.readline(128) ++ match = re.fullmatch(rb'([a-f0-9]{40}|[a-f0-9]{64}) blob ([0-9]+)\n', header) ++ if not match or match[1].decode() != oid: ++ raise Unverified('invalid-object-batch') ++ size = int(match[2]) ++ if size > MAX_BLOB_BYTES or size > MAX_BATCH_BYTES: ++ raise Unverified('staged-byte-limit') ++ content = output.read(size) ++ if len(content) != size or output.read(1) != b'\n': ++ raise Unverified('blob-size-mismatch') ++ algorithm = 'sha1' if len(oid) == 40 else 'sha256' ++ digest = hashlib.new(algorithm, b'blob ' + str(size).encode() + b'\0' + content) ++ if digest.hexdigest() != oid: ++ raise Unverified('blob-digest-mismatch') ++ blobs.append((oid, content)) ++ if output.read(1): ++ raise Unverified('invalid-object-batch') ++ return blobs ++ ++ + def scan_staged(repo: Path, binary: Path) -> dict: + """Return only fixed statuses/counts and Git blob IDs, never finding contents.""" + scanned = 0 + descriptor = None ++ executable = None ++ deadline = time.monotonic() + SCAN_SECONDS + try: + descriptor = os.open(binary, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + info = os.fstat(descriptor) +@@ -74,47 +254,149 @@ + or not info.st_mode & 0o111 or info.st_uid not in (0, os.getuid()) + or info.st_size > 64 * 1024 * 1024): + raise Unverified('scanner-file-refused') ++ executable = os.memfd_create('rmgr-verified-scanner', os.MFD_ALLOW_SEALING) + digest = hashlib.sha256() ++ copied = 0 + while chunk := os.read(descriptor, 1024 * 1024): ++ copied += len(chunk) ++ if copied > 64 * 1024 * 1024: ++ raise Unverified('scanner-file-refused') + digest.update(chunk) ++ with memoryview(chunk) as view: ++ while view: ++ written = os.write(executable, view) ++ if written <= 0: ++ raise Unverified('scanner-file-refused') ++ view = view[written:] + if digest.hexdigest() != BINARY_SHA256: + raise Unverified('scanner-digest-mismatch') ++ fcntl.fcntl(executable, fcntl.F_ADD_SEALS, fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW ++ | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL) + snapshot, objects = staged_objects(repo) + blocked = [] +- total = 0 +- # Do not honor repository-local config/ignore files, comments or ambient +- # GITLEAKS settings. No scanner report or credential-bearing diagnostic is kept. +- with tempfile.TemporaryDirectory(prefix='rmgr-secret-scan-') as temporary: +- directory = Path(temporary) ++ # Neither source names nor repository config/ignore files enter this ++ # private directory. Reports contain ONLY neutral blob paths. ++ with private_storage() as (directory, watcher): ++ temporary = str(directory) + config = directory / 'scanner.toml'; config.write_text(CONFIG) + ignore = directory / '.gitleaksignore'; ignore.write_text('') +- env = {'PATH': '/usr/bin:/bin', 'HOME': temporary, 'LANG': 'C', 'TMPDIR': temporary} +- for oid in objects: +- size = int(git(repo, 'cat-file', '-s', oid)) +- total += size +- if size < 0 or size > MAX_BLOB_BYTES or total > MAX_TOTAL_BYTES: +- raise Unverified('staged-byte-limit') +- content = git(repo, 'cat-file', 'blob', oid) +- if len(content) != size: ++ template = directory / 'report.tmpl' ++ template.write_text('{{range .}}{{.File}}\n{{end}}') ++ env = {'PATH': '/usr/bin:/bin', 'HOME': temporary, 'LANG': 'C', ++ 'TMPDIR': temporary, 'GOMAXPROCS': '2'} ++ # One bounded metadata query replaces a size subprocess per object. ++ request = ''.join(oid + '\n' for oid in objects).encode('ascii') ++ with tempfile.TemporaryFile(dir=directory) as output: ++ result = subprocess.run(['git', '-C', str(repo), 'cat-file', '--batch-check'], ++ input=request, stdout=output, stderr=subprocess.DEVNULL, ++ env={**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'}, timeout=20, ++ check=False, preexec_fn=functools.partial(child_limits, MAX_BLOBS * 128, os.getpid())) ++ if result.returncode: ++ raise Unverified('git-read-failed') ++ output.seek(0) ++ metadata = output.read(MAX_BLOBS * 128 + 1).splitlines() ++ if len(metadata) != len(objects): ++ raise Unverified('invalid-object-batch') ++ sizes = [] ++ for oid, line in zip(objects, metadata): ++ match = re.fullmatch(rb'([a-f0-9]{40}|[a-f0-9]{64}) blob ([0-9]+)', line) ++ if not match or match[1].decode() != oid: ++ raise Unverified('invalid-object-batch') ++ sizes.append(int(match[2])) ++ if any(size > MAX_BLOB_BYTES for size in sizes) or sum(sizes) > MAX_TOTAL_BYTES: ++ raise Unverified('staged-byte-limit') ++ ++ def invoke(mode: str, content: bytes | None = None) -> list[str]: ++ if watcher.poll() is not None: ++ raise Unverified('cleanup-watchdog-unavailable') ++ remaining = deadline - time.monotonic() ++ if remaining <= 0: ++ raise Unverified('scan-deadline-exceeded') ++ report = directory / 'report.paths' ++ report.unlink(missing_ok=True) ++ command = [str(binary), mode, '--config', str(config), ++ '--gitleaks-ignore-path', str(ignore), '--ignore-gitleaks-allow', ++ '--redact=100', '--no-banner', '--no-color', '--log-level', 'warn', ++ '--exit-code', '20', '--timeout', '15'] ++ if mode == 'dir': ++ command += ['--report-format', 'template', '--report-template', str(template), '--report-path', str(report), ++ '--max-target-megabytes', '0', 'blobs'] ++ with tempfile.TemporaryFile(dir=directory) as diagnostics: ++ result = subprocess.run(command, executable=f'/proc/self/fd/{executable}', ++ pass_fds=(executable,), input=content, cwd=directory, env=env, ++ stdout=subprocess.DEVNULL, stderr=diagnostics, ++ timeout=min(20, remaining), check=False, ++ preexec_fn=functools.partial(child_limits, MAX_REPORT_BYTES, os.getpid())) ++ # The scanner can warn and skip a file without an error exit. ++ # Refuse ALL warnings/errors, including partial traversal. ++ diagnostics.seek(0) ++ messages = diagnostics.read(MAX_REPORT_BYTES + 1) ++ # v8.30.1 emits exactly this summary for a successful blocked ++ # scan. Every other warning/error is a refusal, never output. ++ summary = re.fullmatch( ++ rb'[0-9]{1,2}:[0-9]{2}(?:AM|PM) WRN leaks found: [1-9][0-9]*\n', ++ messages) ++ if (result.returncode not in (0, 20) ++ or (messages and not (result.returncode == 20 and summary))): ++ raise Unverified('scanner-failed') ++ if mode == 'stdin': ++ return ['stdin'] if result.returncode == 20 else [] ++ if not report.exists() or report.stat().st_size > MAX_REPORT_BYTES: ++ raise Unverified('scanner-report-refused') ++ paths = report.read_bytes().splitlines() ++ known = {('blobs/' + oid).encode(): oid for oid in small} ++ if any(path not in known for path in paths): ++ raise Unverified('scanner-report-refused') ++ findings = sorted({known[path] for path in paths}) ++ if bool(findings) != (result.returncode == 20): ++ raise Unverified('scanner-report-refused') ++ return findings ++ ++ offset = 0 ++ while offset < len(objects): ++ batch = [] ++ batch_bytes = 0 ++ while offset < len(objects) and len(batch) < MAX_BATCH_BLOBS: ++ size = sizes[offset] ++ if batch and batch_bytes + size > MAX_BATCH_BYTES: ++ break ++ batch.append(objects[offset]); batch_bytes += size; offset += 1 ++ if watcher.poll() is not None: ++ raise Unverified('cleanup-watchdog-unavailable') ++ blobs = object_batch(repo, batch, directory) ++ # Check metadata again against exact bytes, including same-size ++ # substitution via corrupt object storage. Hashes are checked above. ++ expected = dict(zip(objects, sizes)) ++ if any(len(content) != expected[oid] for oid, content in blobs): + raise Unverified('blob-size-mismatch') +- command = [str(binary), 'stdin', '--config', str(config), +- '--gitleaks-ignore-path', str(ignore), '--ignore-gitleaks-allow', +- '--redact=100', '--no-banner', '--no-color', '--log-level', 'fatal', +- '--exit-code', '20', '--timeout', '15'] +- result = subprocess.run(command, executable=f'/proc/self/fd/{descriptor}', +- pass_fds=(descriptor,), input=content, cwd=directory, env=env, +- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, +- timeout=20, check=False) +- if result.returncode not in (0, 20): +- raise Unverified('scanner-failed') +- scanned += 1 +- if result.returncode == 20: +- blocked.append(oid) ++ payload = directory / 'blobs'; payload.mkdir(mode=0o700) ++ small = [] ++ try: ++ for oid, content in blobs: ++ if len(content) > DIRECTORY_BLOB_BYTES: ++ if invoke('stdin', content): ++ blocked.append(oid) ++ scanned += 1 ++ else: ++ path = payload / oid ++ with path.open('xb') as target: ++ target.write(content) ++ path.chmod(0o600) ++ small.append(oid) ++ if small: ++ blocked.extend(invoke('dir')) ++ scanned += len(small) ++ finally: ++ for path in payload.iterdir(): ++ path.unlink() ++ payload.rmdir() + if git(repo, 'ls-files', '--stage', '-z') != snapshot: + raise Unverified('index-changed-during-scan') ++ if time.monotonic() > deadline: ++ raise Unverified('scan-deadline-exceeded') + return {'status': 'blocked' if blocked else 'clean', 'scope': 'changed-staged-blobs', + 'scanner_version': VERSION, 'scanner_sha256': BINARY_SHA256, +- 'scanned_blobs': scanned, 'blocked_blob_ids': blocked} ++ 'scanned_blobs': scanned, 'blocked_blob_ids': sorted(blocked)} + except Unverified as error: + reason = error.args[0] + except (OSError, ValueError, subprocess.SubprocessError): +@@ -122,6 +404,8 @@ + finally: + if descriptor is not None: + os.close(descriptor) ++ if executable is not None: ++ os.close(executable) + return {'status': 'unverified', 'scope': 'changed-staged-blobs', + 'reason': reason, 'scanned_blobs': scanned} + +--- a/tests/test_secret_scan.py ++++ b/tests/test_secret_scan.py +@@ -182,7 +182,7 @@ + stage(repo, b'clean\n') + original = secret_scan.subprocess.run + def broken(command, **kwargs): +- if len(command) > 1 and command[1] == 'stdin': ++ if len(command) > 1 and command[1] in ('stdin', 'dir'): + if failure == 'timeout': + raise subprocess.TimeoutExpired(command, 20, output=b'synthetic-sensitive-diagnostic') + return subprocess.CompletedProcess(command, 2) +@@ -210,3 +210,263 @@ + assert git(repo, 'rev-parse', 'HEAD') == before + assert bool(private_key.strip() in result.stdout + result.stderr) is False + assert b'"status": "blocked"' in result.stdout + result.stderr ++ ++ ++def test_empty_and_deletion_only_index(binary, repo): ++ assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 ++ stage(repo, b'clean\n'); git(repo, 'commit', '-qm', 'seed') ++ assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 ++ git(repo, 'rm', '-q', 'application.txt') ++ assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 ++ ++ ++def test_duplicate_blobs_and_exact_attribution(binary, repo, private_key): ++ stage(repo, private_key, 'secret.png') # original filename cannot allowlist content ++ stage(repo, private_key, 'duplicate.txt') ++ stage(repo, b'clean\n', 'clean.txt') ++ oid = git(repo, 'rev-parse', ':secret.png').decode().strip() ++ result = secret_scan.scan_staged(repo, binary) ++ assert result['status'] == 'blocked' ++ assert result['scanned_blobs'] == 2 ++ assert result['blocked_blob_ids'] == [oid] ++ ++ ++@pytest.mark.parametrize('size', [4096, 4097, 100000, 100001]) ++def test_small_batch_and_large_fallback_equivalence(binary, repo, private_key, size): ++ data = b'\n' * (size - len(private_key)) + private_key ++ stage(repo, data) ++ result = secret_scan.scan_staged(repo, binary) ++ assert result['status'] == 'blocked' ++ assert result['blocked_blob_ids'] == [git(repo, 'rev-parse', ':application.txt').decode().strip()] ++ ++ ++def test_file_boundaries_never_synthesize_a_finding(binary, repo, private_key): ++ cut = private_key.index(b'\n') // 2 ++ stage(repo, private_key[:cut], 'first') ++ stage(repo, private_key[cut:], 'second') ++ # Each independent fragment matches the original single-blob stdin outcome; ++ # concatenation would synthesize the complete private-key opening marker. ++ result = secret_scan.scan_staged(repo, binary) ++ assert result['status'] == 'clean' ++ assert result['scanned_blobs'] == 2 ++ ++ ++def test_binary_and_encoded_fixture_equivalence(binary, repo, private_key, monkeypatch): ++ import base64 ++ original = secret_scan.DIRECTORY_BLOB_BYTES ++ for data in [b'\x00\x01\x02\n' + private_key, base64.b64encode(private_key)]: ++ stage(repo, data) ++ monkeypatch.setattr(secret_scan, 'DIRECTORY_BLOB_BYTES', 0) ++ stdin = secret_scan.scan_staged(repo, binary) ++ monkeypatch.setattr(secret_scan, 'DIRECTORY_BLOB_BYTES', original) ++ batched = secret_scan.scan_staged(repo, binary) ++ assert batched == stdin ++ assert batched['status'] == 'blocked' ++ ++ ++def test_batch_count_bound_and_independent_findings(binary, repo, private_key, monkeypatch): ++ monkeypatch.setattr(secret_scan, 'MAX_BATCH_BLOBS', 3) ++ for i in range(7): ++ stage(repo, (f'fixture {i}\n'.encode() + private_key) if i % 2 else f'clean {i}\n'.encode(), str(i)) ++ result = secret_scan.scan_staged(repo, binary) ++ expected = sorted(git(repo, 'rev-parse', ':' + str(i)).decode().strip() for i in [1, 3, 5]) ++ assert result['status'] == 'blocked' ++ assert result['scanned_blobs'] == 7 ++ assert result['blocked_blob_ids'] == expected ++ ++ ++def test_total_and_object_count_bounds_refuse(binary, repo, monkeypatch): ++ stage(repo, b'12345', 'a'); stage(repo, b'67890', 'b') ++ monkeypatch.setattr(secret_scan, 'MAX_TOTAL_BYTES', 9) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'staged-byte-limit' ++ monkeypatch.setattr(secret_scan, 'MAX_BLOBS', 1) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'staged-blob-limit' ++ ++ ++@pytest.mark.parametrize('mutation', ['path', 'in-place']) ++def test_verified_executable_snapshot_survives_mutation(binary, repo, monkeypatch, tmp_path, mutation): ++ import shutil ++ scanner = tmp_path / 'scanner'; shutil.copyfile(binary, scanner); scanner.chmod(0o700) ++ stage(repo, b'clean\n') ++ original = secret_scan.staged_objects ++ def mutate(repo): ++ if mutation == 'path': ++ scanner.unlink() ++ scanner.write_bytes(b'not executable scanner bytes\n') ++ scanner.chmod(0o700) ++ return original(repo) ++ monkeypatch.setattr(secret_scan, 'staged_objects', mutate) ++ assert secret_scan.scan_staged(repo, scanner)['status'] == 'clean' ++ ++ ++@pytest.mark.parametrize('fault', ['warning', 'unknown-path', 'missing-report', 'returncode']) ++def test_partial_scans_and_invalid_reports_refuse(binary, repo, monkeypatch, fault): ++ stage(repo, b'clean\n') ++ original = secret_scan.subprocess.run ++ def fail(command, **kwargs): ++ result = original(command, **kwargs) ++ if command[1] == 'dir': ++ report = Path(command[command.index('--report-path') + 1]) ++ if fault == 'warning': ++ kwargs['stderr'].write(b'synthetic-sensitive-diagnostic\n') ++ elif fault == 'unknown-path': ++ report.write_text('blobs/unknown\n') ++ elif fault == 'missing-report': ++ report.unlink() ++ else: ++ return subprocess.CompletedProcess(command, 20) ++ return result ++ monkeypatch.setattr(secret_scan.subprocess, 'run', fail) ++ result = secret_scan.scan_staged(repo, binary) ++ assert result['status'] == 'unverified' ++ assert 'synthetic-sensitive-diagnostic' not in json.dumps(result) ++ ++ ++def test_deadline_exhaustion_refuses(binary, repo, monkeypatch): ++ stage(repo, b'clean\n') ++ monkeypatch.setattr(secret_scan, 'SCAN_SECONDS', 0) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'scan-deadline-exceeded' ++ ++ ++def test_git_object_output_corruption_refuses(binary, repo, monkeypatch): ++ stage(repo, b'clean\n') ++ original = secret_scan.subprocess.run ++ def corrupt(command, **kwargs): ++ result = original(command, **kwargs) ++ if command[-1] == '--batch': ++ output = kwargs['stdout'] ++ output.seek(-3, 2); output.write(b'xx\n') ++ return result ++ monkeypatch.setattr(secret_scan.subprocess, 'run', corrupt) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'blob-digest-mismatch' ++ ++ ++def test_report_growth_limit_refuses(binary, repo, private_key, monkeypatch): ++ stage(repo, private_key) ++ monkeypatch.setattr(secret_scan, 'MAX_REPORT_BYTES', 16) ++ assert secret_scan.scan_staged(repo, binary)['status'] == 'unverified' ++ ++ ++def test_sealed_executable_rejects_write(binary, repo, monkeypatch): ++ import errno ++ stage(repo, b'clean\n') ++ original = secret_scan.subprocess.run ++ checked = [] ++ def assert_sealed(command, **kwargs): ++ if command[1] in ('dir', 'stdin'): ++ fd = kwargs['pass_fds'][0] ++ with pytest.raises(OSError) as error: ++ os.pwrite(fd, b'X', 0) ++ assert error.value.errno == errno.EPERM ++ checked.append(True) ++ return original(command, **kwargs) ++ monkeypatch.setattr(secret_scan.subprocess, 'run', assert_sealed) ++ assert secret_scan.scan_staged(repo, binary)['status'] == 'clean' ++ assert checked ++ ++ ++def test_nonvolatile_storage_refuses(binary, repo, monkeypatch): ++ stage(repo, b'clean\n') ++ monkeypatch.setattr(secret_scan, 'is_tmpfs', lambda directory: False) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'volatile-storage-unavailable' ++ ++ ++def test_cleanup_watchdog_loss_refuses(binary, repo, monkeypatch): ++ stage(repo, b'clean\n') ++ original = secret_scan.object_batch ++ def kill_watchdog(repo, objects, directory): ++ # Discover only our child watchdog using the context manager wrapper. ++ watcher.kill(); watcher.wait(timeout=5) ++ return original(repo, objects, directory) ++ original_storage = secret_scan.private_storage ++ import contextlib ++ @contextlib.contextmanager ++ def storage(): ++ nonlocal watcher ++ with original_storage() as pair: ++ watcher = pair[1] ++ yield pair ++ watcher = None ++ monkeypatch.setattr(secret_scan, 'private_storage', storage) ++ monkeypatch.setattr(secret_scan, 'object_batch', kill_watchdog) ++ assert secret_scan.scan_staged(repo, binary)['reason'] == 'cleanup-watchdog-unavailable' ++ ++ ++def test_parent_sigkill_triggers_watchdog_cleanup(tmp_path): ++ import time ++ script = ('from repo_manager.secret_scan import private_storage; import time\n' ++ 'with private_storage() as (directory, watcher):\n' ++ ' (directory / "fixture").write_bytes(b"public generated watchdog fixture")\n' ++ ' print(str(directory), flush=True)\n' ++ ' time.sleep(30)\n') ++ process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, ++ stderr=subprocess.DEVNULL, text=True) ++ try: ++ import select ++ ready, _, _ = select.select([process.stdout], [], [], 5) ++ assert ready ++ directory = Path(process.stdout.readline().strip()) ++ assert directory.is_dir() ++ process.kill(); process.wait(timeout=5) ++ until = time.monotonic() + 5 ++ while directory.exists() and time.monotonic() < until: ++ time.sleep(.02) ++ assert not directory.exists() ++ finally: ++ if process.poll() is None: ++ process.kill(); process.wait(timeout=5) ++ process.stdout.close() ++ ++ ++def test_watchdog_deadline_quiesces_exact_parent_before_cleanup(): ++ import select ++ import time ++ script = ('from repo_manager import secret_scan as s; import time\n' ++ 's._CLEANUP = s._CLEANUP.replace("[], [], 180)", "[], [], .2)")\n' ++ 'with s.private_storage() as (directory, watcher):\n' ++ ' (directory / "fixture").write_bytes(b"public generated fixture")\n' ++ ' print(str(directory), flush=True)\n' ++ ' time.sleep(30)\n') ++ process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, ++ stderr=subprocess.DEVNULL, text=True) ++ try: ++ ready, _, _ = select.select([process.stdout], [], [], 5) ++ assert ready ++ directory = Path(process.stdout.readline().strip()) ++ assert directory.is_dir() ++ assert process.wait(timeout=5) < 0 ++ until = time.monotonic() + 5 ++ while directory.exists() and time.monotonic() < until: ++ time.sleep(.02) ++ assert not directory.exists() ++ finally: ++ if process.poll() is None: ++ process.kill(); process.wait(timeout=5) ++ process.stdout.close() ++ ++ ++def test_parent_death_kills_source_child(): ++ import select ++ script = ('from repo_manager.secret_scan import child_limits\n' ++ 'import functools, os, subprocess, sys, time\n' ++ 'child = subprocess.Popen([sys.executable, "-c", "import time;time.sleep(30)"], ' ++ 'preexec_fn=functools.partial(child_limits, 1024, os.getpid()))\n' ++ 'print(child.pid, flush=True)\n' ++ 'time.sleep(30)\n') ++ process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, ++ stderr=subprocess.DEVNULL, text=True) ++ descriptor = None ++ try: ++ ready, _, _ = select.select([process.stdout], [], [], 5) ++ assert ready ++ child_pid = int(process.stdout.readline()) ++ descriptor = os.pidfd_open(child_pid) ++ process.kill(); process.wait(timeout=5) ++ dead, _, _ = select.select([descriptor], [], [], 5) ++ assert dead ++ finally: ++ if descriptor is not None: ++ os.close(descriptor) ++ if process.poll() is None: ++ process.kill(); process.wait(timeout=5) ++ process.stdout.close() diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-probe-manifest.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-probe-manifest.json new file mode 100644 index 00000000..db0e9308 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-probe-manifest.json @@ -0,0 +1,41 @@ +{ + "state": "prepared; independent review and coordinator execution pending", + "driver_sha256": "c91d2dc83f04ec17147c0e1410e42f4817b69e4cc5a02201682e5fa7b021b6f4", + "expected_owner_source_sha256": "e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "expected_hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "expected_scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "roots": [ + "/home/worsch/repo-manager", + "/home/worsch/the-custodian" + ], + "cases": [ + "clean", + "generated-private-key" + ], + "expected_cases": 4, + "source_read_scope": "trusted scanner/hook code and executable; HEAD tree metadata; exact generated changed fixture blob only", + "real_index_access": "lstat identity only; no open/read/write", + "provider_network_calls": 0, + "real_credentials_read": false, + "generated_fixture_storage": "reviewed private tmpfs+watchdog; alternate indexes/object stores; generated key", + "bounds": { + "git_command_timeout_seconds": 20, + "hook_command_timeout_seconds": 30, + "fixture_git_child_file_limit_MiB": 16, + "hook_child_file_limit_MiB": 64, + "child_cpu_seconds": 20, + "child_virtual_address_GiB": 8, + "parent_watchdog_seconds": 180, + "parent_term_grace_seconds": 5 + }, + "unavailable_scanner_actual_hook_status": "pending; host scanner never modified", + "receipt": "configured-hook-results.json", + "fixture_git_config_overrides": { + "core.fsmonitor": "false", + "core.splitIndex": "false", + "core.sparseCheckout": "false", + "core.untrackedCache": "false" + }, + "existing_source_ref_config_hook_scanner_mutations": false, + "persistent_write": "configured-hook-results.json fixed-metadata receipt only" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-results.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-results.json new file mode 100644 index 00000000..78c57191 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/configured-hook-results.json @@ -0,0 +1,53 @@ +{ + "status": "verified", + "scope": "actual configured shared and Custodian hooks; generated alternate-index fixtures only", + "source_sha256": "e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "provider_calls": 0, + "real_credentials_read": false, + "real_index_opened": false, + "actual_hook_unavailable_scanner": "pending; no host scanner mutation performed", + "cases": [ + { + "repo": "repo-manager", + "cases": [ + { + "case": "clean", + "status": "clean", + "exit_code": 0, + "scanned_blobs": 1 + }, + { + "case": "generated-private-key", + "status": "blocked", + "exit_code": 1, + "scanned_blobs": 1 + } + ] + }, + { + "repo": "the-custodian", + "cases": [ + { + "case": "clean", + "status": "clean", + "exit_code": 0, + "scanned_blobs": 1 + }, + { + "case": "generated-private-key", + "status": "blocked", + "exit_code": 1, + "scanned_blobs": 1 + } + ] + } + ], + "fixture_removed": true, + "new_scan_storage_absent": true, + "real_index_stat_unchanged": true, + "real_head_unchanged": true, + "routing_and_artifacts_unchanged": true, + "driver_sha256": "c91d2dc83f04ec17147c0e1410e42f4817b69e4cc5a02201682e5fa7b021b6f4" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/manifest.json new file mode 100644 index 00000000..bab89cfb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/manifest.json @@ -0,0 +1,45 @@ +{ + "state": "isolated candidate independently reviewed and fixture-validated; owner publication/runtime admission pending", + "owner_repository": "/home/worsch/repo-manager", + "owner_base_commit": "97ea2489257de08911cc5da1e1b84d8e15780dc3", + "owner_worktree_clean": true, + "files": [ + { + "path": "src/repo_manager/secret_scan.py", + "base_sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d", + "candidate_sha256": "e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "artifact": "secret_scan.candidate.py" + }, + { + "path": "tests/test_secret_scan.py", + "base_sha256": "0b2efb9ad8949e6b8397036e81ecd29acfcdd11e0723d670e3b7e6d4ea2dd54c", + "candidate_sha256": "827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876", + "artifact": "test_secret_scan.candidate.py" + } + ], + "patch_sha256": "d2f65f2af5444b241ddcb85a3e71e66d1bbe071bd9a8a7be415be84b5e2f8fdb", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "scanner_version": "8.30.1", + "validation_runner_sha256": "f6b462b136fe9cebb7fd3511c69d407466d625a4a58295625850acff61ba08d6", + "baseline_receipt_sha256": "3fff21555622d26977d31b81b168564aff42cae7b8c6000e9991b67abdbdbb35", + "measured_scope": "generated disposable fixtures only; no receive-path, CI, real workload or control-admission claim", + "provider_calls": 0, + "real_credentials_read": false, + "validation": { + "tests": 42, + "tests_passed": 42, + "test_seconds": 32.95, + "parity_cases": 13, + "parity_exact_summaries_equal": true, + "fixtures_removed": true, + "final_source_unchanged": true, + "benchmark_receipt_sha256": "0dae6e12f4067b95864e1d62f8760e6aece302304f2aee4ce0b90144fce64143", + "parity_receipt_sha256": "146166a51db02ef8b232c4a0b8dc3210b09c5d08a043041f7abeb4bb6a083658", + "test_receipt_sha256": "e4368e79e93da5b61258fc9a2afb46dc38f5c5ba54ceb16a0282e509e9165344" + }, + "owner_workplan_patch_sha256": "e0d08a2e571664a6ec8c8c22b67c3788d248878c1644f9b758ba57c30a321488", + "runtime_routing_receipt_sha256": "a22d82e9e8f6d23d5e7c5980ec7cafbcdc1d9e23837c5678c88f33e25fd3a65e", + "publication_plan_sha256": "2a9c57a2de9740770655b61d4fbc60f3f9cd92f47b163749de178fb39aab2329", + "owner_workplan_base_sha256": "fa649c2f3da205627f98a1dfc87ea6cf44dd78fd34e601f8c0597ec55e179d96", + "owner_workplan_candidate_sha256": "16826eab96d619ff90af25f09e14328b9719b0befedb0ff7128ba7bee7c30fb0" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/measure_scanner.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/measure_scanner.py new file mode 100644 index 00000000..6ad1fa17 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/measure_scanner.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Bounded synthetic measurement of the isolated scanner batching candidate. + +Never reads a real repository index or credential. All fixture Git objects and +generated key material live in an automatically removed disposable directory. +Only fixed statuses, timings, counts and public source hashes are retained. +""" +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import resource +import statistics +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone + +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +SOURCE = Path('/tmp/rmgr-scanner-batch-r1/src/repo_manager/secret_scan.py') +BINARY = Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks') + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def command(argv, cwd, *, env=None): + subprocess.run(argv, cwd=cwd, env=env, check=True, timeout=20, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def main(): + source_before = sha(SOURCE) + binary_before = sha(BINARY) + spec = importlib.util.spec_from_file_location('admitted_scanner', SOURCE) + adapter = importlib.util.module_from_spec(spec) + spec.loader.exec_module(adapter) + if sha(BINARY) != adapter.BINARY_SHA256: + raise RuntimeError('public scanner pin mismatch') + observations = [] + with tempfile.TemporaryDirectory(prefix='synthetic-scanner-', dir=HERE) as td: + root = Path(td) + env = {'PATH': '/usr/bin:/bin', 'HOME': td, 'LANG': 'C', + 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null'} + for name, count, size, expected in [ + ('clean-1x1KiB', 1, 1024, 'clean'), + ('clean-16x1KiB', 16, 1024, 'clean'), + ('clean-64x1KiB', 64, 1024, 'clean'), + ('clean-1x1MiB', 1, 1024*1024, 'clean'), + ('synthetic-private-key', 1, None, 'blocked'), + ]: + repo = root / name + repo.mkdir() + command(['git', '-c', 'core.hooksPath=/dev/null', 'init', '-q', str(repo)], root, env=env) + if size is None: + command(['openssl', 'genrsa', '-traditional', '-out', str(repo/'fixture.txt'), '2048'], repo, env=env) + byte_count = (repo/'fixture.txt').stat().st_size + else: + for i in range(count): + # Distinct contents avoid Git-object deduplication. + prefix = f'public synthetic baseline row {i:03d}\n'.encode() + (repo/f'fixture-{i}.txt').write_bytes(prefix + b'a'*(size-len(prefix))) + byte_count = count * size + command(['git', '-C', str(repo), 'add', '--all'], repo, env=env) + samples = [] + # The first call is retained separately, not silently discarded. + for repeat in range(8): + cpu_before = resource.getrusage(resource.RUSAGE_CHILDREN) + start = time.perf_counter_ns() + result = adapter.scan_staged(repo, BINARY) + wall_ms = (time.perf_counter_ns()-start)/1e6 + cpu_after = resource.getrusage(resource.RUSAGE_CHILDREN) + sample = {'repeat': repeat, 'wall_ms': round(wall_ms, 3), + 'child_cpu_ms': round(1000*((cpu_after.ru_utime+cpu_after.ru_stime)-(cpu_before.ru_utime+cpu_before.ru_stime)), 3), + 'status': result['status'], 'scanned_blobs': result['scanned_blobs']} + samples.append(sample) + if result['status'] != expected or result['scanned_blobs'] != count: + raise RuntimeError('synthetic baseline expectation failed') + ordered = sorted(item['wall_ms'] for item in samples[1:]) + observations.append({'case': name, 'bytes': byte_count, 'unique_blobs': count, + 'samples': samples, 'warm_samples': len(ordered), + 'warm_p50_ms': statistics.median(ordered), + 'warm_p95_nearest_rank_ms': ordered[math.ceil(.95*len(ordered))-1], + 'warm_median_MiB_per_second': round(byte_count/(1024*1024)/(statistics.median(ordered)/1000), 3)}) + negative = [] + for label, binary in [('missing-scanner', root/'absent-scanner'), ('wrong-scanner-digest', Path('/usr/bin/true'))]: + start = time.perf_counter_ns() + result = adapter.scan_staged(repo, binary) + negative.append({'case': label, 'status': result['status'], 'reason': result.get('reason'), + 'scanned_blobs': result['scanned_blobs'], 'wall_ms': round((time.perf_counter_ns()-start)/1e6, 3)}) + if result['status'] != 'unverified' or adapter.exit_code(result) != 2: + raise RuntimeError('unavailable scanner did not refuse') + receipt = {'observed_at': datetime.now(timezone.utc).isoformat(), + 'scope': 'Actual local repo-manager staged-object adapter; generated disposable fixtures only. Isolated candidate, not admitted; not receive-path, CI, native remote coverage, workload latency or TSA-15 closure.', + 'benchmark_sha256': sha(Path(__file__)), 'adapter_path': str(SOURCE), + 'adapter_sha256': sha(SOURCE), 'scanner_sha256': sha(BINARY), + 'scanner_version': adapter.VERSION, 'platform': os.uname().sysname+' '+os.uname().machine, + 'timer': 'perf_counter_ns; child CPU from RUSAGE_CHILDREN deltas', + 'percentile_method': 'nearest rank, seven warm repetitions; descriptive sample, not production tail estimate', + 'observations': observations, 'negative_cases': negative, + 'fixture_removed': not root.exists(), 'provider_calls': 0, 'real_credentials_read': False, + 'candidate_source_unchanged': sha(SOURCE) == source_before, + 'scanner_unchanged': sha(BINARY) == binary_before} + (HERE/'scanner-candidate.json').write_text(json.dumps(receipt, indent=2)+'\n') + print(json.dumps({'ok': True, 'cases': len(observations), 'negative_cases': len(negative), 'fixture_removed': not root.exists()})) + + +if __name__ == '__main__': + main() diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/owner-workplan.patch b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/owner-workplan.patch new file mode 100644 index 00000000..620c3653 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/owner-workplan.patch @@ -0,0 +1,51 @@ +--- a/workplans/RMGR-WP-0021-secret-detection-controls.md ++++ b/workplans/RMGR-WP-0021-secret-detection-controls.md +@@ -8,7 +8,7 @@ + owner: codex + topic_slug: repo-manager + created: "2026-10-12" +-updated: "2026-10-12" ++updated: "2026-10-17" + flavor: implementation + depends_on: [] + state_hub_workstream_id: "5aa6f84a-c682-5996-ae26-96daef670833" +@@ -149,3 +149,39 @@ + execute founder-deferred rotations or treat suppressed output as remediation. + Close only after native coverage and response evidence are adequate for the + applicable DSM-DETECT requirements; retain residuals as live work records. ++ ++ ++## Publish measured staged-scanner batching and verify runtime routing ++ ++```task ++id: RMGR-WP-0021-T05 ++status: todo ++priority: medium ++``` ++ ++2026-10-17 prepared refinement for the local staged-object adapter. Independent ++review and a coordinator-executed generated-fixture run cover the exact source ++SHA256 `e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400`: ++42 tests, 13 exact old/new summary parity cases, immutable scanner snapshot, ++volatile-only bounded batches, watchdog and parent-loss refusal/cleanup. Warm ++p50 for 1/16/64 distinct 1 KiB blobs is 0.835/0.884/0.904 seconds, compared with ++0.702/12.065/49.172 seconds in the retained baseline. The one-blob case regressed; ++this is descriptive local fixture evidence, not aggregate workload or tail ++acceptance. Larger blobs retain the original stdin route. ++ ++Publish the reviewed patch against the exact owner base, verify the configured ++shared and Custodian hooks still resolve to the admitted owner source, then run ++clean/private-key/scanner-unavailable generated alternate-index probes without ++reading or changing user source/indexes. Verify actual CLI import routing; the ++repo-local `.venv/bin/rmgr` imports owner source, but the previously advertised ++`/home/worsch/.local/bin/rmgr` is absent in the read-only observation and must not ++be described as installed operational coverage. Reconcile that exact CLI route ++before claiming CLI publication. Preserve existing hook guards and routing; ++retain rollback to the exact prior adapter bytes. Source publication and runtime ++acceptance must have separate receipts. ++ ++Review packet: Custodian ++`docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/`. ++No canonical owner mutation or runtime admission has occurred in the packet. ++This task does not close T03/T04, independent receive/CI coverage, source-control ++bypass limits or whole TSA15. diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-driver-result.txt b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-driver-result.txt new file mode 100644 index 00000000..b2e62cca --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-driver-result.txt @@ -0,0 +1 @@ +{"parity_cases": 13, "fixtures_removed": true} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-results.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-results.json new file mode 100644 index 00000000..47a1a319 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity-results.json @@ -0,0 +1,101 @@ +{ + "cases": [ + { + "case": "empty-index", + "equal": true, + "status": "clean", + "scanned_blobs": 0, + "blocked_blobs": 0 + }, + { + "case": "clean-two-distinct", + "equal": true, + "status": "clean", + "scanned_blobs": 2, + "blocked_blobs": 0 + }, + { + "case": "original-ignore-path", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "duplicate-attribution", + "equal": true, + "status": "blocked", + "scanned_blobs": 2, + "blocked_blobs": 1 + }, + { + "case": "boundary-negative", + "equal": true, + "status": "clean", + "scanned_blobs": 2, + "blocked_blobs": 0 + }, + { + "case": "nul-prefix", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "base64", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "4096-boundary", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "4097-fallback", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "100000-boundary", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "100001-boundary", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + }, + { + "case": "archive-magic", + "equal": true, + "status": "clean", + "scanned_blobs": 1, + "blocked_blobs": 0 + }, + { + "case": "inline-allow", + "equal": true, + "status": "blocked", + "scanned_blobs": 1, + "blocked_blobs": 1 + } + ], + "fixtures_removed": true, + "before_sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d", + "candidate_sha256": "e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "provider_calls": 0, + "real_credentials_read": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity_scanner.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity_scanner.py new file mode 100644 index 00000000..c2619ec3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/parity_scanner.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Compare exact old/new summaries using generated data only, retain no contents.""" +import base64 +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent +BINARY = Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks') +CANDIDATE = Path('/tmp/rmgr-scanner-batch-r1/src/repo_manager/secret_scan.py') + + +def load(name, source): + spec = importlib.util.spec_from_file_location(name, source) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + old = load('before', HERE / 'secret_scan.before.py') + new = load('candidate', CANDIDATE) + records = [] + with tempfile.TemporaryDirectory(prefix='rmgr-parity-') as td: + root = Path(td) + env = {'PATH': '/usr/bin:/bin', 'HOME': td, 'LANG': 'C', + 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null'} + def cmd(args, cwd): + return subprocess.check_output(args, cwd=cwd, env=env, stderr=subprocess.DEVNULL) + cmd(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(root / 'key')], root) + key = (root / 'key').read_bytes() + cases = [ + ('empty-index', []), + ('clean-two-distinct', [('one', b'first clean\n'), ('two', b'second clean\n')]), + ('original-ignore-path', [('vendor/modules.txt', key), ('secret.png', key)]), + ('duplicate-attribution', [('key', key), ('copy', key), ('clean', b'clean\n')]), + ('boundary-negative', [('left', key[:15]), ('right', key[15:])]), + ('nul-prefix', [('key', b'\x00\x01\x02\n' + key)]), + ('base64', [('key', base64.b64encode(key))]), + ('4096-boundary', [('key', b'\n'*(4096-len(key)) + key)]), + ('4097-fallback', [('key', b'\n'*(4097-len(key)) + key)]), + ('100000-boundary', [('key', b'\n'*(100000-len(key)) + key)]), + ('100001-boundary', [('key', b'\n'*(100001-len(key)) + key)]), + ('archive-magic', [('key', b'PK\x03\x04' + b'\x00'*26 + key)]), + ('inline-allow', [('key', b'# gitleaks:allow\n' + key)]), + ] + for name, files in cases: + repo = root / name; repo.mkdir() + cmd(['git', '-c', 'core.hooksPath=/dev/null', 'init', '-q', str(repo)], root) + for path, content in files: + target = repo / path; target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + cmd(['git', 'add', '--all'], repo) + before = old.scan_staged(repo, BINARY) + after = new.scan_staged(repo, BINARY) + if before != after: + raise RuntimeError('summary parity failed: ' + name) + records.append({'case': name, 'equal': True, 'status': after['status'], + 'scanned_blobs': after['scanned_blobs'], + 'blocked_blobs': len(after['blocked_blob_ids'])}) + receipt = {'cases': records, 'fixtures_removed': not root.exists(), + 'before_sha256': hashlib.sha256((HERE/'secret_scan.before.py').read_bytes()).hexdigest(), + 'candidate_sha256': hashlib.sha256(CANDIDATE.read_bytes()).hexdigest(), + 'scanner_sha256': hashlib.sha256(BINARY.read_bytes()).hexdigest(), + 'provider_calls': 0, 'real_credentials_read': False} + (HERE/'parity-results.json').write_text(json.dumps(receipt, indent=2)+'\n') + print(json.dumps({'parity_cases': len(records), 'fixtures_removed': not root.exists()})) + + +if __name__ == '__main__': + main() diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/probe_configured_hooks.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/probe_configured_hooks.py new file mode 100644 index 00000000..cdd1981c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/probe_configured_hooks.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Reviewed-fixture-only probes of two actual hooks; never opens a real index. + +No hook/scanner/source/config or Git commit/ref mutation, network/provider calls, +real credential reads, or unavailable-scanner injection. The only persistent +write is the requested fixed-metadata receipt in this review packet. +""" +from __future__ import annotations + +import functools +import hashlib +import json +import os +from pathlib import Path +import signal +import stat +import subprocess +import sys +import types + +SOURCE = Path('/home/worsch/repo-manager/src/repo_manager/secret_scan.py') +SOURCE_SHA = 'e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400' +BINARY = Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks') +BINARY_SHA = '88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509' +HOOK_SHA = 'd50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd' +ROOTS = ( + (Path('/home/worsch/repo-manager'), Path('/home/worsch/repo-manager/.githooks/pre-commit')), + (Path('/home/worsch/the-custodian'), Path('/home/worsch/the-custodian/.git/hooks/pre-commit')), +) +HERE = Path(__file__).resolve().parent + + +class Refused(Exception): + pass + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def info(path): + try: + item = path.lstat() + except FileNotFoundError: + return None + return (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns, item.st_mode) + + +def fixed_env(): + return {'PATH': '/usr/bin:/bin', 'HOME': '/home/worsch', 'LANG': 'C', + 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_NO_REPLACE_OBJECTS': '1'} + + +def run(argv, root, env, *, data=None, adapter=None): + kwargs = {} + if adapter is not None: + kwargs['preexec_fn'] = functools.partial(adapter.child_limits, 16 * 1024**2, os.getpid()) + result = subprocess.run(argv, cwd=root, env=env, input=data, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, timeout=20, check=False, **kwargs) + if result.returncode: + raise Refused('fixture-command-failed') + return result.stdout + + +def git(root, env, *args, data=None, adapter=None): + return run(['/usr/bin/git', '-C', str(root), *args], root, env, data=data, adapter=adapter) + + +def precheck(): + if digest(SOURCE) != SOURCE_SHA or digest(BINARY) != BINARY_SHA: + raise Refused('source-or-scanner-drift') + binary_info = BINARY.lstat() + if (not stat.S_ISREG(binary_info.st_mode) or binary_info.st_mode & 0o022 + or not binary_info.st_mode & 0o111 or binary_info.st_uid not in (0, os.getuid())): + raise Refused('scanner-mode-refused') + records = [] + for root, hook in ROOTS: + env = fixed_env() + raw = git(root, env, 'rev-parse', '--git-path', 'hooks/pre-commit').decode().strip() + routed = Path(raw) + if not routed.is_absolute(): + routed = root / routed + if routed.resolve() != hook.resolve() or digest(hook) != HOOK_SHA: + raise Refused('hook-routing-drift') + raw_index = git(root, env, 'rev-parse', '--git-path', 'index').decode().strip() + index = Path(raw_index) + if not index.is_absolute(): + index = root / index + objects = git(root, env, 'rev-parse', '--git-path', 'objects').decode().strip() + if not Path(objects).is_absolute(): + objects = str(root / objects) + records.append({'root': root, 'hook': hook, 'index': index, + 'index_stat': info(index), 'head': git(root, env, 'rev-parse', 'HEAD').strip(), + 'objects': objects}) + return records + + +def main(): + receipt = {'status': 'unverified', 'scope': 'actual configured shared and Custodian hooks; generated alternate-index fixtures only', + 'source_sha256': SOURCE_SHA, 'scanner_sha256': BINARY_SHA, 'hook_sha256': HOOK_SHA, + 'provider_calls': 0, 'real_credentials_read': False, 'real_index_opened': False, + 'actual_hook_unavailable_scanner': 'pending; no host scanner mutation performed'} + fixture = None + observations = [] + try: + records = precheck() + # Execute only the hash-bound module bytes already reviewed. This module + # provides the reviewed tmpfs/watchdog/child-death lifecycle primitives. + source_bytes = SOURCE.read_bytes() + if hashlib.sha256(source_bytes).hexdigest() != SOURCE_SHA: + raise Refused('source-drift') + adapter = types.ModuleType('reviewed_scanner_probe_storage') + exec(compile(source_bytes, str(SOURCE), 'exec'), adapter.__dict__) + prior_storage = set(Path('/dev/shm').glob('rmgr-secret-scan-*')) + with adapter.private_storage() as (fixture, watcher): + env = fixed_env() + env['GIT_CONFIG_GLOBAL'] = '/dev/null' + # Temporary-index reads must not invoke a configured fsmonitor or + # create split-index auxiliaries in the real repository directory. + overrides = [('core.fsmonitor', 'false'), ('core.splitIndex', 'false'), + ('core.sparseCheckout', 'false'), ('core.untrackedCache', 'false')] + env['GIT_CONFIG_COUNT'] = str(len(overrides)) + for number, (name, value) in enumerate(overrides): + env[f'GIT_CONFIG_KEY_{number}'] = name + env[f'GIT_CONFIG_VALUE_{number}'] = value + key_path = fixture / 'generated-fixture-key' + run(['/usr/bin/ssh-keygen', '-q', '-t', 'ed25519', '-N', '', + '-C', 'public-generated-hook-probe', '-f', str(key_path)], fixture, env, + adapter=adapter) + key = key_path.read_bytes() + for number, record in enumerate(records): + root = record['root']; hook = record['hook'] + local = fixture / str(number); local.mkdir(mode=0o700) + objects = local / 'objects'; objects.mkdir(mode=0o700) + probe_env = dict(env, GIT_INDEX_FILE=str(local / 'index'), + GIT_OBJECT_DIRECTORY=str(objects), + GIT_ALTERNATE_OBJECT_DIRECTORIES=record['objects']) + name = '__rmgr_scanner_batch_generated_fixture__.txt' + if git(root, probe_env, 'ls-tree', '--name-only', 'HEAD', '--', name, adapter=adapter): + raise Refused('fixture-name-collision') + row = {'repo': root.name, 'cases': []} + for case, payload, expected, expected_exit in [ + ('clean', b'public generated clean hook probe\n', 'clean', 0), + ('generated-private-key', key, 'blocked', 1), + ]: + if watcher.poll() is not None: + raise Refused('fixture-watchdog-lost') + # HEAD metadata seeds only the temporary index. No worktree + # files, existing blob contents, real index or refs are read. + git(root, probe_env, 'read-tree', 'HEAD', adapter=adapter) + oid = git(root, probe_env, 'hash-object', '-w', '--stdin', data=payload, + adapter=adapter).decode().strip() + git(root, probe_env, 'update-index', '--add', '--cacheinfo', + '100644,' + oid + ',' + name, adapter=adapter) + changed = git(root, probe_env, 'diff', '--cached', '--name-only', '-z', + '--no-renames', '--no-ext-diff', '--no-textconv', adapter=adapter) + if changed != name.encode() + b'\0': + raise Refused('fixture-changed-object-scope-refused') + result = subprocess.run([str(hook)], cwd=root, env=probe_env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, + check=False, preexec_fn=functools.partial(adapter.child_limits, + 64 * 1024**2, os.getpid())) + if result.stderr or key.strip() in result.stdout: + raise Refused('hook-output-refused') + body = json.loads(result.stdout) + if set(body) != {'status', 'scope', 'scanner_version', 'scanner_sha256', + 'scanned_blobs', 'blocked_blob_ids'}: + raise Refused('hook-summary-schema-refused') + if (result.returncode != expected_exit or body['status'] != expected + or body['scope'] != 'changed-staged-blobs' + or body['scanner_version'] != '8.30.1' + or body['scanner_sha256'] != BINARY_SHA or body['scanned_blobs'] != 1 + or body['blocked_blob_ids'] != ([oid] if expected == 'blocked' else [])): + raise Refused('hook-result-refused') + row['cases'].append({'case': case, 'status': expected, + 'exit_code': expected_exit, 'scanned_blobs': 1}) + observations.append(row) + # Watcher normal completion, source children and fixture directory cleanup + # have all completed before these postconditions are evaluated. + if fixture.exists() or set(Path('/dev/shm').glob('rmgr-secret-scan-*')) - prior_storage: + raise Refused('fixture-cleanup-unverified') + again = precheck() + for before, after in zip(records, again): + if before['index_stat'] != after['index_stat'] or before['head'] != after['head']: + raise Refused('real-repository-state-changed') + receipt.update(status='verified', cases=observations, fixture_removed=True, + new_scan_storage_absent=True, real_index_stat_unchanged=True, + real_head_unchanged=True, routing_and_artifacts_unchanged=True) + except Refused as error: + receipt['reason'] = error.args[0] + except (OSError, ValueError, subprocess.SubprocessError, adapter.Unverified if 'adapter' in locals() else OSError): + receipt['reason'] = 'probe-unavailable' + receipt['fixture_removed'] = fixture is None or not fixture.exists() + receipt['driver_sha256'] = digest(Path(__file__)) + (HERE / 'configured-hook-results.json').write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps({'status': receipt['status'], 'fixture_removed': receipt['fixture_removed'], + 'cases': sum(len(row['cases']) for row in observations)})) + return 0 if receipt['status'] == 'verified' else 2 + + +if __name__ == '__main__': + for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP): + signal.signal(signum, lambda *_: (_ for _ in ()).throw(Refused('probe-interrupted'))) + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/publication-plan.md b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/publication-plan.md new file mode 100644 index 00000000..882cc404 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/publication-plan.md @@ -0,0 +1,57 @@ +# Proposed owner publication and runtime acceptance + +Nothing in this plan has been executed against canonical owner files. The local +bounded staged-scanner candidate was independently reviewed and fixture-validated. +Whole TSA15, remote receive/CI coverage and independent source-control bypass +handling remain open under the existing owner records. + +1. Reconfirm repo-manager HEAD is `97ea2489257de08911cc5da1e1b84d8e15780dc3`, + the worktree/index are clean, and both base file SHA256 values match + `manifest.json`. Recheck the pinned scanner and current hook hashes. Stop for + drift; do not overwrite other work or reinterpret a different revision as the + reviewed candidate. +2. Apply `candidate.patch` and `owner-workplan.patch` only in repo-manager. T05 + starts `todo` because source publication and actual routing acceptance are + pending. Move it to `progress` when authorized owner work begins. Preserve + existing T01/T02 done evidence and T03/T04 open statuses. +3. Run the 42 scanner tests against the canonical source with the exact scanner + pin. Record source import path/hash and subprocess cleanup results. The + validated candidate's runtime requires Linux pidfds, parent-death signals, + sealing, and private tmpfs at `/dev/shm`; inability to establish these refuses. +4. Verify runtime routing without scanning any user index. The shared hook and + Custodian local hook both call the canonical source directly using `python3`; + the verified hook SHA256 is + `d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd`. + Neither hook content nor Git routing needs to change. The repo-local + `/home/worsch/repo-manager/.venv/bin/rmgr` uses that venv's Python and imports + canonical source. `/home/worsch/.local/bin/rmgr` was absent; choose and record + the explicit repo-local CLI route for publication, or separately review an + installation repair before claiming a global installed CLI. +5. Reuse the existing reviewed alternate-index/object-store probe pattern in + `docs/changes/CUST-WP-0080/activate_secret_hooks.py` to exercise actual shared + and Custodian hook commands with clean, disposable private-key and unavailable + scanner cases. Prepare a bounded generated-fixture driver and exact revision + before execution. Never point these probes at a real user index, alter real + scanner permissions to simulate failure, or change user source/branches. + The earlier executor temporarily changed actual scanner permissions; do not + reuse that failure branch unchanged. Actual-hook unavailability requires a + separately reviewed namespace fixture that shadows the exact scanner path + with an unavailable disposable binary without mutating the host path. Until + that exists, report source-adapter unavailable-scanner tests separately and + keep actual-hook unavailability pending. Recheck exact hooks/routing and verify + all fixture directories, indexes, + object stores, subprocesses and per-scan tmpfs directories are gone. +6. Record source publication separately from actual configured-hook and CLI + acceptance. Publish owner Git changes through the verified repo-local + `.venv/bin/rmgr sync --path /home/worsch/repo-manager --push` workflow only in + the coordinator's authorized scope. Verify the pushed revision and owner + projection receipt; pending sync is not completed publication. Complete T05 + only after its explicit runtime checks and publication receipts exist. +7. Rollback restores `secret_scan.before.py` only after checking the exact + executed revision and preserving intervening work, then reruns generated + clean/blocked/unavailable probes. Do not change hooks, scanner bytes, ignore + policy, receive enforcement or CI as an optimization rollback. Record the + failure and rolled-back outcome rather than claiming a successful admission. + +The existing workplan remains active. No residual is silently implemented or +parked as a completed workplan's prose-only leftover. diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/run_validation.sh b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/run_validation.sh new file mode 100644 index 00000000..6bbbe06e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/run_validation.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail +export PYTHONPATH=/tmp/rmgr-scanner-batch-r1/src +export RMGR_TEST_GITLEAKS=/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks +export PYTHONDONTWRITEBYTECODE=1 +artifact=/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1 +/home/worsch/repo-manager/.venv/bin/python -c 'from repo_manager import secret_scan; print(secret_scan.__file__)' +/home/worsch/repo-manager/.venv/bin/pytest -q /tmp/rmgr-scanner-batch-r1/tests/test_secret_scan.py --override-ini pythonpath=/tmp/rmgr-scanner-batch-r1/src --tb=short > "$artifact/test-results.txt" 2>&1 +/home/worsch/repo-manager/.venv/bin/python "$artifact/parity_scanner.py" > "$artifact/parity-driver-result.txt" +/home/worsch/repo-manager/.venv/bin/python "$artifact/measure_scanner.py" > "$artifact/benchmark-driver-result.txt" diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/runtime-routing-readonly.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/runtime-routing-readonly.json new file mode 100644 index 00000000..86b83232 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/runtime-routing-readonly.json @@ -0,0 +1,20 @@ +{ + "scope": "Read-only routing observation; no hook execution or actual user index scan", + "shared_hook": "/home/worsch/repo-manager/.githooks/pre-commit", + "custodian_hook": "/home/worsch/the-custodian/.git/hooks/pre-commit", + "hook_sha256": "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd", + "shared_routing_origin": "/home/worsch/.gitconfig", + "custodian_routing_origin": ".git/config", + "hook_command": "python3 /home/worsch/repo-manager/src/repo_manager/secret_scan.py --path . --binary /home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks", + "owner_module_sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d", + "repo_local_cli": "/home/worsch/repo-manager/.venv/bin/rmgr", + "repo_local_cli_shebang": "/home/worsch/repo-manager/.venv/bin/python3", + "repo_local_cli_import": "/home/worsch/repo-manager/src/repo_manager/secret_scan.py", + "advertised_global_cli": "/home/worsch/.local/bin/rmgr", + "global_cli_lexists": false, + "fresh_python_shutil_which_rmgr": null, + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "scanner_uid": 1000, + "scanner_mode": "0755", + "canonical_owner_unchanged": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate-pre-tmpfs.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate-pre-tmpfs.json new file mode 100644 index 00000000..0e0314c1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate-pre-tmpfs.json @@ -0,0 +1,370 @@ +{ + "observed_at": "2026-10-16T21:54:56.246092+00:00", + "scope": "Actual local repo-manager staged-object adapter; generated disposable fixtures only. Isolated candidate, not admitted; not receive-path, CI, native remote coverage, workload latency or TSA-15 closure.", + "benchmark_sha256": "64a23195acc944d72d86fd3475cfee3793c9668c243be8f700252a4e2f6e07ea", + "adapter_path": "/tmp/rmgr-scanner-batch-r1/src/repo_manager/secret_scan.py", + "adapter_sha256": "3e050b8fb6eacadafeda89c1359cd41dd7de4e6aec589500543e71a9f1c8d912", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "scanner_version": "8.30.1", + "platform": "Linux x86_64", + "timer": "perf_counter_ns; child CPU from RUSAGE_CHILDREN deltas", + "percentile_method": "nearest rank, seven warm repetitions; descriptive sample, not production tail estimate", + "observations": [ + { + "case": "clean-1x1KiB", + "bytes": 1024, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 844.617, + "child_cpu_ms": 885.127, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 852.748, + "child_cpu_ms": 861.88, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 898.323, + "child_cpu_ms": 899.353, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 1350.571, + "child_cpu_ms": 1396.562, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 892.773, + "child_cpu_ms": 921.18, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 1221.998, + "child_cpu_ms": 1278.625, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 1212.14, + "child_cpu_ms": 1247.379, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 1057.982, + "child_cpu_ms": 1077.583, + "status": "clean", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 1057.982, + "warm_p95_nearest_rank_ms": 1350.571, + "warm_median_MiB_per_second": 0.001 + }, + { + "case": "clean-16x1KiB", + "bytes": 16384, + "unique_blobs": 16, + "samples": [ + { + "repeat": 0, + "wall_ms": 1102.96, + "child_cpu_ms": 1129.128, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 1, + "wall_ms": 1048.106, + "child_cpu_ms": 1066.386, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 2, + "wall_ms": 1253.198, + "child_cpu_ms": 1291.404, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 3, + "wall_ms": 1056.019, + "child_cpu_ms": 1097.947, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 4, + "wall_ms": 1015.067, + "child_cpu_ms": 1036.418, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 5, + "wall_ms": 1016.387, + "child_cpu_ms": 1050.141, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 6, + "wall_ms": 1067.741, + "child_cpu_ms": 1070.473, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 7, + "wall_ms": 1006.301, + "child_cpu_ms": 1030.995, + "status": "clean", + "scanned_blobs": 16 + } + ], + "warm_samples": 7, + "warm_p50_ms": 1048.106, + "warm_p95_nearest_rank_ms": 1253.198, + "warm_median_MiB_per_second": 0.015 + }, + { + "case": "clean-64x1KiB", + "bytes": 65536, + "unique_blobs": 64, + "samples": [ + { + "repeat": 0, + "wall_ms": 1130.747, + "child_cpu_ms": 1170.592, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 1, + "wall_ms": 1138.654, + "child_cpu_ms": 1132.462, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 2, + "wall_ms": 1176.231, + "child_cpu_ms": 1211.938, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 3, + "wall_ms": 1131.997, + "child_cpu_ms": 1161.589, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 4, + "wall_ms": 1135.43, + "child_cpu_ms": 1147.329, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 5, + "wall_ms": 1136.396, + "child_cpu_ms": 1176.039, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 6, + "wall_ms": 1188.481, + "child_cpu_ms": 1198.842, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 7, + "wall_ms": 856.473, + "child_cpu_ms": 860.987, + "status": "clean", + "scanned_blobs": 64 + } + ], + "warm_samples": 7, + "warm_p50_ms": 1136.396, + "warm_p95_nearest_rank_ms": 1188.481, + "warm_median_MiB_per_second": 0.055 + }, + { + "case": "clean-1x1MiB", + "bytes": 1048576, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 1843.761, + "child_cpu_ms": 1933.58, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 1820.879, + "child_cpu_ms": 1938.028, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 1770.395, + "child_cpu_ms": 1884.46, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 2793.449, + "child_cpu_ms": 2988.36, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 2430.372, + "child_cpu_ms": 2585.196, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 2216.581, + "child_cpu_ms": 2307.441, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 2015.729, + "child_cpu_ms": 2154.6, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 2144.403, + "child_cpu_ms": 2268.378, + "status": "clean", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 2144.403, + "warm_p95_nearest_rank_ms": 2793.449, + "warm_median_MiB_per_second": 0.466 + }, + { + "case": "synthetic-private-key", + "bytes": 1679, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 966.045, + "child_cpu_ms": 962.95, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 1290.468, + "child_cpu_ms": 1328.337, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 1098.709, + "child_cpu_ms": 1134.349, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 1276.263, + "child_cpu_ms": 1332.774, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 1027.382, + "child_cpu_ms": 1038.319, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 946.017, + "child_cpu_ms": 984.236, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 1046.621, + "child_cpu_ms": 1102.267, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 1042.157, + "child_cpu_ms": 1098.071, + "status": "blocked", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 1046.621, + "warm_p95_nearest_rank_ms": 1290.468, + "warm_median_MiB_per_second": 0.002 + } + ], + "negative_cases": [ + { + "case": "missing-scanner", + "status": "unverified", + "reason": "scan-unavailable", + "scanned_blobs": 0, + "wall_ms": 0.12 + }, + { + "case": "wrong-scanner-digest", + "status": "unverified", + "reason": "scanner-file-refused", + "scanned_blobs": 0, + "wall_ms": 0.059 + } + ], + "fixture_removed": true, + "provider_calls": 0, + "real_credentials_read": false, + "candidate_source_unchanged": true, + "scanner_unchanged": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate.json b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate.json new file mode 100644 index 00000000..e62ac216 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/scanner-candidate.json @@ -0,0 +1,370 @@ +{ + "observed_at": "2026-10-16T22:02:26.299135+00:00", + "scope": "Actual local repo-manager staged-object adapter; generated disposable fixtures only. Isolated candidate, not admitted; not receive-path, CI, native remote coverage, workload latency or TSA-15 closure.", + "benchmark_sha256": "64a23195acc944d72d86fd3475cfee3793c9668c243be8f700252a4e2f6e07ea", + "adapter_path": "/tmp/rmgr-scanner-batch-r1/src/repo_manager/secret_scan.py", + "adapter_sha256": "e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "scanner_version": "8.30.1", + "platform": "Linux x86_64", + "timer": "perf_counter_ns; child CPU from RUSAGE_CHILDREN deltas", + "percentile_method": "nearest rank, seven warm repetitions; descriptive sample, not production tail estimate", + "observations": [ + { + "case": "clean-1x1KiB", + "bytes": 1024, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 893.468, + "child_cpu_ms": 881.557, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 885.5, + "child_cpu_ms": 867.973, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 835.286, + "child_cpu_ms": 873.731, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 834.08, + "child_cpu_ms": 854.397, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 830.389, + "child_cpu_ms": 857.233, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 931.695, + "child_cpu_ms": 934.367, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 883.212, + "child_cpu_ms": 893.292, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 834.664, + "child_cpu_ms": 852.063, + "status": "clean", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 835.286, + "warm_p95_nearest_rank_ms": 931.695, + "warm_median_MiB_per_second": 0.001 + }, + { + "case": "clean-16x1KiB", + "bytes": 16384, + "unique_blobs": 16, + "samples": [ + { + "repeat": 0, + "wall_ms": 833.649, + "child_cpu_ms": 847.682, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 1, + "wall_ms": 884.239, + "child_cpu_ms": 909.121, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 2, + "wall_ms": 836.421, + "child_cpu_ms": 871.648, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 3, + "wall_ms": 965.383, + "child_cpu_ms": 993.295, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 4, + "wall_ms": 840.22, + "child_cpu_ms": 879.648, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 5, + "wall_ms": 890.157, + "child_cpu_ms": 904.13, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 6, + "wall_ms": 833.55, + "child_cpu_ms": 874.533, + "status": "clean", + "scanned_blobs": 16 + }, + { + "repeat": 7, + "wall_ms": 895.576, + "child_cpu_ms": 900.844, + "status": "clean", + "scanned_blobs": 16 + } + ], + "warm_samples": 7, + "warm_p50_ms": 884.239, + "warm_p95_nearest_rank_ms": 965.383, + "warm_median_MiB_per_second": 0.018 + }, + { + "case": "clean-64x1KiB", + "bytes": 65536, + "unique_blobs": 64, + "samples": [ + { + "repeat": 0, + "wall_ms": 846.557, + "child_cpu_ms": 885.481, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 1, + "wall_ms": 953.142, + "child_cpu_ms": 960.011, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 2, + "wall_ms": 899.775, + "child_cpu_ms": 906.141, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 3, + "wall_ms": 904.024, + "child_cpu_ms": 940.226, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 4, + "wall_ms": 900.545, + "child_cpu_ms": 929.867, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 5, + "wall_ms": 893.788, + "child_cpu_ms": 909.19, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 6, + "wall_ms": 1144.16, + "child_cpu_ms": 1200.029, + "status": "clean", + "scanned_blobs": 64 + }, + { + "repeat": 7, + "wall_ms": 929.213, + "child_cpu_ms": 936.338, + "status": "clean", + "scanned_blobs": 64 + } + ], + "warm_samples": 7, + "warm_p50_ms": 904.024, + "warm_p95_nearest_rank_ms": 1144.16, + "warm_median_MiB_per_second": 0.069 + }, + { + "case": "clean-1x1MiB", + "bytes": 1048576, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 1784.665, + "child_cpu_ms": 1891.776, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 1801.742, + "child_cpu_ms": 1916.564, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 1947.073, + "child_cpu_ms": 2072.659, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 1761.028, + "child_cpu_ms": 1876.085, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 2524.1, + "child_cpu_ms": 2683.968, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 2464.395, + "child_cpu_ms": 2627.421, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 1810.765, + "child_cpu_ms": 1907.677, + "status": "clean", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 1902.956, + "child_cpu_ms": 1998.419, + "status": "clean", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 1902.956, + "warm_p95_nearest_rank_ms": 2524.1, + "warm_median_MiB_per_second": 0.525 + }, + { + "case": "synthetic-private-key", + "bytes": 1675, + "unique_blobs": 1, + "samples": [ + { + "repeat": 0, + "wall_ms": 888.022, + "child_cpu_ms": 897.98, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 1, + "wall_ms": 845.49, + "child_cpu_ms": 879.73, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 2, + "wall_ms": 882.783, + "child_cpu_ms": 881.311, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 3, + "wall_ms": 949.596, + "child_cpu_ms": 937.631, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 4, + "wall_ms": 883.517, + "child_cpu_ms": 871.651, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 5, + "wall_ms": 882.714, + "child_cpu_ms": 896.918, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 6, + "wall_ms": 877.168, + "child_cpu_ms": 879.988, + "status": "blocked", + "scanned_blobs": 1 + }, + { + "repeat": 7, + "wall_ms": 833.886, + "child_cpu_ms": 865.671, + "status": "blocked", + "scanned_blobs": 1 + } + ], + "warm_samples": 7, + "warm_p50_ms": 882.714, + "warm_p95_nearest_rank_ms": 949.596, + "warm_median_MiB_per_second": 0.002 + } + ], + "negative_cases": [ + { + "case": "missing-scanner", + "status": "unverified", + "reason": "scan-unavailable", + "scanned_blobs": 0, + "wall_ms": 0.134 + }, + { + "case": "wrong-scanner-digest", + "status": "unverified", + "reason": "scanner-digest-mismatch", + "scanned_blobs": 0, + "wall_ms": 0.222 + } + ], + "fixture_removed": true, + "provider_calls": 0, + "real_credentials_read": false, + "candidate_source_unchanged": true, + "scanner_unchanged": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.before.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.before.py new file mode 100644 index 00000000..9422eb51 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.before.py @@ -0,0 +1,144 @@ +"""Content-silent adapter for staged-blob secret detection with pinned Gitleaks. + +This is a repository control adapter, not credential custody or an authorization +boundary. It does not install hooks, alter indexes, send content off-host, or +claim history/push/CI coverage. Its caller must treat unverified as refusal. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import subprocess +import tempfile +from pathlib import Path + +VERSION = '8.30.1' +BINARY_SHA256 = '88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509' +MAX_BLOB_BYTES = 8 * 1024 * 1024 +MAX_TOTAL_BYTES = 64 * 1024 * 1024 +MAX_BLOBS = 500 +CONFIG = '[extend]\nuseDefault = true\n' + + +class Unverified(Exception): + """A fixed reason code, never a tool exception or repository content.""" + + +def git(repo: Path, *args: str) -> bytes: + env = {**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'} + result = subprocess.run(['git', '-C', str(repo), *args], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, env=env, timeout=20, check=False) + if result.returncode: + raise Unverified('git-read-failed') + return result.stdout + + +def staged_objects(repo: Path) -> tuple[bytes, list[str]]: + snapshot = git(repo, 'ls-files', '--stage', '-z') + changed = set(git(repo, 'diff', '--cached', '--name-only', '-z', '--no-renames', + '--no-ext-diff', '--no-textconv', '--diff-filter=ACMRTUXB').split(b'\0')) - {b''} + objects: set[str] = set() + seen: set[bytes] = set() + for record in snapshot.split(b'\0'): + if not record: + continue + header, path = record.split(b'\t', 1) + if path not in changed: + continue + mode, oid, stage = header.split() + if stage != b'0' or mode not in (b'100644', b'100755', b'120000'): + raise Unverified('unsupported-index-entry') + if not re.fullmatch(rb'[a-f0-9]{40}|[a-f0-9]{64}', oid): + raise Unverified('invalid-object-id') + objects.add(oid.decode('ascii')) + seen.add(path) + if changed != seen: + raise Unverified('incomplete-index-enumeration') + if len(objects) > MAX_BLOBS: + raise Unverified('staged-blob-limit') + return snapshot, sorted(objects) + + +def scan_staged(repo: Path, binary: Path) -> dict: + """Return only fixed statuses/counts and Git blob IDs, never finding contents.""" + scanned = 0 + descriptor = None + try: + descriptor = os.open(binary, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + info = os.fstat(descriptor) + if (not stat.S_ISREG(info.st_mode) or info.st_mode & 0o022 + or not info.st_mode & 0o111 or info.st_uid not in (0, os.getuid()) + or info.st_size > 64 * 1024 * 1024): + raise Unverified('scanner-file-refused') + digest = hashlib.sha256() + while chunk := os.read(descriptor, 1024 * 1024): + digest.update(chunk) + if digest.hexdigest() != BINARY_SHA256: + raise Unverified('scanner-digest-mismatch') + snapshot, objects = staged_objects(repo) + blocked = [] + total = 0 + # Do not honor repository-local config/ignore files, comments or ambient + # GITLEAKS settings. No scanner report or credential-bearing diagnostic is kept. + with tempfile.TemporaryDirectory(prefix='rmgr-secret-scan-') as temporary: + directory = Path(temporary) + config = directory / 'scanner.toml'; config.write_text(CONFIG) + ignore = directory / '.gitleaksignore'; ignore.write_text('') + env = {'PATH': '/usr/bin:/bin', 'HOME': temporary, 'LANG': 'C', 'TMPDIR': temporary} + for oid in objects: + size = int(git(repo, 'cat-file', '-s', oid)) + total += size + if size < 0 or size > MAX_BLOB_BYTES or total > MAX_TOTAL_BYTES: + raise Unverified('staged-byte-limit') + content = git(repo, 'cat-file', 'blob', oid) + if len(content) != size: + raise Unverified('blob-size-mismatch') + command = [str(binary), 'stdin', '--config', str(config), + '--gitleaks-ignore-path', str(ignore), '--ignore-gitleaks-allow', + '--redact=100', '--no-banner', '--no-color', '--log-level', 'fatal', + '--exit-code', '20', '--timeout', '15'] + result = subprocess.run(command, executable=f'/proc/self/fd/{descriptor}', + pass_fds=(descriptor,), input=content, cwd=directory, env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=20, check=False) + if result.returncode not in (0, 20): + raise Unverified('scanner-failed') + scanned += 1 + if result.returncode == 20: + blocked.append(oid) + if git(repo, 'ls-files', '--stage', '-z') != snapshot: + raise Unverified('index-changed-during-scan') + return {'status': 'blocked' if blocked else 'clean', 'scope': 'changed-staged-blobs', + 'scanner_version': VERSION, 'scanner_sha256': BINARY_SHA256, + 'scanned_blobs': scanned, 'blocked_blob_ids': blocked} + except Unverified as error: + reason = error.args[0] + except (OSError, ValueError, subprocess.SubprocessError): + reason = 'scan-unavailable' + finally: + if descriptor is not None: + os.close(descriptor) + return {'status': 'unverified', 'scope': 'changed-staged-blobs', + 'reason': reason, 'scanned_blobs': scanned} + + +def exit_code(result: dict) -> int: + return {'clean': 0, 'blocked': 1, 'unverified': 2}[result['status']] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--path', type=Path, default=Path('.')) + parser.add_argument('--binary', type=Path, required=True) + args = parser.parse_args(argv) + result = scan_staged(args.path, args.binary) + print(json.dumps(result, sort_keys=True)) + return exit_code(result) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.candidate.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.candidate.py new file mode 100644 index 00000000..c2203bd2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/secret_scan.candidate.py @@ -0,0 +1,428 @@ +"""Content-silent adapter for staged-blob secret detection with pinned Gitleaks. + +This is a repository control adapter, not credential custody or an authorization +boundary. It does not install hooks, alter indexes, send content off-host, or +claim history/push/CI coverage. Its caller must treat unverified as refusal. +""" +from __future__ import annotations + +import argparse +import contextlib +import ctypes +import fcntl +import functools +import hashlib +import json +import os +import re +import shutil +import signal +import resource +import stat +import sys +import subprocess +import tempfile +import time +from pathlib import Path + +# Resolve libc and prctl before fork; child_limits performs no library loading. +_LIBC = ctypes.CDLL(None, use_errno=True) +_PRCTL = _LIBC.prctl +_PRCTL.argtypes = [ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong] +_PRCTL.restype = ctypes.c_int + +VERSION = '8.30.1' +BINARY_SHA256 = '88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509' +MAX_BLOB_BYTES = 8 * 1024 * 1024 +MAX_TOTAL_BYTES = 64 * 1024 * 1024 +MAX_BLOBS = 500 +MAX_BATCH_BLOBS = 64 +MAX_BATCH_BYTES = 8 * 1024 * 1024 +# PIPE_BUF bytes fit the original stdin writer's single atomic write. Larger +# blobs retain the stdin path, avoiding a change to fragment boundaries. +DIRECTORY_BLOB_BYTES = 4096 +MAX_REPORT_BYTES = 1024 * 1024 +SCAN_SECONDS = 120 +CONFIG = '[extend]\nuseDefault = true\n' + + +class Unverified(Exception): + """A fixed reason code, never a tool exception or repository content.""" + + +def git(repo: Path, *args: str) -> bytes: + env = {**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'} + result = subprocess.run(['git', '-C', str(repo), *args], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, env=env, timeout=20, check=False, + preexec_fn=functools.partial(child_limits, MAX_TOTAL_BYTES, os.getpid())) + if result.returncode: + raise Unverified('git-read-failed') + return result.stdout + + +def staged_objects(repo: Path) -> tuple[bytes, list[str]]: + snapshot = git(repo, 'ls-files', '--stage', '-z') + changed = set(git(repo, 'diff', '--cached', '--name-only', '-z', '--no-renames', + '--no-ext-diff', '--no-textconv', '--diff-filter=ACMRTUXB').split(b'\0')) - {b''} + objects: set[str] = set() + seen: set[bytes] = set() + for record in snapshot.split(b'\0'): + if not record: + continue + header, path = record.split(b'\t', 1) + if path not in changed: + continue + mode, oid, stage = header.split() + if stage != b'0' or mode not in (b'100644', b'100755', b'120000'): + raise Unverified('unsupported-index-entry') + if not re.fullmatch(rb'[a-f0-9]{40}|[a-f0-9]{64}', oid): + raise Unverified('invalid-object-id') + objects.add(oid.decode('ascii')) + seen.add(path) + if changed != seen: + raise Unverified('incomplete-index-enumeration') + if len(objects) > MAX_BLOBS: + raise Unverified('staged-blob-limit') + return snapshot, sorted(objects) + + +# Source bytes must never be copied onto persistent storage. The watchdog owns +# no source data; its pipe EOF detects parent death without a service or hook. +_CLEANUP = """ +import os, select, shutil, signal, stat, sys +path, dev, ino, uid, parent = sys.argv[1:] +root = os.open('/dev/shm', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) +if os.getppid() != int(parent): + sys.exit(4) +pid = os.pidfd_open(int(parent)) +if os.getppid() != int(parent): + sys.exit(4) +os.write(1, b'R') +ready, _, _ = select.select([0, pid], [], [], 180) +# Exceptional abort targets only the exact parent, never a process group. +# If kernel quiescence stalls, retain this observer/duty until death is proven. +if not ready: + try: + signal.pidfd_send_signal(pid, signal.SIGTERM) + except ProcessLookupError: + pass + dead, _, _ = select.select([pid], [], [], 5) + if not dead: + try: + signal.pidfd_send_signal(pid, signal.SIGKILL) + except ProcessLookupError: + pass + select.select([pid], [], []) +name = os.path.basename(path) +try: + info = os.stat(name, dir_fd=root, follow_symlinks=False) +except FileNotFoundError: + sys.exit(0) +if (info.st_dev, info.st_ino, info.st_uid) != (int(dev), int(ino), int(uid)): + sys.exit(3) +if not stat.S_ISDIR(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o700: + sys.exit(3) +shutil.rmtree(name, dir_fd=root) +""" + + +def is_tmpfs(directory: Path) -> bool: + descriptor = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + result = ctypes.create_string_buffer(256) + if _LIBC.fstatfs(descriptor, ctypes.byref(result)): + raise OSError(ctypes.get_errno(), 'statfs failed') + return ctypes.c_long.from_buffer(result).value == 0x01021994 + finally: + os.close(descriptor) + + +def remove_storage(directory: Path, identity: os.stat_result) -> None: + root = os.open('/dev/shm', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + current = os.stat(directory.name, dir_fd=root, follow_symlinks=False) + except FileNotFoundError: + return + if ((current.st_dev, current.st_ino, current.st_uid) != + (identity.st_dev, identity.st_ino, identity.st_uid) + or not stat.S_ISDIR(current.st_mode) + or stat.S_IMODE(current.st_mode) != 0o700): + raise Unverified('volatile-storage-identity-changed') + shutil.rmtree(directory.name, dir_fd=root) + finally: + os.close(root) + + +@contextlib.contextmanager +def private_storage(): + if not is_tmpfs(Path('/dev/shm')): + raise Unverified('volatile-storage-unavailable') + temporary = tempfile.mkdtemp(prefix='rmgr-secret-scan-', dir='/dev/shm') + directory = Path(temporary) + identity = directory.lstat() + watcher = None + try: + if not is_tmpfs(directory): + raise Unverified('volatile-storage-unavailable') + watcher = subprocess.Popen([sys.executable, '-I', '-c', _CLEANUP, temporary, + str(identity.st_dev), str(identity.st_ino), str(identity.st_uid), str(os.getpid())], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + env={'PATH': '/usr/bin:/bin', 'LANG': 'C'}, close_fds=True) + import select + ready, _, _ = select.select([watcher.stdout], [], [], 5) + if not ready or watcher.stdout.read(1) != b'R' or watcher.poll() is not None: + raise Unverified('cleanup-watchdog-unavailable') + watcher.stdout.close() + yield directory, watcher + if watcher.poll() is not None: + raise Unverified('cleanup-watchdog-unavailable') + finally: + pending_error = sys.exc_info()[0] is not None + # All synchronous Git/scanner children have exited before yield unwinds. + try: + remove_storage(directory, identity) + finally: + if watcher is not None: + watcher.stdin.close() + watcher.stdout.close() + try: + code = watcher.wait(timeout=5) + except subprocess.TimeoutExpired: + watcher.kill() + code = watcher.wait(timeout=5) + if code and not pending_error: + raise Unverified('cleanup-watchdog-failed') + + +def child_limits(file_bytes: int, parent_pid: int) -> None: + # Parent loss must quiesce every source-reading/writing child. Install the + # signal before any exec and close the fork/prctl race using the exact PID. + if _PRCTL(1, signal.SIGKILL, 0, 0, 0) or os.getppid() != parent_pid: + os._exit(127) + # Linux adapter already requires /proc/self/fd. Limits apply only to children. + resource.setrlimit(resource.RLIMIT_FSIZE, (file_bytes, file_bytes)) + resource.setrlimit(resource.RLIMIT_CPU, (20, 20)) + resource.setrlimit(resource.RLIMIT_AS, (8 * 1024**3, 8 * 1024**3)) + + +def object_batch(repo: Path, objects: list[str], directory: Path) -> list[tuple[str, bytes]]: + """Bound Git's output before capture, then validate the complete framing.""" + request = ''.join(oid + '\n' for oid in objects).encode('ascii') + env = {**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'} + with tempfile.TemporaryFile(dir=directory) as output: + result = subprocess.run(['git', '-C', str(repo), 'cat-file', '--batch'], + input=request, stdout=output, stderr=subprocess.DEVNULL, + env=env, timeout=20, check=False, + preexec_fn=functools.partial(child_limits, + MAX_BATCH_BYTES + len(objects) * 128, os.getpid())) + if result.returncode: + raise Unverified('git-read-failed') + output.seek(0) + blobs = [] + for oid in objects: + header = output.readline(128) + match = re.fullmatch(rb'([a-f0-9]{40}|[a-f0-9]{64}) blob ([0-9]+)\n', header) + if not match or match[1].decode() != oid: + raise Unverified('invalid-object-batch') + size = int(match[2]) + if size > MAX_BLOB_BYTES or size > MAX_BATCH_BYTES: + raise Unverified('staged-byte-limit') + content = output.read(size) + if len(content) != size or output.read(1) != b'\n': + raise Unverified('blob-size-mismatch') + algorithm = 'sha1' if len(oid) == 40 else 'sha256' + digest = hashlib.new(algorithm, b'blob ' + str(size).encode() + b'\0' + content) + if digest.hexdigest() != oid: + raise Unverified('blob-digest-mismatch') + blobs.append((oid, content)) + if output.read(1): + raise Unverified('invalid-object-batch') + return blobs + + +def scan_staged(repo: Path, binary: Path) -> dict: + """Return only fixed statuses/counts and Git blob IDs, never finding contents.""" + scanned = 0 + descriptor = None + executable = None + deadline = time.monotonic() + SCAN_SECONDS + try: + descriptor = os.open(binary, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + info = os.fstat(descriptor) + if (not stat.S_ISREG(info.st_mode) or info.st_mode & 0o022 + or not info.st_mode & 0o111 or info.st_uid not in (0, os.getuid()) + or info.st_size > 64 * 1024 * 1024): + raise Unverified('scanner-file-refused') + executable = os.memfd_create('rmgr-verified-scanner', os.MFD_ALLOW_SEALING) + digest = hashlib.sha256() + copied = 0 + while chunk := os.read(descriptor, 1024 * 1024): + copied += len(chunk) + if copied > 64 * 1024 * 1024: + raise Unverified('scanner-file-refused') + digest.update(chunk) + with memoryview(chunk) as view: + while view: + written = os.write(executable, view) + if written <= 0: + raise Unverified('scanner-file-refused') + view = view[written:] + if digest.hexdigest() != BINARY_SHA256: + raise Unverified('scanner-digest-mismatch') + fcntl.fcntl(executable, fcntl.F_ADD_SEALS, fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW + | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL) + snapshot, objects = staged_objects(repo) + blocked = [] + # Neither source names nor repository config/ignore files enter this + # private directory. Reports contain ONLY neutral blob paths. + with private_storage() as (directory, watcher): + temporary = str(directory) + config = directory / 'scanner.toml'; config.write_text(CONFIG) + ignore = directory / '.gitleaksignore'; ignore.write_text('') + template = directory / 'report.tmpl' + template.write_text('{{range .}}{{.File}}\n{{end}}') + env = {'PATH': '/usr/bin:/bin', 'HOME': temporary, 'LANG': 'C', + 'TMPDIR': temporary, 'GOMAXPROCS': '2'} + # One bounded metadata query replaces a size subprocess per object. + request = ''.join(oid + '\n' for oid in objects).encode('ascii') + with tempfile.TemporaryFile(dir=directory) as output: + result = subprocess.run(['git', '-C', str(repo), 'cat-file', '--batch-check'], + input=request, stdout=output, stderr=subprocess.DEVNULL, + env={**os.environ, 'GIT_NO_REPLACE_OBJECTS': '1'}, timeout=20, + check=False, preexec_fn=functools.partial(child_limits, MAX_BLOBS * 128, os.getpid())) + if result.returncode: + raise Unverified('git-read-failed') + output.seek(0) + metadata = output.read(MAX_BLOBS * 128 + 1).splitlines() + if len(metadata) != len(objects): + raise Unverified('invalid-object-batch') + sizes = [] + for oid, line in zip(objects, metadata): + match = re.fullmatch(rb'([a-f0-9]{40}|[a-f0-9]{64}) blob ([0-9]+)', line) + if not match or match[1].decode() != oid: + raise Unverified('invalid-object-batch') + sizes.append(int(match[2])) + if any(size > MAX_BLOB_BYTES for size in sizes) or sum(sizes) > MAX_TOTAL_BYTES: + raise Unverified('staged-byte-limit') + + def invoke(mode: str, content: bytes | None = None) -> list[str]: + if watcher.poll() is not None: + raise Unverified('cleanup-watchdog-unavailable') + remaining = deadline - time.monotonic() + if remaining <= 0: + raise Unverified('scan-deadline-exceeded') + report = directory / 'report.paths' + report.unlink(missing_ok=True) + command = [str(binary), mode, '--config', str(config), + '--gitleaks-ignore-path', str(ignore), '--ignore-gitleaks-allow', + '--redact=100', '--no-banner', '--no-color', '--log-level', 'warn', + '--exit-code', '20', '--timeout', '15'] + if mode == 'dir': + command += ['--report-format', 'template', '--report-template', str(template), '--report-path', str(report), + '--max-target-megabytes', '0', 'blobs'] + with tempfile.TemporaryFile(dir=directory) as diagnostics: + result = subprocess.run(command, executable=f'/proc/self/fd/{executable}', + pass_fds=(executable,), input=content, cwd=directory, env=env, + stdout=subprocess.DEVNULL, stderr=diagnostics, + timeout=min(20, remaining), check=False, + preexec_fn=functools.partial(child_limits, MAX_REPORT_BYTES, os.getpid())) + # The scanner can warn and skip a file without an error exit. + # Refuse ALL warnings/errors, including partial traversal. + diagnostics.seek(0) + messages = diagnostics.read(MAX_REPORT_BYTES + 1) + # v8.30.1 emits exactly this summary for a successful blocked + # scan. Every other warning/error is a refusal, never output. + summary = re.fullmatch( + rb'[0-9]{1,2}:[0-9]{2}(?:AM|PM) WRN leaks found: [1-9][0-9]*\n', + messages) + if (result.returncode not in (0, 20) + or (messages and not (result.returncode == 20 and summary))): + raise Unverified('scanner-failed') + if mode == 'stdin': + return ['stdin'] if result.returncode == 20 else [] + if not report.exists() or report.stat().st_size > MAX_REPORT_BYTES: + raise Unverified('scanner-report-refused') + paths = report.read_bytes().splitlines() + known = {('blobs/' + oid).encode(): oid for oid in small} + if any(path not in known for path in paths): + raise Unverified('scanner-report-refused') + findings = sorted({known[path] for path in paths}) + if bool(findings) != (result.returncode == 20): + raise Unverified('scanner-report-refused') + return findings + + offset = 0 + while offset < len(objects): + batch = [] + batch_bytes = 0 + while offset < len(objects) and len(batch) < MAX_BATCH_BLOBS: + size = sizes[offset] + if batch and batch_bytes + size > MAX_BATCH_BYTES: + break + batch.append(objects[offset]); batch_bytes += size; offset += 1 + if watcher.poll() is not None: + raise Unverified('cleanup-watchdog-unavailable') + blobs = object_batch(repo, batch, directory) + # Check metadata again against exact bytes, including same-size + # substitution via corrupt object storage. Hashes are checked above. + expected = dict(zip(objects, sizes)) + if any(len(content) != expected[oid] for oid, content in blobs): + raise Unverified('blob-size-mismatch') + payload = directory / 'blobs'; payload.mkdir(mode=0o700) + small = [] + try: + for oid, content in blobs: + if len(content) > DIRECTORY_BLOB_BYTES: + if invoke('stdin', content): + blocked.append(oid) + scanned += 1 + else: + path = payload / oid + with path.open('xb') as target: + target.write(content) + path.chmod(0o600) + small.append(oid) + if small: + blocked.extend(invoke('dir')) + scanned += len(small) + finally: + for path in payload.iterdir(): + path.unlink() + payload.rmdir() + if git(repo, 'ls-files', '--stage', '-z') != snapshot: + raise Unverified('index-changed-during-scan') + if time.monotonic() > deadline: + raise Unverified('scan-deadline-exceeded') + return {'status': 'blocked' if blocked else 'clean', 'scope': 'changed-staged-blobs', + 'scanner_version': VERSION, 'scanner_sha256': BINARY_SHA256, + 'scanned_blobs': scanned, 'blocked_blob_ids': sorted(blocked)} + except Unverified as error: + reason = error.args[0] + except (OSError, ValueError, subprocess.SubprocessError): + reason = 'scan-unavailable' + finally: + if descriptor is not None: + os.close(descriptor) + if executable is not None: + os.close(executable) + return {'status': 'unverified', 'scope': 'changed-staged-blobs', + 'reason': reason, 'scanned_blobs': scanned} + + +def exit_code(result: dict) -> int: + return {'clean': 0, 'blocked': 1, 'unverified': 2}[result['status']] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--path', type=Path, default=Path('.')) + parser.add_argument('--binary', type=Path, required=True) + args = parser.parse_args(argv) + result = scan_staged(args.path, args.binary) + print(json.dumps(result, sort_keys=True)) + return exit_code(result) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test-results.txt b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test-results.txt new file mode 100644 index 00000000..1d07870d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test-results.txt @@ -0,0 +1,2 @@ +.......................................... [100%] +42 passed in 32.95s diff --git a/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test_secret_scan.candidate.py b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test_secret_scan.candidate.py new file mode 100644 index 00000000..d3555f95 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/scanner-batch-r1/test_secret_scan.candidate.py @@ -0,0 +1,472 @@ +"""Real scanner integration on disposable Git indexes and never-valid credentials.""" +import hashlib +import json +import os +import shlex +import subprocess +import sys +from pathlib import Path + +import pytest + +from repo_manager import secret_scan + + +@pytest.fixture +def binary(): + configured = os.environ.get('RMGR_TEST_GITLEAKS') + if not configured: + pytest.skip('set RMGR_TEST_GITLEAKS to the reviewed v8.30.1 Linux x64 binary') + path = Path(configured) + assert hashlib.sha256(path.read_bytes()).hexdigest() == secret_scan.BINARY_SHA256 + return path + + +@pytest.fixture +def repo(tmp_path): + repo = tmp_path / 'repo'; repo.mkdir() + git(repo, 'init', '-q') + hooks = tmp_path / 'hooks'; hooks.mkdir() + git(repo, 'config', 'core.hooksPath', str(hooks)) + git(repo, 'config', 'user.name', 'Disposable Test') + git(repo, 'config', 'user.email', 'test@example.test') + return repo + + +def git(repo, *args): + return subprocess.check_output(['git', '-C', str(repo), *args], stderr=subprocess.DEVNULL) + + +@pytest.fixture +def private_key(tmp_path): + path = tmp_path / 'disposable-key' + subprocess.run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(path)], + check=True, capture_output=True) + return path.read_bytes() + + +def stage(repo, data, name='application.txt'): + (repo / name).write_bytes(data) + git(repo, 'add', '--', name) + + +def test_clean_staged_blob(binary, repo): + stage(repo, b'print("hello")\n') + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'clean' + assert result['scanned_blobs'] == 1 + + +def test_real_secret_commit_is_refused_without_disclosure(binary, repo, private_key): + hook = Path(git(repo, 'rev-parse', '--git-path', 'hooks/pre-commit').decode().strip()) + script = str(Path(secret_scan.__file__)) + hook.write_text('#!/bin/sh\nexec ' + ' '.join(shlex.quote(s) for s in + [sys.executable, script, '--path', str(repo), '--binary', str(binary)]) + '\n') + hook.chmod(0o700) + stage(repo, private_key) + result = subprocess.run(['git', 'commit', '-qm', 'synthetic detection probe'], + cwd=repo, capture_output=True, check=False) + assert result.returncode != 0 + assert bool(private_key.strip() in result.stdout + result.stderr) is False + assert b'"status": "blocked"' in result.stdout + result.stderr + assert subprocess.run(['git', 'rev-parse', '--verify', 'HEAD'], cwd=repo, + capture_output=True, check=False).returncode != 0 + + +def test_staged_secret_refused_even_if_worktree_is_clean(binary, repo, private_key): + stage(repo, private_key) + (repo / 'application.txt').write_text('clean worktree\n') + assert secret_scan.scan_staged(repo, binary)['status'] == 'blocked' + + +def test_unstaged_secret_not_misrepresented_as_index_scan(binary, repo, private_key): + stage(repo, b'clean staged data\n') + (repo / 'application.txt').write_bytes(private_key) + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'clean' + assert result['scope'] == 'changed-staged-blobs' + + +def test_repo_config_environment_and_inline_allow_cannot_disable_scan(binary, repo, private_key, monkeypatch): + (repo / '.gitleaks.toml').write_text('[allowlist]\nregexes=[".*"]\n') + (repo / '.gitleaksignore').write_text('*\n') + monkeypatch.setenv('GITLEAKS_CONFIG', str(repo / '.gitleaks.toml')) + monkeypatch.setenv('GITLEAKS_CONFIG_TOML', '[allowlist]\nregexes=[".*"]\n') + stage(repo, b'# gitleaks:allow\n' + private_key) + assert secret_scan.scan_staged(repo, binary)['status'] == 'blocked' + + +def test_symlink_content_is_not_dereferenced(binary, repo, private_key, tmp_path): + target = tmp_path / 'outside-secret'; target.write_bytes(private_key) + (repo / 'link').symlink_to(target) + git(repo, 'add', '--', 'link') + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'clean' + assert result['scanned_blobs'] == 1 + + +def test_missing_scanner_is_unverified(repo): + stage(repo, b'clean\n') + result = secret_scan.scan_staged(repo, repo / 'missing') + assert result['status'] == 'unverified' + assert secret_scan.exit_code(result) == 2 + + +def test_wrong_digest_is_refused_before_execution(repo): + marker = repo / 'executed' + fake = repo / 'fake'; fake.write_text('#!/bin/sh\ntouch ' + str(marker) + '\n'); fake.chmod(0o700) + result = secret_scan.scan_staged(repo, fake) + assert result['reason'] == 'scanner-digest-mismatch' + assert not marker.exists() + + +def test_changed_submodule_is_unverified(binary, repo): + stage(repo, b'clean\n'); git(repo, 'commit', '-qm', 'seed') + commit = git(repo, 'rev-parse', 'HEAD').decode().strip() + git(repo, 'update-index', '--add', '--cacheinfo', '160000,' + commit + ',submodule') + assert secret_scan.scan_staged(repo, binary)['reason'] == 'unsupported-index-entry' + + +def test_oversize_blob_refuses_instead_of_skipping(binary, repo, monkeypatch): + monkeypatch.setattr(secret_scan, 'MAX_BLOB_BYTES', 8) + stage(repo, b'0123456789') + assert secret_scan.scan_staged(repo, binary)['reason'] == 'staged-byte-limit' + + +def test_git_replace_cannot_hide_staged_secret(binary, repo, private_key): + stage(repo, private_key) + secret = git(repo, 'rev-parse', ':application.txt').decode().strip() + clean = subprocess.check_output(['git', 'hash-object', '-w', '--stdin'], input=b'clean\n', cwd=repo).decode().strip() + git(repo, 'replace', secret, clean) + assert secret_scan.scan_staged(repo, binary)['status'] == 'blocked' + + +def test_index_change_during_scan_refuses(binary, repo, monkeypatch): + stage(repo, b'clean\n') + original = secret_scan.git + count = 0 + def changing(repo, *args): + nonlocal count + if args == ('ls-files', '--stage', '-z'): + count += 1 + if count == 2: stage(repo, b'changed after scan\n') + return original(repo, *args) + monkeypatch.setattr(secret_scan, 'git', changing) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'index-changed-during-scan' + + +def test_explicit_git_index_file_is_honored(binary, repo, private_key, monkeypatch, tmp_path): + stage(repo, b'clean default index\n') + default = Path(git(repo, 'rev-parse', '--git-path', 'index').decode().strip()) + if not default.is_absolute(): default = repo / default + other = tmp_path / 'alternate-index'; other.write_bytes(default.read_bytes()) + monkeypatch.setenv('GIT_INDEX_FILE', str(other)) + stage(repo, private_key) + assert secret_scan.scan_staged(repo, binary)['status'] == 'blocked' + monkeypatch.delenv('GIT_INDEX_FILE') + assert secret_scan.scan_staged(repo, binary)['status'] == 'clean' + + +def test_cli_outputs_only_summary(binary, repo, private_key): + stage(repo, private_key) + result = subprocess.run([sys.executable, '-m', 'repo_manager.secret_scan', '--path', str(repo), + '--binary', str(binary)], capture_output=True, check=False) + assert result.returncode == 1 + assert json.loads(result.stdout)['status'] == 'blocked' + assert result.stderr == b'' + assert bool(private_key.strip() in result.stdout) is False + + +@pytest.mark.parametrize('failure', ['exit', 'timeout']) +def test_scanner_failure_never_passes_or_emits_diagnostics(binary, repo, monkeypatch, failure): + stage(repo, b'clean\n') + original = secret_scan.subprocess.run + def broken(command, **kwargs): + if len(command) > 1 and command[1] in ('stdin', 'dir'): + if failure == 'timeout': + raise subprocess.TimeoutExpired(command, 20, output=b'synthetic-sensitive-diagnostic') + return subprocess.CompletedProcess(command, 2) + return original(command, **kwargs) + monkeypatch.setattr(secret_scan.subprocess, 'run', broken) + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'unverified' + assert 'synthetic-sensitive-diagnostic' not in json.dumps(result) + + +def test_actual_commit_only_hook_scans_its_temporary_index(binary, repo, private_key): + stage(repo, b'clean\n'); stage(repo, b'also clean\n', 'unrelated.txt') + git(repo, 'commit', '-qm', 'clean baseline') + hook = Path(git(repo, 'rev-parse', '--git-path', 'hooks/pre-commit').decode().strip()) + hook.write_text('#!/bin/sh\nexec ' + ' '.join(shlex.quote(s) for s in + [sys.executable, str(Path(secret_scan.__file__)), '--path', str(repo), + '--binary', str(binary)]) + '\n') + hook.chmod(0o700) + stage(repo, b'unrelated staged change\n', 'unrelated.txt') + (repo / 'application.txt').write_bytes(private_key) + before = git(repo, 'rev-parse', 'HEAD') + result = subprocess.run(['git', 'commit', '--only', '-qm', 'synthetic selective commit', '--', + 'application.txt'], cwd=repo, capture_output=True, check=False) + assert result.returncode != 0 + assert git(repo, 'rev-parse', 'HEAD') == before + assert bool(private_key.strip() in result.stdout + result.stderr) is False + assert b'"status": "blocked"' in result.stdout + result.stderr + + +def test_empty_and_deletion_only_index(binary, repo): + assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 + stage(repo, b'clean\n'); git(repo, 'commit', '-qm', 'seed') + assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 + git(repo, 'rm', '-q', 'application.txt') + assert secret_scan.scan_staged(repo, binary)['scanned_blobs'] == 0 + + +def test_duplicate_blobs_and_exact_attribution(binary, repo, private_key): + stage(repo, private_key, 'secret.png') # original filename cannot allowlist content + stage(repo, private_key, 'duplicate.txt') + stage(repo, b'clean\n', 'clean.txt') + oid = git(repo, 'rev-parse', ':secret.png').decode().strip() + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'blocked' + assert result['scanned_blobs'] == 2 + assert result['blocked_blob_ids'] == [oid] + + +@pytest.mark.parametrize('size', [4096, 4097, 100000, 100001]) +def test_small_batch_and_large_fallback_equivalence(binary, repo, private_key, size): + data = b'\n' * (size - len(private_key)) + private_key + stage(repo, data) + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'blocked' + assert result['blocked_blob_ids'] == [git(repo, 'rev-parse', ':application.txt').decode().strip()] + + +def test_file_boundaries_never_synthesize_a_finding(binary, repo, private_key): + cut = private_key.index(b'\n') // 2 + stage(repo, private_key[:cut], 'first') + stage(repo, private_key[cut:], 'second') + # Each independent fragment matches the original single-blob stdin outcome; + # concatenation would synthesize the complete private-key opening marker. + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'clean' + assert result['scanned_blobs'] == 2 + + +def test_binary_and_encoded_fixture_equivalence(binary, repo, private_key, monkeypatch): + import base64 + original = secret_scan.DIRECTORY_BLOB_BYTES + for data in [b'\x00\x01\x02\n' + private_key, base64.b64encode(private_key)]: + stage(repo, data) + monkeypatch.setattr(secret_scan, 'DIRECTORY_BLOB_BYTES', 0) + stdin = secret_scan.scan_staged(repo, binary) + monkeypatch.setattr(secret_scan, 'DIRECTORY_BLOB_BYTES', original) + batched = secret_scan.scan_staged(repo, binary) + assert batched == stdin + assert batched['status'] == 'blocked' + + +def test_batch_count_bound_and_independent_findings(binary, repo, private_key, monkeypatch): + monkeypatch.setattr(secret_scan, 'MAX_BATCH_BLOBS', 3) + for i in range(7): + stage(repo, (f'fixture {i}\n'.encode() + private_key) if i % 2 else f'clean {i}\n'.encode(), str(i)) + result = secret_scan.scan_staged(repo, binary) + expected = sorted(git(repo, 'rev-parse', ':' + str(i)).decode().strip() for i in [1, 3, 5]) + assert result['status'] == 'blocked' + assert result['scanned_blobs'] == 7 + assert result['blocked_blob_ids'] == expected + + +def test_total_and_object_count_bounds_refuse(binary, repo, monkeypatch): + stage(repo, b'12345', 'a'); stage(repo, b'67890', 'b') + monkeypatch.setattr(secret_scan, 'MAX_TOTAL_BYTES', 9) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'staged-byte-limit' + monkeypatch.setattr(secret_scan, 'MAX_BLOBS', 1) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'staged-blob-limit' + + +@pytest.mark.parametrize('mutation', ['path', 'in-place']) +def test_verified_executable_snapshot_survives_mutation(binary, repo, monkeypatch, tmp_path, mutation): + import shutil + scanner = tmp_path / 'scanner'; shutil.copyfile(binary, scanner); scanner.chmod(0o700) + stage(repo, b'clean\n') + original = secret_scan.staged_objects + def mutate(repo): + if mutation == 'path': + scanner.unlink() + scanner.write_bytes(b'not executable scanner bytes\n') + scanner.chmod(0o700) + return original(repo) + monkeypatch.setattr(secret_scan, 'staged_objects', mutate) + assert secret_scan.scan_staged(repo, scanner)['status'] == 'clean' + + +@pytest.mark.parametrize('fault', ['warning', 'unknown-path', 'missing-report', 'returncode']) +def test_partial_scans_and_invalid_reports_refuse(binary, repo, monkeypatch, fault): + stage(repo, b'clean\n') + original = secret_scan.subprocess.run + def fail(command, **kwargs): + result = original(command, **kwargs) + if command[1] == 'dir': + report = Path(command[command.index('--report-path') + 1]) + if fault == 'warning': + kwargs['stderr'].write(b'synthetic-sensitive-diagnostic\n') + elif fault == 'unknown-path': + report.write_text('blobs/unknown\n') + elif fault == 'missing-report': + report.unlink() + else: + return subprocess.CompletedProcess(command, 20) + return result + monkeypatch.setattr(secret_scan.subprocess, 'run', fail) + result = secret_scan.scan_staged(repo, binary) + assert result['status'] == 'unverified' + assert 'synthetic-sensitive-diagnostic' not in json.dumps(result) + + +def test_deadline_exhaustion_refuses(binary, repo, monkeypatch): + stage(repo, b'clean\n') + monkeypatch.setattr(secret_scan, 'SCAN_SECONDS', 0) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'scan-deadline-exceeded' + + +def test_git_object_output_corruption_refuses(binary, repo, monkeypatch): + stage(repo, b'clean\n') + original = secret_scan.subprocess.run + def corrupt(command, **kwargs): + result = original(command, **kwargs) + if command[-1] == '--batch': + output = kwargs['stdout'] + output.seek(-3, 2); output.write(b'xx\n') + return result + monkeypatch.setattr(secret_scan.subprocess, 'run', corrupt) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'blob-digest-mismatch' + + +def test_report_growth_limit_refuses(binary, repo, private_key, monkeypatch): + stage(repo, private_key) + monkeypatch.setattr(secret_scan, 'MAX_REPORT_BYTES', 16) + assert secret_scan.scan_staged(repo, binary)['status'] == 'unverified' + + +def test_sealed_executable_rejects_write(binary, repo, monkeypatch): + import errno + stage(repo, b'clean\n') + original = secret_scan.subprocess.run + checked = [] + def assert_sealed(command, **kwargs): + if command[1] in ('dir', 'stdin'): + fd = kwargs['pass_fds'][0] + with pytest.raises(OSError) as error: + os.pwrite(fd, b'X', 0) + assert error.value.errno == errno.EPERM + checked.append(True) + return original(command, **kwargs) + monkeypatch.setattr(secret_scan.subprocess, 'run', assert_sealed) + assert secret_scan.scan_staged(repo, binary)['status'] == 'clean' + assert checked + + +def test_nonvolatile_storage_refuses(binary, repo, monkeypatch): + stage(repo, b'clean\n') + monkeypatch.setattr(secret_scan, 'is_tmpfs', lambda directory: False) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'volatile-storage-unavailable' + + +def test_cleanup_watchdog_loss_refuses(binary, repo, monkeypatch): + stage(repo, b'clean\n') + original = secret_scan.object_batch + def kill_watchdog(repo, objects, directory): + # Discover only our child watchdog using the context manager wrapper. + watcher.kill(); watcher.wait(timeout=5) + return original(repo, objects, directory) + original_storage = secret_scan.private_storage + import contextlib + @contextlib.contextmanager + def storage(): + nonlocal watcher + with original_storage() as pair: + watcher = pair[1] + yield pair + watcher = None + monkeypatch.setattr(secret_scan, 'private_storage', storage) + monkeypatch.setattr(secret_scan, 'object_batch', kill_watchdog) + assert secret_scan.scan_staged(repo, binary)['reason'] == 'cleanup-watchdog-unavailable' + + +def test_parent_sigkill_triggers_watchdog_cleanup(tmp_path): + import time + script = ('from repo_manager.secret_scan import private_storage; import time\n' + 'with private_storage() as (directory, watcher):\n' + ' (directory / "fixture").write_bytes(b"public generated watchdog fixture")\n' + ' print(str(directory), flush=True)\n' + ' time.sleep(30)\n') + process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True) + try: + import select + ready, _, _ = select.select([process.stdout], [], [], 5) + assert ready + directory = Path(process.stdout.readline().strip()) + assert directory.is_dir() + process.kill(); process.wait(timeout=5) + until = time.monotonic() + 5 + while directory.exists() and time.monotonic() < until: + time.sleep(.02) + assert not directory.exists() + finally: + if process.poll() is None: + process.kill(); process.wait(timeout=5) + process.stdout.close() + + +def test_watchdog_deadline_quiesces_exact_parent_before_cleanup(): + import select + import time + script = ('from repo_manager import secret_scan as s; import time\n' + 's._CLEANUP = s._CLEANUP.replace("[], [], 180)", "[], [], .2)")\n' + 'with s.private_storage() as (directory, watcher):\n' + ' (directory / "fixture").write_bytes(b"public generated fixture")\n' + ' print(str(directory), flush=True)\n' + ' time.sleep(30)\n') + process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True) + try: + ready, _, _ = select.select([process.stdout], [], [], 5) + assert ready + directory = Path(process.stdout.readline().strip()) + assert directory.is_dir() + assert process.wait(timeout=5) < 0 + until = time.monotonic() + 5 + while directory.exists() and time.monotonic() < until: + time.sleep(.02) + assert not directory.exists() + finally: + if process.poll() is None: + process.kill(); process.wait(timeout=5) + process.stdout.close() + + +def test_parent_death_kills_source_child(): + import select + script = ('from repo_manager.secret_scan import child_limits\n' + 'import functools, os, subprocess, sys, time\n' + 'child = subprocess.Popen([sys.executable, "-c", "import time;time.sleep(30)"], ' + 'preexec_fn=functools.partial(child_limits, 1024, os.getpid()))\n' + 'print(child.pid, flush=True)\n' + 'time.sleep(30)\n') + process = subprocess.Popen([sys.executable, '-c', script], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True) + descriptor = None + try: + ready, _, _ = select.select([process.stdout], [], [], 5) + assert ready + child_pid = int(process.stdout.readline()) + descriptor = os.pidfd_open(child_pid) + process.kill(); process.wait(timeout=5) + dead, _, _ = select.select([descriptor], [], [], 5) + assert dead + finally: + if descriptor is not None: + os.close(descriptor) + if process.poll() is None: + process.kill(); process.wait(timeout=5) + process.stdout.close() diff --git a/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/README.md b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/README.md new file mode 100644 index 00000000..f792c44a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/README.md @@ -0,0 +1,29 @@ +# Secrets owner selection for preparatory installation + +Completed follow-through: after coordinator review, owner selection was published as `a4f1802`; the coordinator then installed the exact public bundle and17-file empty-key configuration through the unchanged owner installer. `actual-prekey-install.json` retains the installation and refusal proof. The actual-install work-record successor was published as `5356810`. These later observations supersede the preparation-time “not installed” statements below only for the inert local installation. Native key/caller/rollout/effect acceptance remains absent. + +Disposition: **select the exact public runtime bundle and empty-key configuration for preparatory installation through the existing offline installer**. This resolves source/package/config selection; it does not admit a signer, mint a caller, authorize effects or supply a native P4 owner disposition. The canonical owner work-record patch is supplied for coordinator review before any owner write. + +The applicable owner requirement in `secrets-engine/docs/cli-release-bundle.md` is: “The owner must review/admit the source, toolchain, complete bundle and manifest hash, and control the bundle/install prefix.” It requires independent package selection and controlled delivery; it does not require a new founder disposition for this inert public installation. This source-owner selection is provided here for coordinator review and authorized preparation. The distinct founder/five-owner requirements in P4 govern native custody execution. Filesystem escalation for the actual home destinations remains a tool permission boundary, independent of runtime authority. + +Select runtime `4dd004a2250104ba56293b2d7b0d96f4e076ea99`, documentation `3718e60d7385db091d3423352f44e922863087de`, and existing bundle manifest SHA-256 `105dcebe6f03e9799f91ea75175e20500df2aa7f08b5e32d39c1c367593ca47e`. Select the public consumer baseline SHA-256 `72521ff7729f8ddf5176092f3949cd50ac5f9241b0a5d1d30f3c63ce1d83091b`, including the exact ten catalog rows, policies, total stance, `environment.proposed.json` and empty trust. It is a configuration description for the existing loader, not an executable environment file or launcher. Install to the selected fresh prefix `/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03`; place config under the selected fresh `/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1`. Do not replace either revision. Use the existing `bin/secrets-engine` entrypoint with its fixed helper; the internal interpreter is only for the explicitly scoped offline receipt proof. + +The next concrete step is an ordinary local preparatory install by the authorized filesystem owner, after independently checking the selected source bundle and full parent chain are owner-controlled directories without symlinks. The repository sandbox does not authorize writes to these actual home paths. Create only the fresh release/config/evidence parents with mode0700 and copy the selected public configuration files0600. No caller runtime directory/file is needed for this installation. Supply a minimal explicit environment when invoking Config.load; do not inherit backend, Approval, clock, unsafe-demo or Python environment overrides. Keep the endpoint and all paths exactly as selected; blank BAO/VAULT and Approval URLs are intentional. Execute the existing installer, without source modification: + +```bash +/usr/bin/python3 /tmp/secrets-native-cli-4dd004a-one/install.py \ + --bundle /tmp/secrets-native-cli-4dd004a-one \ + --prefix /home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03 \ + --manifest-sha256 105dcebe6f03e9799f91ea75175e20500df2aa7f08b5e32d39c1c367593ca47e \ + --python /usr/bin/python3 --uv /home/worsch/.local/bin/uv +``` + +The `/tmp` bundle is a retained preparation artifact; recheck its complete manifest/artifacts and installer hash against the independently retained selections immediately before installation. A missing or changed artifact requires reconstruction and renewed comparison, never substitution. Retain install receipt, help result, copied-file hashes/modes/ownership and an empty-key refusal receipt. Record same-UID code isolation as unestablished. On install failure preserve the partial prefix and `INSTALL-FAILED`; use a newly reviewed prefix after diagnosis. No rolling alias or automatic cleanup is selected. + +Independent checks in `verification.json` establish the entire bundle's 53 source-file hashes still match the owner checkout, whose only committed successor is documentation/workplan changes. Every consumer R1, Platform P4 and Access rollout R2 manifest entry matched. Fresh offline installation, actual installed CLI help, all ten catalog entries, actual installed authorize_action refusal before caller or socket access, and existing-prefix refusal passed. The first proof harness omitted uv from its minimal PATH and produced a preserved failed `/tmp` prefix; the corrected harness supplies the explicit existing uv executable and a fresh prefix. This was a harness correction, not a runtime repair. + +The current join is P4 Platform `84c529f239c81e6d3ca337015700b03e3a862f53`, Access chart `dc042022f7c30671cf0a7605cf43ef6d96d04242`, Access image source `84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, and immutable OCI `forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. Exact projection is SHA-256 `8ce375f065552c551538f3acc7300c427407057eeff1d0b43755bfdf8a46169c`; lifecycle policy is SHA-256 `bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4`. The caller remains `secrets-engine=system:serviceaccount:secrets-engine:secrets-engine`. P4 already binds the exact selected consumer baseline in both trust fields. Historical consumer R1 P1 review text remains historical; its failed400-image finding is resolved by this new P4/R2 source join, not by editing the old receipt or reusing approval references. + +Role joins are correct: Secrets owns consumer/trust; Access owns workload/Helm; Platform owns custody and existing GitOps Application/sync freeze; cluster owns runtime/storage/headroom/PV; founder owns the attended disposition. No new native role or authority is assigned. P4's founder, expiry, approved CCR and held-window fields are still null. This review cannot fill them or act as one of the five native dispositions over a completed exact scope. + +Unchanged gates: T02 remains progress; T03 remains wait. Independently authenticated public custody return, key-bearing successor config/digest and trust admission, exact ready named pod/source/image/projection, bounded caller delivery actor and session, exact lifecycle capture/export scope, signed ALLOW/DENY/rotation/removal/outage proofs, native audit, actual Approval/backend/effect/readback/cleanup and WP13 Plan/grants remain outstanding. TSA-01/02/03 remain open; effects accepted remain **0/11 sites across10 handlers**. No credential, kubeconfig or private custody data was read, no actual home installation was made and no native action or approval was created. No new runtime verifier or service is proposed. diff --git a/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/actual-install-work-record.patch b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/actual-install-work-record.patch new file mode 100644 index 00000000..dfac5f47 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/actual-install-work-record.patch @@ -0,0 +1,25 @@ +--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md ++++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md +@@ -220,3 +220,22 @@ + No credential/kubeconfig/private custody read, caller mint, live action, actual + home install or new authority occurred. Exact review/proof and concrete install + step: Custodian `secrets-native-admission-next/` within the2026-10-16 target wave. ++ ++ ++2026-10-16 actual inert local preparation: coordinator installed the selected ++bundle through the unchanged owner offline installer into the exact selected ++fresh release prefix and copied all17 selected public configuration files0600 ++under the exact selected pre-key revision, with new private directories0700. ++The bounded coordinator runner checked53 runtime source hashes and complete ++bundle/config hashes before writing; installed CLI help and ten catalog rows ++passed. Actual installed authorize_action with the exact selected environment ++refused missing admitted keys before caller or socket access; its audit hook ++would forbid either. Backend and Approval remain blank, no caller was created, ++no key was admitted, no effects were authorized and same-UID code isolation ++remains unestablished. Non-secret observed installation receipt SHA-256 ++1ab9c271dfb236f09c4528ae18897381f5e404541708bf36f50c41c77d183ba0 is ++Custodian `secrets-native-admission-next/actual-prekey-install.json` within the ++2026-10-16 target wave. This supersedes only the preceding historical statement ++that actual local installation was not yet made. Native custody/rollout/caller/ ++trust/Approval/effect gates and T02progress/T03wait remain unchanged; accepted ++protected sites remain0/11 across10handlers, TSA01/02/03 open. diff --git a/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/owner-work-record.patch b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/owner-work-record.patch new file mode 100644 index 00000000..095f104c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/owner-work-record.patch @@ -0,0 +1,36 @@ +--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md ++++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md +@@ -190,3 +190,33 @@ + coverage remains0/11 across10handlers; T02 progress/T03 wait and task-native + WP13/WP8 gates remain open. Published Access receipt-tool2b064ee and verified + source APIs do not substitute for the separately reviewed hardened native image. ++ ++ ++2026-10-16 owner public-source selection for preparatory installation: select ++runtime4dd004a/documentation3718e60 and the canonical offline release manifest ++sha256:105dcebe6f03e9799f91ea75175e20500df2aa7f08b5e32d39c1c367593ca47e, ++with public empty-key consumer baseline ++sha256:72521ff7729f8ddf5176092f3949cd50ac5f9241b0a5d1d30f3c63ce1d83091b. ++Select the fresh owner-controlled release prefix ++`/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03` ++and config revision ++`/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1`. ++Use the existing offline installer and fixed-helper CLI; verify the complete ++owner-controlled parent chain and artifact set before installation. This closes ++public runtime/config selection only; actual home installation is not yet made. ++Fresh isolated install/help, ten catalog rows and real installed authorize_action ++empty-key refusal before caller/socket access passed. All53 runtime source hashes ++and complete consumerR1/PlatformP4/AccessrolloutR2 manifests matched. P4/R2 binds ++the same baseline/policy/caller, projection8ce375f and separately pinned chartdc0420/ ++image84c78e0 OCI digest09a83b2. The historical P1/failed400 finding is superseded ++only for this exact new source join, with old receipts preserved. ++Secrets owns consumer/trust; Access workload/Helm; Platform custody/GitOps sync; ++cluster runtime/storage/PV; founder the attended disposition. No native assent is ++supplied: founder/expiry/approvedCCR/window remain null. Public custody return, ++independent key-bearing successor admission, named-pod/runtime receipt, bounded ++caller delivery and lifecycle capture/export scope remain outstanding. Approval, ++backend/effects/readback/cleanup/WP13 grants remain separate. T02 stays progress, ++T03 wait; TSA01/02/03 remain open, protected acceptance0/11sites across10handlers. ++No credential/kubeconfig/private custody read, caller mint, live action, actual ++home install or new authority occurred. Exact review/proof and concrete install ++step: Custodian `secrets-native-admission-next/` within the2026-10-16 target wave. diff --git a/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/review_public_preparation.py b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/review_public_preparation.py new file mode 100644 index 00000000..5e7ea7cc --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/secrets-native-admission-next/review_public_preparation.py @@ -0,0 +1,117 @@ +"""Reproduce public artifact integrity, joins and disposable install; no credentials.""" +import hashlib +import json +import os +import shutil +from pathlib import Path +import subprocess +import sys +import tempfile + +HERE = Path(__file__).resolve().parent +WAVE = HERE.parent +OWNER = Path('/home/worsch/secrets-engine') +CONSUMER = WAVE / 'decision-native-preparation/consumer-admission-r1' +BUNDLE = Path('/tmp/secrets-native-cli-4dd004a-one') +PLATFORM = WAVE / 'platform-signer-custody/native-adapter-r2/native-packet-p4' +ACCESS = WAVE / 'access-native-admission/rollout-r2' + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + +def read(path): + return json.loads(path.read_bytes()) + +def checked(command, **kwargs): + result = subprocess.run(command, capture_output=True, text=True, timeout=120, **kwargs) + assert result.returncode == 0, result.stderr + return result.stdout + +def verify_manifest(directory, key): + manifest = read(directory / 'manifest.json') + for name, digest in manifest[key].items(): + assert sha(directory / name) == digest.removeprefix('sha256:'), name + return manifest + +consumer = verify_manifest(CONSUMER, 'files_sha256') +platform = verify_manifest(PLATFORM, 'files_sha256') +access = verify_manifest(ACCESS, 'files') +bundle = read(BUNDLE / 'manifest.json') +expected = '105dcebe6f03e9799f91ea75175e20500df2aa7f08b5e32d39c1c367593ca47e' +assert sha(BUNDLE / 'manifest.json') == expected +assert (BUNDLE / 'manifest.json').read_bytes() == (WAVE / 'decision-native-preparation/r2/canonical-bundle-manifest.json').read_bytes() +for name, digest in bundle['source_files'].items(): + assert sha(OWNER / name) == digest, name +for name, digest in bundle['artifacts'].items(): + assert sha(BUNDLE / name) == digest, name +assert sha(OWNER / 'scripts/install_release_bundle.py') == sha(BUNDLE / 'install.py') +baseline = read(CONSUMER / 'consumer-baseline.json') +scope = read(PLATFORM / 'admission-scope.incomplete.json') +assert scope['consumer_trust_sha256'] == scope['bindings']['trust_sha256'] == 'sha256:' + sha(CONSUMER / 'consumer-baseline.json') +assert scope['bindings']['deployment_sha256'] == scope['deployment_projection_sha256'] == access['source_projection_sha256'] +assert scope['producer_image'].endswith('@' + scope['bindings']['image_sha256']) +assert scope['caller_binding'] == 'secrets-engine=system:serviceaccount:secrets-engine:secrets-engine' +assert scope['bindings']['policy_sha256'] == baseline['policy']['policy_package_digest'] +assert scope['founder_entity_id'] is None and scope['expires_at'] is None and scope['ccr_sha256'] is None and scope['exclusive_window_sha256'] is None +for name, digest in baseline['configuration']['files_sha256'].items(): + path = CONSUMER / 'configuration' / name + assert sha(path) == digest.removeprefix('sha256:') + if name.startswith(('catalog/', 'policies/')) or name == 'pep-stance.yaml': + assert path.read_bytes() == (OWNER / name).read_bytes(), name +assert read(CONSUMER / 'configuration/trust/lifecycle-pre-key.json')['keys'] == {} +parent = Path(tempfile.mkdtemp(prefix='secrets-owner-preparation-')) +prefix = parent / 'installed' +clean_env = {'PATH': '/usr/local/bin:/usr/bin:/bin', 'UV_CACHE_DIR': str(parent / 'uv-cache')} +uv = shutil.which('uv') +assert uv +installed = checked([sys.executable, str(BUNDLE / 'install.py'), '--bundle', str(BUNDLE), '--prefix', str(prefix), '--manifest-sha256', expected, '--uv', uv], env=clean_env, cwd='/tmp') +help_output = checked([str(prefix / 'bin/secrets-engine'), '--help'], env=clean_env, cwd='/tmp') +probe = '''import json,os,pathlib,sys +from secrets_engine.config import Config +from secrets_engine.approval_consume import authorize_action +from secrets_engine.catalog import load_catalog +from secrets_engine.errors import DecisionError +env=json.loads(pathlib.Path(sys.argv[1]).read_bytes());os.environ.update(env) +os.environ['SECRETS_ENGINE_PDP_TRUST_FILE']=sys.argv[2] +os.environ['SECRETS_ENGINE_PDP_VERIFIER']=sys.argv[3] +os.environ['SECRETS_ENGINE_CATALOG']=sys.argv[4] +events=[] +def observe(event,args): + if event=='open' and str(args[0])==env['SECRETS_ENGINE_PDP_TOKEN_FILE']:events.append('caller-read') + if event.startswith('socket.'):events.append(event) +sys.addaudithook(observe) +cfg=Config.load();catalog=load_catalog(cfg.catalog_dir) +try:authorize_action(cfg,None,'apply') +except DecisionError as exc: + assert str(exc)=='access-engine responder trust is missing or invalid',str(exc) + assert str(exc.__cause__)=='missing admitted keys',str(exc.__cause__) +else:raise AssertionError('empty trust did not refuse') +assert not events,events +assert cfg.bao_addr=='' and cfg.approval_url=='' +print(json.dumps({'empty_trust_refusal':'before caller or socket access','catalog_rows':len(catalog),'backend_address':'','approval_url':''})) +''' +probe_output = checked([str(prefix / 'venv/bin/python'), '-c', probe, + str(CONSUMER / 'configuration/environment.proposed.json'), + str(CONSUMER / 'configuration/trust/lifecycle-pre-key.json'), + str(prefix / 'libexec/secrets-engine-pdp-verify'), + str(CONSUMER / 'configuration/catalog')], env=clean_env, cwd='/tmp') +existing = subprocess.run([sys.executable, str(BUNDLE / 'install.py'), '--bundle', str(BUNDLE), '--prefix', str(prefix), '--manifest-sha256', expected], capture_output=True, text=True, env=clean_env, cwd='/tmp', timeout=120) +assert existing.returncode == 1 and 'File exists' in existing.stderr +receipt = { + 'result': 'pass', 'public_source_files_verified': len(bundle['source_files']), + 'consumer_manifest_sha256': 'sha256:' + sha(CONSUMER / 'manifest.json'), + 'platform_p4_manifest_sha256': 'sha256:' + sha(PLATFORM / 'manifest.json'), + 'access_r2_manifest_sha256': 'sha256:' + sha(ACCESS / 'manifest.json'), + 'baseline_sha256': 'sha256:' + sha(CONSUMER / 'consumer-baseline.json'), + 'bundle_manifest_sha256': 'sha256:' + expected, + 'disposable_prefix': str(prefix), 'install_receipt': json.loads(installed), + 'installed_cli_help': bool(help_output), 'baseline_probe': json.loads(probe_output), + 'existing_prefix_refused': True, 'current_owner_runtime_matches_bundle_sources': True, + 'consumer_platform_access_joins': 'pass', 'native_install': False, + 'key_or_credential_read_or_mint': False, 'native_assent_created': False, + 'native_effects_accepted': 0, 'native_effect_sites': 11, 'handlers': 10, + 'retained_harness_failure': 'First minimal PATH omitted uv; installer refused before venv creation and marked partial /tmp prefix. Corrected harness selects the existing explicit uv binary and uses a fresh disposable prefix. No runtime patch or retry into failed prefix.', + 'uv_binary': uv, +} +(HERE / 'verification.json').write_text(json.dumps(receipt, indent=2) + '\n') +print(json.dumps(receipt, indent=2)) diff --git a/docs/changes/netkingdom-target-2026-10-16/source-wave10-integration-plan.json b/docs/changes/netkingdom-target-2026-10-16/source-wave10-integration-plan.json new file mode 100644 index 00000000..516149d3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/source-wave10-integration-plan.json @@ -0,0 +1,60 @@ +[ + { + "repo": "railiance-platform", + "base": "a3e8eb64bae110393ba0aeab043de03a92b8421a", + "patch": "docs/changes/netkingdom-target-2026-10-16/platform-signer-custody/native-owner-boundary-r4/implementation-r4-full.patch", + "patch_sha256": "sha256:25bbff23421121836625e0081213fa2d1535e69a6123fbac21b62163b94293db", + "before": { + "scripts/flex_auth_signer_native.py": "sha256:5a3b3b33bcad45ef2939413bfd3db76e5ee70e3b815d0d64af53147851aa3a0d", + "credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml": "sha256:72fd6e84e866a72f4768dabd66326aebe1fdd00013b2314de961ca72a632679f", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json": "sha256:5f8c37a7d9781884bf06050be80ca192a3b92eab2d20e28f9b304e57e7456844", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-adapter.md": "sha256:0e185a651fbbef96996c38ff18ce338f4d6c0117c3fb069a43e766816e56d5db", + "tests/test_flex_auth_signer_native.py": "sha256:8a99d1819c54fc20660cd6d16e8b429e2fc9f30cf4cefc90704201a588c825a6", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "sha256:180731a9ac1a2b4ccb4458c6cee54151f43e63008e2c35a33db7f01ea9e9d768" + }, + "after": { + "scripts/flex_auth_signer_native.py": "sha256:4fb9ce6608ad04c4f5dc98a95796dc88a08a2453dc15c0d40c5c688fc02f573f", + "credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml": "sha256:1dfcdf206d2bdfcb9b7a21c132e7e3902ad98659dd023bb3993b0825d903c1c7", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json": "sha256:d7b3322d105cdde10ab3003b81b34b8356fdfa1ac08858f6464984ce1c253402", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-adapter.md": "sha256:0e2e431dcd2df66e65434ce4060a4d5b52941f50928be80a075e4f972229d3a9", + "tests/test_flex_auth_signer_native.py": "sha256:f74a2316506737966e2ea539799cb477a9cec2718a6679f417478f9972731b3e", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "sha256:20451474d84be986f7be1e2ff8c48e3a754e981718949f0e9d771081ae15d25b" + }, + "message": "Separate signer runtime storage review from GitOps ownership", + "receipt": "docs/changes/netkingdom-target-2026-10-16/platform-signer-custody/native-owner-boundary-r4/canonical-integration.json" + }, + { + "repo": "access-engine", + "base": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0", + "patch": "docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/implementation-r2.patch", + "patch_sha256": "sha256:14f1a8de20105760340d212b579d669a0e770d20accc433876f024601f206be1", + "before": { + "charts/flex-auth/templates/deployment.yaml": "sha256:b347614283e65d9e8006a93dfbdfec15360632f6c318761e245213460e158666", + "charts/access-engine/templates/deployment.yaml": "sha256:155310e75e3883fc25d9c9dcf79650a8216cf6fd09902421b856cdfeab5e4cc3", + "tests/test_outbox_replicas_chart.py": null, + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:def6f73745d02ea88aa334db3239d2ef22984ebefa8853a5573496a08ed1592c" + }, + "after": { + "charts/flex-auth/templates/deployment.yaml": "sha256:26e29618b9110bdd9d8aa73aae432a95c757a4f46e567ce4fe429c60bd519026", + "charts/access-engine/templates/deployment.yaml": "sha256:04f1fa3c0378f1f28b11438549e0542a85c0604fb19d7ea814eeddf8d99de29b", + "tests/test_outbox_replicas_chart.py": "sha256:dde2b054fa0355cf88034c3482aae45b4116d8ebdb9b0b9b28075960ebc244f0", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:3a3fc05528eb869204811d90d12ef5a3ac3b6d537b5b7a8cccbc15b6483a002f" + }, + "message": "Allow stopped single-writer outbox maintenance through Helm", + "receipt": "docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/canonical-integration.json" + }, + { + "repo": "railiance-cluster", + "base": "750c3e5b9b4623c52bce2b8fbd758f423f81f6c1", + "patch": "docs/changes/netkingdom-target-2026-10-16/access-native-admission/cluster-review-r1/own-work-record.patch", + "patch_sha256": "sha256:40313328d2fd67496705b7369d56f072020222929e91942626ebb9cb82acc7ac", + "before": { + "workplans/RCLUSTER-WP-0017-netkingdom-signer-runtime-storage-review.md": null + }, + "after": { + "workplans/RCLUSTER-WP-0017-netkingdom-signer-runtime-storage-review.md": "sha256:91121c972379fbaf92a4fa66e01bef9a36a7aea11986168a0fdc79f7cce9422f" + }, + "message": "Record scoped NetKingdom signer runtime and storage review", + "receipt": "docs/changes/netkingdom-target-2026-10-16/access-native-admission/cluster-review-r1/work-record-integration.json" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/source-wave7-integration-plan.json b/docs/changes/netkingdom-target-2026-10-16/source-wave7-integration-plan.json new file mode 100644 index 00000000..ee7b9de7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/source-wave7-integration-plan.json @@ -0,0 +1,132 @@ +[ + { + "repo": "access-engine", + "base": "4f6946bbb338f61e4ee19efe32dcfe2757c22dcc", + "patches": [ + { + "path": "access-native-source/b2-b3/r3/trusted-source-r3-over-b1-r2.patch", + "sha256": "46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6" + } + ], + "files_sha256": { + ".forgejo/workflows/image.yaml": "sha256:18307ce17027b263ac0703a9e82fba8d14b672ca8bc9dee4c90b3aed2decd58e", + "Containerfile": "sha256:8ff6dc27d632d4f889b2cd527fc14c9dd1f79359b63384600088c87a47943a98", + "cmd/access-engine/main.go": "sha256:3f799c39843967e7b087e439f73f8ff8718f3153ef418356244379e9b6a997cb", + "cmd/access-engine/trusted_service_http_test.go": "sha256:a8248b5cdda21f576f1a8e5d674d6c4ee1a0d128991fdb95d2bcba53192f1f33", + "docs/trusted-service-task-source.md": "sha256:d8bb98219e3f54129da4f0b5d3a8465b8d59cf68b0f2d61045ac7482cdfbca42", + "examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml": "sha256:542bf9be8bcd11b58e0a92e41730304a9b7203fb7cf1b2558665f71a469fc469", + "examples/secrets-engine-service-task-execution-v2/policy_package.md": "sha256:95def8495596520a286a62dc48aca3f232668534ff687a2f4d47a46805eaac15", + "examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml": "sha256:fccf6ab7345e93893ab335fa34f2c180dde2edc708519a88bb39e73ebfc0fd48", + "go.mod": "sha256:af40d3ad78d6edad8495766f483ae5a857fc320db340a75d7b3be04236e0cf78", + "internal/audit/log.go": "sha256:e47ef2c0e6be529d51e8c0c270759af8e988261437d890254f317cd5c0f0a238", + "internal/decision/engine.go": "sha256:be6ea0e81422dba6b6d2124530263bf6fd4025c8dcac9360c619adbff0ac0bbd", + "internal/decision/trusted_service.go": "sha256:a4d76130c90b75b6a734877d8031943d5d6f6d64b02e4dc8295c497ec894f635", + "internal/decision/trusted_service_time_test.go": "sha256:b72fd3744df23a0e4cf30529c6b31a316d0ce2f0f0a8abc027966db7044b0c83", + "internal/emission/delivery.go": "sha256:ac90171af3c2957b6877b7334d3dbaed671f63b8335c36034c7a1bde9a0d8265", + "internal/emission/numeric_replay_test.go": "sha256:a7420ed712ee8e446995ce8440cf7fcc558fc9d254388b425e61d675f61e493c", + "internal/emission/outbox.go": "sha256:e347dd5b2e8cc1f64d4d231ecf30fe931f1524b6689338ee208c1c15281b09dc", + "internal/policy/package.go": "sha256:6b94f34917fcc344c12d6ff7a9465f712c026bd35cd5bc64a115ab5287b42e13", + "internal/registry/numeric_source_test.go": "sha256:d7b57e0d3fc11a51a6bf249258ccae6eb50dae41a038ff4d588f86ef41bd8698", + "internal/registry/store.go": "sha256:0ccbb6ed6c8d2097009096936e92c2eba7073126cd46b124880e694b84cf2114", + "internal/sign/numeric_decode_test.go": "sha256:d45b4b1708359e32da108d011735c8b4916f817d266d1c5f7059d9cde136fe95", + "internal/sign/sign.go": "sha256:0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde", + "internal/taskexecution/approval_source.go": "sha256:046ebd094a160d8c4a1dd07b73123fb6fce1c6fd45258a2edb99043f10001848", + "internal/taskexecution/approval_source_test.go": "sha256:d1ab01a76dc7f9738a25c20535dd43d892c477a19cdabba0f4c35eb54e36a043", + "internal/taskexecution/service_input.go": "sha256:4dac272cbbe06a6c0ad9d8fc103f03828e01e02568b59c94c733d4de8395928c", + "pkg/api/canonical.go": "sha256:a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19", + "third_party/approvalfact/README.md": "sha256:5d936b7f3de87bbf4e946f0dc44823d2f69b62b4122b55b9064791b7a19fd4f5", + "third_party/approvalfact/SOURCE.json": "sha256:2ec250061aa70ab48c275feeb77f7a4b71ced8268c1064a6f170f59950504d92", + "third_party/approvalfact/canonical.go": "sha256:d48588fb26e9037c8e83421586b8a818f4c0a52afb874f3fadf7229fc971c282", + "third_party/approvalfact/claim.go": "sha256:007a2494f79c47c104635402407e9e68f17d8599d23e0f2c8ebf6cb1b47e3621", + "third_party/approvalfact/go.mod": "sha256:bb09c53ffea926fbfba6e580ed3bd71ca358575401717389b3c7c49c0c674b43", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:cc1479ce3a986b8d2a2027aee0343f883ba972418193fc64911ab49e9f868e9b" + }, + "receipt": "access-native-source/b2-b3/r3/canonical-integration.json", + "managed_id_preservation": "Canonical workplan retains existing T05 and T06 state_hub_task_id lines absent from temporary review snapshot. Exact diff inspected; all remaining bytes equal." + }, + { + "repo": "secrets-engine", + "base": "5a2099f400fb0c97fed2d991832c0e31513bbdfb", + "patches": [ + { + "path": "decision-trusted-successor/successor-r4-full.patch", + "sha256": "759bf5c7b89169ddd32761d36ce1ae71735bb4c471213f5bbb8cd9af25350b86" + } + ], + "files_sha256": { + "docs/task-execution-handoff.md": "sha256:ef049e0601a97bfcf41d8d639d85aa07e2f2bdd8b4c0dfd42b4a62b552c8e88a", + "docs/task-scoped-openbao-execution-contract.md": "sha256:54b4027ead9dde176d99305f5b21f6450305da7ec1a57f212fd694d7ba74ef12", + "src/secrets_engine/approval_consume.py": "sha256:4e3de16ed77bca3023993065556edd635d35073668f1a3b0d079284969111d37", + "src/secrets_engine/decision_signature.py": "sha256:74a9caa43f93aae5c9e945c0a872c0e58c83aea00e9b970a4dbda673af2df805", + "src/secrets_engine/execution_contract.py": "sha256:ca62bfb1716c0e882eac6512320b127405511affdc1ebd46f9496ce030c0ed01", + "src/secrets_engine/task_execution_handoff.py": "sha256:f673443dc5f3a796a09db5ad51bcdf59d83d718cc00912489dbb8d43df95aefe", + "tests/fixtures/pdp-signature/approval-native-vectors.json": "sha256:fbfe3726d9ba2cffd8cc5d35aa6a0cf0729e5b3f1a684ade36d25dccb53d240a", + "tests/test_execution_context.py": "sha256:ee633f5638c3b9972bd2865ae106975a129f1af557d41718841a13dd2a8ab26b", + "tests/test_task_approval_preparation.py": "sha256:10c2de83f6cc5a6d7dfdefd0214da7f51b82c51aa0cd55ab679da685787af0f7", + "tests/test_trusted_successor.py": "sha256:a167cfc7a7445c5631b607aa8a2ae8bf5cae0ac47219eda8244d5e1833399ddb", + "verifier/README.md": "sha256:b6b96e097503e04e4407836e973159dd1ff09c5654f4378fbc95b1f214b6c77f", + "verifier/internal/sign/sign.go": "sha256:0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde", + "verifier/main.go": "sha256:8cb508d7e6d3692cfb7ea1c386ff0354c550ad9be1707d251f0db8b3b6411448", + "verifier/pkg/api/canonical.go": "sha256:a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19", + "verifier/producer-source.json": "sha256:7be4a07c11ca2d8610736b768c39a2611533fb72e87e75980cd9ae4673620124", + "verifier/testsupport/signer/main.go": "sha256:8e1e90a00622ab01ff8f9aae5d93a05a14304cecc6c011c48b9927299b80a304", + "workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md": "sha256:04c9f7432216205b0ded3abc49042988e7730d1a3014f919d60456192fa7fd6c", + "workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md": "sha256:4b9e6f2acb9bd1f085331ac9152ec2658bc0aefd43fd51152a8d535a0073187c" + }, + "receipt": "decision-trusted-successor/canonical-integration-r4.json" + }, + { + "repo": "approval-engine", + "base": "3485415b823faf4f3b682079195e186666b0ed2b", + "patches": [ + { + "path": "approval-timestamp-precision/implementation.patch", + "sha256": "74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd" + }, + { + "path": "approval-timestamp-precision/owner-review-record.patch", + "sha256": "fd69252bfa12825158eb2333d4d3816a1cdde5f67b58e937dd4f8bd321627073" + } + ], + "files_sha256": { + "adapters/go/approvalfact/README.md": "sha256:ac8f85d83c50a59c74a0d6ffa5f2bba9a414b2aab4e11a49c3dcc16e1b5201fc", + "approval_engine/store.py": "sha256:866b275ed2d844d0aa9090ce9899ce955c6bfe32a493c2645c86e72efa343e86", + "docs/approval-consumption.md": "sha256:31eee8a1a975bc1ad6a77ca0973208b2fc636c68cb0c748cb8aa666d5618be85", + "docs/reviews/2026-10-16-fractional-consumption-temporal-approval-owner-review.json": "sha256:fd5a00c498940fcfeaf188bded42de2f55b2fd04d0f78f294cc00610f5af3a6b", + "tests/test_timestamp_precision.py": "sha256:cbffa9265546a685b802702f5baf4e9d9ee822aaacab83c9a15c6c7316ee989b", + "workplans/APPROVAL-WP-0006-fractional-consumption-time.md": "sha256:fbaabd40ee6ab2283a1612d21a9317b55d85493af3394f27297f04c11e63010a" + }, + "receipt": "approval-timestamp-precision/canonical-integration.json" + }, + { + "repo": "railiance-platform", + "base": "76fdc0b861ec2ee7f6512dc5023ffc0cee5940f9", + "patches": [ + { + "path": "protected-execution-trusted-successor/fullchain-r1/implementation.patch", + "sha256": "a26c68ff224663129757fd43d0a5364762180131030e4dda22975935be89f99d" + }, + { + "path": "platform-signer-custody/writer-r2/implementation.patch", + "sha256": "b0c190060f5a41d6002e1fb7b49f047fd4874a51576f29f3c127a8634c2616f6" + }, + { + "path": "platform-signer-custody/writer-r3/r2-to-r3.patch", + "sha256": "a29cdbd2ac92fd09bc6c79144e9aeb84bb6abcd303df997c0f01c0223cf1a508" + } + ], + "files_sha256": { + "docs/credential-lane-designs/flex-auth-envelope-signing-policy.hcl.proposed": "sha256:a8ae0ea97e5bde8a9e5d785fc87b8095e4cc24b5fcf358a4b7b1568ab1dd07ee", + "docs/credential-lane-designs/flex-auth-envelope-signing-projection.yaml.proposed": "sha256:74be3565948d4a70c5b01c24d2e5886c06e8c1a884e4e355517cbcf77a03f514", + "docs/credential-lane-designs/flex-auth-envelope-signing.md": "sha256:35a2ad5b86e54c5432eeb28f755086f057ba889a44412ab83ac1a52ea8548ac6", + "docs/credential-lane-designs/flex-auth-envelope-signing.proposal.json": "sha256:de7855693c424246c2e6c63e5b15f1d5d4509e6b64f3f1b234d10e675525d314", + "docs/task-execution-native-fixture.md": "sha256:055cc0c00fb18e90f0421512737687a0634279f630b4a483b0524b0c6102b5f6", + "scripts/flex_auth_signer_custody.py": "sha256:8ed281a49c1f1cdb23772efe3593e75c62572a12b8d7a6a4c574f27b38722e75", + "tests/task_execution_trusted_servers.py": "sha256:bdaf847b882ef570cd01bb84b338225d61281f949d72cee9c91cb556d04719d7", + "tests/test_flex_auth_signer_custody.py": "sha256:cd47c33bda7a2a18808e0daa027e8a66b2a55eaf0ee79e603337f3c2eb2c3761", + "tests/test_task_execution_trusted_whole.py": "sha256:55040c7c4c89d62818308d75c81c071e1292ed83425e4124d18b8ba5e2d2c22a", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "sha256:d3cbe2ba5e344c1e8fc7e9f422d89030616b98abd53df4b9327f44fcaca865d3" + }, + "receipt": "platform-signer-custody/canonical-integration-r3.json" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/source-wave8-integration-plan.json b/docs/changes/netkingdom-target-2026-10-16/source-wave8-integration-plan.json new file mode 100644 index 00000000..ef0f681a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/source-wave8-integration-plan.json @@ -0,0 +1,128 @@ +[ + { + "repo": "access-engine", + "base": "2b064eee510640224274ccdc7cefceb1ae13162e", + "patches": [ + { + "path": "access-native-admission/outbox-hardening/implementation.patch", + "sha256": "52c0ff3f62056bafe141d74baf2c79f7cfab8f5d5c674ac85cedd24bb07d5ee4" + } + ], + "files_sha256": { + "charts/access-engine/templates/deployment.yaml": "sha256:155310e75e3883fc25d9c9dcf79650a8216cf6fd09902421b856cdfeab5e4cc3", + "charts/access-engine/values.yaml": "sha256:01dafea0b6f4752081ff9dcc10a1bd315c28ac7111fbd7802b2b8e76f1a91b0f", + "charts/flex-auth/templates/deployment.yaml": "sha256:b347614283e65d9e8006a93dfbdfec15360632f6c318761e245213460e158666", + "charts/flex-auth/values.yaml": "sha256:0d5c3fa0c52d87ff6350d8f92527814b74bb8b7adadc92ded71856d9c83b1b03", + "cmd/access-engine/main.go": "sha256:da6d185ac971aaa652a6b242f5c46007f8b0089e71e6d6b499bd7925b1377758", + "cmd/access-engine/outbox_capacity_test.go": "sha256:b50bc34c9b759f32744237f7f029e34c6c5c2611c0377b86569fa123dafa7c6e", + "docs/decision-emission-operations.md": "sha256:b78d54bef54f1a93868a3ced9427297092448d19d26c57148ec56b64f4ff7d2f", + "internal/emission/delivery.go": "sha256:9bd2147108fafade36850ec2e00f0fe602471748af7d94c7b2f54e69658f95f1", + "internal/emission/hardening_linux_test.go": "sha256:1b9389db04080e413a62ead05370ff0281bca35c54e091d2d4dc855fd3d82662", + "internal/emission/lock_linux.go": "sha256:604743d6d2427ae8cc8f64cc5fae4934a4c1cf0ed24fbae171bea270ed668f6f", + "internal/emission/lock_other.go": "sha256:271a879de3d43036ce69a5db0f8c2ad401d7cce5ceab1fa721959516a1576493", + "internal/emission/outbox.go": "sha256:10b7ffa6899d2e89c4c36150f063a348eaf15cfce7a95e814af71b6ffd511ba0", + "tests/test_outbox_chart.py": "sha256:9c839f485e463ad2e84f6d948c81bdd912871efd8fdf54b6dfdf95bd22ef0f6e", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:23911caf0fe743b24680ccfc8223a5894c2f8bf382eb61426100310488b42e28" + }, + "receipt": "access-native-admission/outbox-hardening/canonical-integration.json", + "before_sha256": { + "charts/access-engine/templates/deployment.yaml": "sha256:8b537f42c076c0235bf120106fd649189ddee436497ba700b542a1c17e163755", + "charts/access-engine/values.yaml": "sha256:e762114881e0e3e2c71795588352d0a87880ee08309ac390c65b0e3c4f6421be", + "charts/flex-auth/templates/deployment.yaml": "sha256:8d08371c1b944e5a4ab494e42874346a29d2ffdcc5331a65f417f2f8a04bbaf0", + "charts/flex-auth/values.yaml": "sha256:05a11ad50d64299e1dbc8b0950528e7e40885f98f9157691b40a53b92aba3643", + "cmd/access-engine/main.go": "sha256:3f799c39843967e7b087e439f73f8ff8718f3153ef418356244379e9b6a997cb", + "cmd/access-engine/outbox_capacity_test.go": null, + "docs/decision-emission-operations.md": "sha256:24724b89f2cef7a193fe2b42a31a4d6817cfdeaf9da34e99599383cc0b5affa6", + "internal/emission/delivery.go": "sha256:ac90171af3c2957b6877b7334d3dbaed671f63b8335c36034c7a1bde9a0d8265", + "internal/emission/hardening_linux_test.go": null, + "internal/emission/lock_linux.go": null, + "internal/emission/lock_other.go": null, + "internal/emission/outbox.go": "sha256:e347dd5b2e8cc1f64d4d231ecf30fe931f1524b6689338ee208c1c15281b09dc", + "tests/test_outbox_chart.py": "sha256:f6c68dfc6f8a1a8dbf2afcab18b428111765b965a419890f52fb19eb51d3ceea", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:cc1479ce3a986b8d2a2027aee0343f883ba972418193fc64911ab49e9f868e9b" + } + }, + { + "repo": "ops-warden", + "base": "958352089b443459a6368efc0e591c8a0a8b8604", + "patches": [ + { + "path": "warden-admin-routing/ops-warden.patch", + "sha256": "362996a03c0e260e68744de7d041aa4d296c16c6d166341ad61ab3057a454c24" + } + ], + "files_sha256": { + "src/warden/routing/catalog.py": "sha256:34baa0f709c00a02412380e17481bf08654f6e0a86c4df094f822dad3b50a60a", + "tests/test_openbao_configuration_routing.py": "sha256:597c3092827b684cb86986a12e489d6fd609b62dc739a33b7dd42e59c9879721", + "workplans/WARDEN-WP-0038-plan-mutation-intent.md": "sha256:09b0ed8c6315255796a2da0d64dfd001b1f6de2e903ac2ea79bc5667b499fb83" + }, + "receipt": "warden-admin-routing/canonical-integration.json", + "before_sha256": { + "src/warden/routing/catalog.py": "sha256:bca219450bb7eb92dfac2273e85a93edccba04575f01d1ef74762453df805ccb", + "tests/test_openbao_configuration_routing.py": "sha256:efbb1c0be1e8cba7f1ca8ebd860abad9da90cfd9bd9e77206bc0b9a6c65e0c02", + "workplans/WARDEN-WP-0038-plan-mutation-intent.md": "sha256:73e5a2d20731abbce3e719fc655767dc93b385bfa932e53fb200df0e2235d8d5" + } + }, + { + "repo": "secrets-engine", + "base": "4dd004a2250104ba56293b2d7b0d96f4e076ea99", + "patches": [ + { + "path": "decision-native-preparation/r4/native-cli-admission-r4-over-r3.patch", + "sha256": "4deb45d649221b2f303ba12e634d1213845410848af0f2919bee47a365b856c0" + } + ], + "files_sha256": { + "docs/proposals/native-cli-admission/README.md": "sha256:13c84e6cb5a925166856f587b6bfaee3dda3536d2ad441bf8d6a03461f05091b", + "docs/proposals/native-cli-admission/proposal.json": "sha256:6a76c1156a5f142e247a42abe435b092eebbc09391948d78fcb6367f7fc34b9a", + "workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md": "sha256:2951cb7876801eff74af121c5d948c03f82ef53f25812e656342333e7561acb6" + }, + "receipt": "decision-native-preparation/r4/canonical-integration.json", + "before_sha256": { + "docs/proposals/native-cli-admission/README.md": "sha256:13c84e6cb5a925166856f587b6bfaee3dda3536d2ad441bf8d6a03461f05091b", + "docs/proposals/native-cli-admission/proposal.json": "sha256:fcf38c5923c9932ae8c75d039afc895c0295efe7bc61fcaa9211ae5f56dd4d5d", + "workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md": "sha256:2951cb7876801eff74af121c5d948c03f82ef53f25812e656342333e7561acb6" + } + }, + { + "repo": "railiance-platform", + "base": "7be8b6e76d602a5306052f9e42881fbea250ede8", + "patches": [ + { + "path": "platform-signer-custody/native-preparation-r1/implementation.patch", + "sha256": "a9d4631d3320ac3fa56f5a61965005c546eae241d2004cc1e6385e04668b7f7f" + }, + { + "path": "platform-signer-custody/native-adapter-r2/implementation-r2.patch", + "sha256": "c5b8f0d10650f13eb5939874bda1fb457b3dc5835e63e043af9824e43edb9f80" + } + ], + "files_sha256": { + "credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml": "sha256:72fd6e84e866a72f4768dabd66326aebe1fdd00013b2314de961ca72a632679f", + "openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl": "sha256:b9f816da25c7770863a5fc7b3b873e9da90b087e71a9052e766bf0a187ff85ef", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json": "sha256:5f8c37a7d9781884bf06050be80ca192a3b92eab2d20e28f9b304e57e7456844", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.md": "sha256:241e3e46d7823c1fb0dd62ce2d61dadf659d109286acea872e0aaad9cac26760", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-projection.yaml.proposed": "sha256:4972402b6d6490eb4dd540525b5ce475443d6b99373c28eda197c932438d745f", + "tests/test_flex_auth_signer_native_preparation.py": "sha256:8b05c5f519e69f66be9dc1ac942f5a7a8fe60edda63d4c38daaba7e15a166314", + "workplans/RPF-WP-0035-credential-lane-implementation.md": "sha256:180731a9ac1a2b4ccb4458c6cee54151f43e63008e2c35a33db7f01ea9e9d768", + "scripts/flex_auth_signer_custody.py": "sha256:f169ad12f90d729b647085c3bed2c0d8d005db23a2341b6d3d5f1793bcfd110c", + "scripts/flex_auth_signer_native.py": "sha256:5a3b3b33bcad45ef2939413bfd3db76e5ee70e3b815d0d64af53147851aa3a0d", + "tests/test_flex_auth_signer_native.py": "sha256:8a99d1819c54fc20660cd6d16e8b429e2fc9f30cf4cefc90704201a588c825a6", + "docs/credential-lane-designs/flex-auth-envelope-signing-native-adapter.md": "sha256:0e185a651fbbef96996c38ff18ce338f4d6c0117c3fb069a43e766816e56d5db" + }, + "receipt": "platform-signer-custody/native-adapter-r2/canonical-integration.json", + "before_sha256": { + "credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml": null, + "openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl": null, + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json": null, + "docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.md": null, + "docs/credential-lane-designs/flex-auth-envelope-signing-native-projection.yaml.proposed": null, + "tests/test_flex_auth_signer_native_preparation.py": null, + "workplans/RPF-WP-0035-credential-lane-implementation.md": "sha256:d3cbe2ba5e344c1e8fc7e9f422d89030616b98abd53df4b9327f44fcaca865d3", + "scripts/flex_auth_signer_custody.py": "sha256:8ed281a49c1f1cdb23772efe3593e75c62572a12b8d7a6a4c574f27b38722e75", + "scripts/flex_auth_signer_native.py": null, + "tests/test_flex_auth_signer_native.py": null, + "docs/credential-lane-designs/flex-auth-envelope-signing-native-adapter.md": null + } + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/sync-session-2439.json b/docs/changes/netkingdom-target-2026-10-16/sync-session-2439.json new file mode 100644 index 00000000..fea7b7e1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/sync-session-2439.json @@ -0,0 +1,7 @@ +{ + "chunk_id": "358d76", + "wall_time_seconds": 0.000016274, + "exit_code": 0, + "original_token_count": 1262, + "output": "WARN: working-memory commit failed: Generated-only commits rejected: staged\n{\n \"repo_slug\": \"the-custodian\",\n \"repo_path\": \"/home/worsch/the-custodian\",\n \"source_revision\": \"d170e6be76fb94ec0c37079dacdbf90146137b9d\",\n \"source_revision_reason\": null,\n \"issues\": [\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-35\",\n \"message\": \"Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']\",\n \"file_path\": \"workplans/\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-35\",\n \"message\": \"Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry\",\n \"file_path\": \"workplans/ADHOC-2026-08-25-retired.md\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-20\",\n \"message\": \"Workplan dependency target 'STATE-WP-0092' is not linked to State Hub\",\n \"file_path\": \"workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md\",\n \"db_id\": \"\",\n \"file_value\": \"STATE-WP-0092\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-40\",\n \"message\": \"blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \\u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved\",\n \"file_path\": \"workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05\",\n \"db_id\": \"\",\n \"file_value\": \"1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-39\",\n \"message\": \"Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \\u2014 unrouted human gate\",\n \"file_path\": \"workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06\",\n \"db_id\": \"\",\n \"file_value\": \"needs_human: true\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-22\",\n \"message\": \"Task description drift 'CUST-WP-0080-T06': file content differs from DB (file wins)\",\n \"file_path\": \"workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md#CUST-WP-0080-T06\",\n \"db_id\": \"6aca534c-5c7a-556f-b6b6-406433670c29\",\n \"file_value\": \"Founder implementation request, 2026-10-16. Document the optimized target and\\ncoordinate GPT-6.1-Sol medium agents again\",\n \"db_value\": \"Founder implementation request, 2026-10-16. Document the optimized target and\\ncoordinate GPT-6.1-Sol medium agents again\",\n \"fixable\": true\n },\n {\n \"severity\": \"INFO\",\n \"check_id\": \"C-08\",\n \"message\": \"Closed DB workstream 'cust-wp-0010b' (id=416ff571\\u2026, status=finished) has no backing workplan file\",\n \"file_path\": \"\",\n \"db_id\": \"416ff571-18cf-5cb2-9c8b-836f68a301b3\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"INFO\",\n \"check_id\": \"C-08\",\n \"message\": \"Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\\u2026, status=finished) has no backing workplan file \\u2014 projection tombstone, not live work\",\n \"file_path\": \"\",\n \"db_id\": \"370c2481-6806-41eb-a917-f8874f03184f\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"INFO\",\n \"check_id\": \"C-08\",\n \"message\": \"Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\\u2026, status=finished) has no backing workplan file \\u2014 projection tombstone, not live work\",\n \"file_path\": \"\",\n \"db_id\": \"3f54ce9c-1f95-42de-894f-0f81a52ba2e8\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"INFO\",\n \"check_id\": \"C-08\",\n \"message\": \"Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\\u2026, status=finished) has no backing workplan file \\u2014 projection tombstone, not live work\",\n \"file_path\": \"\",\n \"db_id\": \"5c53733b-c077-4a93-be66-9c3af8f9b6fd\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-26\",\n \"message\": \"Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \\u2014 new plans must conform\",\n \"file_path\": \"workplans/THE-WP-0001-federation-interface.md\",\n \"db_id\": \"\",\n \"file_value\": \"THE-WP-0001\",\n \"db_value\": \"CUST-WP\",\n \"fixable\": false\n }\n ],\n \"fixes_applied\": [\n \"bindings: synced 89/89\",\n \"C-17 cleared: pushed 1 backlogged commit(s)\",\n \"C-22 fixed: task 6aca534c\\u2026 description updated\",\n \"push: pushed\"\n ],\n \"summary\": {\n \"fail\": 0,\n \"automation_error\": 0,\n \"warn\": 7,\n \"info\": 4\n },\n \"result\": \"warn\"\n}\n" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-lane-before.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-lane-before.json new file mode 100644 index 00000000..01b079e0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-lane-before.json @@ -0,0 +1,13 @@ +{ + "command": [ + "/home/worsch/.local/bin/warden", + "route", + "show", + "openbao-platform-admin-login", + "--json" + ], + "cwd": "/tmp", + "exit_code": 0, + "stdout": "{\n \"id\": \"openbao-platform-admin-login\",\n \"title\": \"Attended OpenBao platform administration login\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao operator OIDC via key-cape\",\n \"warden_executes\": false,\n \"warden_role\": \"assist\",\n \"exec_capable\": true,\n \"resolvable\": true,\n \"wiki_ref\": \"wiki/playbooks/openbao-platform-admin-login.md#worker-checklist\",\n \"canon_ref\": \"railiance-platform/docs/openbao.md\",\n \"reviewed\": \"2026-08-22\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"not-applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": \"Attended human operator identity act; the governed admin operation supplies its own resource identity.\",\n \"unknown_reason\": null,\n \"resolution\": \"not-applicable\"\n },\n \"has_rotation\": false,\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": null,\n \"reviewed\": \"2026-08-22\",\n \"verified\": \"source-read\",\n \"is_verified\": true,\n \"implicit\": false\n },\n \"need_keywords\": [\n \"openbao\",\n \"platform-admin\",\n \"platform\",\n \"admin\",\n \"administrator\",\n \"first-time\",\n \"bootstrap\",\n \"database-engine\",\n \"database/config\",\n \"policy\",\n \"policies\",\n \"token-role\",\n \"token-roles\",\n \"mount\",\n \"auth-role\"\n ],\n \"next_action\": \"ops-warden can proxy this as the caller: `warden access --fetch` (or `--exec -- `); runs railiance-platform's tool with your identity. See `wiki/playbooks/openbao-platform-admin-login.md#worker-checklist`.\"\n}\n", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-plan-before.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-plan-before.json new file mode 100644 index 00000000..518df201 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/installed-plan-before.json @@ -0,0 +1,12 @@ +{ + "command": [ + "/home/worsch/.local/bin/warden", + "plan", + "attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata", + "--json" + ], + "cwd": "/tmp", + "exit_code": 0, + "stdout": "{\n \"need\": \"attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata\",\n \"verdict\": \"founder_required\",\n \"organization_posture\": \"build\",\n \"policy_gate\": \"zone-aware \\u2014 fetches gated by flex-auth-ops-warden-fetch at http://127.0.0.1:19091\",\n \"lane_id\": \"openbao-structure-survey\",\n \"lane_title\": \"OpenBao existing-structure survey for a construction plan (section 2)\",\n \"match_score\": 11,\n \"commands\": [\n \"ops-mason/scripts/plan-survey.py --kv-path --policy --approle --consumer --keyword \",\n \"# follow owner playbook: wiki/playbooks/openbao-structure-survey.md#worker-checklist\"\n ],\n \"founder_act\": {\n \"kind\": \"approve\",\n \"summary\": \"Attended owner ceremony approval required for openbao-structure-survey\",\n \"details\": {\n \"lane_id\": \"openbao-structure-survey\",\n \"wiki_ref\": \"wiki/playbooks/openbao-structure-survey.md#worker-checklist\",\n \"desk_hint\": \"warden desk --act approve --lane openbao-structure-survey\"\n }\n },\n \"ccr_stub\": null,\n \"catalog\": {\n \"path\": \"/home/worsch/.local/share/uv/tools/ops-warden/lib/python3.12/site-packages/warden/_registry/routing/catalog.yaml\",\n \"source\": \"bundled\",\n \"content_hash\": \"c47065c0892f\",\n \"mtime_iso\": \"2026-10-15T02:25:26.455960+00:00\",\n \"package_version\": \"0.1.2\",\n \"entry_count\": 44,\n \"active_count\": 40,\n \"newest_reviewed\": \"2026-10-11\",\n \"oldest_reviewed\": \"2026-06-18\",\n \"stale_entry_count\": 17,\n \"using_bundled\": true,\n \"warnings\": [\n \"using wheel-bundled catalog fallback \\u2014 reinstall from checkout (`uv tool install -e .` or `pip install -e .`) if lanes look missing\",\n \"17 catalog entries past 90d review cadence\",\n \"17 interim delegations need re-verifying (risk-scaled blocker cadence) \\u2014 see `warden route gaps`\"\n ]\n },\n \"candidates\": [\n {\n \"id\": \"openbao-structure-survey\",\n \"title\": \"OpenBao existing-structure survey for a construction plan (section 2)\",\n \"score\": 11,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": false,\n \"warden_executes\": false,\n \"lane\": \"ceremony\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"openbao-platform-admin-login\",\n \"title\": \"Attended OpenBao platform administration login\",\n \"score\": 10,\n \"status\": \"active\",\n \"resolvable\": true,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"login\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"ops-warden-warden-sign-token\",\n \"title\": \"Scoped OpenBao token for ops-warden SSH signing (warden-sign)\",\n \"score\": 8,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": false,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"openbao-api-key\",\n \"title\": \"API key, DB credential, or dynamic lease\",\n \"score\": 7,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"agent-harness-forgejo-deploy\",\n \"title\": \"agent-harness Forgejo deploy key (write sandbox; binky-control at cutover)\",\n \"score\": 6,\n \"status\": \"active\",\n \"resolvable\": true,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n }\n ],\n \"reasons\": [\n \"ceremony lane requires attended owner approval\"\n ],\n \"actor\": null,\n \"domain\": null\n}\n", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/manifest.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/manifest.json new file mode 100644 index 00000000..77e33e56 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/manifest.json @@ -0,0 +1,32 @@ +{ + "owner_repo": "ops-warden", + "base_commit": "958352089b443459a6368efc0e591c8a0a8b8604", + "checkout": "/tmp/warden-admin-routing-20261016", + "scope": "source-only; independent review and integration pending", + "request": "attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata", + "files": [ + { + "path": "src/warden/routing/catalog.py", + "before_sha256": "bca219450bb7eb92dfac2273e85a93edccba04575f01d1ef74762453df805ccb", + "after_sha256": "34baa0f709c00a02412380e17481bf08654f6e0a86c4df094f822dad3b50a60a" + }, + { + "path": "tests/test_openbao_configuration_routing.py", + "before_sha256": "efbb1c0be1e8cba7f1ca8ebd860abad9da90cfd9bd9e77206bc0b9a6c65e0c02", + "after_sha256": "597c3092827b684cb86986a12e489d6fd609b62dc739a33b7dd42e59c9879721" + }, + { + "path": "workplans/WARDEN-WP-0038-plan-mutation-intent.md", + "before_sha256": "73e5a2d20731abbce3e719fc655767dc93b385bfa932e53fb200df0e2235d8d5", + "after_sha256": "09b0ed8c6315255796a2da0d64dfd001b1f6de2e903ac2ea79bc5667b499fb83" + } + ], + "receipt_files": [ + "installed-plan-before.json", + "installed-lane-before.json", + "source-plan-after.json", + "source-lane-after.json", + "source-route-after.json" + ], + "patch_sha256": "362996a03c0e260e68744de7d041aa4d296c16c6d166341ad61ab3057a454c24" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/ops-warden.patch b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/ops-warden.patch new file mode 100644 index 00000000..ed031c4d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/ops-warden.patch @@ -0,0 +1,132 @@ +diff --git a/src/warden/routing/catalog.py b/src/warden/routing/catalog.py +index 54af6db..a8a0236 100644 +--- a/src/warden/routing/catalog.py ++++ b/src/warden/routing/catalog.py +@@ -206,6 +206,14 @@ def _openbao_configuration_need(query: str) -> bool: + first; reads and surveys continue through ordinary catalog ranking. + """ + text = query.lower().replace("-", " ") ++ # The attended administration front door is itself explicit intent. Its ++ # reviewed operation may concern signer custody or KV metadata rather than ++ # one of the control nouns below; keyword ranking must not turn that into a ++ # metadata-survey approval. This selects identity, never grants authority. ++ if (re.search(r"\battended\b", text) ++ and re.search(r"\bplatform\s+administration\b", text) ++ and re.search(r"\bopenbao\b", text)): ++ return True + control = re.search(r"\b(?:polic(?:y|ies)|auth\s+roles?|token\s+roles?|" + r"database\s*(?:/| )\s*config|secrets?\s+engine)\b", text) + change = re.search(r"\b(?:configure|configuring|create|apply|install|update|" +diff --git a/tests/test_openbao_configuration_routing.py b/tests/test_openbao_configuration_routing.py +index d85efee..4e831f2 100644 +--- a/tests/test_openbao_configuration_routing.py ++++ b/tests/test_openbao_configuration_routing.py +@@ -1,4 +1,5 @@ + """Configuration intent must never inherit a metadata survey's founder act.""" ++from dataclasses import replace + from pathlib import Path + + import pytest +@@ -9,6 +10,8 @@ from warden.routing.catalog import Catalog, load_catalog + ROOT = Path(__file__).resolve().parents[1] + NEED = ('attended OpenBao platform administration to configure read policies and auth roles ' + 'for Coulomb and email backup delivery') ++SIGNER_NEED = ('attended OpenBao platform administration for reviewed flex-auth ' ++ 'signer custody CAS0 and fixed KV metadata') + + + @pytest.fixture +@@ -18,18 +21,24 @@ def catalog(monkeypatch): + + + @pytest.mark.parametrize('need', [NEED, ++ SIGNER_NEED, ++ 'Attended OpenBao platform administration login', + 'configure OpenBao policies and Kubernetes auth roles for backup delivery', + 'apply OpenBao read policy for an explicitly reviewed service account']) + def test_configuration_requires_attended_admin_identity(catalog, need): + plan = build_plan(need, catalog=catalog) + assert plan.lane_id == 'openbao-platform-admin-login' + assert plan.verdict == 'founder_required' and plan.founder_act.kind == 'oidc_login' ++ assert plan.commands == [ ++ 'warden access openbao-platform-admin-login --exec -- ', ++ '# owner login is contained by warden; do not invoke it separately'] + assert all('--fetch' not in command and '--out' not in command for command in plan.commands) + + + @pytest.mark.parametrize('need', [ + 'survey existing OpenBao policies and roles for backup delivery', + 'OpenBao read-only metadata survey policy approle consumer backup', ++ 'survey OpenBao signer custody CAS0 and fixed KV metadata', + 'inspect OpenBao structure metadata policy approle']) + def test_survey_remains_metadata_only(catalog, need): + plan = build_plan(need, catalog=catalog) +@@ -37,11 +46,23 @@ def test_survey_remains_metadata_only(catalog, need): + assert plan.founder_act.kind == 'approve' + + +-def test_explicit_lane_id_still_wins_and_missing_admin_fails_closed(catalog): ++@pytest.mark.parametrize('need', [NEED, SIGNER_NEED]) ++def test_explicit_lane_id_still_wins_and_missing_admin_fails_closed(catalog, need): + assert catalog.find('openbao-structure-survey')[0].id == 'openbao-structure-survey' + unavailable = Catalog(catalog.path, [entry for entry in catalog.entries + if entry.id != 'openbao-platform-admin-login']) +- plan = build_plan(NEED, catalog=unavailable) ++ plan = build_plan(need, catalog=unavailable) ++ assert plan.verdict == 'unroutable' and plan.lane_id is None ++ assert not plan.commands ++ ++ ++@pytest.mark.parametrize('changes', [{'status': 'draft'}, {'lane': 'secret'}]) ++def test_attended_admin_cannot_use_inactive_or_secret_lane(catalog, changes): ++ admin = catalog.get('openbao-platform-admin-login') ++ unavailable = Catalog(catalog.path, [ ++ replace(entry, **changes) if entry.id == admin.id else entry ++ for entry in catalog.entries]) ++ plan = build_plan(SIGNER_NEED, catalog=unavailable, include_draft=True) + assert plan.verdict == 'unroutable' and plan.lane_id is None + assert not plan.commands + +diff --git a/workplans/WARDEN-WP-0038-plan-mutation-intent.md b/workplans/WARDEN-WP-0038-plan-mutation-intent.md +index 047148c..8c9c5b6 100644 +--- a/workplans/WARDEN-WP-0038-plan-mutation-intent.md ++++ b/workplans/WARDEN-WP-0038-plan-mutation-intent.md +@@ -11,7 +11,7 @@ planning_priority: P1 + depends_on_workplans: + - WARDEN-WP-0029 + created: "2026-09-08" +-updated: "2026-09-09" ++updated: "2026-10-16" + state_hub_workstream_id: "751ad530-e44b-52c4-b70e-cb47568a0179" + --- + +@@ -119,6 +119,29 @@ anything undeclared — is not built. It is a catalog schema change that would f + closed on the majority of lanes, which say nothing today, and it needs its own + argument rather than arriving as a side effect of this fix. + ++## Recognize explicit attended OpenBao platform administration ++ ++```task ++id: WARDEN-WP-0038-T01 ++status: done ++priority: high ++``` ++ ++2026-10-16 source follow-up: the exact request "attended OpenBao platform ++administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata" ++fell through to keyword ranking and selected `openbao-structure-survey` / ++`approve`. The configuration override required policy/auth-role nouns even ++though the caller explicitly named attended platform administration. Recognize ++that explicit intent before keyword ranking. The plan now selects ++`openbao-platform-admin-login` / `founder_required` / `oidc_login`; the governed ++reviewed-command transport and identity gate are unchanged. Survey requests ++remain metadata-only, and a missing administration lane stays unroutable. ++ ++Prepared and tested in an isolated checkout for independent review. This task ++records source repair only; it neither authorizes nor claims installation, ++attended login, signer provisioning or full16TSA native acceptance. Integration ++and the existing platform custody work retain those next actions. ++ + ### Not claimed + + This narrows what `warden plan` will assert; it does not make the tool an diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/owner-review.md b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/owner-review.md new file mode 100644 index 00000000..9a22a442 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/owner-review.md @@ -0,0 +1,63 @@ +# ops-warden attended administration routing review + +Prepared 2026-10-16 by the delegated ops-warden source owner, for independent +review and requester integration. Base: `958352089b443459a6368efc0e591c8a0a8b8604`. + +The reported request deterministically selects the wrong lane in both the +installed CLI and unmodified checkout. This is a source matcher defect, not a +missing lane or stale catalog. Installed and checkout catalogs have the same +reported content hash, `c47065c0892f`; the installed package is `0.1.2` using +the wheel-bundled fallback. `route show openbao-platform-admin-login` succeeds. + +The existing configuration override requires control nouns such as policies or +auth roles. The exact signer-custody request omits those nouns; keyword overlap +therefore ranks the metadata survey first. Naming the exact lane ID already +works, but natural language explicitly naming attended platform administration +must resolve consistently with that lane's own title. + +The patch adds a narrow branch requiring all of `OpenBao`, `attended`, and +`platform administration`, using the existing case/hyphen normalization. It +selects the existing administration override. No catalog, credential transport, +approval check, policy check, or execution implementation changes. + +Before: `openbao-structure-survey`, `founder_required`, `approve`. +After: `openbao-platform-admin-login`, `founder_required`, `oidc_login`, offering +only the existing governed reviewed-command execution plus its containment +comment. This does not authorize or perform that execution. Explicit lane IDs +retain precedence, and provisioning classification retains its existing order. + +Validation: **135 passed** with +`PYTHONPATH=src /home/worsch/ops-warden/.venv/bin/python -m pytest tests/test_openbao_configuration_routing.py tests/test_plan.py tests/test_routing.py -q`. +`git diff --check` passes. The first test run had five failures because the new +assertion omitted the existing containment comment from expected commands; +the assertion was corrected without changing command behavior. Regressions cover +the exact signer request, the lane title, existing configuration requests, +ordinary metadata surveys, exact-ID selection, provisioning, and missing, +inactive or wrongly typed administration lanes. The latter remain unroutable. + +`manifest.json` binds the patch, base commit and before/after file digests. +The five JSON receipts retain actual commands, working directory, exit status, +stdout and stderr. `source-plan-after.json` is the matching source-route evidence; +it is not installed or live-readiness evidence. + +The existing owner item `WARDEN-WP-0038` receives a source-repair follow-up task, +`WARDEN-WP-0038-T01`; no parallel governance workplan was created. Its patch +records source preparation only. The canonical owner checkout was left with +exactly its pre-existing dirty `.custodian-brief.md` and `WORK-RECORDS.md` files. +No credential read, mint, login, grant, deployment, installation or external +message occurred. Requester integration must apply the reviewed patch, run +`statehub fix-consistency --repo ops-warden` from the integrated owner checkout, +and record progress through the existing task chain. Doing that from this +isolated source-preparation checkout would prematurely publish owner state. + +Knowledge orientation: Emergent Security `0.2.6`, package digest +`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`. +Both `start` and `local-work` retrievals were withheld because owner sources +changed. Reviewed the relevant source boundaries directly: current ops-warden +AGENTS.md/INTENT.md, Custodian environment orientation, current NetKingdom +companion front-door rule and Gate House owner boundary. This is orientation +only; authority comes from the delegated source-only assignment. + +Remaining acceptance belongs to the original platform/NetKingdom chain: review, +integration and installed matching-route evidence, then separately authorized +attended custody and full16TSA native acceptance. No success is claimed there. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-lane-after.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-lane-after.json new file mode 100644 index 00000000..ecd3f9c5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-lane-after.json @@ -0,0 +1,15 @@ +{ + "command": [ + "/home/worsch/ops-warden/.venv/bin/python", + "-c", + "from warden.cli import app; app()", + "route", + "show", + "openbao-platform-admin-login", + "--json" + ], + "cwd": "/tmp/warden-admin-routing-20261016", + "exit_code": 0, + "stdout": "{\n \"id\": \"openbao-platform-admin-login\",\n \"title\": \"Attended OpenBao platform administration login\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao operator OIDC via key-cape\",\n \"warden_executes\": false,\n \"warden_role\": \"assist\",\n \"exec_capable\": true,\n \"resolvable\": true,\n \"wiki_ref\": \"wiki/playbooks/openbao-platform-admin-login.md#worker-checklist\",\n \"canon_ref\": \"railiance-platform/docs/openbao.md\",\n \"reviewed\": \"2026-08-22\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"not-applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": \"Attended human operator identity act; the governed admin operation supplies its own resource identity.\",\n \"unknown_reason\": null,\n \"resolution\": \"not-applicable\"\n },\n \"has_rotation\": false,\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": null,\n \"reviewed\": \"2026-08-22\",\n \"verified\": \"source-read\",\n \"is_verified\": true,\n \"implicit\": false\n },\n \"need_keywords\": [\n \"openbao\",\n \"platform-admin\",\n \"platform\",\n \"admin\",\n \"administrator\",\n \"first-time\",\n \"bootstrap\",\n \"database-engine\",\n \"database/config\",\n \"policy\",\n \"policies\",\n \"token-role\",\n \"token-roles\",\n \"mount\",\n \"auth-role\"\n ],\n \"next_action\": \"ops-warden can proxy this as the caller: `warden access --fetch` (or `--exec -- `); runs railiance-platform's tool with your identity. See `wiki/playbooks/openbao-platform-admin-login.md#worker-checklist`.\"\n}\n", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-plan-after.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-plan-after.json new file mode 100644 index 00000000..4858b841 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-plan-after.json @@ -0,0 +1,14 @@ +{ + "command": [ + "/home/worsch/ops-warden/.venv/bin/python", + "-c", + "from warden.cli import app; app()", + "plan", + "attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata", + "--json" + ], + "cwd": "/tmp/warden-admin-routing-20261016", + "exit_code": 0, + "stdout": "{\n \"need\": \"attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata\",\n \"verdict\": \"founder_required\",\n \"organization_posture\": \"build\",\n \"policy_gate\": \"zone-aware \\u2014 fetches gated by flex-auth-ops-warden-fetch at http://127.0.0.1:19091\",\n \"lane_id\": \"openbao-platform-admin-login\",\n \"lane_title\": \"Attended OpenBao platform administration login\",\n \"match_score\": 10,\n \"commands\": [\n \"warden access openbao-platform-admin-login --exec -- \",\n \"# owner login is contained by warden; do not invoke it separately\"\n ],\n \"founder_act\": {\n \"kind\": \"oidc_login\",\n \"summary\": \"Interactive OIDC/MFA login via railiance-platform\",\n \"details\": {\n \"lane_id\": \"openbao-platform-admin-login\",\n \"auth_method\": \"attended KeyCape OIDC/MFA at OpenBao auth mount netkingdom, role platform-admin\",\n \"fetch_command\": \"warden access openbao-platform-admin-login --exec -- \",\n \"desk_hint\": \"warden desk --from-plan (act=oidc_login); execute only through: warden access openbao-platform-admin-login --exec -- \"\n }\n },\n \"ccr_stub\": null,\n \"catalog\": {\n \"path\": \"/tmp/warden-admin-routing-20261016/registry/routing/catalog.yaml\",\n \"source\": \"override\",\n \"content_hash\": \"c47065c0892f\",\n \"mtime_iso\": \"2026-10-16T15:44:19.751415+00:00\",\n \"package_version\": \"0.1.2\",\n \"entry_count\": 44,\n \"active_count\": 40,\n \"newest_reviewed\": \"2026-10-11\",\n \"oldest_reviewed\": \"2026-06-18\",\n \"stale_entry_count\": 17,\n \"using_bundled\": false,\n \"warnings\": [\n \"17 catalog entries past 90d review cadence\",\n \"17 interim delegations need re-verifying (risk-scaled blocker cadence) \\u2014 see `warden route gaps`\"\n ]\n },\n \"candidates\": [\n {\n \"id\": \"openbao-platform-admin-login\",\n \"title\": \"Attended OpenBao platform administration login\",\n \"score\": 10,\n \"status\": \"active\",\n \"resolvable\": true,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"login\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"openbao-structure-survey\",\n \"title\": \"OpenBao existing-structure survey for a construction plan (section 2)\",\n \"score\": 11,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": false,\n \"warden_executes\": false,\n \"lane\": \"ceremony\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"ops-warden-warden-sign-token\",\n \"title\": \"Scoped OpenBao token for ops-warden SSH signing (warden-sign)\",\n \"score\": 8,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": false,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"openbao-api-key\",\n \"title\": \"API key, DB credential, or dynamic lease\",\n \"score\": 7,\n \"status\": \"active\",\n \"resolvable\": false,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n },\n {\n \"id\": \"agent-harness-forgejo-deploy\",\n \"title\": \"agent-harness Forgejo deploy key (write sandbox; binky-control at cutover)\",\n \"score\": 6,\n \"status\": \"active\",\n \"resolvable\": true,\n \"exec_capable\": true,\n \"warden_executes\": false,\n \"lane\": \"secret\",\n \"risk\": \"high\"\n }\n ],\n \"reasons\": [\n \"login lane requires interactive founder/operator identity act\"\n ],\n \"actor\": null,\n \"domain\": null\n}\n", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-route-after.json b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-route-after.json new file mode 100644 index 00000000..e5ec1ba3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-admin-routing/source-route-after.json @@ -0,0 +1,15 @@ +{ + "command": [ + "/home/worsch/ops-warden/.venv/bin/python", + "-c", + "from warden.cli import app; app()", + "route", + "find", + "attended OpenBao platform administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata", + "--json" + ], + "cwd": "/tmp/warden-admin-routing-20261016", + "exit_code": 0, + "stdout": "[\n {\n \"id\": \"openbao-platform-admin-login\",\n \"title\": \"Attended OpenBao platform administration login\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao operator OIDC via key-cape\",\n \"warden_executes\": false,\n \"warden_role\": \"assist\",\n \"exec_capable\": true,\n \"resolvable\": true,\n \"wiki_ref\": \"wiki/playbooks/openbao-platform-admin-login.md#worker-checklist\",\n \"canon_ref\": \"railiance-platform/docs/openbao.md\",\n \"reviewed\": \"2026-08-22\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"not-applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": \"Attended human operator identity act; the governed admin operation supplies its own resource identity.\",\n \"unknown_reason\": null,\n \"resolution\": \"not-applicable\"\n },\n \"has_rotation\": false,\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": null,\n \"reviewed\": \"2026-08-22\",\n \"verified\": \"source-read\",\n \"is_verified\": true,\n \"implicit\": false\n }\n },\n {\n \"id\": \"openbao-structure-survey\",\n \"title\": \"OpenBao existing-structure survey for a construction plan (section 2)\",\n \"owner_repo\": \"ops-mason\",\n \"subsystem\": \"OpenBao structure metadata via read-only ops-mason-survey grant\",\n \"warden_executes\": false,\n \"warden_role\": \"route\",\n \"exec_capable\": false,\n \"resolvable\": false,\n \"exec_owner\": \"ops-mason\",\n \"exec_command\": \"ops-mason/scripts/plan-survey.py --kv-path --policy --approle --consumer --keyword \",\n \"pointer_command\": null,\n \"wiki_ref\": \"wiki/playbooks/openbao-structure-survey.md#worker-checklist\",\n \"canon_ref\": \"ops-mason/docs/construction-plan-format.md\",\n \"reviewed\": \"2026-10-08\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"not-applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": \"Attended read-only survey under a founder-minted grant; reads OpenBao structure metadata, not a workload credential.\",\n \"unknown_reason\": null,\n \"resolution\": \"not-applicable\"\n },\n \"has_rotation\": false,\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"ops-mason\",\n \"blocked_on\": null,\n \"reviewed\": \"2026-10-08\",\n \"verified\": \"source-read\",\n \"is_verified\": true,\n \"implicit\": false\n }\n },\n {\n \"id\": \"ops-warden-warden-sign-token\",\n \"title\": \"Scoped OpenBao token for ops-warden SSH signing (warden-sign)\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao credential broker\",\n \"warden_executes\": false,\n \"warden_role\": \"route\",\n \"exec_capable\": false,\n \"resolvable\": false,\n \"exec_owner\": \"railiance-platform\",\n \"exec_command\": \"scripts/credential.py exec --grant ops-warden/warden-sign --ttl 15m -- \",\n \"pointer_command\": \"make credential-exec-ops-warden-smoke\",\n \"wiki_ref\": \"wiki/playbooks/ops-warden-warden-sign-token.md#worker-checklist\",\n \"canon_ref\": \"net-kingdom/docs/platform-identity-security-architecture.md\",\n \"reviewed\": \"2026-07-01\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"applicable\",\n \"rapp_id\": null,\n \"name\": \"ops-warden\",\n \"deployable\": null,\n \"declaration_ref\": \"tenancy.yaml\",\n \"reason\": null,\n \"unknown_reason\": null,\n \"resolution\": \"resolved\"\n },\n \"has_rotation\": true,\n \"rotation\": {\n \"method\": \"rotate\",\n \"owner\": \"railiance-platform\",\n \"automatable\": true,\n \"steps\": [\n \"This lane vends a short-lived (15m) child VAULT_TOKEN \\u2014 routine renewal is just re-running `credential.py exec`; the token auto-expires, nothing to revoke.\",\n \"To rotate the underlying grant/issuer: railiance-platform edits `credential-grants/catalog.yaml` for `ops-warden/warden-sign` (scope/policy) and re-mints the issuer token behind OPENBAO_TOKEN_FILE.\",\n \"Verify capabilities-safe: `make credential-exec-ops-warden-smoke` and a `bao token capabilities` check on ssh/sign/{adm,agt,atm}-role (never read a value).\"\n ]\n },\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": null,\n \"reviewed\": \"2026-08-15\",\n \"verified\": \"unverified\",\n \"is_verified\": false,\n \"implicit\": false\n }\n },\n {\n \"id\": \"openbao-api-key\",\n \"title\": \"API key, DB credential, or dynamic lease\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao\",\n \"warden_executes\": false,\n \"warden_role\": \"assist\",\n \"exec_capable\": true,\n \"resolvable\": false,\n \"wiki_ref\": \"wiki/CredentialRouting.md#routing-table\",\n \"canon_ref\": \"net-kingdom/docs/platform-identity-security-architecture.md\",\n \"reviewed\": \"2026-06-27\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"not-applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": \"Generic credential-path pattern; concrete lanes carry the workload reference.\",\n \"unknown_reason\": null,\n \"resolution\": \"not-applicable\"\n },\n \"has_rotation\": true,\n \"rotation\": {\n \"method\": \"rotate\",\n \"owner\": \"railiance-platform\",\n \"automatable\": false,\n \"steps\": [\n \"Generic template lane \\u2014 rotate per the concrete workload's own catalog entry when one exists.\",\n \"Provider re-mint (or OpenBao dynamic-secret rotation): mint a fresh value at the source, then `bao kv put =@file` (value from a mode-0600 file, never on argv).\",\n \"For dynamic-lease secrets, revoke the old lease (`bao lease revoke`) instead of a KV put.\",\n \"Verify capabilities-safe (`bao token capabilities` on the data path); notify consumers to re-fetch.\"\n ]\n },\n \"delegation\": {\n \"mode\": \"native\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": \"NOT A DELEGABLE LANE. Refused by secrets-engine 2026-08-21: this is a generic routing template (path_template is a // pattern), not one secret lane, so there is no front door for anyone to own. ops-warden agrees. The concrete lanes it resolves to are delegated individually; this entry stays a pointer and should not be counted as an interim cover.\",\n \"reviewed\": \"2026-08-21\",\n \"verified\": \"unverified\",\n \"is_verified\": false,\n \"implicit\": false\n }\n },\n {\n \"id\": \"agent-harness-forgejo-deploy\",\n \"title\": \"agent-harness Forgejo deploy key (write sandbox; binky-control at cutover)\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao + Forgejo + agent-harness\",\n \"warden_executes\": false,\n \"warden_role\": \"assist\",\n \"exec_capable\": true,\n \"resolvable\": true,\n \"wiki_ref\": \"wiki/playbooks/agent-harness-secrets.md#lane-2-forgejo-deploy-key\",\n \"canon_ref\": \"binky-control/integrations/executor-worker-secrets.md\",\n \"reviewed\": \"2026-07-17\",\n \"status\": \"active\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": null,\n \"unknown_reason\": \"agent-harness has not published an authoritative workload identity declaration.\",\n \"resolution\": \"unknown\"\n },\n \"has_rotation\": true,\n \"rotation\": {\n \"method\": \"re-establish\",\n \"owner\": \"railiance-platform\",\n \"automatable\": false,\n \"steps\": [\n \"On railiance01 generate a new ed25519 keypair under ~/.local/agent-harness/ssh/ (mode 600); do not paste the private key into chat or Git.\",\n \"`bao kv put platform/workloads/agent-harness/forgejo-deploy-key SSH_PRIVATE_KEY=@file SSH_PUBLIC_KEY=@file` then register the public key as a write deploy key on coulomb/executor-sandbox (and binky-control at cutover); remove the old deploy key.\",\n \"Verify `ssh -p 30022 -i -T git@forgejo.coulomb.social` authenticates as the deploy key; test push to executor-sandbox only.\"\n ]\n },\n \"delegation\": {\n \"mode\": \"interim\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": \"re-establish plus an alternative host-local key path; two ways in, neither owner-fronted (also agent-harness)\",\n \"reviewed\": \"2026-08-11\",\n \"verified\": \"unverified\",\n \"is_verified\": false,\n \"implicit\": false\n }\n }\n]\n", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/implementation.patch new file mode 100644 index 00000000..3bbd1a64 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/implementation.patch @@ -0,0 +1,120 @@ +commit d97652a3bb5fa733b063d71a1b74068efcccf572 +Author: tegwick +AuthorDate: Fri Oct 16 21:35:04 2026 +0200 +Commit: tegwick +CommitDate: Fri Oct 16 21:35:04 2026 +0200 + + fix(cases): admit reviewed descriptor pin validator revision + + Assistant: codex + Assistant-Model: gpt-6-astra + Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786 + +diff --git a/docs/reviews/2026-10-16-browser-producer-pin-refresh.md b/docs/reviews/2026-10-16-browser-producer-pin-refresh.md +new file mode 100644 +index 0000000..be35190 +--- /dev/null ++++ b/docs/reviews/2026-10-16-browser-producer-pin-refresh.md +@@ -0,0 +1,37 @@ ++# Browser producer pin refresh — 2026-10-16 ++ ++WARDEN-WP-0044-T03 source review, scoped to NetKingdom commit ++`015694ee414c802dc2ef1fa0120dfed610086e69`, against Warden `d79f065`. ++ ++Reviewed the exact committed schema, validator, evidence changes and the 21 ++producer contract tests. The optional bounded `execution_descriptors` map now ++requires lowercase exact SHA-256 byte pins keyed to declared steps. The direct ++evidence API refuses malformed maps. Absent and incomplete maps retain plan-only ++behavior; the repair supplies no descriptor, adapter or execution authority. ++This is compatible with Warden's independently authenticated admission gates. ++ ++Refresh only the validator and evidence module pins. Composer/capability module ++hashes and both reviewed parameter-definition hashes remain exact and unchanged. ++All four copied module bytes match the named NetKingdom commit and canonical ++checkout. No fallback, wildcard or allowed-drift path is added. ++ ++Before the refresh, explicit canonical producer integration failed both positive ++version tests (1.1/1.2), with 20 refusals passing. After refresh, 87 producer, ++Executor and reducer checks pass, including 14 new malformed descriptor cases ++and existing missing/changed-source checks across all four pinned modules. ++The 21 producer descriptor-contract checks and focused Ruff checks also pass. ++Use `WARDEN_TEST_NETKINGDOM_ROOT=/home/worsch/net-kingdom` explicitly: the ++isolated checkout's default sibling can resolve another checkout. ++ ++Emergent knowledge 0.2.6 digest ++`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02` ++was withheld due to source drift; start/local-work cards and actual source were ++reviewed directly. It conveys no authority. ++ ++No new browser or identity-restore adapter was admitted or attested. No credential ++or privileged execution occurred. Native structural receipts, authenticated owner ++provenance, descriptor assent, attended sessions, ordered verification and the ++first native case run remain open. T03 stays progress; T04 stays wait. ++ ++Exact pin hashes, tests and publication identity are retained under the Custodian ++`docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/` directory. +diff --git a/src/warden/case_inputs.py b/src/warden/case_inputs.py +index baa00b2..9c876d7 100644 +--- a/src/warden/case_inputs.py ++++ b/src/warden/case_inputs.py +@@ -15,9 +15,11 @@ import tempfile + + from .case_receipt_state import Refused + ++# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69. ++# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md + SOURCE_PINS = { +- "tools/security-case-types/security_case_evidence.py": "3c5d021ca7f1fc41fba4b2ff42fcb036b916a2a09ffc5e147cc3db1874dd2401", +- "tools/security-case-types/security_case_validator.py": "cbb9ab532d328f78cf37aca57891400e2e65d63a8ab3c5fe7bd1a4ff21dee56f", ++ "tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75", ++ "tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b", + "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", + "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", + } +diff --git a/tests/test_case_inputs.py b/tests/test_case_inputs.py +index 01e9407..50a132f 100644 +--- a/tests/test_case_inputs.py ++++ b/tests/test_case_inputs.py +@@ -183,3 +183,25 @@ def test_required_browser_parameter_is_not_defaulted_away(rig): + with pytest.raises(Refused, match="exact-version"): + executor.admit(*sources, **kwargs) + assert not calls ++ ++ ++@pytest.mark.parametrize("version", ["1.1", "1.2"]) ++@pytest.mark.parametrize( ++ "pins", ++ [ ++ None, ++ [], ++ {"mint-secret": "sha256:" + "A" * 64}, ++ {"mint-secret": "sha256:" + "a" * 64 + "\n"}, ++ {"undeclared-step": "sha256:" + "a" * 64}, ++ {"mint-secret": ["sha256:" + "a" * 64]}, ++ {"step-" + str(n): "sha256:" + "a" * 64 for n in range(129)}, ++ ], ++) ++def test_actual_producer_refuses_malformed_descriptor_pins(rig, version, pins): ++ executor, calls, sources, _ = browser(rig, version) ++ case, typ = (yaml.safe_load(source) for source in sources[:2]) ++ typ["spec"]["execution_descriptors"] = pins ++ with pytest.raises(Refused, match="exact-version"): ++ executor.browser_inputs.target(case, typ, "client_id") ++ assert not calls +diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md +index 7d6799d..00cfb99 100644 +--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md ++++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md +@@ -263,3 +263,12 @@ Executor/producer tests cover positive versions and refusal/containment paths. + Acknowledged machine descriptors, authenticated owner evidence, real Forgejo + materialization and contained native sessions remain mandatory and unimplemented; + T03 stays progress, T04 stays wait. No type or case status changed. ++ ++2026-10-16 producer source refresh: independently reviewed NetKingdom ++`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair. ++Only the validator/evidence source pins change; exact parameter and transitive ++source identity remain enforced. Actual canonical producer, Executor and reducer ++suite passes 87 checks, including 14 new malformed-map integration cases; all ++four missing/changed modules still refuse. Review: ++`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation, ++native acceptance, credential use or privileged dispatch. T03 remains progress. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/progress-receipt.json new file mode 100644 index 00000000..d05c8c94 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/progress-receipt.json @@ -0,0 +1 @@ +{"workplan_id":"f7e81013-6868-5d99-8528-ba75a442eca9","id":"6b6bb2b5-6d74-495e-818f-0e8dd73937fb","topic_id":null,"task_id":"bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89","decision_id":null,"event_type":"note","summary":"WARDEN-WP-0044-T03 source-only pin refresh d97652a3bb5fa733b063d71a1b74068efcccf572: independently reviewed NetKingdom 015694ee exact validator/evidence hashes; 87 actual producer/Executor/reducer checks plus 21 producer checks pass, 14 new malformed descriptor integration cases; all-four-module missing/drift refusal retained. No adapter attestation, native acceptance or credentials. Evidence: the-custodian/docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/.","detail":null,"author":"codex","session_id":null,"created_at":"2026-10-16T19:35:30.469810Z","workstream_id":"f7e81013-6868-5d99-8528-ba75a442eca9"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/README.md b/docs/changes/netkingdom-target-2026-10-16/warden-browser/README.md new file mode 100644 index 00000000..57241fc2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/README.md @@ -0,0 +1,46 @@ +# Warden browser input source implementation + +Prepared from ops-warden HEAD in isolated `/tmp/warden-browser-target-20261016`. +Canonical owner checkout is untouched. Only tracked baseline diffs for generated +`.custodian-brief.md` and `WORK-RECORDS.md` were retained separately; no untracked +owner files, private credentials or live resources were copied. + +`implementation.patch` adds a real Executor browser path backed by the actual +NetKingdom validator. Exact reviewed 1.1/1.2 parameter definition digests prevent +widening accepted fields/sensitivity/constraints. The complete case is validated +using verified producer validator/composer/contract/evidence bytes in an isolated temporary +Python tree with empty environment and a timeout. Unknown/fixed/secret inputs, +credential-shaped values and changed producer code refuse. Only the descriptor's +public/operational target is projected to the owner adapter; the journal retains +fingerprints, without browser parameter payloads. Every previous independent +case/type/descriptor/catalog/session/materialization gate remains mandatory. + +Validation: **102 passed**, covering actual producer validation, real +Executor/SQLite/owner callback path, existing execution/reducer suites, planning +and type checks. Ruff format/check passed. The test-only owner adapter and fake +provenance verifier do not establish native completion. Actual source types +without reviewed execution descriptors continue to refuse before dispatch. + +Source preparation advances WARDEN-WP-0044-T03. It does not install native owner +adapters/provenance, contained login sessions, full ordered run orchestration or +a dispatch CLI. First Forgejo acceptance still needs actual Mason structure, +accepted machine descriptor/type revision, source/installed broker reconciliation, +separate authorized mint/deliveries, consumer sign-in/group denial, authenticated +owner receipts, both revocation scopes and created-only native rollback. No +acknowledged type or case source was changed or promoted. A journal digest does +not authenticate its owner. T03 stays progress and T04 stays wait. + +Knowledge orientation: emergent-security 0.2.6, digest +`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`, +withheld/orientation-only. Warden INTENT/SCOPE, workplan and actual producer source +were read directly. No live credentials or changes, authority promotion, owner +assent or public push was performed. Canonical integration and State Hub +reconciliation remain with the parent session. + +Revision r2 pins the exact accepted NetKingdom browser-case patch +`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05`. +The lazy evidence module is imported only by its check-evidence CLI branch, but +Warden includes it in the reviewed pinned source set. Every one of the four +producer modules has explicit missing/modified-source refusal coverage. +The original artifacts and manifest are preserved in `r1/`; this directory's +manifest and patch are the final r2 deliverables. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/warden-browser/implementation.patch new file mode 100644 index 00000000..55248af5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/implementation.patch @@ -0,0 +1,425 @@ +diff --git a/docs/security-case-execution-kernel-v1.md b/docs/security-case-execution-kernel-v1.md +index bb9d775..35289d7 100644 +--- a/docs/security-case-execution-kernel-v1.md ++++ b/docs/security-case-execution-kernel-v1.md +@@ -84,3 +84,44 @@ lose a response then reconcile without mint, require explicit retry, check exact + pins/session expiry, withhold malicious output and unconfirmed revocation, and + prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and + checks pass. No live token or target action occurred. ++ ++## Browser input projection — 2026-10-16 source preparation ++ ++An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser ++cases before admission. It calls the real NetKingdom type/case validator over ++verified producer bytes in a disposable Python process (`-I`, empty environment, ++timeout, no inherited output). The validator, composer, capability contract and lazy evidence ++modules have exact source pins; missing or changed code refuses. There is no ++automatic discovery or download. A producer upgrade needs reviewed pin changes. ++ ++Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest ++of each complete parameter definition list prevents reclassifying secrets as ++metadata, loosening constraints, or adding fields under a recognized version. ++The complete case passes producer validation, including required fields, ++constraints and credential-shape checks. Explicit fixed/default overrides, ++unknown fields and secret references refuse. Security-sensitive browser metadata ++can therefore be present without being passed to an owner adapter: the only ++projection is its descriptor's exact public/operational target identifier. ++No raw parameter values are added to invocations or persisted receipts. ++ ++`require_implemented=False` validates the acknowledged type's source, not its ++execution authority. Existing descriptor/type/source/catalog/caller provenance ++gates stay mandatory. Neither historical browser type contains the required ++exact machine-descriptor map, and neither is promoted by this change. There are ++still no installed native owner adapters, authenticated provenance verifier, ++contained session/ordered run orchestration or live CLI dispatch. A journal digest ++is a fingerprint and cannot authenticate native evidence. Completion still needs ++owner-admitted descriptors and actual structural, mint/delivery, consumer and ++revocation/rollback evidence. Local fixture success is not native acceptance. ++ ++Tests use the actual producer checkout (override sibling location with ++`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected ++test-only owner adapter/evidence verifier. Their descriptor extensions and case ++materialized labels exist only in memory. Canonical case/type bytes stay intact. ++ ++Producer integration refinement: the accepted NetKingdom browser-case patch ++`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` ++is now the tested producer revision. Its evidence module is imported lazily only ++by `check-evidence`, outside this admission path; Warden nevertheless pins and ++copies it with the complete reviewed producer set. Missing or modified bytes in ++any of the four modules refuse admission. The test covers every dependency. +diff --git a/src/warden/case_inputs.py b/src/warden/case_inputs.py +new file mode 100644 +index 0000000..baa00b2 +--- /dev/null ++++ b/src/warden/case_inputs.py +@@ -0,0 +1,96 @@ ++"""Reviewed browser metadata projection; no native trust or command dispatch. ++ ++The producer validator and all reviewed producer modules are pinned below. ++Their verified bytes run in a disposable tree, without caller environment or ++ambient Python imports. Type parameters must match the exact reviewed version. ++Machine descriptor extensions still require Executor's independent admission. ++""" ++ ++import hashlib ++import json ++from pathlib import Path ++import subprocess ++import sys ++import tempfile ++ ++from .case_receipt_state import Refused ++ ++SOURCE_PINS = { ++ "tools/security-case-types/security_case_evidence.py": "3c5d021ca7f1fc41fba4b2ff42fcb036b916a2a09ffc5e147cc3db1874dd2401", ++ "tools/security-case-types/security_case_validator.py": "cbb9ab532d328f78cf37aca57891400e2e65d63a8ab3c5fe7bd1a4ff21dee56f", ++ "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", ++ "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", ++} ++PARAMETER_PINS = { ++ "1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c", ++ "1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6", ++} ++ ++_VALIDATE = """ ++import json, sys ++from pathlib import Path ++sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) ++import security_case_validator as v ++case, typ = json.load(sys.stdin) ++issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) ++print('valid' if not v.has_errors(issues) else 'invalid') ++""" ++ ++ ++class BrowserInputs: ++ """Explicit owner configuration, never inferred from a case-supplied path.""" ++ ++ def __init__(self, netkingdom_root: Path): ++ self.root = Path(netkingdom_root) ++ ++ def target(self, case: dict, typ: dict, target_parameter: str) -> str: ++ try: ++ spec = typ["spec"] ++ parameters = spec["parameters"] ++ fingerprint = hashlib.sha256( ++ json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode() ++ ).hexdigest() ++ if ( ++ spec["id"] != "oidc-browser-relying-party" ++ or PARAMETER_PINS.get(spec["version"]) != fingerprint ++ ): ++ raise ValueError ++ declared = {item["name"]: item for item in parameters} ++ definition = declared.get(target_parameter, {}) ++ if ( ++ definition.get("sensitivity") not in ("public", "operational") ++ or definition.get("case_settable") is not True ++ ): ++ raise ValueError ++ # No secret references or newly classified metadata are grandfathered. ++ for name in case["spec"]["parameters"]: ++ item = declared.get(name, {}) ++ if item.get("case_settable") is not True or item.get( ++ "sensitivity" ++ ) not in ("public", "operational", "security_sensitive"): ++ raise ValueError ++ with tempfile.TemporaryDirectory( ++ prefix="warden-case-validator-" ++ ) as directory: ++ for relative, expected in SOURCE_PINS.items(): ++ source = (self.root / relative).read_bytes() ++ if hashlib.sha256(source).hexdigest() != expected: ++ raise ValueError ++ dest = Path(directory) / relative ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ dest.write_bytes(source) ++ result = subprocess.run( ++ [sys.executable, "-I", "-c", _VALIDATE, directory], ++ input=json.dumps([case, typ]), ++ text=True, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.DEVNULL, ++ env={}, ++ timeout=10, ++ check=True, ++ ) ++ if result.stdout != "valid\n": ++ raise ValueError ++ return case["spec"]["parameters"][target_parameter] ++ except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError): ++ raise Refused("browser metadata failed exact-version validation") from None +diff --git a/src/warden/case_run.py b/src/warden/case_run.py +index 7f1cce1..f16ab2a 100644 +--- a/src/warden/case_run.py ++++ b/src/warden/case_run.py +@@ -20,6 +20,7 @@ import uuid + from typing import Callable + + import yaml ++from .case_inputs import BrowserInputs + from .case_receipt_state import ( + Outcome, + Pins, +@@ -329,6 +330,8 @@ class Executor: + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], ++ *, ++ browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, +@@ -336,6 +339,7 @@ class Executor: + evidence_verifier, + ) + self._admitted = {} ++ self.browser_inputs = browser_inputs + + def admit( + self, +@@ -421,15 +425,24 @@ class Executor: + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") +- for name in parameters: +- definition = declared.get(name) +- if ( +- definition is None +- or definition.get("sensitivity") not in ("public", "operational") +- or definition.get("case_settable") is False +- ): +- raise Refused("unknown, fixed or secret-sensitivity case parameter") +- target = parameters.get(descriptor.target_parameter) ++ if case_type.get("id") == "oidc-browser-relying-party": ++ if type(self.browser_inputs) is not BrowserInputs: ++ raise Refused("browser metadata validator is not configured") ++ target = self.browser_inputs.target( ++ yaml.safe_load(case_source), ++ yaml.safe_load(type_source), ++ descriptor.target_parameter, ++ ) ++ else: ++ for name in parameters: ++ definition = declared.get(name) ++ if ( ++ definition is None ++ or definition.get("sensitivity") not in ("public", "operational") ++ or definition.get("case_settable") is False ++ ): ++ raise Refused("unknown, fixed or secret-sensitivity case parameter") ++ target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) +diff --git a/tests/test_case_inputs.py b/tests/test_case_inputs.py +new file mode 100644 +index 0000000..01e9407 +--- /dev/null ++++ b/tests/test_case_inputs.py +@@ -0,0 +1,185 @@ ++"""Actual producer validation and Executor; fake owner evidence, no native claims.""" ++ ++import os ++from pathlib import Path ++ ++import pytest ++import yaml ++ ++from warden.case_inputs import BrowserInputs, SOURCE_PINS ++from warden.case_run import digest ++from warden.case_receipt_state import Outcome, Refused ++from tests.test_case_run import rig as base_rig ++ ++ ++@pytest.fixture ++def rig(tmp_path): ++ return base_rig.__wrapped__(tmp_path) ++ ++ ++NK = Path( ++ os.environ.get( ++ "WARDEN_TEST_NETKINGDOM_ROOT", ++ Path(__file__).resolve().parents[2] / "net-kingdom", ++ ) ++) ++ ++ ++def browser(rig, version="1.1", mutate=None): ++ executor, _, calls, sources, kwargs = rig ++ case = yaml.safe_load( ++ (NK / "security-cases/forgejo-netkingdom-oidc.yaml").read_bytes() ++ ) ++ relative = ( ++ "security-case-types" ++ if version == "1.1" ++ else "security-case-type-revisions/1.2" ++ ) ++ typ = yaml.safe_load( ++ (NK / relative / "oidc-browser-relying-party/type.yaml").read_bytes() ++ ) ++ case["spec"]["type"] = "oidc-browser-relying-party@" + version ++ case["spec"]["status"] = "materialized" ++ if version == "1.2": ++ case["spec"]["parameters"]["consumer_namespace"] = "forgejo" ++ # This extension and all native provenance are TEST ONLY, never source assent. ++ typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])} ++ if mutate: ++ mutate(case, typ) ++ changed = ( ++ yaml.safe_dump(case).encode(), ++ yaml.safe_dump(typ).encode(), ++ *sources[2:], ++ ) ++ old = executor.evidence_verifier ++ ++ def verified(purpose, source, evidence): ++ return old( ++ purpose, ++ source.replace(digest(changed[0]), digest(sources[0])).replace( ++ digest(changed[1]), digest(sources[1]) ++ ), ++ evidence, ++ ) ++ ++ executor.evidence_verifier = verified ++ executor.browser_inputs = BrowserInputs(NK) ++ return executor, calls, changed, kwargs ++ ++ ++@pytest.mark.parametrize("version", ["1.1", "1.2"]) ++def test_actual_browser_executor_and_owner_projection(rig, version): ++ executor, calls, sources, kwargs = browser(rig, version) ++ args = executor.admit(*sources, **kwargs) ++ assert args[1].target == "forgejo" ++ assert not calls ++ assert executor.run(*args).last.outcome is Outcome.SUCCESS ++ assert calls[0].target == "forgejo" ++ assert not hasattr(calls[0], "parameters") ++ # Owner callbacks/journal get target and fingerprints, no full metadata. ++ persisted = (executor.journal.directory / "journal.sqlite").read_bytes() ++ assert b"https://forgejo.coulomb.social" not in persisted ++ assert b"forge-admins" not in persisted ++ ++ ++@pytest.mark.parametrize( ++ "field,value", ++ [ ++ ("redirect_uris", "https://*.example.org/callback"), ++ ("redirect_uris", "http://example.org/callback"), ++ ("allowed_scopes", "openid,admin"), ++ ("client_type", "arbitrary"), ++ ("mfa_required", False), ++ ("unknown", "value"), ++ ("issuer", "https://other.example"), ++ ("display_name", "hvs." + "A" * 30), ++ ], ++) ++def test_case_refusals_even_with_test_evidence(rig, field, value): ++ executor, calls, sources, kwargs = browser( ++ rig, mutate=lambda c, t: c["spec"]["parameters"].update({field: value}) ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++@pytest.mark.parametrize( ++ "mutation", ++ [ ++ lambda c, t: t["spec"].update(version="1.3"), ++ lambda c, t: t["spec"]["parameters"][2].update(sensitivity="public"), ++ lambda c, t: t["spec"]["parameters"][2].update(constraints={}), ++ lambda c, t: t["spec"]["parameters"].append( ++ { ++ "name": "secret", ++ "type": "string", ++ "case_settable": True, ++ "sensitivity": "secret_reference", ++ } ++ ), ++ ], ++) ++def test_type_revision_cannot_widen_reviewed_parameters(rig, mutation): ++ def change(case, typ): ++ mutation(case, typ) ++ case["spec"]["type"] = typ["spec"]["id"] + "@" + typ["spec"]["version"] ++ ++ executor, calls, sources, kwargs = browser(rig, mutate=change) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_namespace_and_missing_required_are_validated(rig): ++ executor, calls, sources, kwargs = browser( ++ rig, "1.2", lambda c, t: c["spec"]["parameters"].update(consumer_namespace="*") ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++@pytest.mark.parametrize("changed_dependency", list(SOURCE_PINS)) ++def test_missing_producer_and_changed_transitive_source_refuse( ++ rig, tmp_path, changed_dependency ++): ++ executor, calls, sources, kwargs = browser(rig) ++ executor.browser_inputs = BrowserInputs(tmp_path) ++ for relative in SOURCE_PINS: ++ path = tmp_path / relative ++ path.parent.mkdir(parents=True, exist_ok=True) ++ if relative != changed_dependency: ++ path.write_bytes((NK / relative).read_bytes()) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ (tmp_path / changed_dependency).write_text('raise RuntimeError("must not execute")') ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_browser_without_validator_still_refuses(rig): ++ executor, calls, sources, kwargs = browser(rig) ++ executor.browser_inputs = None ++ with pytest.raises(Refused, match="not configured"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_real_unextended_type_stays_plan_only(rig): ++ executor, calls, sources, kwargs = browser(rig) ++ typ = yaml.safe_load(sources[1]) ++ del typ["spec"]["execution_descriptors"] ++ with pytest.raises(Refused, match="plan only"): ++ executor.admit(sources[0], yaml.safe_dump(typ).encode(), *sources[2:], **kwargs) ++ assert not calls ++ ++ ++def test_required_browser_parameter_is_not_defaulted_away(rig): ++ executor, calls, sources, kwargs = browser( ++ rig, mutate=lambda c, t: c["spec"]["parameters"].pop("redirect_uris") ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls +diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md +index b9ef795..7d6799d 100644 +--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md ++++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md +@@ -251,3 +251,15 @@ and receipt provenance. No installed native adapter or live CLI execution is + claimed. Current prose types stay plan-only; acknowledged descriptor/type machine + seams and authenticated owner/caller receipts are still required. T03 progresses + for implementation but cannot finish before ordered native Forgejo acceptance. ++ ++2026-10-16 source preparation (isolated checkout; no native acceptance): ++T03's browser parameter mismatch now has a producer-validator-backed projection ++in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2 ++parameter definitions and all four reviewed producer modules are pinned; full case ++validation admits reviewed security-sensitive metadata while unknown/fixed/secret ++inputs and type/producer drift refuse. Only the reviewed descriptor target ++reaches the owner callback; no parameter journal payload is added. Actual ++Executor/producer tests cover positive versions and refusal/containment paths. ++Acknowledged machine descriptors, authenticated owner evidence, real Forgejo ++materialization and contained native sessions remain mandatory and unimplemented; ++T03 stays progress, T04 stays wait. No type or case status changed. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/manifest.json b/docs/changes/netkingdom-target-2026-10-16/warden-browser/manifest.json new file mode 100644 index 00000000..e703f5aa --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/manifest.json @@ -0,0 +1,46 @@ +{ + "artifact_hashes": { + "README.md": "sha256:17a1dbd5314ee21665ef9f6bb9a7b01f9b4890d3f2dd2189747c570cb9550f15", + "implementation.patch": "sha256:1fb05b513c4db784f8691b5bc2790f5ca396071d4354615d2da7fc505c45684b", + "owner-dirty-baseline.patch": "sha256:d8a848c3bcaed69c1d17b86546632f22d742a33261321f0a4fdfb6edefb22da0", + "validation.json": "sha256:8da01d4322cf6d2b17afdd8ced4f05a8a0b1ff03a90720b1004a460fa6aafa84" + }, + "base_commit": "d2cd3fa9405f0766eef67a8b0e897239d8817fb7", + "checkout": "/tmp/warden-browser-target-20261016", + "files": { + "docs/security-case-execution-kernel-v1.md": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb", + "src/warden/case_inputs.py": "sha256:3c7aab59bcff6bbc678ec64261f5023a050d61bc3a72ad0f87588d82e7e74f86", + "src/warden/case_run.py": "sha256:afb5ce9c59b9748f85c9f1d4af04b255087632c58de8dcd8c9426fc1ed4db068", + "tests/test_case_inputs.py": "sha256:f0af9163494d86343415e7009e1d652085f305d5058511404bbf52d22daa384e", + "workplans/WARDEN-WP-0044-security-case-follow-through.md": "sha256:1f4823ffd331ffe737ed6aa1779a30c2c8c05a3a0230be2e2734f4e0ae6da230" + }, + "implementation_patch_sha256": "sha256:1fb05b513c4db784f8691b5bc2790f5ca396071d4354615d2da7fc505c45684b", + "live_changes": false, + "native_complete": false, + "owner": "ops-warden", + "preserved_dirty_tracked_paths": [ + ".custodian-brief.md", + "WORK-RECORDS.md" + ], + "prior_revision_manifest_sha256": "sha256:5fbd1e5417a0f74f46bfd17b2de1b52938c78a2a157f2d6b22dcf63adc8a5b82", + "revision": "r2", + "schema": "netkingdom-target-patch-manifest.v1", + "source_dependencies": { + "net-kingdom": { + "accepted_patch_sha256": "sha256:d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05", + "files": { + "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml": "sha256:9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578", + "security-case-types/oidc-browser-relying-party/type.yaml": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "security-cases/forgejo-netkingdom-oidc.yaml": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d", + "tools/playbook-capability-contract/playbook_contract_validator.py": "sha256:2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", + "tools/security-case-types/security_case_evidence.py": "sha256:3c5d021ca7f1fc41fba4b2ff42fcb036b916a2a09ffc5e147cc3db1874dd2401", + "tools/security-case-types/security_case_validator.py": "sha256:cbb9ab532d328f78cf37aca57891400e2e65d63a8ab3c5fe7bd1a4ff21dee56f", + "tools/security-scenario-composer/security_scenario_composer.py": "sha256:9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f" + }, + "head": "994d11c869daf9624ac6da2adfdaeb86d8cdf537" + } + }, + "status": "source-ready against accepted NetKingdom browser-case patch; native owner admission pending", + "untracked_owner_files_copied": false, + "work_item": "WARDEN-WP-0044-T03" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/warden-browser/owner-dirty-baseline.patch new file mode 100644 index 00000000..77e38bff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/owner-dirty-baseline.patch @@ -0,0 +1,65 @@ +diff --git a/.custodian-brief.md b/.custodian-brief.md +index 09161f4..68a2a73 100644 +--- a/.custodian-brief.md ++++ b/.custodian-brief.md +@@ -2,7 +2,7 @@ + # Custodian Brief — ops-warden + + **Domain:** infotech +-**Last synced:** 2026-10-11 18:37 UTC ++**Last synced:** 2026-10-15 02:28 UTC + **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + + ## Active Workstreams +@@ -14,7 +14,7 @@ Progress: 1/4 done | workplan_id: `c8ee441e-1be1-5219-910c-e79ff23cc9ec` + - ! Tasks `611f0901` + *(wait: Warden's own declaration cannot classify the repair-path target workloads; the owner must declare the continuity actor/target and its z2-continuity resolution (see 2026-09-28 review).)* + - ! Tasks `54ad4864` +- *(wait: Owners of agt-codex-interhub-bootstrap, agt-state-hub-bridge and atm-backup-daily must publish authoritative workload identity declarations.)* ++ *(wait: Owners of agt-codex-interhub-bootstrap (ops-hub) and agt-state-hub-bridge (ops-bridge) must publish authoritative workload identity declarations; atm-backup-daily declared by activity-core and compiled 2026-10-09 (z2-protected, admission unsatisfied: M1 below M2 floor).)* + - ! Tasks `93eaf1f2` + *(wait: Gated on T01 and on layer model v0.8 (or successor) reaching accepted; never convert against a proposed standard.)* + +@@ -30,6 +30,7 @@ Progress: 2/4 done | workplan_id: `f7e81013-6868-5d99-8528-ba75a442eca9` + + **Open tasks:** + - ! T03 — Run and verify a case `4a0fa4a7` ++ *(wait: Catalog integration follows a proven run.)* + - ► T02 — Plan a case run `bcbf6c91` + + ### Tamper-resistant credential governance + mass rotation/lockdown (Strand B) +@@ -43,12 +44,12 @@ Progress: 2/3 done | workplan_id: `42a097db-1c24-558e-a724-030bb2b4443e` + + **Open tasks:** + - ! Prove routing and publication `a8b1b855` +- *(wait: Founder HOLD 2026-09-21: no rotation until SECRETS-WP-0006-T06 moves the publish lane off the legacy whynot-design npm path; then rotate and prove a fresh publish.)* ++ *(wait: Hold kept by D3 until SECRETS-WP-0006-T06 moves publishing off the legacy whynot-design npm path; then rotate and prove a fresh publish.)* + + ## Inbox Hygiene + + **Stale unread:** 2 message(s) older than 3 day(s) — triage at session start. +-**Missing thread_id:** 3 unread message(s) lack supersession chains. ++**Missing thread_id:** 2 unread message(s) lack supersession chains. + + --- + ## MCP Orientation (when available) +diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md +index 34cf268..6c39975 100644 +--- a/WORK-RECORDS.md ++++ b/WORK-RECORDS.md +@@ -32,13 +32,13 @@ + | workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | + | workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | + | workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +-| workplan | WARDEN-WP-0034 | backlog | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | ++| workplan | WARDEN-WP-0034 | active | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | + | workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | + | workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | + | workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | + | workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | + | workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +-| workplan | WARDEN-WP-0040 | backlog | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | ++| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | + | workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | + | workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | + | workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/README.md b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/README.md new file mode 100644 index 00000000..1213516d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/README.md @@ -0,0 +1,42 @@ +# Warden browser input source implementation + +Prepared from ops-warden HEAD in isolated `/tmp/warden-browser-target-20261016`. +Canonical owner checkout is untouched. Only tracked baseline diffs for generated +`.custodian-brief.md` and `WORK-RECORDS.md` were retained separately; no untracked +owner files, private credentials or live resources were copied. + +`implementation.patch` adds a real Executor browser path backed by the actual +NetKingdom validator. Exact reviewed 1.1/1.2 parameter definition digests prevent +widening accepted fields/sensitivity/constraints. The complete case is validated +using verified producer validator/composer/contract bytes in an isolated temporary +Python tree with empty environment and a timeout. Unknown/fixed/secret inputs, +credential-shaped values and changed producer code refuse. Only the descriptor's +public/operational target is projected to the owner adapter; the journal retains +fingerprints, without browser parameter payloads. Every previous independent +case/type/descriptor/catalog/session/materialization gate remains mandatory. + +Validation: **99 passed**, covering actual producer validation, real +Executor/SQLite/owner callback path, existing execution/reducer suites, planning +and type checks. Ruff format/check passed. The test-only owner adapter and fake +provenance verifier do not establish native completion. Actual source types +without reviewed execution descriptors continue to refuse before dispatch. + +Source preparation advances WARDEN-WP-0044-T03. It does not install native owner +adapters/provenance, contained login sessions, full ordered run orchestration or +a dispatch CLI. First Forgejo acceptance still needs actual Mason structure, +accepted machine descriptor/type revision, source/installed broker reconciliation, +separate authorized mint/deliveries, consumer sign-in/group denial, authenticated +owner receipts, both revocation scopes and created-only native rollback. No +acknowledged type or case source was changed or promoted. A journal digest does +not authenticate its owner. T03 stays progress and T04 stays wait. + +Knowledge orientation: emergent-security 0.2.6, digest +`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`, +withheld/orientation-only. Warden INTENT/SCOPE, workplan and actual producer source +were read directly. No live credentials or changes, authority promotion, owner +assent or public push was performed. Canonical integration and State Hub +reconciliation remain with the parent session. + +The current producer pin is the canonical source at preparation time. Integrating +the separate NetKingdom browser-case patch changes its validator hash and must +include reviewed Warden source-pin update plus tests against that exact tree. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/implementation.patch new file mode 100644 index 00000000..4318ad96 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/implementation.patch @@ -0,0 +1,415 @@ +diff --git a/docs/security-case-execution-kernel-v1.md b/docs/security-case-execution-kernel-v1.md +index bb9d775..5805bf8 100644 +--- a/docs/security-case-execution-kernel-v1.md ++++ b/docs/security-case-execution-kernel-v1.md +@@ -84,3 +84,37 @@ lose a response then reconcile without mint, require explicit retry, check exact + pins/session expiry, withhold malicious output and unconfirmed revocation, and + prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and + checks pass. No live token or target action occurred. ++ ++## Browser input projection — 2026-10-16 source preparation ++ ++An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser ++cases before admission. It calls the real NetKingdom type/case validator over ++verified producer bytes in a disposable Python process (`-I`, empty environment, ++timeout, no inherited output). The validator, composer and capability contract ++modules have exact source pins; missing or changed code refuses. There is no ++automatic discovery or download. A producer upgrade needs reviewed pin changes. ++ ++Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest ++of each complete parameter definition list prevents reclassifying secrets as ++metadata, loosening constraints, or adding fields under a recognized version. ++The complete case passes producer validation, including required fields, ++constraints and credential-shape checks. Explicit fixed/default overrides, ++unknown fields and secret references refuse. Security-sensitive browser metadata ++can therefore be present without being passed to an owner adapter: the only ++projection is its descriptor's exact public/operational target identifier. ++No raw parameter values are added to invocations or persisted receipts. ++ ++`require_implemented=False` validates the acknowledged type's source, not its ++execution authority. Existing descriptor/type/source/catalog/caller provenance ++gates stay mandatory. Neither historical browser type contains the required ++exact machine-descriptor map, and neither is promoted by this change. There are ++still no installed native owner adapters, authenticated provenance verifier, ++contained session/ordered run orchestration or live CLI dispatch. A journal digest ++is a fingerprint and cannot authenticate native evidence. Completion still needs ++owner-admitted descriptors and actual structural, mint/delivery, consumer and ++revocation/rollback evidence. Local fixture success is not native acceptance. ++ ++Tests use the actual producer checkout (override sibling location with ++`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected ++test-only owner adapter/evidence verifier. Their descriptor extensions and case ++materialized labels exist only in memory. Canonical case/type bytes stay intact. +diff --git a/src/warden/case_inputs.py b/src/warden/case_inputs.py +new file mode 100644 +index 0000000..96fa851 +--- /dev/null ++++ b/src/warden/case_inputs.py +@@ -0,0 +1,95 @@ ++"""Reviewed browser metadata projection; no native trust or command dispatch. ++ ++The producer validator and both transitive producer modules are pinned below. ++Their verified bytes run in a disposable tree, without caller environment or ++ambient Python imports. Type parameters must match the exact reviewed version. ++Machine descriptor extensions still require Executor's independent admission. ++""" ++ ++import hashlib ++import json ++from pathlib import Path ++import subprocess ++import sys ++import tempfile ++ ++from .case_receipt_state import Refused ++ ++SOURCE_PINS = { ++ "tools/security-case-types/security_case_validator.py": "5b9e46a5d0a8f387c6e0d87216c5695c03753c564378b1e43c07b4628cd07dc7", ++ "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", ++ "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", ++} ++PARAMETER_PINS = { ++ "1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c", ++ "1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6", ++} ++ ++_VALIDATE = """ ++import json, sys ++from pathlib import Path ++sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) ++import security_case_validator as v ++case, typ = json.load(sys.stdin) ++issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) ++print('valid' if not v.has_errors(issues) else 'invalid') ++""" ++ ++ ++class BrowserInputs: ++ """Explicit owner configuration, never inferred from a case-supplied path.""" ++ ++ def __init__(self, netkingdom_root: Path): ++ self.root = Path(netkingdom_root) ++ ++ def target(self, case: dict, typ: dict, target_parameter: str) -> str: ++ try: ++ spec = typ["spec"] ++ parameters = spec["parameters"] ++ fingerprint = hashlib.sha256( ++ json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode() ++ ).hexdigest() ++ if ( ++ spec["id"] != "oidc-browser-relying-party" ++ or PARAMETER_PINS.get(spec["version"]) != fingerprint ++ ): ++ raise ValueError ++ declared = {item["name"]: item for item in parameters} ++ definition = declared.get(target_parameter, {}) ++ if ( ++ definition.get("sensitivity") not in ("public", "operational") ++ or definition.get("case_settable") is not True ++ ): ++ raise ValueError ++ # No secret references or newly classified metadata are grandfathered. ++ for name in case["spec"]["parameters"]: ++ item = declared.get(name, {}) ++ if item.get("case_settable") is not True or item.get( ++ "sensitivity" ++ ) not in ("public", "operational", "security_sensitive"): ++ raise ValueError ++ with tempfile.TemporaryDirectory( ++ prefix="warden-case-validator-" ++ ) as directory: ++ for relative, expected in SOURCE_PINS.items(): ++ source = (self.root / relative).read_bytes() ++ if hashlib.sha256(source).hexdigest() != expected: ++ raise ValueError ++ dest = Path(directory) / relative ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ dest.write_bytes(source) ++ result = subprocess.run( ++ [sys.executable, "-I", "-c", _VALIDATE, directory], ++ input=json.dumps([case, typ]), ++ text=True, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.DEVNULL, ++ env={}, ++ timeout=10, ++ check=True, ++ ) ++ if result.stdout != "valid\n": ++ raise ValueError ++ return case["spec"]["parameters"][target_parameter] ++ except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError): ++ raise Refused("browser metadata failed exact-version validation") from None +diff --git a/src/warden/case_run.py b/src/warden/case_run.py +index 7f1cce1..f16ab2a 100644 +--- a/src/warden/case_run.py ++++ b/src/warden/case_run.py +@@ -20,6 +20,7 @@ import uuid + from typing import Callable + + import yaml ++from .case_inputs import BrowserInputs + from .case_receipt_state import ( + Outcome, + Pins, +@@ -329,6 +330,8 @@ class Executor: + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], ++ *, ++ browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, +@@ -336,6 +339,7 @@ class Executor: + evidence_verifier, + ) + self._admitted = {} ++ self.browser_inputs = browser_inputs + + def admit( + self, +@@ -421,15 +425,24 @@ class Executor: + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") +- for name in parameters: +- definition = declared.get(name) +- if ( +- definition is None +- or definition.get("sensitivity") not in ("public", "operational") +- or definition.get("case_settable") is False +- ): +- raise Refused("unknown, fixed or secret-sensitivity case parameter") +- target = parameters.get(descriptor.target_parameter) ++ if case_type.get("id") == "oidc-browser-relying-party": ++ if type(self.browser_inputs) is not BrowserInputs: ++ raise Refused("browser metadata validator is not configured") ++ target = self.browser_inputs.target( ++ yaml.safe_load(case_source), ++ yaml.safe_load(type_source), ++ descriptor.target_parameter, ++ ) ++ else: ++ for name in parameters: ++ definition = declared.get(name) ++ if ( ++ definition is None ++ or definition.get("sensitivity") not in ("public", "operational") ++ or definition.get("case_settable") is False ++ ): ++ raise Refused("unknown, fixed or secret-sensitivity case parameter") ++ target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) +diff --git a/tests/test_case_inputs.py b/tests/test_case_inputs.py +new file mode 100644 +index 0000000..c045b98 +--- /dev/null ++++ b/tests/test_case_inputs.py +@@ -0,0 +1,183 @@ ++"""Actual producer validation and Executor; fake owner evidence, no native claims.""" ++ ++import os ++from pathlib import Path ++ ++import pytest ++import yaml ++ ++from warden.case_inputs import BrowserInputs, SOURCE_PINS ++from warden.case_run import digest ++from warden.case_receipt_state import Outcome, Refused ++from tests.test_case_run import rig as base_rig ++ ++ ++@pytest.fixture ++def rig(tmp_path): ++ return base_rig.__wrapped__(tmp_path) ++ ++ ++NK = Path( ++ os.environ.get( ++ "WARDEN_TEST_NETKINGDOM_ROOT", ++ Path(__file__).resolve().parents[2] / "net-kingdom", ++ ) ++) ++ ++ ++def browser(rig, version="1.1", mutate=None): ++ executor, _, calls, sources, kwargs = rig ++ case = yaml.safe_load( ++ (NK / "security-cases/forgejo-netkingdom-oidc.yaml").read_bytes() ++ ) ++ relative = ( ++ "security-case-types" ++ if version == "1.1" ++ else "security-case-type-revisions/1.2" ++ ) ++ typ = yaml.safe_load( ++ (NK / relative / "oidc-browser-relying-party/type.yaml").read_bytes() ++ ) ++ case["spec"]["type"] = "oidc-browser-relying-party@" + version ++ case["spec"]["status"] = "materialized" ++ if version == "1.2": ++ case["spec"]["parameters"]["consumer_namespace"] = "forgejo" ++ # This extension and all native provenance are TEST ONLY, never source assent. ++ typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])} ++ if mutate: ++ mutate(case, typ) ++ changed = ( ++ yaml.safe_dump(case).encode(), ++ yaml.safe_dump(typ).encode(), ++ *sources[2:], ++ ) ++ old = executor.evidence_verifier ++ ++ def verified(purpose, source, evidence): ++ return old( ++ purpose, ++ source.replace(digest(changed[0]), digest(sources[0])).replace( ++ digest(changed[1]), digest(sources[1]) ++ ), ++ evidence, ++ ) ++ ++ executor.evidence_verifier = verified ++ executor.browser_inputs = BrowserInputs(NK) ++ return executor, calls, changed, kwargs ++ ++ ++@pytest.mark.parametrize("version", ["1.1", "1.2"]) ++def test_actual_browser_executor_and_owner_projection(rig, version): ++ executor, calls, sources, kwargs = browser(rig, version) ++ args = executor.admit(*sources, **kwargs) ++ assert args[1].target == "forgejo" ++ assert not calls ++ assert executor.run(*args).last.outcome is Outcome.SUCCESS ++ assert calls[0].target == "forgejo" ++ assert not hasattr(calls[0], "parameters") ++ # Owner callbacks/journal get target and fingerprints, no full metadata. ++ persisted = (executor.journal.directory / "journal.sqlite").read_bytes() ++ assert b"https://forgejo.coulomb.social" not in persisted ++ assert b"forge-admins" not in persisted ++ ++ ++@pytest.mark.parametrize( ++ "field,value", ++ [ ++ ("redirect_uris", "https://*.example.org/callback"), ++ ("redirect_uris", "http://example.org/callback"), ++ ("allowed_scopes", "openid,admin"), ++ ("client_type", "arbitrary"), ++ ("mfa_required", False), ++ ("unknown", "value"), ++ ("issuer", "https://other.example"), ++ ("display_name", "hvs." + "A" * 30), ++ ], ++) ++def test_case_refusals_even_with_test_evidence(rig, field, value): ++ executor, calls, sources, kwargs = browser( ++ rig, mutate=lambda c, t: c["spec"]["parameters"].update({field: value}) ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++@pytest.mark.parametrize( ++ "mutation", ++ [ ++ lambda c, t: t["spec"].update(version="1.3"), ++ lambda c, t: t["spec"]["parameters"][2].update(sensitivity="public"), ++ lambda c, t: t["spec"]["parameters"][2].update(constraints={}), ++ lambda c, t: t["spec"]["parameters"].append( ++ { ++ "name": "secret", ++ "type": "string", ++ "case_settable": True, ++ "sensitivity": "secret_reference", ++ } ++ ), ++ ], ++) ++def test_type_revision_cannot_widen_reviewed_parameters(rig, mutation): ++ def change(case, typ): ++ mutation(case, typ) ++ case["spec"]["type"] = typ["spec"]["id"] + "@" + typ["spec"]["version"] ++ ++ executor, calls, sources, kwargs = browser(rig, mutate=change) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_namespace_and_missing_required_are_validated(rig): ++ executor, calls, sources, kwargs = browser( ++ rig, "1.2", lambda c, t: c["spec"]["parameters"].update(consumer_namespace="*") ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_missing_producer_and_changed_transitive_source_refuse(rig, tmp_path): ++ executor, calls, sources, kwargs = browser(rig) ++ executor.browser_inputs = BrowserInputs(tmp_path) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ for relative in SOURCE_PINS: ++ path = tmp_path / relative ++ path.parent.mkdir(parents=True, exist_ok=True) ++ path.write_bytes((NK / relative).read_bytes()) ++ (tmp_path / next(reversed(SOURCE_PINS))).write_text( ++ 'raise RuntimeError("must not execute")' ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_browser_without_validator_still_refuses(rig): ++ executor, calls, sources, kwargs = browser(rig) ++ executor.browser_inputs = None ++ with pytest.raises(Refused, match="not configured"): ++ executor.admit(*sources, **kwargs) ++ assert not calls ++ ++ ++def test_real_unextended_type_stays_plan_only(rig): ++ executor, calls, sources, kwargs = browser(rig) ++ typ = yaml.safe_load(sources[1]) ++ del typ["spec"]["execution_descriptors"] ++ with pytest.raises(Refused, match="plan only"): ++ executor.admit(sources[0], yaml.safe_dump(typ).encode(), *sources[2:], **kwargs) ++ assert not calls ++ ++ ++def test_required_browser_parameter_is_not_defaulted_away(rig): ++ executor, calls, sources, kwargs = browser( ++ rig, mutate=lambda c, t: c["spec"]["parameters"].pop("redirect_uris") ++ ) ++ with pytest.raises(Refused, match="exact-version"): ++ executor.admit(*sources, **kwargs) ++ assert not calls +diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md +index b9ef795..fe96e87 100644 +--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md ++++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md +@@ -251,3 +251,15 @@ and receipt provenance. No installed native adapter or live CLI execution is + claimed. Current prose types stay plan-only; acknowledged descriptor/type machine + seams and authenticated owner/caller receipts are still required. T03 progresses + for implementation but cannot finish before ordered native Forgejo acceptance. ++ ++2026-10-16 source preparation (isolated checkout; no native acceptance): ++T03's browser parameter mismatch now has a producer-validator-backed projection ++in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2 ++parameter definitions and all three producer modules are pinned; full case ++validation admits reviewed security-sensitive metadata while unknown/fixed/secret ++inputs and type/producer drift refuse. Only the reviewed descriptor target ++reaches the owner callback; no parameter journal payload is added. Actual ++Executor/producer tests cover positive versions and refusal/containment paths. ++Acknowledged machine descriptors, authenticated owner evidence, real Forgejo ++materialization and contained native sessions remain mandatory and unimplemented; ++T03 stays progress, T04 stays wait. No type or case status changed. diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/manifest.json new file mode 100644 index 00000000..33ba0d2b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/manifest.json @@ -0,0 +1,41 @@ +{ + "artifact_hashes": { + "README.md": "sha256:50b6ca240cc44d81cb6a709b4241c04480a31b39396bc541e705e83bbb48a0fb", + "implementation.patch": "sha256:305db5d1101c0d6dc7bee0e2948b9d906e6b235b1d582535661aee49c66e30f7", + "owner-dirty-baseline.patch": "sha256:d8a848c3bcaed69c1d17b86546632f22d742a33261321f0a4fdfb6edefb22da0", + "validation.json": "sha256:5647fbdcec20434edd6e96858761ac528b65d9bb917c82aff542563a60628582" + }, + "base_commit": "d2cd3fa9405f0766eef67a8b0e897239d8817fb7", + "checkout": "/tmp/warden-browser-target-20261016", + "files": { + "docs/security-case-execution-kernel-v1.md": "sha256:a84d2ae4d7078c37ecc96c48c118b4f5e3656deeef3d1384a85cc8920161b5da", + "src/warden/case_inputs.py": "sha256:f46e1156d04d705581dc06953bfc6bae24f553d77207a4025e216fd68ec6213b", + "src/warden/case_run.py": "sha256:afb5ce9c59b9748f85c9f1d4af04b255087632c58de8dcd8c9426fc1ed4db068", + "tests/test_case_inputs.py": "sha256:04d3f9012a576600d514c47bce22ffe867136cb7c92395e9e87c6c7eeeb3a497", + "workplans/WARDEN-WP-0044-security-case-follow-through.md": "sha256:67775912a16967d6f6c43f1f3b96cf3a83c883472f4420daa55dcabd89144176" + }, + "live_changes": false, + "native_complete": false, + "owner": "ops-warden", + "preserved_dirty_tracked_paths": [ + ".custodian-brief.md", + "WORK-RECORDS.md" + ], + "schema": "netkingdom-target-patch-manifest.v1", + "source_dependencies": { + "net-kingdom": { + "files": { + "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml": "sha256:9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578", + "security-case-types/oidc-browser-relying-party/type.yaml": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "security-cases/forgejo-netkingdom-oidc.yaml": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d", + "tools/playbook-capability-contract/playbook_contract_validator.py": "sha256:2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", + "tools/security-case-types/security_case_validator.py": "sha256:5b9e46a5d0a8f387c6e0d87216c5695c03753c564378b1e43c07b4628cd07dc7", + "tools/security-scenario-composer/security_scenario_composer.py": "sha256:9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f" + }, + "head": "994d11c869daf9624ac6da2adfdaeb86d8cdf537" + } + }, + "status": "source-ready; exact dependency integration and native owner admission pending", + "untracked_owner_files_copied": false, + "work_item": "WARDEN-WP-0044-T03" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/owner-dirty-baseline.patch new file mode 100644 index 00000000..77e38bff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/owner-dirty-baseline.patch @@ -0,0 +1,65 @@ +diff --git a/.custodian-brief.md b/.custodian-brief.md +index 09161f4..68a2a73 100644 +--- a/.custodian-brief.md ++++ b/.custodian-brief.md +@@ -2,7 +2,7 @@ + # Custodian Brief — ops-warden + + **Domain:** infotech +-**Last synced:** 2026-10-11 18:37 UTC ++**Last synced:** 2026-10-15 02:28 UTC + **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + + ## Active Workstreams +@@ -14,7 +14,7 @@ Progress: 1/4 done | workplan_id: `c8ee441e-1be1-5219-910c-e79ff23cc9ec` + - ! Tasks `611f0901` + *(wait: Warden's own declaration cannot classify the repair-path target workloads; the owner must declare the continuity actor/target and its z2-continuity resolution (see 2026-09-28 review).)* + - ! Tasks `54ad4864` +- *(wait: Owners of agt-codex-interhub-bootstrap, agt-state-hub-bridge and atm-backup-daily must publish authoritative workload identity declarations.)* ++ *(wait: Owners of agt-codex-interhub-bootstrap (ops-hub) and agt-state-hub-bridge (ops-bridge) must publish authoritative workload identity declarations; atm-backup-daily declared by activity-core and compiled 2026-10-09 (z2-protected, admission unsatisfied: M1 below M2 floor).)* + - ! Tasks `93eaf1f2` + *(wait: Gated on T01 and on layer model v0.8 (or successor) reaching accepted; never convert against a proposed standard.)* + +@@ -30,6 +30,7 @@ Progress: 2/4 done | workplan_id: `f7e81013-6868-5d99-8528-ba75a442eca9` + + **Open tasks:** + - ! T03 — Run and verify a case `4a0fa4a7` ++ *(wait: Catalog integration follows a proven run.)* + - ► T02 — Plan a case run `bcbf6c91` + + ### Tamper-resistant credential governance + mass rotation/lockdown (Strand B) +@@ -43,12 +44,12 @@ Progress: 2/3 done | workplan_id: `42a097db-1c24-558e-a724-030bb2b4443e` + + **Open tasks:** + - ! Prove routing and publication `a8b1b855` +- *(wait: Founder HOLD 2026-09-21: no rotation until SECRETS-WP-0006-T06 moves the publish lane off the legacy whynot-design npm path; then rotate and prove a fresh publish.)* ++ *(wait: Hold kept by D3 until SECRETS-WP-0006-T06 moves publishing off the legacy whynot-design npm path; then rotate and prove a fresh publish.)* + + ## Inbox Hygiene + + **Stale unread:** 2 message(s) older than 3 day(s) — triage at session start. +-**Missing thread_id:** 3 unread message(s) lack supersession chains. ++**Missing thread_id:** 2 unread message(s) lack supersession chains. + + --- + ## MCP Orientation (when available) +diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md +index 34cf268..6c39975 100644 +--- a/WORK-RECORDS.md ++++ b/WORK-RECORDS.md +@@ -32,13 +32,13 @@ + | workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | + | workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | + | workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +-| workplan | WARDEN-WP-0034 | backlog | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | ++| workplan | WARDEN-WP-0034 | active | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | + | workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | + | workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | + | workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | + | workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | + | workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | +-| workplan | WARDEN-WP-0040 | backlog | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | ++| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | + | workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | + | workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | + | workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/validation.json b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/validation.json new file mode 100644 index 00000000..e0a2a4f0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/r1/validation.json @@ -0,0 +1,9 @@ +{ + "test_only": true, + "native_complete": false, + "command": "WARDEN_TEST_NETKINGDOM_ROOT=/home/worsch/net-kingdom PYTHONPATH=src /home/worsch/ops-warden/.venv/bin/python -m pytest -q tests/test_case_inputs.py tests/test_case_run.py tests/test_case_receipt_state.py tests/test_case_plan.py tests/test_case_type.py", + "result": "99 passed in 5.78s", + "ruff": "format and check passed", + "native_effects": false, + "source_notes": "Tests use real producer code and Executor with explicit test-only owner receipts; no authenticated native claim." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/warden-browser/validation.json b/docs/changes/netkingdom-target-2026-10-16/warden-browser/validation.json new file mode 100644 index 00000000..ca14d337 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/warden-browser/validation.json @@ -0,0 +1,12 @@ +{ + "test_only": true, + "native_complete": false, + "command": "WARDEN_TEST_NETKINGDOM_ROOT=/home/worsch/net-kingdom PYTHONPATH=src /home/worsch/ops-warden/.venv/bin/python -m pytest -q tests/test_case_inputs.py tests/test_case_run.py tests/test_case_receipt_state.py tests/test_case_plan.py tests/test_case_type.py", + "result": "102 passed in 47.09s", + "ruff": "format and check passed", + "native_effects": false, + "source_notes": "Tests use real producer code and Executor with explicit test-only owner receipts; no authenticated native claim.", + "revision": "r2", + "accepted_netkingdom_patch_sha256": "sha256:d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05", + "producer_dependency_tests": "Missing producer and modified-source refusal for each of four pinned modules, including lazy security_case_evidence.py" +}