docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
c2b9f674e1
commit
a9e64d05fe
45 changed files with 12119 additions and 0 deletions
|
|
@ -0,0 +1,62 @@
|
|||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# 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.
|
||||
|
|
@ -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 |
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"reason": "Owner disclosure consent recorded for all seven exact source revisions; restricted hub-side reader, independent technical reviews and native authenticated index/entry fetch verification remain pending. Repositories stay private.",
|
||||
"repos": [
|
||||
"rapp-openbao",
|
||||
"rapp-policy-nexus",
|
||||
"rapp-postgres",
|
||||
"rapp-sbom-nexus",
|
||||
"rapp-telemetry",
|
||||
"rapp-tenant-engine",
|
||||
"rapp-user-engine"
|
||||
],
|
||||
"draft_registrations_not_ready": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-openbao",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-openbao/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-policy-nexus",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-policy-nexus/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-postgres",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-postgres/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-telemetry",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-telemetry/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-tenant-engine",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-tenant-engine/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-user-engine",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-user-engine/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/repos",
|
||||
"body": {
|
||||
"repo": "rapp-sbom-nexus",
|
||||
"url": "https://forgejo.coulomb.social/coulomb/rapp-sbom-nexus/raw/branch/main/registry/indexes/capabilities.yaml",
|
||||
"domain": "infotech",
|
||||
"enabled": true,
|
||||
"required": false,
|
||||
"registered_by": "codex-CUST-WP-0081"
|
||||
}
|
||||
}
|
||||
],
|
||||
"needed_fields": [
|
||||
"auth_env name for an already admitted hub-side source-read credential",
|
||||
"proof that the credential is limited to the intended repository read scope",
|
||||
"successful hub-side index fetch without value disclosure"
|
||||
],
|
||||
"consent_receipt": "private-source-owner-consent-r1.json",
|
||||
"registration_preparation": "hub-private-registration-preparation-r1.json"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"workplan_id": "e8992815-389a-5c40-8569-a2b0ee3945f0",
|
||||
"id": "5cd22087-e5d8-5d5e-a252-610a6ac13653",
|
||||
"topic_id": "cee7bedf-2b48-46ef-8601-006474f2ad7a",
|
||||
"title": "Complete scoped owner provisioning for reuse-surface discovery",
|
||||
"description": "Execution/evidence gate for CUST-WP-0081-T02. Public batch applied and verified (63/70 hosted). Founder metadata disclosure consent for all seven private source revisions recorded separately as decision d8f8f216-ac94-4295-97f3-825fd21254ea; do not request it again. Remaining: independent technical reviews, restricted hub-only repository reader through railiance-platform reviewed custody/ESO delivery, sanitized auth_env and native fetch receipts, seven registrations and final all-70 audit. This gate stays open until verified provisioning; metadata consent is not a credential or rollout grant. See docs/assessments/netkingdom-2026-10-14/private-source-owner-consent-r1.json and scoped-access-handoff.json.",
|
||||
"decision_type": "pending",
|
||||
"status": "open",
|
||||
"rationale": "All 70 owner records published; 63/70 verified hosted. Owner disclosure consent satisfied; seven-source reader and native fetch evidence pending.",
|
||||
"decided_by": null,
|
||||
"decided_at": null,
|
||||
"deadline": null,
|
||||
"escalation_note": null,
|
||||
"superseded_by": null,
|
||||
"created_at": "2026-10-14T07:51:34.426143Z",
|
||||
"updated_at": "2026-10-14T22:31:45.188478Z",
|
||||
"workstream_id": "e8992815-389a-5c40-8569-a2b0ee3945f0"
|
||||
}
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
[
|
||||
{
|
||||
"repo": "access-engine",
|
||||
"branch": "main",
|
||||
"local_head": "62ac2b4e99dec2005c7c407fe633612578a7907d",
|
||||
"remote_head": "62ac2b4e99dec2005c7c407fe633612578a7907d",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"branch": "main",
|
||||
"local_head": "f9f8df039258a0d8b6fc29dc1f7c4bcc75fdc8c0",
|
||||
"remote_head": "f9f8df039258a0d8b6fc29dc1f7c4bcc75fdc8c0",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "agentic-resources",
|
||||
"branch": "main",
|
||||
"local_head": "c4cedab945e047de068428fb943f346f093b22dc",
|
||||
"remote_head": "c4cedab945e047de068428fb943f346f093b22dc",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "approval-engine",
|
||||
"branch": "main",
|
||||
"local_head": "db850aded5fc3b5d35938d26e8084e436a83943b",
|
||||
"remote_head": "db850aded5fc3b5d35938d26e8084e436a83943b",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "audit-core",
|
||||
"branch": "main",
|
||||
"local_head": "8d4bde6ece39ce0df3a3cc49da38613123e8f215",
|
||||
"remote_head": "8d4bde6ece39ce0df3a3cc49da38613123e8f215",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "coordination-engine",
|
||||
"branch": "main",
|
||||
"local_head": "16de8fc3f70020df646f1ebdca705a1bf88485a0",
|
||||
"remote_head": "16de8fc3f70020df646f1ebdca705a1bf88485a0",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "emergent-security",
|
||||
"branch": "main",
|
||||
"local_head": "6eead2a922ae2eb81593213f54bf1f35e1c9970f",
|
||||
"remote_head": "6eead2a922ae2eb81593213f54bf1f35e1c9970f",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "fin-hub",
|
||||
"branch": "main",
|
||||
"local_head": "cace0fda7474304cc42028287f7ba1327ab07d00",
|
||||
"remote_head": "cace0fda7474304cc42028287f7ba1327ab07d00",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "gate-house",
|
||||
"branch": "main",
|
||||
"local_head": "ab1a299674679ccfefa8df84b877614ac44c9d38",
|
||||
"remote_head": "ab1a299674679ccfefa8df84b877614ac44c9d38",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "glas-harness",
|
||||
"branch": "main",
|
||||
"local_head": "dc88f302044c7c2b41795958527e64be8a83e3ab",
|
||||
"remote_head": "dc88f302044c7c2b41795958527e64be8a83e3ab",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "info-tech-canon",
|
||||
"branch": "main",
|
||||
"local_head": "9b9be2ec8e846ceaa05fa644323b02d8603e39b6",
|
||||
"remote_head": "9b9be2ec8e846ceaa05fa644323b02d8603e39b6",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"branch": "main",
|
||||
"local_head": "faae9cb0dbaf4c8d23f838183f513c302ad3b9f0",
|
||||
"remote_head": "faae9cb0dbaf4c8d23f838183f513c302ad3b9f0",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "issue-core",
|
||||
"branch": "main",
|
||||
"local_head": "1427d268a09b9334a98a167230ec7dd1d4a82053",
|
||||
"remote_head": "1427d268a09b9334a98a167230ec7dd1d4a82053",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"branch": "main",
|
||||
"local_head": "8c6c071ad9c73865793a075ed1b34b93e5075436",
|
||||
"remote_head": "44b243410006a2df32adcd4cae2ab5e6f806da82",
|
||||
"same_head": false,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "kings-guard",
|
||||
"branch": "main",
|
||||
"local_head": "6aea834dc556067672f31ed762f44a39bad988e3",
|
||||
"remote_head": "6aea834dc556067672f31ed762f44a39bad988e3",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "maturity-engine",
|
||||
"branch": "main",
|
||||
"local_head": "2f1de542cc8f24a79452f906d99f95be39d8b6a9",
|
||||
"remote_head": "363e8160a4910766e9380fabee68a8642dd45ccf",
|
||||
"same_head": false,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"branch": "main",
|
||||
"local_head": "2cc09ceb05c6f2132c6fc3478901c4ee1085fa1b",
|
||||
"remote_head": "8f0a5902004c47415846d818e400eb5d5e1e037d",
|
||||
"same_head": false,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "ops-bridge",
|
||||
"branch": "main",
|
||||
"local_head": "2246656f43fa5eae3ae917f63ea1dfa3f53f4a00",
|
||||
"remote_head": "2246656f43fa5eae3ae917f63ea1dfa3f53f4a00",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "ops-mason",
|
||||
"branch": "main",
|
||||
"local_head": "97d1fed9496aacb1836982096b8aecb6da13cf0d",
|
||||
"remote_head": "97d1fed9496aacb1836982096b8aecb6da13cf0d",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "ops-warden",
|
||||
"branch": "main",
|
||||
"local_head": "1baeb0821cb6304464fab78932d8c6501f8c4cc6",
|
||||
"remote_head": "1baeb0821cb6304464fab78932d8c6501f8c4cc6",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "policy-nexus",
|
||||
"branch": "main",
|
||||
"local_head": "c14561b2e5318e9637ba93e4c96c06a2db92d0c4",
|
||||
"remote_head": "c14561b2e5318e9637ba93e4c96c06a2db92d0c4",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rail-kubernetes",
|
||||
"branch": "main",
|
||||
"local_head": "5fad47fdfb7d4cabc21cf10f52ac733ef59803b0",
|
||||
"remote_head": "5fad47fdfb7d4cabc21cf10f52ac733ef59803b0",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-apps",
|
||||
"branch": "main",
|
||||
"local_head": "72bcad86d7531bcc27f0e465a9385be559ba0c9f",
|
||||
"remote_head": "72bcad86d7531bcc27f0e465a9385be559ba0c9f",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "railiance-bootstrap",
|
||||
"branch": "main",
|
||||
"local_head": "70bd14d3b941a49c78bbc51d8faab5ab1da1df76",
|
||||
"remote_head": "70bd14d3b941a49c78bbc51d8faab5ab1da1df76",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-cluster",
|
||||
"branch": "main",
|
||||
"local_head": "750c3e5b9b4623c52bce2b8fbd758f423f81f6c1",
|
||||
"remote_head": "750c3e5b9b4623c52bce2b8fbd758f423f81f6c1",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-enablement",
|
||||
"branch": "main",
|
||||
"local_head": "ce9f6d1be8ff73afdedd6d666a8485e6c7348069",
|
||||
"remote_head": "ce9f6d1be8ff73afdedd6d666a8485e6c7348069",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-fabric",
|
||||
"branch": "main",
|
||||
"local_head": "095342c72ab6f02c72dd5d40e869dcba536bc5c1",
|
||||
"remote_head": "095342c72ab6f02c72dd5d40e869dcba536bc5c1",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-forge",
|
||||
"branch": "main",
|
||||
"local_head": "6e8f657e55e96b4e9f3d9d74a27b420b68467e61",
|
||||
"remote_head": "6e8f657e55e96b4e9f3d9d74a27b420b68467e61",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "railiance-infra",
|
||||
"branch": "main",
|
||||
"local_head": "32f64b54fd94a5e6a145ef7c2c8faf051f721a71",
|
||||
"remote_head": "99bc66f8c98a5404339074bf5bb11271d9f6d58d",
|
||||
"same_head": false,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-master",
|
||||
"branch": "main",
|
||||
"local_head": "a0be0d3142b5392f1fb6271cf91325ce91a0f9d4",
|
||||
"remote_head": "a0be0d3142b5392f1fb6271cf91325ce91a0f9d4",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-platform",
|
||||
"branch": "main",
|
||||
"local_head": "0f0282a43646edbb5743318bc923749af1c0cea2",
|
||||
"remote_head": "b797594a21fb0969ec987e633521245e0277267b",
|
||||
"same_head": false,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "railiance-telemetry",
|
||||
"branch": "main",
|
||||
"local_head": "8cdb2d11dda03bd6839750d059b37932f069e66a",
|
||||
"remote_head": "8cdb2d11dda03bd6839750d059b37932f069e66a",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-openbao",
|
||||
"branch": "main",
|
||||
"local_head": "35672be138036c8433a5ac5de91e068c03c2b124",
|
||||
"remote_head": "35672be138036c8433a5ac5de91e068c03c2b124",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-playwright",
|
||||
"branch": "main",
|
||||
"local_head": "4f76a87f67fdd9dab19564bb84235397e2f2470a",
|
||||
"remote_head": "4f76a87f67fdd9dab19564bb84235397e2f2470a",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-policy-nexus",
|
||||
"branch": "main",
|
||||
"local_head": "088b78ac03b0cebeec34a69aae3569eb7ec2f9c3",
|
||||
"remote_head": "088b78ac03b0cebeec34a69aae3569eb7ec2f9c3",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-postgres",
|
||||
"branch": "main",
|
||||
"local_head": "a9f569c96d74aa384ac8d3ac4aa960e4061550b5",
|
||||
"remote_head": "a9f569c96d74aa384ac8d3ac4aa960e4061550b5",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-telemetry",
|
||||
"branch": "main",
|
||||
"local_head": "f158307e24b86bab88a1af4830ff6b4f85afdeb5",
|
||||
"remote_head": "f158307e24b86bab88a1af4830ff6b4f85afdeb5",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-tenant-engine",
|
||||
"branch": "main",
|
||||
"local_head": "9bdc86572728783a5ed94266f04f7b12d414c557",
|
||||
"remote_head": "9bdc86572728783a5ed94266f04f7b12d414c557",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rapp-user-engine",
|
||||
"branch": "main",
|
||||
"local_head": "f35108f9e1b8702fb6c526fe42b2fec5ca032738",
|
||||
"remote_head": "f35108f9e1b8702fb6c526fe42b2fec5ca032738",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "reef-railiance",
|
||||
"branch": "main",
|
||||
"local_head": "33b840faac109f134bf1115d631d5b1242052d9a",
|
||||
"remote_head": "33b840faac109f134bf1115d631d5b1242052d9a",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "rein-aharness",
|
||||
"branch": "main",
|
||||
"local_head": "a58d11ce8ae9ea07baf0682c4f860caa251692b3",
|
||||
"remote_head": "a58d11ce8ae9ea07baf0682c4f860caa251692b3",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "rein-openweights",
|
||||
"branch": "main",
|
||||
"local_head": "a2e428a76592d79982ea3c4eba5f137a12fb44e4",
|
||||
"remote_head": "a2e428a76592d79982ea3c4eba5f137a12fb44e4",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "repo-manager",
|
||||
"branch": "main",
|
||||
"local_head": "97ea2489257de08911cc5da1e1b84d8e15780dc3",
|
||||
"remote_head": "97ea2489257de08911cc5da1e1b84d8e15780dc3",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "resource-control",
|
||||
"branch": "main",
|
||||
"local_head": "ac4e1c43e6d82ecd81108f3b006454ac6aa76722",
|
||||
"remote_head": "ac4e1c43e6d82ecd81108f3b006454ac6aa76722",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "reuse-surface",
|
||||
"branch": "main",
|
||||
"local_head": "03cd049773478e9de7e38035a49deaa82772a357",
|
||||
"remote_head": "03cd049773478e9de7e38035a49deaa82772a357",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "risk-nexus",
|
||||
"branch": "main",
|
||||
"local_head": "0cb565e1e498002b0fe71975e7654b641a4c24d2",
|
||||
"remote_head": "0cb565e1e498002b0fe71975e7654b641a4c24d2",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "role-engine",
|
||||
"branch": "main",
|
||||
"local_head": "c9d6d0763542f632f1d9b799d63b6b85cf8eaaab",
|
||||
"remote_head": "c9d6d0763542f632f1d9b799d63b6b85cf8eaaab",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "runbook-tutorials",
|
||||
"branch": "main",
|
||||
"local_head": "f0ea260895e8ad8e60f24bf64a89be063b7f8e13",
|
||||
"remote_head": "f0ea260895e8ad8e60f24bf64a89be063b7f8e13",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "sand-boxer",
|
||||
"branch": "main",
|
||||
"local_head": "7ddf92070f2fa267d79f8cad0aa07d16f0cd6410",
|
||||
"remote_head": "7ddf92070f2fa267d79f8cad0aa07d16f0cd6410",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "sbom-nexus",
|
||||
"branch": "main",
|
||||
"local_head": "db76194330b9502e9a1085ae25cdd27ce264ec7b",
|
||||
"remote_head": "db76194330b9502e9a1085ae25cdd27ce264ec7b",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "secrets-engine",
|
||||
"branch": "main",
|
||||
"local_head": "6bd7129eac94131b4cf689e47cbdc9899418beb3",
|
||||
"remote_head": "6bd7129eac94131b4cf689e47cbdc9899418beb3",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "security-canon",
|
||||
"branch": "main",
|
||||
"local_head": "c31c3008a53bcff07be09d0507d4693d27b40a5a",
|
||||
"remote_head": "c31c3008a53bcff07be09d0507d4693d27b40a5a",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "state-hub",
|
||||
"branch": "main",
|
||||
"local_head": "2a2ac2eb195a073633f4db0f166b927ca1877d67",
|
||||
"remote_head": "2a2ac2eb195a073633f4db0f166b927ca1877d67",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "tenant-engine",
|
||||
"branch": "main",
|
||||
"local_head": "79a61905957bd78d3f4c93da42d7034ad3e3c8de",
|
||||
"remote_head": "79a61905957bd78d3f4c93da42d7034ad3e3c8de",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "the-custodian",
|
||||
"branch": "main",
|
||||
"local_head": "0e7bcd128bb80d3630d84c2ba1bedf88578ccde1",
|
||||
"remote_head": "0e7bcd128bb80d3630d84c2ba1bedf88578ccde1",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": [
|
||||
".custodian-brief.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "user-engine",
|
||||
"branch": "main",
|
||||
"local_head": "e9534ef00f02f2b4772cd80f3b7c7aad5f9dae23",
|
||||
"remote_head": "e9534ef00f02f2b4772cd80f3b7c7aad5f9dae23",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "whitehat-security",
|
||||
"branch": "main",
|
||||
"local_head": "42fe7a12c7d9d64142ec0e820ba4e975ffeac593",
|
||||
"remote_head": "42fe7a12c7d9d64142ec0e820ba4e975ffeac593",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
},
|
||||
{
|
||||
"repo": "zone-engine",
|
||||
"branch": "main",
|
||||
"local_head": "7e7e3f307ca996c373e91009ed1b9b0cbc6fcb9e",
|
||||
"remote_head": "7e7e3f307ca996c373e91009ed1b9b0cbc6fcb9e",
|
||||
"same_head": true,
|
||||
"remote_read_exit": 0,
|
||||
"staged_paths": []
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"workplan": "CUST-WP-0081",
|
||||
"user_disposition": "Arrange scoped owner access",
|
||||
"status": "Public batch verified; founder metadata disclosure consent recorded for all seven sources. Restricted hub reader and native fetch verification remain pending.",
|
||||
"owner_repo": "railiance-platform",
|
||||
"write_access": {
|
||||
"route": "reuse-surface-hub-write-token",
|
||||
"kv_path": "platform/workloads/reuse/reuse-surface/runtime-secrets",
|
||||
"field": "REUSE_SURFACE_TOKEN",
|
||||
"existing_read_policy": "workload-kv-read-reuse-surface-runtime",
|
||||
"request": "Owner to admit the caller through the existing scoped route and verify its metadata lookup and selected-field read. Retain existing denial evidence; do not grant administrative or unrelated workload access.",
|
||||
"delivery": "warden access --exec, without exporting or recording the value",
|
||||
"approval_record": "CCR-2026-0005 is active and was previously approved by platform-operator and reuse-surface-owner; it does not name the caller principal.",
|
||||
"principal_binding": "Identify the caller OpenBao OIDC entity/accessor without recording any token. If a binding change is needed, record approval for that exact principal with both required approvers before applying the existing policy.",
|
||||
"existing_policy": "workload-kv-read-reuse-surface-runtime",
|
||||
"exact_allowed_paths": [
|
||||
"platform/data/workloads/reuse/reuse-surface/runtime-secrets",
|
||||
"platform/metadata/workloads/reuse/reuse-surface/runtime-secrets"
|
||||
],
|
||||
"negative_check": "An unrelated sibling path must remain denied. Do not broaden policy contents or attach administrative/ESO-only permissions.",
|
||||
"verified_execution_receipt": "hosted-public-registration-verification.json"
|
||||
},
|
||||
"private_source_access": {
|
||||
"repositories": [
|
||||
"rapp-openbao",
|
||||
"rapp-policy-nexus",
|
||||
"rapp-postgres",
|
||||
"rapp-sbom-nexus",
|
||||
"rapp-telemetry",
|
||||
"rapp-tenant-engine",
|
||||
"rapp-user-engine"
|
||||
],
|
||||
"request": "Owner to provision a repository-read credential at the federation hub, confirm source-owner consent to public capability metadata, and verify hub-side index and entry fetches.",
|
||||
"return_only": [
|
||||
"configured auth_env name",
|
||||
"auth_header name if nondefault",
|
||||
"scope and verification receipts"
|
||||
],
|
||||
"visibility_change_authorized": false,
|
||||
"credential_design": "A new dedicated Forgejo identity/PAT restricted to repository-read for exactly these seven sources; verify a denied-write/admin negative. Keep it distinct from the hub writer token and from other workload credentials.",
|
||||
"delivery": "Owner-reviewed OpenBao custody and ESO delivery to the reuse-surface hub runtime, then configure the registration auth_env with the environment variable name only.",
|
||||
"approval": "Platform operator and each relevant source owner must approve the limited private capability-metadata disclosure and reader scope before provisioning.",
|
||||
"metadata_disclosure_consent": {
|
||||
"status": "approved",
|
||||
"receipt": "private-source-owner-consent-r1.json",
|
||||
"decision_id": "d8f8f216-ac94-4295-97f3-825fd21254ea",
|
||||
"proposal_sha256": "46ac4fe49a08054cc215aade2fbaf3b36ee334520997857ed84f530b52da9d35"
|
||||
},
|
||||
"consumer_delivery_gate": "Reuse owner reports native authenticated redirect forwarding hazard. Require owner-reviewed native fetch fix and verified fixed hub image before introducing source-reader PAT; initial same-image assumption is superseded by private-reader-owner-assembly-receipt.json."
|
||||
},
|
||||
"resume": "Update and retain the exact registration proposal revision with admitted auth_env references, verify source parity, execute bounded registrations, then require all 70 reviewed IDs exactly once in a fresh hosted federation.",
|
||||
"secret_values_requested_in_chat": false,
|
||||
"owner_message_sent": true,
|
||||
"attended_sequence": [
|
||||
"Review CCR-2026-0005 and identify the actual intended caller principal. The existing OIDC identity is not named in the CCR.",
|
||||
"If needed, obtain and record platform-operator plus reuse-surface-owner approval for binding that exact identity to the existing read-only policy. Verify read on the two exact paths and denial on a sibling path.",
|
||||
"Use warden access reuse-surface-hub-write-token --exec --field REUSE_SURFACE_TOKEN with a silent helper. Do not use --fetch or print/export the field.",
|
||||
"After the public-only helper receipt confirms 31 registrations, request the distinct seven-repository read-only source credential and owner disclosure consent. Return only the auth_env name, scope and sanitized verification receipts."
|
||||
],
|
||||
"public_batch_prepared": {
|
||||
"proposal": "hub-public-registration-proposal-r1.json",
|
||||
"plan": "hub-public-resume-plan.json",
|
||||
"helper": "register_public_hub.py",
|
||||
"repositories": 31,
|
||||
"status": "31 registrations applied, readback verified, hosted public capabilities verified, login session revoked"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# 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.
|
||||
|
|
@ -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 |
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"path": ".custodian-brief.md",
|
||||
"index": "100644 6aad3a5788fad3abd60c0f1270f160062dc0168b 0\t.custodian-brief.md"
|
||||
},
|
||||
{
|
||||
"path": "WORK-RECORDS.md",
|
||||
"index": "100644 d535e0f2dcc3d454e041a8f1b547b5dea4493380 0\tWORK-RECORDS.md"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"exit_code": 0,
|
||||
"commit": "16236ef479135df4361686aa69e9eeb987f33129",
|
||||
"base": "90490002c16d353d675f4295f395d1917d30b9ef",
|
||||
"files": [
|
||||
{
|
||||
"path": "docs/netkingdom-target-implementation.md",
|
||||
"sha256": "798093602f9c11d086af960b97a97f1a1d116962136e01de1b336df8a9603cec"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/publication-summary.json",
|
||||
"sha256": "69f2797c42f6ce33b731956457fc958f189202463845ae7a7c6cc930673651aa"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/public-digest-refinement.json",
|
||||
"sha256": "770a5831999df8a8dc1b66dc65b738f58bedd4eb0dab56127f8d37b2b10107b2"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/publication-refinement.json",
|
||||
"sha256": "957d1347771eecb16cb50713dc3cbaa43e96c3b3e162eb357be57bf12632a721"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/dirty-workplan-refinement.json",
|
||||
"sha256": "c90e021d41a3218f8a1e3b13af00dc7cf3a383a8bf0efe52c583c7f0d63dfc97"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/metadata-reconstruction-failure.json",
|
||||
"sha256": "ab943a848c811e22ec7ef4273bf2097ec20ac76902947491de01852c270b26b4"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/reconstruct_public_metadata.py",
|
||||
"sha256": "ec20b3378ece29dcf7faf4ad5e610e1e85bfa260121f2b8c537665f9530466e0"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/recover_pending.py",
|
||||
"sha256": "1cfad4dd82966efcea13dfe02d702a873a0fa6a422e654551fc1c9f530052a0b"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/verify_current.py",
|
||||
"sha256": "8aaa076520f5c954ebca6b1cf5b28df7223b0162e4edba4c7307a5a80cff6c43"
|
||||
}
|
||||
],
|
||||
"stdout": "[main 16236ef] Record verified consumer source and exact public evidence provenance\n 9 files changed, 793 insertions(+)\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/dirty-workplan-refinement.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/metadata-reconstruction-failure.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/public-digest-refinement.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/publication-refinement.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/publication-summary.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/reconstruct_public_metadata.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/recover_pending.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/owner-followthrough-publication-r1/verify_current.py\n",
|
||||
"stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 9, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n"
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"head": "90490002c16d353d675f4295f395d1917d30b9ef",
|
||||
"completed": [
|
||||
{
|
||||
"batch": 1,
|
||||
"commit": "518369dbee08b7f7690ee70d0cbc405ea4158397",
|
||||
"files": 18,
|
||||
"publication": "verified as unchanged ancestor of the exact projected notes commit"
|
||||
},
|
||||
{
|
||||
"batch": 2,
|
||||
"commit": "1513dad6528ab766a7631e88d9c04be4bc605f42",
|
||||
"files": 2,
|
||||
"required_consistency": "passed"
|
||||
},
|
||||
{
|
||||
"batch": 3,
|
||||
"commit": "6548540fd46c7fcd365f399516a5d36e38d1e7fd",
|
||||
"files": 18
|
||||
},
|
||||
{
|
||||
"batch": 4,
|
||||
"commit": "14670441cd567621ca86e466b113944ec8363798",
|
||||
"files": 18
|
||||
},
|
||||
{
|
||||
"batch": 5,
|
||||
"commit": "11a485f1dd9267cc63f9beb0436aa17c0d3317cb",
|
||||
"files": 8
|
||||
},
|
||||
{
|
||||
"batch": 6,
|
||||
"commit": "31f5cbf7b52c909ceebfdfc39ef0100908f63d5b",
|
||||
"files": 1
|
||||
},
|
||||
{
|
||||
"batch": 7,
|
||||
"commit": "0caed1f5300c1f5a6ededcca7948b120a126b85d",
|
||||
"files": 18
|
||||
},
|
||||
{
|
||||
"batch": 8,
|
||||
"commit": "90490002c16d353d675f4295f395d1917d30b9ef",
|
||||
"files": 13
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
"""Verify this source checkpoint, current primary bindings and original edits."""
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.request
|
||||
|
||||
ROOT = Path('/home/worsch/the-custodian')
|
||||
HERE = Path(__file__).resolve().parent
|
||||
summary = json.loads((HERE / 'publication-summary.json').read_text())
|
||||
|
||||
|
||||
def git(root, *args):
|
||||
return subprocess.check_output(['git', '-C', str(root), *args],
|
||||
stderr=subprocess.PIPE, timeout=45).decode().strip()
|
||||
|
||||
|
||||
def get(path):
|
||||
with urllib.request.urlopen('http://127.0.0.1:8000' + path, timeout=20) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
snapshot = summary['root_commit'].removeprefix('git:')
|
||||
head = git(ROOT, 'rev-parse', 'HEAD')
|
||||
projection = json.loads((HERE / 'close-sync.json').read_text())
|
||||
receipt = projection['receipt']
|
||||
assert projection['ok'] and projection['status'] in ('applied', 'noop')
|
||||
assert projection['commit'] == receipt['expected_commit'] == receipt['derived_commit'] == head
|
||||
assert (receipt['instance_role'], receipt['instance_label']) == ('primary', 'railiance01')
|
||||
assert receipt['outcome']['counts']['retired'] == 0
|
||||
assert git(ROOT, 'ls-remote', 'origin', 'refs/heads/main').split()[0] == head
|
||||
exceptions = {row['path']: row['sha256'] for row in summary['current_source_exceptions']}
|
||||
for row in summary['files']:
|
||||
committed = subprocess.check_output(['git', '-C', str(ROOT), 'show', snapshot + ':' + row['path']])
|
||||
assert 'sha256:' + hashlib.sha256(committed).hexdigest() == row['sha256']
|
||||
current_expected = exceptions.get(row['path'], row['sha256'])
|
||||
assert 'sha256:' + hashlib.sha256((ROOT / row['path']).read_bytes()).hexdigest() == current_expected
|
||||
current_committed = subprocess.check_output(['git', '-C', str(ROOT), 'show', head + ':' + row['path']])
|
||||
assert 'sha256:' + hashlib.sha256(current_committed).hexdigest() == current_expected
|
||||
protected = json.loads((HERE.parent / 'source-corrections-publication-r1/protected-state.json').read_text())
|
||||
for row in protected:
|
||||
assert hashlib.sha256((ROOT / row['path']).read_bytes()).hexdigest() == row['sha256']
|
||||
assert git(ROOT, 'ls-files', '--stage', '--', row['path']) == row['index']
|
||||
assert set(git(ROOT, 'diff', '--name-only', 'HEAD').splitlines()) == {r['path'] for r in protected}
|
||||
for owner, expected in summary['expected_owner_heads'].items():
|
||||
path = ROOT.parent / owner
|
||||
assert git(path, 'rev-parse', 'HEAD') == expected.removeprefix('git:')
|
||||
assert git(path, 'ls-remote', 'origin', 'refs/heads/main').split()[0] == expected.removeprefix('git:')
|
||||
checks = []
|
||||
for owner, filename, count in [
|
||||
('the-custodian', 'CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md', 6),
|
||||
('ops-warden', 'WARDEN-WP-0044-security-case-follow-through.md', 4),
|
||||
('net-kingdom', 'NK-WP-0048-security-case-types.md', 39),
|
||||
('key-cape', 'KEY-WP-0039-openbao-negative-probe-tokens.md', 3),
|
||||
('railiance-platform', 'RPF-WP-0060-browser-case-platform-bindings.md', 4),
|
||||
]:
|
||||
text = (ROOT.parent / owner / 'workplans' / filename).read_text()
|
||||
wp = re.search(r'state_hub_workstream_id: "([^"]+)"', text).group(1)
|
||||
tasks = get('/tasks/?workplan_id=' + wp)
|
||||
assert len(tasks) == count
|
||||
by_uuid = {t['id']: t for t in tasks}
|
||||
rows = []
|
||||
for block in re.findall(r'```task\n.*?```', text, re.S):
|
||||
record = re.search(r'^id: (.+)$', block, re.M).group(1)
|
||||
uid = re.search(r'state_hub_task_id: "([^"]+)"', block).group(1)
|
||||
status = re.search(r'^status: (.+)$', block, re.M).group(1)
|
||||
actual = by_uuid[uid]
|
||||
assert (actual['record_id'], actual['status'], actual['workplan_id']) == (record, status, wp)
|
||||
rows.append({'record_id': record, 'uuid': uid, 'status': status})
|
||||
assert len(rows) == count
|
||||
item = {'owner': owner, 'task_count': count, 'workplan_uuid': wp, 'bindings': rows}
|
||||
if owner == 'net-kingdom':
|
||||
selected = {row['record_id']: row['uuid'] for row in rows}
|
||||
dependencies = get('/workplans/' + wp + '/dependencies/')
|
||||
matching = [d for d in dependencies if d.get('from_task_id') == selected['NK-WP-0048-T39']
|
||||
and d.get('to_task_id') == selected['NK-WP-0048-T38']]
|
||||
assert len(matching) == 1 and matching[0]['relationship_type'] == 'starts_after'
|
||||
# Existing source edges retain their creation commit. Reconciliation
|
||||
# only creates/removes edges; a status-only commit does not rewrite it.
|
||||
creation = '6201a1d2de282a218fde8d81b32f790a526942ef'
|
||||
current = summary['expected_owner_heads'][owner].removeprefix('git:')
|
||||
assert matching[0]['source_owner'] == 'forge:net-kingdom'
|
||||
assert matching[0]['source_commit'] == creation
|
||||
assert git(ROOT.parent / owner, 'merge-base', creation, current) == creation
|
||||
wp_path = 'workplans/' + filename
|
||||
created_text = subprocess.check_output(
|
||||
['git', '-C', str(ROOT.parent / owner), 'show', creation + ':' + wp_path]).decode()
|
||||
def dependency_declaration(value):
|
||||
blocks = re.findall(r'```task\n.*?```', value, re.S)
|
||||
task = next(b for b in blocks if 'id: NK-WP-0048-T39\n' in b)
|
||||
return re.search(r'^depends_on: (.+)$', task, re.M).group(1)
|
||||
assert dependency_declaration(text) == dependency_declaration(created_text) == '[NK-WP-0048-T38]'
|
||||
first_projection = json.loads((HERE.parent / 'nk48-consumer-gate-implementation-r1/projection.json').read_text())
|
||||
current_projection = json.loads((HERE.parent / 'nk48-release-build-progress-r1/projection.json').read_text())
|
||||
for evidence, commit in ((first_projection, creation), (current_projection, current)):
|
||||
assert evidence['ok'] and evidence['status'] == 'applied'
|
||||
assert evidence['commit'] == evidence['receipt']['expected_commit'] == evidence['receipt']['derived_commit'] == commit
|
||||
assert evidence['receipt']['instance_role'] == 'primary'
|
||||
assert evidence['receipt']['instance_label'] == 'railiance01'
|
||||
assert current_projection['receipt']['outcome']['counts']['created_edges'] == 0
|
||||
assert current_projection['receipt']['outcome']['counts']['removed_edges'] == 0
|
||||
item['release_dependency'] = matching[0]
|
||||
checks.append(item)
|
||||
for name in [r['path'] for r in [json.loads((HERE / 'public-digest-refinement.json').read_text()),
|
||||
*json.loads((HERE / 'public-digest-refinement.json').read_text())['additional_metadata_derivatives']]]:
|
||||
subprocess.run(['python3', str(HERE / 'reconstruct_public_metadata.py'),
|
||||
str((ROOT / name).relative_to(HERE.parent))], check=True, stdout=subprocess.DEVNULL)
|
||||
result = {'status': 'verified', 'root_commit': 'git:' + head,
|
||||
'frozen_publication_snapshot': 'git:' + snapshot, 'snapshot_files_verified': len(summary['files']),
|
||||
'current_document_delta_verified': list(exceptions), 'source_primary_bindings': checks,
|
||||
'task_bindings_verified': sum(c['task_count'] for c in checks), 'protected_original_paths': len(protected),
|
||||
'owner_heads': summary['expected_owner_heads'], 'public_original_metadata_reconstructions': 4,
|
||||
'all_sixteen_native_gates': 'open', 'goal': 'active',
|
||||
'observed_at': datetime.datetime.now(datetime.timezone.utc).isoformat()}
|
||||
(HERE / 'final-verification-r2.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps({'status': 'verified', 'root_commit': head, 'files': len(summary['files']),
|
||||
'task_bindings': result['task_bindings_verified'], 'preserved_original_paths': len(protected),
|
||||
'public_original_metadata_reconstructions': 4, 'goal': 'active'}))
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — railiance-platform
|
||||
|
||||
**Domain:** financials
|
||||
**Last synced:** 2026-10-17 02:33 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
### Prepare the exact seven private-source reader for reuse-surface
|
||||
Progress: 4/5 done | workplan_id: `b55a8aec-7457-563b-990b-4a55ca04d2f6`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Rotate or revoke dedicated seven-source reader within 90 days `f0780e75`
|
||||
|
||||
### Coordinate audit-core temporary custody and recovery exercises
|
||||
Progress: 3/4 done | workplan_id: `f4640325-e89c-591d-b58e-ec6b087900ac`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T03 — Define the coordinated railiance01 reboot exercise `ba729d18`
|
||||
*(wait: Single-node outage exercise needs operator access and a declared restart order coordinated with railiance-infra, railiance-cluster, audit-core and the OpenBao package owner.)*
|
||||
|
||||
### Admit hourly receiver cadence and exact Qonto collector custody
|
||||
Progress: 3/4 done | workplan_id: `d57eeafb-c406-5e1e-841c-5d1288aacabf`
|
||||
|
||||
**Open tasks:**
|
||||
- ► Collect actual producer and independent observer acceptance `7d8e5315`
|
||||
|
||||
### Let the hub-writer caller prove its own reach and pick the workload role in the OpenBao UI
|
||||
Progress: 3/4 done | workplan_id: `04f7c4aa-0a24-5d61-994d-2487ca6bfb91`
|
||||
|
||||
**Open tasks:**
|
||||
- ► Deploy the overlay `94cdfd1c`
|
||||
|
||||
### Coordinate KeyCape live Secret exposure recovery
|
||||
Progress: 4/6 done | workplan_id: `b2c25a01-4a80-55c1-90cf-8538000f7e0e`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T03 — Collect exact owner acknowledgements `714ae011`
|
||||
*(wait: Owner work first: NetKingdom packages the complete resolver sequence as one receipt-producing command; then it enters the attended-window checklist (D1 standing rule).)*
|
||||
- ! T06 — Publish the Railiance/OpenBao custody handoff `3b9748c4`
|
||||
*(wait: 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.)*
|
||||
|
||||
### Admit the Playwright pilot and establish scoped consumer group access
|
||||
Progress: 5/6 done | workplan_id: `31933d08-c877-5fda-aff9-6220c3705c92`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Connect platform identities and prove managed consumer operation `a82762eb`
|
||||
*(wait: User admitted all identified platform humans and coding agents; their supported Kubernetes authentication/group-mapping route remains unimplemented.)*
|
||||
|
||||
### ArgoCD phase B: adopt the four existing Applications on railiance01
|
||||
Progress: 5/8 done | workplan_id: `98140775-3b9a-5cf9-9af6-722502d487dc`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Adopt issue-core (from rapp-issue-core) `0e619658`
|
||||
- ! Adopt external-secrets (highest risk) `4ca54868`
|
||||
*(wait: Ordered D2 adoption: issue-core T04 must provide automated Argo health/diff and private/authenticated smoke proof; target-revenue T05 is already proven. No further founder approval is required.)*
|
||||
- ! Turn the root back to the production lane `844ff115`
|
||||
*(wait: Restore prune/selfHeal once T03-T06 have automated proof; no further founder go-ahead (D2).)*
|
||||
|
||||
### Onboard rapp-policy-nexus to the ArgoCD production lane
|
||||
Progress: 3/5 done | workplan_id: `ec41a4bd-df18-5b07-9b63-ccb80d9f001c`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Adopt policy-nexus (live, founder go-ahead) `8638fa73`
|
||||
*(wait: Merge approved by D2; runs once T03 diff and root automation (RPF-WP-0044-T07) are proven.)*
|
||||
- ! Prepare the Application and the dry-run evidence `2afb1aac`
|
||||
*(wait: Steps 1 and 3 done 2026-10-11 (docs/evidence/2026-10-11-policy-nexus-adoption-readonly-precheck.json, no stop condition). Step 2 render diff needs the railiance-apps values path from RAPP-POLICY-NEXUS-WP-0002-T02; agents can run it read-only via the k3s tunnel.)*
|
||||
|
||||
### Close Forgejo primary backup coverage on Scaleway
|
||||
Progress: 3/4 done | workplan_id: `7beec1a7-aa82-5a36-9a66-6b60008a2455`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Establish primary full-archive delivery and application recovery `a4807df0`
|
||||
*(wait: Durable scheduled caller, verified inventory and quota/retention gates remain open; September 15 prohibits expiration and scheduled secondary cutover.)*
|
||||
|
||||
### Close S3 service assurance and ownership gaps
|
||||
Progress: 4/7 done | workplan_id: `ca639c3d-3a87-5fa4-ad13-6f2e014b0c84`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Make backup freshness and recurring recovery evidence checkable `d64446fb`
|
||||
*(wait: D2 accepts the activity-core scheduling role; ACTIVITY-WP-0042 must supply the implemented bounded scheduler and scoped custody/execution proof before recurring backup/restore work can execute. No new founder go-ahead is required.)*
|
||||
- ! Produce S3 signals and prove their delivery to the evidence owner `5351e0e4`
|
||||
*(wait: Needs a concrete receiving contract from railiance-telemetry (RTEL-WP-0002) before health transport is accepted; unmonitored state retained meanwhile.)*
|
||||
- ! Obtain acceptance for compatibility assets and derived-record cleanup `3b74f79c`
|
||||
*(wait: Owner-ready handoffs to railiance-forge, rapp-openbao and the ArgoCD bootstrap owner await acceptance; S3 custody contracts stay here until then.)*
|
||||
|
||||
### Adopt activity-core application runtime into railiance01 GitOps
|
||||
Progress: 2/3 done | workplan_id: `b8cf2fc2-60c2-5d4e-a60a-55f1304d9f54`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Observe adoption and enforce bounded promotion readiness `63b44949`
|
||||
|
||||
### Implement reviewed credential lanes with separate owner gates
|
||||
Progress: 7/13 done | workplan_id: `975db491-5412-5e27-8e34-14a2417bb039`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Accept and provision secrets-engine service JWT login `e0c82ea9`
|
||||
*(wait: Agent work, not a human gate: claim refusals (KEY-WP-0039-T02 probe) and capabilities-self deny, then ops-mason MASON-WP-0009-T05.)*
|
||||
- ! Implement the platform operator-write CCR contract and Fluid lane `f47e5bc5`
|
||||
*(wait: Founder session done 2026-10-09; waits on ops-mason verification (MASON-WP-0005-T05) and the FT-WP-0002 owner salt CAS probe.)*
|
||||
- ! Dispose of the ungoverned whynot-design npm duplicate `0dd7c9a5`
|
||||
*(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.)*
|
||||
- ! Admit the two factory audit-sender custody lanes `0ef52c26`
|
||||
*(wait: Approved conditionally (ab85ae8a, standing change rule): executes once independent archive readback and the interim bearer revocation receipt exist; owner work with audit-core.)*
|
||||
- ! Retire the approval-engine-operator client delivery and custody (CCR-2026-0018) `af2d4dba`
|
||||
*(wait: 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.)*
|
||||
- ► Implement and prove the separately approved task execution entitlement `914bf383`
|
||||
|
||||
### Coordinate KeyCape live Secret exposure recovery
|
||||
Progress: 4/6 done | workplan_id: `038bc3c0-4492-5b91-95eb-ae515ca205df`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T03 — Collect exact owner acknowledgements `e019c166`
|
||||
*(wait: Canonical RPF-WP-0027-T03: complete platform custody acknowledgements remain missing.)*
|
||||
- ! T06 — Publish the Railiance/OpenBao custody handoff `69326850`
|
||||
*(wait: Canonical RPF-WP-0027-T06: complete field/auth/expiry/handoff contract absent; September 15 operator block remains in force.)*
|
||||
|
||||
### Coordinate audit-core temporary custody and recovery exercises
|
||||
Progress: 2/4 done | workplan_id: `88c4ef7f-0af8-580e-90dc-a2bae2675a4d`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T02 — Define the runtime database lease recovery exercise `caba7fcd`
|
||||
*(wait: Canonical RPF-WP-0015-T02: approved sender identity, fresh bounded attended window, named abort operator and live recovery receipt required.)*
|
||||
- ! T03 — Define the coordinated railiance01 reboot exercise `09cf4065`
|
||||
*(wait: Canonical RPF-WP-0015-T03: fresh outage approval, off-host snapshot, quorum/access evidence and owner execution required.)*
|
||||
|
||||
### Retract public OpenBao listener behind operator-only access
|
||||
Progress: 2/3 done | workplan_id: `6f8a6cbc-c076-5f0a-ade2-281a7ec71360`
|
||||
|
||||
**Open tasks:**
|
||||
- ! T03 — Complete the attended operator cutover `99f8b41f`
|
||||
|
||||
## Inbox Hygiene
|
||||
|
||||
**Missing thread_id:** 14 unread message(s) lack supersession chains.
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
||||
If the state-hub MCP server is reachable, call:
|
||||
`get_domain_summary("financials")`
|
||||
This provides richer cross-domain context.
|
||||
If the MCP call fails, use this file as your orientation source.
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
# Work Records — railiance-platform
|
||||
|
||||
> 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 | RPF-WP-ADHOC-2026-10-10 | finished | — | workplans/ADHOC-2026-10-10.md |
|
||||
| workplan | RPF-WP-ADHOC-2026-10-11 | finished | — | workplans/ADHOC-2026-10-11.md |
|
||||
| workplan | RPF-WP-0015 | active | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md |
|
||||
| workplan | RPF-WP-0025 | finished | — | workplans/RPF-WP-0025-openbao-operator-only-access.md |
|
||||
| workplan | RPF-WP-0027 | blocked | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| workplan | RPF-WP-0029 | finished | — | workplans/RPF-WP-0029-backup-credential-default-removal.md |
|
||||
| workplan | RPF-WP-0035 | blocked | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| workplan | RPF-WP-0036 | blocked | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| workplan | RPF-WP-0038 | blocked | — | workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md |
|
||||
| workplan | RPF-WP-0039 | finished | — | workplans/RPF-WP-0039-vergabe-demo-company-database.md |
|
||||
| workplan | RPF-WP-0040 | finished | — | workplans/RPF-WP-0040-keycape-factor-credential-custody.md |
|
||||
| workplan | RPF-WP-0041 | finished | — | workplans/RPF-WP-0041-state-hub-105m-not-provisioning.md |
|
||||
| workplan | RPF-WP-0042 | finished | — | workplans/RPF-WP-0042-informed-decision-sitting-requester.md |
|
||||
| workplan | RPF-WP-0043 | blocked | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| workplan | RPF-WP-0044 | blocked | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| workplan | RPF-WP-0045 | finished | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| workplan | RPF-WP-0046 | finished | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| workplan | RPF-WP-0047 | finished | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md |
|
||||
| workplan | RPF-WP-0048 | blocked | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md |
|
||||
| workplan | RPF-WP-0049 | finished | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| workplan | RPF-WP-0050 | finished | — | workplans/RPF-WP-0050-attended-provisioning-preparation.md |
|
||||
| workplan | RPF-WP-0051 | finished | — | workplans/RPF-WP-0051-informed-decision-requester-workload-delivery.md |
|
||||
| workplan | RPF-WP-0052 | finished | — | workplans/RPF-WP-0052-authelia-platform-smtp-admission.md |
|
||||
| workplan | RPF-WP-0053 | proposed | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| workplan | RPF-WP-0054 | finished | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| workplan | RPF-WP-0055 | backlog | — | workplans/RPF-WP-0055-keycape-client-secret-rotation-residual.md |
|
||||
| workplan | RPF-WP-0056 | blocked | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| workplan | RPF-WP-0057 | active | — | workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md |
|
||||
| workplan | RPF-WP-0058 | active | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| workplan | RPF-WP-0059 | active | — | workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md |
|
||||
| task | RPF-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md |
|
||||
| task | RPF-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md |
|
||||
| task | RPF-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md |
|
||||
| task | RPF-WP-ADHOC-2026-10-11-T03 | done | — | workplans/ADHOC-2026-10-11.md |
|
||||
| task | RPF-WP-0015-T01 | done | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md |
|
||||
| task | RPF-WP-0015-T02 | done | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md |
|
||||
| task | RPF-WP-0015-T03 | wait | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md |
|
||||
| task | RPF-WP-0015-T04 | done | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md |
|
||||
| task | RPF-WP-0025-T01 | done | — | workplans/RPF-WP-0025-openbao-operator-only-access.md |
|
||||
| task | RPF-WP-0025-T02 | done | — | workplans/RPF-WP-0025-openbao-operator-only-access.md |
|
||||
| task | RPF-WP-0025-T03 | done | — | workplans/RPF-WP-0025-openbao-operator-only-access.md |
|
||||
| task | RPF-WP-0027-T01 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0027-T02 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0027-T03 | wait | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0027-T04 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0027-T05 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0027-T06 | wait | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md |
|
||||
| task | RPF-WP-0029-T01 | done | — | workplans/RPF-WP-0029-backup-credential-default-removal.md |
|
||||
| task | RPF-WP-0029-T02 | done | — | workplans/RPF-WP-0029-backup-credential-default-removal.md |
|
||||
| task | RPF-WP-0029-T03 | done | — | workplans/RPF-WP-0029-backup-credential-default-removal.md |
|
||||
| task | RPF-WP-0035-T01 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T02 | wait | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T03 | wait | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T04 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T05 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T06 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T07 | wait | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T08 | wait | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T09 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T10 | wait | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T11 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T12 | progress | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0035-T13 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md |
|
||||
| task | RPF-WP-0036-T01 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T02 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T03 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T04 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T05 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T06 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0036-T07 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md |
|
||||
| task | RPF-WP-0038-T01 | done | — | workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md |
|
||||
| task | RPF-WP-0038-T02 | done | — | workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md |
|
||||
| task | RPF-WP-0038-T03 | done | — | workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md |
|
||||
| task | RPF-WP-0038-T04 | wait | — | workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md |
|
||||
| task | RPF-WP-0039-T01 | done | — | workplans/RPF-WP-0039-vergabe-demo-company-database.md |
|
||||
| task | RPF-WP-0039-T02 | done | — | workplans/RPF-WP-0039-vergabe-demo-company-database.md |
|
||||
| task | RPF-WP-0040-T01 | done | — | workplans/RPF-WP-0040-keycape-factor-credential-custody.md |
|
||||
| task | RPF-WP-0040-T02 | done | — | workplans/RPF-WP-0040-keycape-factor-credential-custody.md |
|
||||
| task | RPF-WP-0040-T03 | done | — | workplans/RPF-WP-0040-keycape-factor-credential-custody.md |
|
||||
| task | RPF-WP-0040-T04 | done | — | workplans/RPF-WP-0040-keycape-factor-credential-custody.md |
|
||||
| task | RPF-WP-0041-T01 | done | — | workplans/RPF-WP-0041-state-hub-105m-not-provisioning.md |
|
||||
| task | RPF-WP-0042-T01 | done | — | workplans/RPF-WP-0042-informed-decision-sitting-requester.md |
|
||||
| task | RPF-WP-0042-T02 | done | — | workplans/RPF-WP-0042-informed-decision-sitting-requester.md |
|
||||
| task | RPF-WP-0043-T01 | done | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| task | RPF-WP-0043-T02 | done | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| task | RPF-WP-0043-T03 | wait | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| task | RPF-WP-0043-T04 | wait | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| task | RPF-WP-0043-T05 | done | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md |
|
||||
| task | RPF-WP-0044-T01 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T02 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T03 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T04 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T05 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T06 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T07 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0044-T08 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md |
|
||||
| task | RPF-WP-0045-T01 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0045-T02 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0045-T03 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0045-T04 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0045-T05 | cancel | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0045-T06 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md |
|
||||
| task | RPF-WP-0046-T01 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0046-T02 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0046-T03 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0046-T04 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0046-T05 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0046-T06 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md |
|
||||
| task | RPF-WP-0047-T01 | done | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md |
|
||||
| task | RPF-WP-0047-T02 | done | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md |
|
||||
| task | RPF-WP-0047-T03 | done | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md |
|
||||
| task | RPF-WP-0048-T01 | done | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md |
|
||||
| task | RPF-WP-0048-T02 | wait | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md |
|
||||
| task | RPF-WP-0048-T03 | done | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md |
|
||||
| task | RPF-WP-0049-T01 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| task | RPF-WP-0049-T02 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| task | RPF-WP-0049-T03 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| task | RPF-WP-0049-T04 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| task | RPF-WP-0049-T05 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md |
|
||||
| task | RPF-WP-0050-T01 | done | — | workplans/RPF-WP-0050-attended-provisioning-preparation.md |
|
||||
| task | RPF-WP-0050-T02 | done | — | workplans/RPF-WP-0050-attended-provisioning-preparation.md |
|
||||
| task | RPF-WP-0050-T03 | cancel | — | workplans/RPF-WP-0050-attended-provisioning-preparation.md |
|
||||
| task | RPF-WP-0050-T04 | done | — | workplans/RPF-WP-0050-attended-provisioning-preparation.md |
|
||||
| task | RPF-WP-0051-T01 | done | — | workplans/RPF-WP-0051-informed-decision-requester-workload-delivery.md |
|
||||
| task | RPF-WP-0051-T02 | done | — | workplans/RPF-WP-0051-informed-decision-requester-workload-delivery.md |
|
||||
| task | RPF-WP-0051-T03 | done | — | workplans/RPF-WP-0051-informed-decision-requester-workload-delivery.md |
|
||||
| task | RPF-WP-0052-T01 | done | — | workplans/RPF-WP-0052-authelia-platform-smtp-admission.md |
|
||||
| task | RPF-WP-0052-T02 | done | — | workplans/RPF-WP-0052-authelia-platform-smtp-admission.md |
|
||||
| task | RPF-WP-0052-T03 | done | — | workplans/RPF-WP-0052-authelia-platform-smtp-admission.md |
|
||||
| task | RPF-WP-0053-T01 | progress | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0053-T02 | todo | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0053-T03 | todo | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0053-T04 | todo | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0053-T05 | progress | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0053-T06 | done | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md |
|
||||
| task | RPF-WP-0054-T01 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T02 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T03 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T04 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T05 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T06 | done | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0054-T07 | cancel | — | workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md |
|
||||
| task | RPF-WP-0055-T01 | todo | — | workplans/RPF-WP-0055-keycape-client-secret-rotation-residual.md |
|
||||
| task | RPF-WP-0056-T01 | done | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0056-T02 | done | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0056-T03 | wait | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0056-T04 | done | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0056-T05 | done | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0056-T06 | done | — | workplans/RPF-WP-0056-playwright-consumer-admission.md |
|
||||
| task | RPF-WP-0057-T01 | done | — | workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md |
|
||||
| task | RPF-WP-0057-T02 | done | — | workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md |
|
||||
| task | RPF-WP-0057-T03 | done | — | workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md |
|
||||
| task | RPF-WP-0057-T04 | progress | — | workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md |
|
||||
| task | RPF-WP-0058-T01 | done | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| task | RPF-WP-0058-T02 | done | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| task | RPF-WP-0058-T03 | done | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| task | RPF-WP-0058-T04 | done | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| task | RPF-WP-0058-T05 | wait | — | workplans/RPF-WP-0058-reuse-private-source-reader.md |
|
||||
| task | RPF-WP-0059-T01 | done | — | workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md |
|
||||
| task | RPF-WP-0059-T02 | done | — | workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md |
|
||||
| task | RPF-WP-0059-T03 | done | — | workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md |
|
||||
| task | RPF-WP-0059-T04 | progress | — | workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md |
|
||||
| intake | RPF-IN-0001 | closed | — | intakes/intakes.md |
|
||||
| intake | RPF-IN-0002 | open | — | intakes/intakes.md |
|
||||
| intake | RPF-IN-0003 | open | — | intakes/intakes.md |
|
||||
| intake | RPF-IN-0004 | open | — | intakes/intakes.md |
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"path": ".custodian-brief.md",
|
||||
"index": "100644 1c9e635012d170c76646bf76d5202f6de163a643 0\t.custodian-brief.md"
|
||||
},
|
||||
{
|
||||
"path": "WORK-RECORDS.md",
|
||||
"index": "100644 dc7f3aab6de945558ddb9e48a482a9d92d734c3b 0\tWORK-RECORDS.md"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Platform delivery backend source preparation r1
|
||||
|
||||
Prepared for existing NK-WP-0048-T16 / RPF-1.3-T4. The exact patch targets
|
||||
railiance-platform `104ed90f42a9a86bf9fda343b49b65a85c9ee4b9`. No canonical
|
||||
Platform, NetKingdom or Warden source was changed; no commit, native action,
|
||||
credential read, grant, authority, acknowledgement or owner acceptance occurred.
|
||||
|
||||
`railiance-platform.patch` adds a concrete command implementation and meaningful
|
||||
offline regressions, and extends the existing contract. `prepared-source/`
|
||||
contains the exact resulting files. `source-pins.json` pins inputs, reviewed
|
||||
facilities and output hashes. `regressions.json` records final runs and the one
|
||||
initial test assertion failure. Both final suites pass: 17 command regressions
|
||||
and 8 existing fixed-contract tests. Delivery tests clear the environment, forbid
|
||||
socket creation and intercept subprocesses; no HOME, ambient credentials or
|
||||
network supply fixture authority.
|
||||
|
||||
The code creates only the fixed ExternalSecret after validating a preexisting
|
||||
fixed Store, reports matching existing objects unchanged, refuses conflicting
|
||||
objects, waits boundedly for Ready, captures public UID/resourceVersion, and
|
||||
deletes only verified instance-created objects with UID **and** resourceVersion
|
||||
API preconditions. Lost responses and uncertain cleanup preserve reconciliation
|
||||
obligations. The receipt has the existing closed metadata-only adapter shape.
|
||||
Receipt construction by these private mechanics is not authenticated evidence.
|
||||
|
||||
| Facility | Existing source and actual status |
|
||||
| --- | --- |
|
||||
| Fixed Store/ExternalSecret metadata | Real `keycape_issuer_eso.documents` and `require_fixed_store`; reused without Store creation or caller overrides. Template SHA-256 remains `e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743`. |
|
||||
| Contained command execution | Real `state_hub_preflight_lane.command`; reused for actual kubectl argv, fixed error containment and process timeout. Source-preparation transport has empty env and `/dev/null` kubeconfig. |
|
||||
| Safe remote transport | Real `remote_exec.run_remote` and custody-projection Operator exist for attended procedures; reviewed, deliberately not borrowed as an authenticated delivery session. |
|
||||
| Caller case verification | Real mint broker `check_case` checks exact bytes/reference, materialized/confidential/client match and @1.1. It does not acknowledge @1.3 or authenticate a delivery caller. **Missing for this delivery binding.** |
|
||||
| Current human aal2 delivery caller/session | Mint-lane OpenBao lookup-self and CCR-0043 issuer binding are real but mint-specific. No scoped Kubernetes delivery credential/session issuer exists here. **Missing; production factory refuses before dispatch.** |
|
||||
| Durable admission/recovery journal | `task_execution_journal.Journal` exists and explicitly cannot authorize native work. No lane-specific trusted admission/consume binding or creation-to-evidence durable transaction exists. **Missing for this lane; not replaced with in-memory provenance.** |
|
||||
| Evidence publication/authentication | Credential-change evidence writes and State Hub progress logging exist for their original purpose; neither authenticates delivery adapter payloads. **Missing.** No always-true verifier or local receipt oracle is added. |
|
||||
| Per-attempt delivery session revocation | OpenBao revoke-self exists in other lanes, but there is no actual delivery session/accessor/owner identity binding to revoke or prove revoked. **Missing.** |
|
||||
| Adapter integration | Existing frozen deliver-secret adapter still expects owner deliver/remove/publish/authenticate/revoke_session. `production_bindings()` deliberately refuses rather than provide any forged binding. Descriptor/source digest remains unchanged. |
|
||||
|
||||
This is a concrete backend algorithm prepared for owner review, **not completion
|
||||
of RPF-1.3-T4**. Real authenticated callable production capabilities remain open.
|
||||
Future work must supply the missing existing-obligation facilities with reviewed
|
||||
authority, exact admitted case/client binding, one-shot durable journaling,
|
||||
authenticated evidence and actual per-attempt revocation. Removing the deliberate
|
||||
transport restrictions alone would not satisfy those obligations.
|
||||
|
||||
The shared ESO service account/role pattern is grounded only in CCR-0041. No
|
||||
family-wide role grant, new Store, service, admission authority or native access
|
||||
is inferred. Target Secret cleanup by Kubernetes/ESO remains outside these
|
||||
commands; no direct Secret read/list/delete occurs. A lost create response with
|
||||
unknown UID requires owner reconciliation at the fixed resource name, never a
|
||||
blind delete. Process restart never recreates deletion provenance from a supplied
|
||||
revision or unsigned receipt.
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# Proposed fixed issuer ESO contract — RPF-1.3-T4 / NK48-R5-F2
|
||||
|
||||
Status: proposed source; not admitted, deployed, or owner-acknowledged. This is a
|
||||
new Platform declaration for review, not an existing Forgejo grant discovery.
|
||||
|
||||
The only substitution in `manifests/templates/keycape-issuer-eso.yaml`
|
||||
(sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743) is the admitted `client_id`. The proposed issuer
|
||||
store is **ClusterSecretStore `openbao-keycape-{client_id}-issuer`**, cluster
|
||||
scoped (no metadata namespace), restricted by conditions to ExternalSecrets in
|
||||
`sso`. For Forgejo it is `openbao-keycape-forgejo-issuer`. The suffix makes the
|
||||
issuer purpose explicit and keeps type-admitted IDs ending in `-` representable.
|
||||
The store's DNS subdomain name can exceed 63 characters; the existing adapter's
|
||||
63-character target Secret reference bound is preserved (client ID at most 48).
|
||||
No type parameter is added or changed.
|
||||
|
||||
The provider is `http://openbao.openbao.svc:8200`, KV v2 mount `platform`,
|
||||
Kubernetes auth mount `kubernetes`, role `external-secrets-keycape-{client_id}`,
|
||||
serviceAccountRef `external-secrets/external-secrets`, audience `openbao`.
|
||||
CCR-2026-0041 already records that Forgejo role/identity/audience, not this new
|
||||
store name or an unrestricted client family grant. Other clients still require
|
||||
their own existing owner-controlled role/case admission.
|
||||
|
||||
The ExternalSecret is `sso/keycape-{client_id}-client`, references precisely that
|
||||
ClusterSecretStore, refreshes every 5m and targets the same name with Owner
|
||||
creation and Retain remote deletion. Its only data entry maps `client-secret`
|
||||
from remoteRef `workloads/keycape/{client_id}`, property `CLIENT_SECRET`. The
|
||||
provider mount plus mount-relative remoteRef equals custody reference
|
||||
`platform/workloads/keycape/{client_id}`; do not double-prefix `platform/`.
|
||||
This is distinct from the Apps-owned consumer namespace SecretStore
|
||||
`openbao-keycape-{client_id}-consumer` and its dedicated consumer identity.
|
||||
|
||||
Store provisioning belongs to Platform's separate owner-controlled path. The
|
||||
proposed `platform-secret-delivery` lane retains ONLY create/get/watch/delete on
|
||||
the exact ExternalSecret in `sso`; this source does not delegate store creation
|
||||
or Secret read/list. Never apply the multi-document template through that lane.
|
||||
The pure `scripts/keycape_issuer_eso.py` module exposes only the ExternalSecret
|
||||
document through `external_secret()` and validates fixed public observations
|
||||
through `require_fixed_store()` and `require_ready_delivery()`. It performs no
|
||||
API call and issues no evidence/receipt. It is not a real capability or admission
|
||||
boundary; a caller-supplied observation alone establishes no authenticity.
|
||||
|
||||
The existing RPF-1.3-T4 delivery binding must validate the case/client binding,
|
||||
verify this exact preexisting store's public identity/spec/Ready state, apply or
|
||||
compare the exact ExternalSecret, and wait for matching spec plus Ready=True.
|
||||
The proposed helper accepts extra controller metadata but deliberately refuses
|
||||
spec drift. Actual API defaulting/normalization and installed ESO schema support
|
||||
must be checked in that implementation; these fixtures establish no native
|
||||
compatibility or credential isolation. ESO's shared controller remains trusted.
|
||||
No Secret bodies, values, arbitrary remoteRefs, or raw errors may enter receipts.
|
||||
|
||||
Existing owner requirements remain: closed `netkingdom.deliver-secret.v1`
|
||||
metadata receipt with authenticated before/after UID/resourceVersion provenance,
|
||||
unchanged objects reported NO_CHANGE, attempt-session revocation, and created-only
|
||||
rollback guarded by exact UID/resourceVersion (drift or uncertain ownership goes
|
||||
to owner reconciliation). The helper neither deletes a target Secret nor creates
|
||||
an OpenBao role/policy nor changes CCR-0041. These capabilities and catalog routing
|
||||
remain unimplemented/unaccepted under RPF-1.3-T4. Independent Platform source
|
||||
review of this declaration precedes adopting the conditional NetKingdom type
|
||||
text; no descriptor acceptance, acknowledgment, pin, grant, catalog or native
|
||||
permission is inferred.
|
||||
|
||||
Grounding: CCR-2026-0041 exact role/identity; the approved historical
|
||||
`docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml` issuer
|
||||
ClusterSecretStore and target pattern; `scripts/catalog_lane_scaffold.py`
|
||||
ClusterSecretStore/namespace restriction and mount-relative remoteRef model;
|
||||
RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical
|
||||
audience field placement differs from the issuer draft; this candidate uses
|
||||
the issuer draft's serviceAccountRef.audiences, not a claim of installed support.
|
||||
|
||||
## Prepared delivery mechanics (still unavailable as a production lane)
|
||||
|
||||
`scripts/keycape_issuer_delivery.py` now contains concrete command mechanics for
|
||||
the existing RPF-1.3-T4 obligation. It reuses `keycape_issuer_eso.documents()` and
|
||||
`require_fixed_store()` and the existing contained `state_hub_preflight_lane.command()`
|
||||
transport. Only exact named public ESO resources are read. It proves absence
|
||||
with successful empty `get --ignore-not-found`, creates only the ExternalSecret,
|
||||
compares an existing object's full spec, and polls at most ten times for Ready
|
||||
with matching spec and stable UID. Each request has a 5s kubectl request timeout
|
||||
inside the existing 60s process bound; nine sleeps of 1s bound each poll loop.
|
||||
Controller resourceVersion updates during successful readiness are captured.
|
||||
|
||||
Removal is permitted only for that instance's verified case-created revision,
|
||||
rechecks spec/UID/resourceVersion, uses Kubernetes DeleteOptions with both UID
|
||||
and resourceVersion preconditions, and confirms absence with bounded polling.
|
||||
Existing matching objects return changed=false and are never rollback candidates.
|
||||
Creation response loss, readiness failure after creation, replacement, delete
|
||||
conflict or unconfirmed deletion raises a fixed uncertainty diagnostic and keeps
|
||||
the last verified revision when known. No retry adopts an uncertain create; no
|
||||
replacement is deleted. An unknown create outcome requires owner reconciliation
|
||||
using the fixed resource name even when no UID could be recovered. Target Secret
|
||||
garbage collection by ESO/Kubernetes is outside this helper; it never reads or
|
||||
directly deletes Secret objects. There is no claim that deletion proves target
|
||||
Secret disappearance.
|
||||
|
||||
The callable `production_bindings()` **always refuses before any command**.
|
||||
The private mechanics use empty environment and `/dev/null` kubeconfig and
|
||||
therefore cannot borrow the caller's native session. They are source preparation,
|
||||
not an adapter/evidence authority. Tests intercept the actual subprocess call;
|
||||
they do not provide production callbacks or attestations. A real lane requires
|
||||
an owner-reviewed authenticated human aal2 session, acknowledged case revision
|
||||
and caller binding, scoped Kubernetes credentials, one-shot durable admission
|
||||
journaling and authenticated evidence publication/verification, plus real
|
||||
per-attempt session revocation and recovery. None exists for this lane at the
|
||||
pinned source. The generic task_execution_journal module is real storage but
|
||||
explicitly does not admit native work; it is not silently repurposed here.
|
||||
The mint broker's case checker and OpenBao lookup-self are real for its existing
|
||||
mint lane, not delivery authentication. The attended custody-projection SSH
|
||||
transport and credential-change evidence writer likewise confer no delivery
|
||||
lane authority. No unsigned receipt or `True` oracle replaces these facilities.
|
||||
|
||||
Consequently RPF-1.3-T4 remains open for real capability binding and owner review;
|
||||
this patch completes only concrete ESO command/state mechanics and regressions.
|
||||
No ESO role, policy, Store, admission service, grant, catalog acceptance, native
|
||||
permission or owner descriptor acceptance is created or inferred.
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
"""RPF-1.3-T4 concrete source-preparation mechanics; NO production admission.
|
||||
|
||||
Only public ESO objects are fetched. The production factory refuses before
|
||||
dispatch: no authenticated delivery session/evidence service currently exists.
|
||||
Private mechanics are exercised by fake-command tests, never a grant boundary.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from keycape_issuer_eso import documents, require_fixed_store, Refused
|
||||
from state_hub_preflight_lane import command, LaneError
|
||||
|
||||
POLL_COUNT = 10
|
||||
POLL_DELAY = 1
|
||||
API = '/apis/external-secrets.io/v1'
|
||||
|
||||
|
||||
class Uncertain(Refused):
|
||||
"""No success receipt: retain exact metadata for owner reconciliation."""
|
||||
def __init__(self, reason, revision=None):
|
||||
super().__init__(reason)
|
||||
self.revision = deepcopy(revision)
|
||||
|
||||
|
||||
def production_bindings(*args, **kwargs):
|
||||
# Neither Warden invocation fields, an operator's kubeconfig, an unsigned
|
||||
# local receipt nor mint-lane lookup-self authenticates THIS lane/caller.
|
||||
raise Refused('delivery_admission_session_journal_and_evidence_unavailable')
|
||||
|
||||
|
||||
def _revision(obj):
|
||||
meta = obj.get('metadata', {})
|
||||
result = {'uid': meta.get('uid'), 'resource_version': meta.get('resourceVersion')}
|
||||
if any(not isinstance(x, str) or not re.fullmatch(r'[A-Za-z0-9._:-]{1,192}', x)
|
||||
for x in result.values()):
|
||||
raise Refused('public_revision_invalid')
|
||||
return result
|
||||
|
||||
|
||||
class _DeliveryMechanics:
|
||||
"""Concrete fixed kubectl algorithm, not an authenticated owner capability.
|
||||
|
||||
No caller resource, remoteRef, namespace, store or path argument is accepted.
|
||||
The deliberately unauthenticated transport cannot load ambient credentials.
|
||||
A reviewed session transport must replace it only after real lane admission,
|
||||
one-shot durable journaling, evidence and revocation are implemented.
|
||||
"""
|
||||
def __init__(self, client_id):
|
||||
self.client_id = client_id
|
||||
self.store, self.external = documents(client_id)
|
||||
self.created = None
|
||||
self.pending = False
|
||||
|
||||
def _call(self, args, payload=None):
|
||||
try:
|
||||
result = command(['kubectl', '--kubeconfig=/dev/null',
|
||||
'--request-timeout=5s', *args],
|
||||
payload=payload, env={})
|
||||
if len(result.stdout) > 262144:
|
||||
raise Refused('public_response_too_large')
|
||||
if not result.stdout.strip():
|
||||
return None
|
||||
value = json.loads(result.stdout)
|
||||
if not isinstance(value, dict):
|
||||
raise Refused('public_response_shape')
|
||||
return value
|
||||
except (LaneError, OSError, ValueError, TimeoutError):
|
||||
raise Refused('public_command_failed') from None
|
||||
except Exception:
|
||||
# Includes subprocess timeouts; never expose stdout/stderr or argv.
|
||||
raise Refused('public_command_failed') from None
|
||||
|
||||
def _get(self, store=False):
|
||||
obj = self.store if store else self.external
|
||||
resource = 'clustersecretstores.external-secrets.io' if store else 'externalsecrets.external-secrets.io'
|
||||
args = [] if store else ['-n', 'sso']
|
||||
return self._call([*args, 'get', resource, obj['metadata']['name'],
|
||||
'--ignore-not-found=true', '-o', 'json'])
|
||||
|
||||
def _match(self, obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise Refused('delivery_missing')
|
||||
metadata = obj.get('metadata', {})
|
||||
if (obj.get('apiVersion') != self.external['apiVersion']
|
||||
or obj.get('kind') != 'ExternalSecret'
|
||||
or metadata.get('name') != self.external['metadata']['name']
|
||||
or metadata.get('namespace') != 'sso'
|
||||
or metadata.get('deletionTimestamp')
|
||||
or obj.get('spec') != self.external['spec']):
|
||||
raise Refused('delivery_conflict')
|
||||
return _revision(obj)
|
||||
|
||||
@staticmethod
|
||||
def _ready(obj):
|
||||
status = obj.get('status', {})
|
||||
conditions = status.get('conditions', []) if isinstance(status, dict) else []
|
||||
return isinstance(conditions, list) and any(
|
||||
isinstance(c, dict) and c.get('type') == 'Ready' and c.get('status') == 'True'
|
||||
for c in conditions)
|
||||
|
||||
def _receipt(self, operation, before, after):
|
||||
return {'schema': 'netkingdom.deliver-secret.v1', 'operation': operation,
|
||||
'namespace': 'sso', 'name': self.external['metadata']['name'],
|
||||
'custody_reference': 'platform/workloads/keycape/' + self.client_id,
|
||||
'key_names': ['client-secret'], 'values_emitted': False,
|
||||
'changed': before is None if operation == 'deliver' else True,
|
||||
'before': before, 'after': after}
|
||||
|
||||
def deliver(self):
|
||||
if self.pending or self.created is not None:
|
||||
raise Refused('attempt_already_used_or_unreconciled')
|
||||
# A successful empty response proves absence; errors never do.
|
||||
store = self._get(store=True)
|
||||
require_fixed_store(store, self.client_id)
|
||||
if store.get('metadata', {}).get('deletionTimestamp'):
|
||||
raise Refused('store_terminating')
|
||||
old = self._get()
|
||||
before = self._match(old) if old is not None else None
|
||||
uid = before['uid'] if before else None
|
||||
if old is None:
|
||||
# Set before dispatch: a lost create response MUST NOT be retried,
|
||||
# adopted, or claimed as a pre-existing no-change result.
|
||||
self.pending = True
|
||||
try:
|
||||
created = self._call(['-n', 'sso', 'create', '-f', '-', '-o', 'json'], self.external)
|
||||
self.created = self._match(created)
|
||||
uid = self.created['uid']
|
||||
except Refused:
|
||||
raise Uncertain('create_outcome_unverified') from None
|
||||
try:
|
||||
for index in range(POLL_COUNT):
|
||||
current = self._get()
|
||||
after = self._match(current)
|
||||
if after['uid'] != uid:
|
||||
raise Refused('delivery_replaced')
|
||||
if self._ready(current):
|
||||
if self.created is not None:
|
||||
self.created = after
|
||||
self.pending = False
|
||||
return self._receipt('deliver', before, after)
|
||||
if index + 1 < POLL_COUNT:
|
||||
time.sleep(POLL_DELAY)
|
||||
raise Refused('delivery_ready_timeout')
|
||||
except Refused:
|
||||
if self.created is not None:
|
||||
raise Uncertain('created_delivery_requires_reconciliation', self.created) from None
|
||||
raise
|
||||
|
||||
def remove(self, revision):
|
||||
# An arbitrary adapter argument alone is never deletion provenance.
|
||||
if self.pending or self.created is None or revision != self.created:
|
||||
raise Refused('no_verified_case_creation')
|
||||
try:
|
||||
if self._match(self._get()) != self.created:
|
||||
raise Refused('created_revision_changed')
|
||||
except Refused:
|
||||
raise Uncertain('created_revision_changed', self.created) from None
|
||||
uri = API + '/namespaces/sso/externalsecrets/' + self.external['metadata']['name']
|
||||
options = {'apiVersion': 'v1', 'kind': 'DeleteOptions',
|
||||
'preconditions': {'uid': self.created['uid'],
|
||||
'resourceVersion': self.created['resource_version']}}
|
||||
prior = deepcopy(self.created)
|
||||
self.pending = True
|
||||
try:
|
||||
self._call(['delete', '--raw', uri, '-f', '-'], options)
|
||||
for index in range(POLL_COUNT):
|
||||
current = self._get()
|
||||
if current is None:
|
||||
self.created = None
|
||||
self.pending = False
|
||||
return self._receipt('remove', prior, None)
|
||||
if _revision(current)['uid'] != prior['uid']:
|
||||
raise Refused('replacement_preserved')
|
||||
if index + 1 < POLL_COUNT:
|
||||
time.sleep(POLL_DELAY)
|
||||
raise Refused('delete_unconfirmed')
|
||||
except Refused:
|
||||
raise Uncertain('cleanup_requires_reconciliation', prior) from None
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
"""Offline command-level regressions. No HOME/env/network/credential access."""
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
import keycape_issuer_delivery as lane
|
||||
|
||||
|
||||
class DeliveryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.environment = patch.dict(os.environ, {}, clear=True)
|
||||
self.environment.start()
|
||||
self.addCleanup(self.environment.stop)
|
||||
self.network = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
self.network.start()
|
||||
self.addCleanup(self.network.stop)
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
self.store = self.resource(self.m.store)
|
||||
self.obj = self.resource(self.m.external)
|
||||
self.calls = []
|
||||
self.responses = []
|
||||
self.native = patch('subprocess.run', side_effect=self.run_command)
|
||||
self.native.start()
|
||||
self.addCleanup(self.native.stop)
|
||||
self.sleep = patch.object(lane.time, 'sleep')
|
||||
self.sleep.start()
|
||||
self.addCleanup(self.sleep.stop)
|
||||
|
||||
def resource(self, template, uid='uid-1', rv='10', ready=True):
|
||||
obj = deepcopy(template)
|
||||
obj['metadata'].update(uid=uid, resourceVersion=rv)
|
||||
obj['status'] = {'conditions': [{'type': 'Ready', 'status': str(ready)}]}
|
||||
return obj
|
||||
|
||||
def run_command(self, argv, **kw):
|
||||
self.calls.append((deepcopy(argv), deepcopy(kw)))
|
||||
self.assertEqual(kw['env'], {})
|
||||
self.assertEqual(argv[:3], ['kubectl', '--kubeconfig=/dev/null', '--request-timeout=5s'])
|
||||
self.assertTrue(kw['capture_output'])
|
||||
self.assertEqual(kw['timeout'], 60)
|
||||
if not self.responses:
|
||||
self.fail('unexpected command dispatch')
|
||||
result = self.responses.pop(0)
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
if result == 'error':
|
||||
return subprocess.CompletedProcess(argv, 1, b'PRIVATE PROVIDER VALUE', b'PRIVATE ERROR')
|
||||
return subprocess.CompletedProcess(argv, 0, b'' if result is None else json.dumps(result).encode(), b'')
|
||||
|
||||
def create(self):
|
||||
self.responses += [self.store, None, self.obj, self.obj]
|
||||
return self.m.deliver()
|
||||
|
||||
def test_production_factory_fails_before_dispatch(self):
|
||||
with self.assertRaisesRegex(lane.Refused, 'unavailable'):
|
||||
lane.production_bindings(case_source=b'arbitrary', admitted=object())
|
||||
self.assertEqual(self.calls, [])
|
||||
|
||||
def test_absent_create_ready_and_uid_rv_precondition_delete(self):
|
||||
receipt = self.create()
|
||||
self.assertTrue(receipt['changed'])
|
||||
self.assertIsNone(receipt['before'])
|
||||
self.assertEqual(receipt['after'], {'uid': 'uid-1', 'resource_version': '10'})
|
||||
self.responses += [self.obj, {'kind': 'Status'}, None]
|
||||
removed = self.m.remove(receipt['after'])
|
||||
self.assertEqual(removed['operation'], 'remove')
|
||||
delete = next(c for c in self.calls if 'delete' in c[0])
|
||||
options = json.loads(delete[1]['input'])
|
||||
self.assertEqual(options['preconditions'], {'uid': 'uid-1', 'resourceVersion': '10'})
|
||||
self.assertEqual(delete[0][5], '/apis/external-secrets.io/v1/namespaces/sso/externalsecrets/keycape-forgejo-client')
|
||||
self.assertFalse(any('secret' == a or 'secrets' == a for c in self.calls for a in c[0]))
|
||||
|
||||
def test_matching_existing_no_change_never_delete(self):
|
||||
self.responses = [self.store, self.obj, self.obj]
|
||||
receipt = self.m.deliver()
|
||||
self.assertFalse(receipt['changed'])
|
||||
with self.assertRaises(lane.Refused):
|
||||
self.m.remove(receipt['after'])
|
||||
self.assertFalse(any('create' in a or 'delete' in a for a, k in self.calls))
|
||||
|
||||
def test_conflicting_existing_untouched(self):
|
||||
drift = deepcopy(self.obj)
|
||||
drift['spec']['data'][0]['remoteRef']['key'] = 'elsewhere'
|
||||
self.responses = [self.store, drift]
|
||||
with self.assertRaises(lane.Refused):
|
||||
self.m.deliver()
|
||||
self.assertEqual(len(self.calls), 2)
|
||||
|
||||
def test_missing_drifted_unready_store_never_create(self):
|
||||
for kind in ('missing', 'spec', 'ready', 'namespace', 'terminating'):
|
||||
with self.subTest(kind=kind):
|
||||
store = deepcopy(self.store)
|
||||
if kind == 'missing': store = None
|
||||
if kind == 'spec': store['spec']['provider']['vault']['path'] = 'wrong'
|
||||
if kind == 'ready': store['status']['conditions'] = []
|
||||
if kind == 'namespace': store['metadata']['namespace'] = 'sso'
|
||||
if kind == 'terminating': store['metadata']['deletionTimestamp'] = 'now'
|
||||
self.responses = [store]
|
||||
with self.assertRaises(lane.Refused):
|
||||
lane._DeliveryMechanics('forgejo').deliver()
|
||||
self.assertFalse(any('create' in a for a,k in self.calls))
|
||||
|
||||
def test_get_error_not_absence_and_output_contained(self):
|
||||
self.responses = [self.store, 'error']
|
||||
with self.assertRaisesRegex(lane.Refused, '^public_command_failed$'):
|
||||
self.m.deliver()
|
||||
self.assertEqual(len(self.calls), 2)
|
||||
|
||||
def test_create_lost_response_or_race_uncertain_no_retry(self):
|
||||
self.responses = [self.store, None, 'error']
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
self.assertEqual(len(self.calls), 3)
|
||||
|
||||
def test_ready_poll_is_bounded_and_created_provenance_retained(self):
|
||||
obj = deepcopy(self.obj)
|
||||
obj['status']['conditions'] = []
|
||||
self.responses = [self.store, None, obj] + [obj] * lane.POLL_COUNT
|
||||
with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
self.assertEqual(error.exception.revision['uid'], 'uid-1')
|
||||
self.assertEqual(len(self.calls), lane.POLL_COUNT + 3)
|
||||
with self.assertRaises(lane.Refused): self.m.remove(error.exception.revision)
|
||||
|
||||
def test_readiness_updates_resource_version(self):
|
||||
fresh = self.resource(self.m.external, rv='11')
|
||||
self.responses = [self.store, None, self.obj, fresh]
|
||||
receipt = self.m.deliver()
|
||||
self.assertEqual(receipt['after']['resource_version'], '11')
|
||||
|
||||
def test_replacement_during_ready_not_adopted(self):
|
||||
other = self.resource(self.m.external, uid='other')
|
||||
self.responses = [self.store, None, self.obj, other]
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
|
||||
def test_drift_and_replacement_never_deleted(self):
|
||||
for field in ('uid', 'resourceVersion'):
|
||||
with self.subTest(field=field):
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
receipt = self.create()
|
||||
changed = deepcopy(self.obj)
|
||||
changed['metadata'][field] = 'different'
|
||||
self.responses = [changed]
|
||||
count = len(self.calls)
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
self.assertEqual(len(self.calls), count + 1)
|
||||
|
||||
def test_delete_response_loss_and_conflict_uncertain(self):
|
||||
receipt = self.create()
|
||||
self.responses = [self.obj, 'error']
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
with self.assertRaises(lane.Refused): self.m.remove(receipt['after'])
|
||||
|
||||
def test_delete_unconfirmed_or_replaced_never_repeated(self):
|
||||
for replacement in (False, True):
|
||||
with self.subTest(replacement=replacement):
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
receipt = self.create()
|
||||
tail = [self.resource(self.m.external, uid='new')] if replacement else [self.obj] * lane.POLL_COUNT
|
||||
self.responses = [self.obj, {'kind': 'Status'}] + tail
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
|
||||
def test_no_caller_paths_or_injection(self):
|
||||
for client in ('a/../../secret', 'x;echo', 'a' * 49, 'A', None):
|
||||
with self.subTest(client=client):
|
||||
with self.assertRaises(lane.Refused): lane._DeliveryMechanics(client)
|
||||
self.assertEqual(self.calls, [])
|
||||
|
||||
def test_forged_revision_and_fresh_process_have_no_delete_provenance(self):
|
||||
receipt = self.create()
|
||||
fresh = lane._DeliveryMechanics('forgejo')
|
||||
with self.assertRaises(lane.Refused): fresh.remove(receipt['after'])
|
||||
with self.assertRaises(lane.Refused): self.m.remove({'uid': 'uid-1', 'resource_version': '11'})
|
||||
self.assertFalse(any('delete' in a for a,k in self.calls))
|
||||
|
||||
def test_malformed_create_or_poll_never_returns_success(self):
|
||||
for malformed in ({}, {'kind': 'Status'}):
|
||||
with self.subTest(malformed=malformed):
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
self.responses = [self.store, None, malformed]
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
|
||||
def test_poll_timeout_contains_provider_details(self):
|
||||
self.responses = [self.store, None, self.obj,
|
||||
subprocess.TimeoutExpired('PRIVATE PROVIDER', 60, output='PRIVATE VALUE')]
|
||||
with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
self.assertNotIn('PRIVATE', str(error.exception))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
--- /dev/null
|
||||
+++ b/scripts/keycape_issuer_delivery.py
|
||||
@@ -0,0 +1,182 @@
|
||||
+"""RPF-1.3-T4 concrete source-preparation mechanics; NO production admission.
|
||||
+
|
||||
+Only public ESO objects are fetched. The production factory refuses before
|
||||
+dispatch: no authenticated delivery session/evidence service currently exists.
|
||||
+Private mechanics are exercised by fake-command tests, never a grant boundary.
|
||||
+"""
|
||||
+from __future__ import annotations
|
||||
+
|
||||
+from copy import deepcopy
|
||||
+import json
|
||||
+import re
|
||||
+import time
|
||||
+
|
||||
+from keycape_issuer_eso import documents, require_fixed_store, Refused
|
||||
+from state_hub_preflight_lane import command, LaneError
|
||||
+
|
||||
+POLL_COUNT = 10
|
||||
+POLL_DELAY = 1
|
||||
+API = '/apis/external-secrets.io/v1'
|
||||
+
|
||||
+
|
||||
+class Uncertain(Refused):
|
||||
+ """No success receipt: retain exact metadata for owner reconciliation."""
|
||||
+ def __init__(self, reason, revision=None):
|
||||
+ super().__init__(reason)
|
||||
+ self.revision = deepcopy(revision)
|
||||
+
|
||||
+
|
||||
+def production_bindings(*args, **kwargs):
|
||||
+ # Neither Warden invocation fields, an operator's kubeconfig, an unsigned
|
||||
+ # local receipt nor mint-lane lookup-self authenticates THIS lane/caller.
|
||||
+ raise Refused('delivery_admission_session_journal_and_evidence_unavailable')
|
||||
+
|
||||
+
|
||||
+def _revision(obj):
|
||||
+ meta = obj.get('metadata', {})
|
||||
+ result = {'uid': meta.get('uid'), 'resource_version': meta.get('resourceVersion')}
|
||||
+ if any(not isinstance(x, str) or not re.fullmatch(r'[A-Za-z0-9._:-]{1,192}', x)
|
||||
+ for x in result.values()):
|
||||
+ raise Refused('public_revision_invalid')
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
+class _DeliveryMechanics:
|
||||
+ """Concrete fixed kubectl algorithm, not an authenticated owner capability.
|
||||
+
|
||||
+ No caller resource, remoteRef, namespace, store or path argument is accepted.
|
||||
+ The deliberately unauthenticated transport cannot load ambient credentials.
|
||||
+ A reviewed session transport must replace it only after real lane admission,
|
||||
+ one-shot durable journaling, evidence and revocation are implemented.
|
||||
+ """
|
||||
+ def __init__(self, client_id):
|
||||
+ self.client_id = client_id
|
||||
+ self.store, self.external = documents(client_id)
|
||||
+ self.created = None
|
||||
+ self.pending = False
|
||||
+
|
||||
+ def _call(self, args, payload=None):
|
||||
+ try:
|
||||
+ result = command(['kubectl', '--kubeconfig=/dev/null',
|
||||
+ '--request-timeout=5s', *args],
|
||||
+ payload=payload, env={})
|
||||
+ if len(result.stdout) > 262144:
|
||||
+ raise Refused('public_response_too_large')
|
||||
+ if not result.stdout.strip():
|
||||
+ return None
|
||||
+ value = json.loads(result.stdout)
|
||||
+ if not isinstance(value, dict):
|
||||
+ raise Refused('public_response_shape')
|
||||
+ return value
|
||||
+ except (LaneError, OSError, ValueError, TimeoutError):
|
||||
+ raise Refused('public_command_failed') from None
|
||||
+ except Exception:
|
||||
+ # Includes subprocess timeouts; never expose stdout/stderr or argv.
|
||||
+ raise Refused('public_command_failed') from None
|
||||
+
|
||||
+ def _get(self, store=False):
|
||||
+ obj = self.store if store else self.external
|
||||
+ resource = 'clustersecretstores.external-secrets.io' if store else 'externalsecrets.external-secrets.io'
|
||||
+ args = [] if store else ['-n', 'sso']
|
||||
+ return self._call([*args, 'get', resource, obj['metadata']['name'],
|
||||
+ '--ignore-not-found=true', '-o', 'json'])
|
||||
+
|
||||
+ def _match(self, obj):
|
||||
+ if not isinstance(obj, dict):
|
||||
+ raise Refused('delivery_missing')
|
||||
+ metadata = obj.get('metadata', {})
|
||||
+ if (obj.get('apiVersion') != self.external['apiVersion']
|
||||
+ or obj.get('kind') != 'ExternalSecret'
|
||||
+ or metadata.get('name') != self.external['metadata']['name']
|
||||
+ or metadata.get('namespace') != 'sso'
|
||||
+ or metadata.get('deletionTimestamp')
|
||||
+ or obj.get('spec') != self.external['spec']):
|
||||
+ raise Refused('delivery_conflict')
|
||||
+ return _revision(obj)
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def _ready(obj):
|
||||
+ status = obj.get('status', {})
|
||||
+ conditions = status.get('conditions', []) if isinstance(status, dict) else []
|
||||
+ return isinstance(conditions, list) and any(
|
||||
+ isinstance(c, dict) and c.get('type') == 'Ready' and c.get('status') == 'True'
|
||||
+ for c in conditions)
|
||||
+
|
||||
+ def _receipt(self, operation, before, after):
|
||||
+ return {'schema': 'netkingdom.deliver-secret.v1', 'operation': operation,
|
||||
+ 'namespace': 'sso', 'name': self.external['metadata']['name'],
|
||||
+ 'custody_reference': 'platform/workloads/keycape/' + self.client_id,
|
||||
+ 'key_names': ['client-secret'], 'values_emitted': False,
|
||||
+ 'changed': before is None if operation == 'deliver' else True,
|
||||
+ 'before': before, 'after': after}
|
||||
+
|
||||
+ def deliver(self):
|
||||
+ if self.pending or self.created is not None:
|
||||
+ raise Refused('attempt_already_used_or_unreconciled')
|
||||
+ # A successful empty response proves absence; errors never do.
|
||||
+ store = self._get(store=True)
|
||||
+ require_fixed_store(store, self.client_id)
|
||||
+ if store.get('metadata', {}).get('deletionTimestamp'):
|
||||
+ raise Refused('store_terminating')
|
||||
+ old = self._get()
|
||||
+ before = self._match(old) if old is not None else None
|
||||
+ uid = before['uid'] if before else None
|
||||
+ if old is None:
|
||||
+ # Set before dispatch: a lost create response MUST NOT be retried,
|
||||
+ # adopted, or claimed as a pre-existing no-change result.
|
||||
+ self.pending = True
|
||||
+ try:
|
||||
+ created = self._call(['-n', 'sso', 'create', '-f', '-', '-o', 'json'], self.external)
|
||||
+ self.created = self._match(created)
|
||||
+ uid = self.created['uid']
|
||||
+ except Refused:
|
||||
+ raise Uncertain('create_outcome_unverified') from None
|
||||
+ try:
|
||||
+ for index in range(POLL_COUNT):
|
||||
+ current = self._get()
|
||||
+ after = self._match(current)
|
||||
+ if after['uid'] != uid:
|
||||
+ raise Refused('delivery_replaced')
|
||||
+ if self._ready(current):
|
||||
+ if self.created is not None:
|
||||
+ self.created = after
|
||||
+ self.pending = False
|
||||
+ return self._receipt('deliver', before, after)
|
||||
+ if index + 1 < POLL_COUNT:
|
||||
+ time.sleep(POLL_DELAY)
|
||||
+ raise Refused('delivery_ready_timeout')
|
||||
+ except Refused:
|
||||
+ if self.created is not None:
|
||||
+ raise Uncertain('created_delivery_requires_reconciliation', self.created) from None
|
||||
+ raise
|
||||
+
|
||||
+ def remove(self, revision):
|
||||
+ # An arbitrary adapter argument alone is never deletion provenance.
|
||||
+ if self.pending or self.created is None or revision != self.created:
|
||||
+ raise Refused('no_verified_case_creation')
|
||||
+ try:
|
||||
+ if self._match(self._get()) != self.created:
|
||||
+ raise Refused('created_revision_changed')
|
||||
+ except Refused:
|
||||
+ raise Uncertain('created_revision_changed', self.created) from None
|
||||
+ uri = API + '/namespaces/sso/externalsecrets/' + self.external['metadata']['name']
|
||||
+ options = {'apiVersion': 'v1', 'kind': 'DeleteOptions',
|
||||
+ 'preconditions': {'uid': self.created['uid'],
|
||||
+ 'resourceVersion': self.created['resource_version']}}
|
||||
+ prior = deepcopy(self.created)
|
||||
+ self.pending = True
|
||||
+ try:
|
||||
+ self._call(['delete', '--raw', uri, '-f', '-'], options)
|
||||
+ for index in range(POLL_COUNT):
|
||||
+ current = self._get()
|
||||
+ if current is None:
|
||||
+ self.created = None
|
||||
+ self.pending = False
|
||||
+ return self._receipt('remove', prior, None)
|
||||
+ if _revision(current)['uid'] != prior['uid']:
|
||||
+ raise Refused('replacement_preserved')
|
||||
+ if index + 1 < POLL_COUNT:
|
||||
+ time.sleep(POLL_DELAY)
|
||||
+ raise Refused('delete_unconfirmed')
|
||||
+ except Refused:
|
||||
+ raise Uncertain('cleanup_requires_reconciliation', prior) from None
|
||||
--- /dev/null
|
||||
+++ b/tests/test_keycape_issuer_delivery.py
|
||||
@@ -0,0 +1,196 @@
|
||||
+"""Offline command-level regressions. No HOME/env/network/credential access."""
|
||||
+from copy import deepcopy
|
||||
+import json
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import subprocess
|
||||
+import sys
|
||||
+import unittest
|
||||
+from unittest.mock import patch
|
||||
+
|
||||
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
+import keycape_issuer_delivery as lane
|
||||
+
|
||||
+
|
||||
+class DeliveryTests(unittest.TestCase):
|
||||
+ def setUp(self):
|
||||
+ self.environment = patch.dict(os.environ, {}, clear=True)
|
||||
+ self.environment.start()
|
||||
+ self.addCleanup(self.environment.stop)
|
||||
+ self.network = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
+ self.network.start()
|
||||
+ self.addCleanup(self.network.stop)
|
||||
+ self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ self.store = self.resource(self.m.store)
|
||||
+ self.obj = self.resource(self.m.external)
|
||||
+ self.calls = []
|
||||
+ self.responses = []
|
||||
+ self.native = patch('subprocess.run', side_effect=self.run_command)
|
||||
+ self.native.start()
|
||||
+ self.addCleanup(self.native.stop)
|
||||
+ self.sleep = patch.object(lane.time, 'sleep')
|
||||
+ self.sleep.start()
|
||||
+ self.addCleanup(self.sleep.stop)
|
||||
+
|
||||
+ def resource(self, template, uid='uid-1', rv='10', ready=True):
|
||||
+ obj = deepcopy(template)
|
||||
+ obj['metadata'].update(uid=uid, resourceVersion=rv)
|
||||
+ obj['status'] = {'conditions': [{'type': 'Ready', 'status': str(ready)}]}
|
||||
+ return obj
|
||||
+
|
||||
+ def run_command(self, argv, **kw):
|
||||
+ self.calls.append((deepcopy(argv), deepcopy(kw)))
|
||||
+ self.assertEqual(kw['env'], {})
|
||||
+ self.assertEqual(argv[:3], ['kubectl', '--kubeconfig=/dev/null', '--request-timeout=5s'])
|
||||
+ self.assertTrue(kw['capture_output'])
|
||||
+ self.assertEqual(kw['timeout'], 60)
|
||||
+ if not self.responses:
|
||||
+ self.fail('unexpected command dispatch')
|
||||
+ result = self.responses.pop(0)
|
||||
+ if isinstance(result, Exception):
|
||||
+ raise result
|
||||
+ if result == 'error':
|
||||
+ return subprocess.CompletedProcess(argv, 1, b'PRIVATE PROVIDER VALUE', b'PRIVATE ERROR')
|
||||
+ return subprocess.CompletedProcess(argv, 0, b'' if result is None else json.dumps(result).encode(), b'')
|
||||
+
|
||||
+ def create(self):
|
||||
+ self.responses += [self.store, None, self.obj, self.obj]
|
||||
+ return self.m.deliver()
|
||||
+
|
||||
+ def test_production_factory_fails_before_dispatch(self):
|
||||
+ with self.assertRaisesRegex(lane.Refused, 'unavailable'):
|
||||
+ lane.production_bindings(case_source=b'arbitrary', admitted=object())
|
||||
+ self.assertEqual(self.calls, [])
|
||||
+
|
||||
+ def test_absent_create_ready_and_uid_rv_precondition_delete(self):
|
||||
+ receipt = self.create()
|
||||
+ self.assertTrue(receipt['changed'])
|
||||
+ self.assertIsNone(receipt['before'])
|
||||
+ self.assertEqual(receipt['after'], {'uid': 'uid-1', 'resource_version': '10'})
|
||||
+ self.responses += [self.obj, {'kind': 'Status'}, None]
|
||||
+ removed = self.m.remove(receipt['after'])
|
||||
+ self.assertEqual(removed['operation'], 'remove')
|
||||
+ delete = next(c for c in self.calls if 'delete' in c[0])
|
||||
+ options = json.loads(delete[1]['input'])
|
||||
+ self.assertEqual(options['preconditions'], {'uid': 'uid-1', 'resourceVersion': '10'})
|
||||
+ self.assertEqual(delete[0][5], '/apis/external-secrets.io/v1/namespaces/sso/externalsecrets/keycape-forgejo-client')
|
||||
+ self.assertFalse(any('secret' == a or 'secrets' == a for c in self.calls for a in c[0]))
|
||||
+
|
||||
+ def test_matching_existing_no_change_never_delete(self):
|
||||
+ self.responses = [self.store, self.obj, self.obj]
|
||||
+ receipt = self.m.deliver()
|
||||
+ self.assertFalse(receipt['changed'])
|
||||
+ with self.assertRaises(lane.Refused):
|
||||
+ self.m.remove(receipt['after'])
|
||||
+ self.assertFalse(any('create' in a or 'delete' in a for a, k in self.calls))
|
||||
+
|
||||
+ def test_conflicting_existing_untouched(self):
|
||||
+ drift = deepcopy(self.obj)
|
||||
+ drift['spec']['data'][0]['remoteRef']['key'] = 'elsewhere'
|
||||
+ self.responses = [self.store, drift]
|
||||
+ with self.assertRaises(lane.Refused):
|
||||
+ self.m.deliver()
|
||||
+ self.assertEqual(len(self.calls), 2)
|
||||
+
|
||||
+ def test_missing_drifted_unready_store_never_create(self):
|
||||
+ for kind in ('missing', 'spec', 'ready', 'namespace', 'terminating'):
|
||||
+ with self.subTest(kind=kind):
|
||||
+ store = deepcopy(self.store)
|
||||
+ if kind == 'missing': store = None
|
||||
+ if kind == 'spec': store['spec']['provider']['vault']['path'] = 'wrong'
|
||||
+ if kind == 'ready': store['status']['conditions'] = []
|
||||
+ if kind == 'namespace': store['metadata']['namespace'] = 'sso'
|
||||
+ if kind == 'terminating': store['metadata']['deletionTimestamp'] = 'now'
|
||||
+ self.responses = [store]
|
||||
+ with self.assertRaises(lane.Refused):
|
||||
+ lane._DeliveryMechanics('forgejo').deliver()
|
||||
+ self.assertFalse(any('create' in a for a,k in self.calls))
|
||||
+
|
||||
+ def test_get_error_not_absence_and_output_contained(self):
|
||||
+ self.responses = [self.store, 'error']
|
||||
+ with self.assertRaisesRegex(lane.Refused, '^public_command_failed$'):
|
||||
+ self.m.deliver()
|
||||
+ self.assertEqual(len(self.calls), 2)
|
||||
+
|
||||
+ def test_create_lost_response_or_race_uncertain_no_retry(self):
|
||||
+ self.responses = [self.store, None, 'error']
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+ with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
+ self.assertEqual(len(self.calls), 3)
|
||||
+
|
||||
+ def test_ready_poll_is_bounded_and_created_provenance_retained(self):
|
||||
+ obj = deepcopy(self.obj)
|
||||
+ obj['status']['conditions'] = []
|
||||
+ self.responses = [self.store, None, obj] + [obj] * lane.POLL_COUNT
|
||||
+ with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
+ self.assertEqual(error.exception.revision['uid'], 'uid-1')
|
||||
+ self.assertEqual(len(self.calls), lane.POLL_COUNT + 3)
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(error.exception.revision)
|
||||
+
|
||||
+ def test_readiness_updates_resource_version(self):
|
||||
+ fresh = self.resource(self.m.external, rv='11')
|
||||
+ self.responses = [self.store, None, self.obj, fresh]
|
||||
+ receipt = self.m.deliver()
|
||||
+ self.assertEqual(receipt['after']['resource_version'], '11')
|
||||
+
|
||||
+ def test_replacement_during_ready_not_adopted(self):
|
||||
+ other = self.resource(self.m.external, uid='other')
|
||||
+ self.responses = [self.store, None, self.obj, other]
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+
|
||||
+ def test_drift_and_replacement_never_deleted(self):
|
||||
+ for field in ('uid', 'resourceVersion'):
|
||||
+ with self.subTest(field=field):
|
||||
+ self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ receipt = self.create()
|
||||
+ changed = deepcopy(self.obj)
|
||||
+ changed['metadata'][field] = 'different'
|
||||
+ self.responses = [changed]
|
||||
+ count = len(self.calls)
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
+ self.assertEqual(len(self.calls), count + 1)
|
||||
+
|
||||
+ def test_delete_response_loss_and_conflict_uncertain(self):
|
||||
+ receipt = self.create()
|
||||
+ self.responses = [self.obj, 'error']
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(receipt['after'])
|
||||
+
|
||||
+ def test_delete_unconfirmed_or_replaced_never_repeated(self):
|
||||
+ for replacement in (False, True):
|
||||
+ with self.subTest(replacement=replacement):
|
||||
+ self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ receipt = self.create()
|
||||
+ tail = [self.resource(self.m.external, uid='new')] if replacement else [self.obj] * lane.POLL_COUNT
|
||||
+ self.responses = [self.obj, {'kind': 'Status'}] + tail
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(receipt['after'])
|
||||
+
|
||||
+ def test_no_caller_paths_or_injection(self):
|
||||
+ for client in ('a/../../secret', 'x;echo', 'a' * 49, 'A', None):
|
||||
+ with self.subTest(client=client):
|
||||
+ with self.assertRaises(lane.Refused): lane._DeliveryMechanics(client)
|
||||
+ self.assertEqual(self.calls, [])
|
||||
+
|
||||
+ def test_forged_revision_and_fresh_process_have_no_delete_provenance(self):
|
||||
+ receipt = self.create()
|
||||
+ fresh = lane._DeliveryMechanics('forgejo')
|
||||
+ with self.assertRaises(lane.Refused): fresh.remove(receipt['after'])
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove({'uid': 'uid-1', 'resource_version': '11'})
|
||||
+ self.assertFalse(any('delete' in a for a,k in self.calls))
|
||||
+
|
||||
+ def test_malformed_create_or_poll_never_returns_success(self):
|
||||
+ for malformed in ({}, {'kind': 'Status'}):
|
||||
+ with self.subTest(malformed=malformed):
|
||||
+ self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ self.responses = [self.store, None, malformed]
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+
|
||||
+ def test_poll_timeout_contains_provider_details(self):
|
||||
+ self.responses = [self.store, None, self.obj,
|
||||
+ subprocess.TimeoutExpired('PRIVATE PROVIDER', 60, output='PRIVATE VALUE')]
|
||||
+ with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
+ self.assertNotIn('PRIVATE', str(error.exception))
|
||||
+
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ unittest.main()
|
||||
--- a/docs/keycape-issuer-eso-contract.md
|
||||
+++ b/docs/keycape-issuer-eso-contract.md
|
||||
@@ -66,3 +66,49 @@
|
||||
RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical
|
||||
audience field placement differs from the issuer draft; this candidate uses
|
||||
the issuer draft's serviceAccountRef.audiences, not a claim of installed support.
|
||||
+
|
||||
+## Prepared delivery mechanics (still unavailable as a production lane)
|
||||
+
|
||||
+`scripts/keycape_issuer_delivery.py` now contains concrete command mechanics for
|
||||
+the existing RPF-1.3-T4 obligation. It reuses `keycape_issuer_eso.documents()` and
|
||||
+`require_fixed_store()` and the existing contained `state_hub_preflight_lane.command()`
|
||||
+transport. Only exact named public ESO resources are read. It proves absence
|
||||
+with successful empty `get --ignore-not-found`, creates only the ExternalSecret,
|
||||
+compares an existing object's full spec, and polls at most ten times for Ready
|
||||
+with matching spec and stable UID. Each request has a 5s kubectl request timeout
|
||||
+inside the existing 60s process bound; nine sleeps of 1s bound each poll loop.
|
||||
+Controller resourceVersion updates during successful readiness are captured.
|
||||
+
|
||||
+Removal is permitted only for that instance's verified case-created revision,
|
||||
+rechecks spec/UID/resourceVersion, uses Kubernetes DeleteOptions with both UID
|
||||
+and resourceVersion preconditions, and confirms absence with bounded polling.
|
||||
+Existing matching objects return changed=false and are never rollback candidates.
|
||||
+Creation response loss, readiness failure after creation, replacement, delete
|
||||
+conflict or unconfirmed deletion raises a fixed uncertainty diagnostic and keeps
|
||||
+the last verified revision when known. No retry adopts an uncertain create; no
|
||||
+replacement is deleted. An unknown create outcome requires owner reconciliation
|
||||
+using the fixed resource name even when no UID could be recovered. Target Secret
|
||||
+garbage collection by ESO/Kubernetes is outside this helper; it never reads or
|
||||
+directly deletes Secret objects. There is no claim that deletion proves target
|
||||
+Secret disappearance.
|
||||
+
|
||||
+The callable `production_bindings()` **always refuses before any command**.
|
||||
+The private mechanics use empty environment and `/dev/null` kubeconfig and
|
||||
+therefore cannot borrow the caller's native session. They are source preparation,
|
||||
+not an adapter/evidence authority. Tests intercept the actual subprocess call;
|
||||
+they do not provide production callbacks or attestations. A real lane requires
|
||||
+an owner-reviewed authenticated human aal2 session, acknowledged case revision
|
||||
+and caller binding, scoped Kubernetes credentials, one-shot durable admission
|
||||
+journaling and authenticated evidence publication/verification, plus real
|
||||
+per-attempt session revocation and recovery. None exists for this lane at the
|
||||
+pinned source. The generic task_execution_journal module is real storage but
|
||||
+explicitly does not admit native work; it is not silently repurposed here.
|
||||
+The mint broker's case checker and OpenBao lookup-self are real for its existing
|
||||
+mint lane, not delivery authentication. The attended custody-projection SSH
|
||||
+transport and credential-change evidence writer likewise confer no delivery
|
||||
+lane authority. No unsigned receipt or `True` oracle replaces these facilities.
|
||||
+
|
||||
+Consequently RPF-1.3-T4 remains open for real capability binding and owner review;
|
||||
+this patch completes only concrete ESO command/state mechanics and regressions.
|
||||
+No ESO role, policy, Store, admission service, grant, catalog acceptance, native
|
||||
+permission or owner descriptor acceptance is created or inferred.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"schema": "platform.delivery-source-regressions.v1",
|
||||
"initial_regression": "First 14-test run had one incorrect test assertion: delete argv URI index 4 instead of 5. Corrected test; no backend behavior changed for that failure.",
|
||||
"final_runs": {
|
||||
"test_keycape_issuer_delivery.py": {
|
||||
"exit_code": 0,
|
||||
"output": "test_absent_create_ready_and_uid_rv_precondition_delete (test_keycape_issuer_delivery.DeliveryTests.test_absent_create_ready_and_uid_rv_precondition_delete) ... ok\ntest_conflicting_existing_untouched (test_keycape_issuer_delivery.DeliveryTests.test_conflicting_existing_untouched) ... ok\ntest_create_lost_response_or_race_uncertain_no_retry (test_keycape_issuer_delivery.DeliveryTests.test_create_lost_response_or_race_uncertain_no_retry) ... ok\ntest_delete_response_loss_and_conflict_uncertain (test_keycape_issuer_delivery.DeliveryTests.test_delete_response_loss_and_conflict_uncertain) ... ok\ntest_delete_unconfirmed_or_replaced_never_repeated (test_keycape_issuer_delivery.DeliveryTests.test_delete_unconfirmed_or_replaced_never_repeated) ... ok\ntest_drift_and_replacement_never_deleted (test_keycape_issuer_delivery.DeliveryTests.test_drift_and_replacement_never_deleted) ... ok\ntest_forged_revision_and_fresh_process_have_no_delete_provenance (test_keycape_issuer_delivery.DeliveryTests.test_forged_revision_and_fresh_process_have_no_delete_provenance) ... ok\ntest_get_error_not_absence_and_output_contained (test_keycape_issuer_delivery.DeliveryTests.test_get_error_not_absence_and_output_contained) ... ok\ntest_malformed_create_or_poll_never_returns_success (test_keycape_issuer_delivery.DeliveryTests.test_malformed_create_or_poll_never_returns_success) ... ok\ntest_matching_existing_no_change_never_delete (test_keycape_issuer_delivery.DeliveryTests.test_matching_existing_no_change_never_delete) ... ok\ntest_missing_drifted_unready_store_never_create (test_keycape_issuer_delivery.DeliveryTests.test_missing_drifted_unready_store_never_create) ... ok\ntest_no_caller_paths_or_injection (test_keycape_issuer_delivery.DeliveryTests.test_no_caller_paths_or_injection) ... ok\ntest_poll_timeout_contains_provider_details (test_keycape_issuer_delivery.DeliveryTests.test_poll_timeout_contains_provider_details) ... ok\ntest_production_factory_fails_before_dispatch (test_keycape_issuer_delivery.DeliveryTests.test_production_factory_fails_before_dispatch) ... ok\ntest_readiness_updates_resource_version (test_keycape_issuer_delivery.DeliveryTests.test_readiness_updates_resource_version) ... ok\ntest_ready_poll_is_bounded_and_created_provenance_retained (test_keycape_issuer_delivery.DeliveryTests.test_ready_poll_is_bounded_and_created_provenance_retained) ... ok\ntest_replacement_during_ready_not_adopted (test_keycape_issuer_delivery.DeliveryTests.test_replacement_during_ready_not_adopted) ... ok\n\n----------------------------------------------------------------------\nRan 17 tests in 0.186s\n\nOK\n"
|
||||
},
|
||||
"test_keycape_issuer_eso.py": {
|
||||
"exit_code": 0,
|
||||
"output": "test_delivery_document_is_only_external_and_detached (test_keycape_issuer_eso.FixedIssuerContract.test_delivery_document_is_only_external_and_detached) ... ok\ntest_external_drift_refuses (test_keycape_issuer_eso.FixedIssuerContract.test_external_drift_refuses) ... ok\ntest_independent_forgejo_binding (test_keycape_issuer_eso.FixedIssuerContract.test_independent_forgejo_binding) ... ok\ntest_missing_or_unready_refuses (test_keycape_issuer_eso.FixedIssuerContract.test_missing_or_unready_refuses) ... ok\ntest_no_caller_store_or_path_override (test_keycape_issuer_eso.FixedIssuerContract.test_no_caller_store_or_path_override) ... ok\ntest_store_drift_refuses (test_keycape_issuer_eso.FixedIssuerContract.test_store_drift_refuses) ... ok\ntest_type_admitted_trailing_dash_is_preserved (test_keycape_issuer_eso.FixedIssuerContract.test_type_admitted_trailing_dash_is_preserved) ... ok\ntest_valid_ready_metadata_allows_extra_controller_metadata (test_keycape_issuer_eso.FixedIssuerContract.test_valid_ready_metadata_allows_extra_controller_metadata) ... ok\n\n----------------------------------------------------------------------\nRan 8 tests in 0.316s\n\nOK\n"
|
||||
}
|
||||
},
|
||||
"scope": "offline only, subprocess mocked; environment cleared and socket.socket forbidden by delivery tests; no native credentials or network"
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"railiance-platform": {
|
||||
"commit": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9",
|
||||
"files": {
|
||||
"AGENTS.md": "sha256:8d2f1a165e3f4eecf2e6e1d6a19c4bac9e8dcb64a76650e7185d55722f233c8b",
|
||||
"INTENT.md": "sha256:12c6540735af591f7a4a9fe85ff10d3cd00eec55b5b6b1ddf148abf2d3ac9a9c",
|
||||
"scripts/keycape_issuer_eso.py": "sha256:21c5797280c2a7825dbda8ac970d34a0cdef4c6458f68bce0b1c12407f122a87",
|
||||
"manifests/templates/keycape-issuer-eso.yaml": "sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743",
|
||||
"scripts/state_hub_preflight_lane.py": "sha256:8bf63beda028de7c2375084afb7c22fb00a731c54ebf0b3ba369a699d920e0f5",
|
||||
"scripts/keycape_client_secret_generate.py": "sha256:9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d",
|
||||
"scripts/task_execution_journal.py": "sha256:604cf285286a8a1be23d9bc0d8a547bd1195b7f80286778e1d4f30c711fd6a7e",
|
||||
"scripts/custody-projection.py": "sha256:d5ca1017b4877a12c7fa24535e6b497492cd63e5e99c0674a8e9fc3ad93ead87",
|
||||
"scripts/remote_exec.py": "sha256:4d60c1bf3f6aa3a0c6867fadb137abf23699b4cf7ebeea317e4ce794bacc7968",
|
||||
"scripts/repair_eso_kubernetes_auth.py": "sha256:dd58e4ab3f88c777aacb44368c2c099b6e32637ce76f5157e7a874cfbe9d5f80",
|
||||
"scripts/credential-change.py": "sha256:54ca1bc33ed4bb7807917f71ed45b6b35da978bc789e013386e1e90a20bee06d"
|
||||
}
|
||||
},
|
||||
"net-kingdom": {
|
||||
"commit": "befb0bc68cd25b625804fce559476eefd702521d",
|
||||
"files": {
|
||||
"sso-mfa/k8s/keycape/deliver-secret-adapter.py": "sha256:cd1a515a1ae2def2093660ed2a810a9c66d1d4cf7126f573ee7e9c4c42afb98d"
|
||||
}
|
||||
},
|
||||
"ops-warden": {
|
||||
"commit": "b593c879831378db7dc62adfabdfedf2d44ddd38",
|
||||
"files": {
|
||||
"registry/routing/catalog.yaml": "sha256:ba9634a92e584dac8c42e8624ab52adce06e79a285209cb64c0daa4dac9cd7d8"
|
||||
}
|
||||
},
|
||||
"owner_review": {
|
||||
"path": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-1.3-owner-review/owner-review-railiance-platform.json",
|
||||
"sha256": "d3f523b60924b530376b7aac4459cecda5da183848dd6e35889c1013c73439d9"
|
||||
},
|
||||
"prepared_files": {
|
||||
"scripts/keycape_issuer_delivery.py": "sha256:bd0fbbbdd0b08037e0d39e76f458e143663afa7e1e4ea9697639863c74e27ef7",
|
||||
"tests/test_keycape_issuer_delivery.py": "sha256:1d35c24a2ddaab17ef6419afd52d2c9b45a72fc1f107a504f5d5ed1d33fd456f",
|
||||
"docs/keycape-issuer-eso-contract.md": "sha256:ac26dd1bf204fba691da4c34308c6051f1cf23d8e19c989c51672de74817c044"
|
||||
},
|
||||
"patch_sha256": "9b9b5a81bfc65693112e136fff44ce98c35a91151dc645659d80e6a5b2da86ca"
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# Platform delivery backend source preparation r2
|
||||
|
||||
Same candidate trial as r1. **Source preparation only; production dispatch
|
||||
remains refused.** No canonical Platform/NetKingdom/Warden files, commits,
|
||||
credentials, grants, owner acceptance, native cluster operations or authenticated
|
||||
delivery evidence were created. Original r1 and its adverse independent review
|
||||
remain preserved in their existing directories.
|
||||
|
||||
`railiance-platform.patch` applies to Platform
|
||||
`104ed90f42a9a86bf9fda343b49b65a85c9ee4b9`.
|
||||
`r1-to-r2.patch` is an alternative incremental patch after the original r1
|
||||
candidate. Do not apply both. Both were checked and applied only in scratch;
|
||||
the resulting files match `prepared-source/` byte for byte. The fixed template
|
||||
is unchanged. These artifacts do not authorize canonical integration.
|
||||
|
||||
## Source-grounded corrections
|
||||
|
||||
Official ESO v0.16.1 resolves to commit
|
||||
`8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af`. Pinned public upstream source,
|
||||
URLs and digests are retained in `upstream/` and `upstream-sources.json`.
|
||||
|
||||
- The controller's syncedResourceVersion is **generation plus metadata hash**,
|
||||
rather than Kubernetes resourceVersion or an observedGeneration field.
|
||||
The exact [upstream metadata token](https://github.com/external-secrets/external-secrets/blob/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/util/util.go)
|
||||
hashes a Go-formatted annotations/labels struct using the
|
||||
[upstream ObjectHash algorithm](https://github.com/external-secrets/external-secrets/blob/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/utils/utils.go).
|
||||
r2 matches this token, including metadata changes. Five actual standalone
|
||||
Go extraction vectors cover nil/empty maps, map ordering, delimiters,
|
||||
newlines, Unicode, NUL and maximum signed generation; Python matches all.
|
||||
- `_ready` additionally requires one Ready=True/SecretSynced condition with
|
||||
the successful-sync message, and a valid refreshTime within the fixed
|
||||
five-minute interval and not in the future. The
|
||||
[actual controller](https://github.com/external-secrets/external-secrets/blob/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/externalsecret/externalsecret_controller.go)
|
||||
sets refreshTime to reconcile start. Its Retain/missing-provider branch can
|
||||
also set Ready=True with the same reason, but uses a different message;
|
||||
r2 refuses that branch. lastTransitionTime cannot replace this check:
|
||||
unchanged Ready conditions retain it across reconciles.
|
||||
- Only the three cached v1 CRD remoteRef defaults are normalized:
|
||||
conversionStrategy=Default, decodingStrategy=None, metadataPolicy=None.
|
||||
Alternate values and any other extra/spec/path/identity changes are refused.
|
||||
The helper remains a metadata contract; the runtime freshness gate is in
|
||||
delivery mechanics.
|
||||
- Nested metadata, generation, maps and collection shapes are checked before
|
||||
use. Malformed create/poll/remove responses yield fixed refusal/uncertainty
|
||||
without provider detail. Valid create-response identity is retained even if
|
||||
later shape/spec validation fails; pending state still bars blind cleanup.
|
||||
- Reads use explicit raw v1 paths. A fixed field-selected ExternalSecret
|
||||
collection GET provides structured successful absence; nonzero/error output
|
||||
is never interpreted as absence. Returned namespace/name/API/kind, item
|
||||
count and continuation are checked. There is no Secret read/list or Store
|
||||
mutation. This changes future session requirements to permit a fixed
|
||||
field-selected **ExternalSecret** list; no such permission is supplied here.
|
||||
- `bounded_public_command.py` enforces an aggregate 262144-byte stdout+stderr
|
||||
cap while reading, retains no stderr, and handles input concurrently.
|
||||
Commands have a six-second maximum; an operation has a 30-second monotonic
|
||||
deadline and refuses success after it. Each cleanup has a separate one-second
|
||||
reap budget. The process group is killed on success, overflow, timeout and
|
||||
failure, before the leader is reaped, avoiding reuse of its process-group ID.
|
||||
Unconfirmed cleanup is refusal. Tests launch only local synthetic Python
|
||||
children, including descendants holding pipes or closing them early.
|
||||
|
||||
## Store freshness and the remaining race
|
||||
|
||||
The [actual Store controller](https://github.com/external-secrets/external-secrets/blob/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/secretstore/common.go)
|
||||
publishes no generation-bound readiness token. r2 requires its fixed spec,
|
||||
one Valid/validated Ready condition, valid UID/RV, no termination and
|
||||
**generation 1**. Under Kubernetes generation/status-subresource semantics,
|
||||
that excludes a previous spec revision; any later generation gets the specific
|
||||
`store_ready_generation_unverifiable` refusal. No observedGeneration is invented
|
||||
and a changed Store cannot simply be accepted because Ready stayed true.
|
||||
|
||||
The sealed Store UID/RV/spec is observed again before create, during readiness
|
||||
and immediately before returning a receipt. These observations detect drift
|
||||
that is visible at those times. They **do not** atomically bind the
|
||||
ExternalSecret create or ESO's Store lookup to that Store revision. A Store
|
||||
change/restoration entirely between observations is undetectable. One negative
|
||||
test deliberately performs this interleaving and observes that private mechanics
|
||||
can still return a receipt. Another changes the Store in the create window and
|
||||
proves that mutation already happened before subsequent refusal.
|
||||
|
||||
Accordingly, actual owner-controlled exclusion/admission remains a production
|
||||
blocker. Double reads are not described as exclusion, and this preparation
|
||||
does not create an admission service or confer new authority. Remove uses
|
||||
ExternalSecret UID **and** RV server preconditions and does not require a
|
||||
working Store, so safe reconciliation is not coupled to Store availability.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`regressions.log` records **42/42** passing tests on an isolated tree produced
|
||||
by applying the full patch: 9 runner abuse/process tests, 25 delivery tests and
|
||||
8 existing fixed-contract tests. `assert_corrected_regressions.py` exercises
|
||||
desired assertions against both versions: untouched r1 produces two failures
|
||||
and two errors; r2 passes all four. The predecessor failure log is retained.
|
||||
`patch-runs.json` records both alternative patch check/application results.
|
||||
|
||||
The initial 33-test run had one fixture assertion failure: a descendant was
|
||||
still scheduled in state R immediately after SIGKILL. Its log is preserved.
|
||||
The verification now allows up to 200ms for the scheduler to expose termination;
|
||||
it accepts only disappearance or zombie state, never a still-running child.
|
||||
No authority or runtime promotion is scored from this correction.
|
||||
|
||||
Run `python3 reproduce.py` to assemble from pinned local Git objects and rerun
|
||||
the final 42 plus four corrected assertions. Tests clear inherited environment,
|
||||
use an empty scratch HOME and deny socket construction. Actual child fixtures
|
||||
also deny socket creation within their own interpreters; no kubectl or other
|
||||
owner executable is launched. Public upstream retrieval occurred separately
|
||||
with no authentication. Go vectors use only local standard-library compilation
|
||||
with GOPROXY/GOSUMDB disabled.
|
||||
|
||||
Supported claims are source-level exact payload/endpoint generation, synthetic
|
||||
current-metadata controller-report checks, bounded collection and conservative
|
||||
unknown-outcome handling. Server enforcement of delete preconditions, installed
|
||||
ESO version, trusted status custody, runtime credentials, present Secret contents
|
||||
and native outcomes have not been tested. Hash tokens are not authentication.
|
||||
Recent refresh checks assume a sane local clock. Store generation 1 does not
|
||||
prove current provider availability or solve cross-object exclusion.
|
||||
|
||||
Process cleanup covers ordinary descendants in the command's process group;
|
||||
it is not a sandbox against a program escaping the group/session. The leader
|
||||
is reaped within the cleanup budget or cleanup is explicitly unconfirmed;
|
||||
orphan descendants may be zombies awaiting their actual parent/reaper. Kernel
|
||||
process creation and scheduling are not a hard real-time guarantee. A production
|
||||
runtime still needs its actual reviewed isolation and executable/session binding.
|
||||
|
||||
`production_bindings()` continues refusing missing actual source/case/client
|
||||
authorization, owner exclusion/admission, scoped caller/session, durable one-shot
|
||||
admission/recovery journal, authenticated evidence publication/verification and
|
||||
per-attempt revocation. Local one-shot state, source hashes, unsigned receipts,
|
||||
public source retrieval and passing tests supply none of those. This is not
|
||||
completion of RPF-1.3-T4 and does not make dispatch operational.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
"""Desired assertions: original r1 must fail, corrected r2 must pass. No dispatch."""
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(sys.argv.pop(1)).resolve() / 'scripts'))
|
||||
import keycape_issuer_delivery as lane
|
||||
|
||||
|
||||
class CorrectedContract(unittest.TestCase):
|
||||
def setUp(self):
|
||||
p = patch.dict(os.environ, {}, clear=True); p.start(); self.addCleanup(p.stop)
|
||||
p = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
p.start(); self.addCleanup(p.stop)
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
self.obj = deepcopy(self.m.external)
|
||||
self.obj['metadata'].update(uid='fixture', resourceVersion='10', generation=2)
|
||||
self.obj['status'] = {'conditions': [{'type': 'Ready', 'status': 'True'}],
|
||||
'syncedResourceVersion': '1-stale',
|
||||
'refreshTime': '2020-01-01T00:00:00Z'}
|
||||
|
||||
def ready(self, value):
|
||||
if hasattr(lane, '_ready'): return lane._ready(value)
|
||||
return self.m._ready(value)
|
||||
|
||||
def test_stale_generation_is_not_ready(self):
|
||||
self.assertFalse(self.ready(self.obj))
|
||||
|
||||
def test_retained_provider_absence_is_not_successful_sync(self):
|
||||
self.obj['status']['conditions'][0].update(
|
||||
reason='SecretSynced', message='secret retained due to DeletionPolicy=Retain')
|
||||
self.assertFalse(self.ready(self.obj))
|
||||
|
||||
def test_actual_crd_defaults_match_fixed_external(self):
|
||||
self.obj['spec']['data'][0]['remoteRef'].update(
|
||||
conversionStrategy='Default', decodingStrategy='None', metadataPolicy='None')
|
||||
self.assertEqual(self.m._match(self.obj)['uid'], 'fixture')
|
||||
|
||||
def test_malformed_metadata_is_a_fixed_refusal(self):
|
||||
self.obj['metadata'] = None
|
||||
with self.assertRaises(lane.Refused): self.m._match(self.obj)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main(verbosity=2)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
[
|
||||
{
|
||||
"generation": 1
|
||||
},
|
||||
{
|
||||
"generation": 2,
|
||||
"annotations": {},
|
||||
"labels": {}
|
||||
},
|
||||
{
|
||||
"generation": 3,
|
||||
"annotations": {
|
||||
"b": "2",
|
||||
"a": "1"
|
||||
},
|
||||
"labels": {
|
||||
"z": "last",
|
||||
"c": "first"
|
||||
}
|
||||
},
|
||||
{
|
||||
"generation": 4,
|
||||
"annotations": {
|
||||
"a:b": "c d",
|
||||
"a b": "[]:%+v"
|
||||
},
|
||||
"labels": {
|
||||
"newline\n": "tab\t"
|
||||
}
|
||||
},
|
||||
{
|
||||
"generation": 9223372036854775807,
|
||||
"annotations": {
|
||||
"é": "好",
|
||||
"🦊": "😀"
|
||||
},
|
||||
"labels": {
|
||||
"": "",
|
||||
"null": "\u0000"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Standalone extraction of ESO v0.16.1 HashMeta/ObjectHash formatting.
|
||||
// Upstream 8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af is Apache-2.0;
|
||||
// original licensed source and exact hashes are retained under upstream/.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type input struct {
|
||||
Generation int64 `json:"generation"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var fixtures []input
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&fixtures); err != nil {
|
||||
panic("fixture invalid")
|
||||
}
|
||||
results := []string{}
|
||||
for _, fixture := range fixtures {
|
||||
type meta struct {
|
||||
annotations map[string]string
|
||||
labels map[string]string
|
||||
}
|
||||
textualVersion := fmt.Sprintf("%+v", meta{
|
||||
annotations: fixture.Annotations,
|
||||
labels: fixture.Labels,
|
||||
})
|
||||
results = append(results, fmt.Sprintf("%d-%x", fixture.Generation, md5.Sum([]byte(textualVersion))))
|
||||
}
|
||||
if err := json.NewEncoder(os.Stdout).Encode(results); err != nil {
|
||||
panic("fixture output failed")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"label": "full-check",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"label": "full-apply-in-scratch",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"label": "full-patch-applied-regressions",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"label": "incremental-check",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"label": "incremental-apply-in-scratch",
|
||||
"exit_code": 0
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
# Proposed fixed issuer ESO contract — RPF-1.3-T4 / NK48-R5-F2
|
||||
|
||||
Status: proposed source; not admitted, deployed, or owner-acknowledged. This is a
|
||||
new Platform declaration for review, not an existing Forgejo grant discovery.
|
||||
|
||||
The only substitution in `manifests/templates/keycape-issuer-eso.yaml`
|
||||
(sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743) is the admitted `client_id`. The proposed issuer
|
||||
store is **ClusterSecretStore `openbao-keycape-{client_id}-issuer`**, cluster
|
||||
scoped (no metadata namespace), restricted by conditions to ExternalSecrets in
|
||||
`sso`. For Forgejo it is `openbao-keycape-forgejo-issuer`. The suffix makes the
|
||||
issuer purpose explicit and keeps type-admitted IDs ending in `-` representable.
|
||||
The store's DNS subdomain name can exceed 63 characters; the existing adapter's
|
||||
63-character target Secret reference bound is preserved (client ID at most 48).
|
||||
No type parameter is added or changed.
|
||||
|
||||
The provider is `http://openbao.openbao.svc:8200`, KV v2 mount `platform`,
|
||||
Kubernetes auth mount `kubernetes`, role `external-secrets-keycape-{client_id}`,
|
||||
serviceAccountRef `external-secrets/external-secrets`, audience `openbao`.
|
||||
CCR-2026-0041 already records that Forgejo role/identity/audience, not this new
|
||||
store name or an unrestricted client family grant. Other clients still require
|
||||
their own existing owner-controlled role/case admission.
|
||||
|
||||
The ExternalSecret is `sso/keycape-{client_id}-client`, references precisely that
|
||||
ClusterSecretStore, refreshes every 5m and targets the same name with Owner
|
||||
creation and Retain remote deletion. Its only data entry maps `client-secret`
|
||||
from remoteRef `workloads/keycape/{client_id}`, property `CLIENT_SECRET`. The
|
||||
provider mount plus mount-relative remoteRef equals custody reference
|
||||
`platform/workloads/keycape/{client_id}`; do not double-prefix `platform/`.
|
||||
This is distinct from the Apps-owned consumer namespace SecretStore
|
||||
`openbao-keycape-{client_id}-consumer` and its dedicated consumer identity.
|
||||
|
||||
Store provisioning belongs to Platform's separate owner-controlled path. The
|
||||
proposed `platform-secret-delivery` lane retains ONLY create/get/watch/delete on
|
||||
the exact ExternalSecret in `sso`; this source does not delegate store creation
|
||||
or Secret read/list. Never apply the multi-document template through that lane.
|
||||
The pure `scripts/keycape_issuer_eso.py` module exposes only the ExternalSecret
|
||||
document through `external_secret()` and validates fixed public observations
|
||||
through `require_fixed_store()` and `require_ready_delivery()`. It performs no
|
||||
API call and issues no evidence/receipt. It is not a real capability or admission
|
||||
boundary; a caller-supplied observation alone establishes no authenticity.
|
||||
|
||||
The existing RPF-1.3-T4 delivery binding must validate the case/client binding,
|
||||
verify this exact preexisting store's public identity/spec/Ready state, apply or
|
||||
compare the exact ExternalSecret, and wait for matching spec plus Ready=True.
|
||||
The proposed helper accepts extra controller metadata but deliberately refuses
|
||||
spec drift. Actual API defaulting/normalization and installed ESO schema support
|
||||
must be checked in that implementation; these fixtures establish no native
|
||||
compatibility or credential isolation. ESO's shared controller remains trusted.
|
||||
No Secret bodies, values, arbitrary remoteRefs, or raw errors may enter receipts.
|
||||
|
||||
Existing owner requirements remain: closed `netkingdom.deliver-secret.v1`
|
||||
metadata receipt with authenticated before/after UID/resourceVersion provenance,
|
||||
unchanged objects reported NO_CHANGE, attempt-session revocation, and created-only
|
||||
rollback guarded by exact UID/resourceVersion (drift or uncertain ownership goes
|
||||
to owner reconciliation). The helper neither deletes a target Secret nor creates
|
||||
an OpenBao role/policy nor changes CCR-0041. These capabilities and catalog routing
|
||||
remain unimplemented/unaccepted under RPF-1.3-T4. Independent Platform source
|
||||
review of this declaration precedes adopting the conditional NetKingdom type
|
||||
text; no descriptor acceptance, acknowledgment, pin, grant, catalog or native
|
||||
permission is inferred.
|
||||
|
||||
Grounding: CCR-2026-0041 exact role/identity; the approved historical
|
||||
`docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml` issuer
|
||||
ClusterSecretStore and target pattern; `scripts/catalog_lane_scaffold.py`
|
||||
ClusterSecretStore/namespace restriction and mount-relative remoteRef model;
|
||||
RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical
|
||||
audience field placement differs from the issuer draft; this candidate uses
|
||||
the issuer draft's serviceAccountRef.audiences, not a claim of installed support.
|
||||
|
||||
## Prepared delivery mechanics (still unavailable as a production lane)
|
||||
|
||||
`scripts/keycape_issuer_delivery.py` now contains concrete command mechanics for
|
||||
the existing RPF-1.3-T4 obligation. It reuses `keycape_issuer_eso.documents()` and
|
||||
`require_fixed_store()` with the byte-bounded `bounded_public_command.command()`
|
||||
transport. Only the fixed public ESO resources are read using raw v1 paths.
|
||||
A successful field-selected collection response proves observed absence.
|
||||
The helper creates only the ExternalSecret, compares its fixed spec with only
|
||||
the three pinned defaults normalized, and polls at most ten times for a recent
|
||||
current-metadata controller report with stable UID. Request, command and total
|
||||
attempt deadlines apply as described below. Controller resourceVersion updates
|
||||
during successful readiness are captured.
|
||||
|
||||
Removal is permitted only for that instance's verified case-created revision,
|
||||
rechecks spec/UID/resourceVersion, uses Kubernetes DeleteOptions with both UID
|
||||
and resourceVersion preconditions, and confirms absence with bounded polling.
|
||||
Existing matching objects return changed=false and are never rollback candidates.
|
||||
Creation response loss, readiness failure after creation, replacement, delete
|
||||
conflict or unconfirmed deletion raises a fixed uncertainty diagnostic and keeps
|
||||
the last verified revision when known. No retry adopts an uncertain create; no
|
||||
replacement is deleted. An unknown create outcome requires owner reconciliation
|
||||
using the fixed resource name even when no UID could be recovered. Target Secret
|
||||
garbage collection by ESO/Kubernetes is outside this helper; it never reads or
|
||||
directly deletes Secret objects. There is no claim that deletion proves target
|
||||
Secret disappearance.
|
||||
|
||||
The callable `production_bindings()` **always refuses before any command**.
|
||||
The private mechanics use empty environment and `/dev/null` kubeconfig and
|
||||
therefore cannot borrow the caller's native session. They are source preparation,
|
||||
not an adapter/evidence authority. Mechanics tests intercept command dispatch;
|
||||
runner tests launch only synthetic local children. Neither provides production
|
||||
callbacks or attestations. A real lane requires
|
||||
an owner-reviewed authenticated human aal2 session, acknowledged case revision
|
||||
and caller binding, scoped Kubernetes credentials, one-shot durable admission
|
||||
journaling and authenticated evidence publication/verification, plus real
|
||||
per-attempt session revocation and recovery. None exists for this lane at the
|
||||
pinned source. The generic task_execution_journal module is real storage but
|
||||
explicitly does not admit native work; it is not silently repurposed here.
|
||||
The mint broker's case checker and OpenBao lookup-self are real for its existing
|
||||
mint lane, not delivery authentication. The attended custody-projection SSH
|
||||
transport and credential-change evidence writer likewise confer no delivery
|
||||
lane authority. No unsigned receipt or `True` oracle replaces these facilities.
|
||||
|
||||
Consequently RPF-1.3-T4 remains open for real capability binding and owner review;
|
||||
this patch prepares concrete ESO command/state mechanics and regressions.
|
||||
No ESO role, policy, Store, admission service, grant, catalog acceptance, native
|
||||
permission or owner descriptor acceptance is created or inferred.
|
||||
|
||||
|
||||
## r2 source mechanics: current controller report and containment
|
||||
|
||||
This preparation is not production admission or owner acceptance. The factory
|
||||
continues to refuse absent actual case/client authorization, scoped caller and
|
||||
session, owner exclusion/admission, durable admission/recovery, authenticated
|
||||
evidence and per-attempt revocation. Empty transport environment and /dev/null
|
||||
kubeconfig deliberately remain unusable for ambient production access.
|
||||
|
||||
The pinned ESO v0.16.1 controller (commit
|
||||
8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af) sets syncedResourceVersion to
|
||||
its metadata generation plus '-' plus the MD5 of its Go-formatted annotations
|
||||
and labels struct. That token is not Kubernetes resourceVersion or authenticated
|
||||
evidence. r2 compares the exact token, a unique Ready=True/SecretSynced condition
|
||||
with the controller's successful 'secret synced' message, and a valid refreshTime
|
||||
no older than the fixed five-minute refresh interval and not in the future.
|
||||
The controller sets refreshTime to reconcile start. Its Retain/missing-provider
|
||||
branch also sets Ready but uses a different message; r2 refuses that branch.
|
||||
These checks prove a recent controller report for current object metadata under
|
||||
the pinned implementation, not present Secret contents, provider availability,
|
||||
a live credential outcome or runtime authentication. Deployed-version matching,
|
||||
trusted API/status custody and a sane local clock remain production prerequisites.
|
||||
|
||||
Store status has no generation binding. r2 accepts only generation 1 with the
|
||||
fixed spec and unique Valid/'store validated' Ready condition, and records its
|
||||
UID/resourceVersion. A later generation is refused with
|
||||
store_ready_generation_unverifiable. Repeated exact Store observations before
|
||||
create, during readiness and before a receipt detect observed drift; they cannot
|
||||
atomically bind create or ESO's Store lookup to a Store revision. A wholly
|
||||
interleaved change/restoration remains invisible. Actual owner-controlled
|
||||
exclusion/admission is missing and remains a production blocker. Cleanup of a
|
||||
verified ExternalSecret uses its own UID/RV preconditions and does not depend on
|
||||
Store availability.
|
||||
|
||||
All reads use explicit raw v1 paths. The fixed ExternalSecret collection query
|
||||
includes an exact metadata.name field selector and limit 2 to obtain structured
|
||||
successful absence. Its response must be a v1 ExternalSecretList with at most
|
||||
one fixed namespace/name item and no continuation. Errors are never absence.
|
||||
This needs field-selected ExternalSecret list access in any future real session;
|
||||
no such access is granted by this code. No Kubernetes Secret is read or listed.
|
||||
|
||||
The new POSIX runner bounds combined stdout+stderr during collection to 262144
|
||||
bytes, discards stderr, uses a six-second maximum command deadline, sends input
|
||||
without a pipe deadlock, and kills the process group before reaping its leader
|
||||
on success or failure. Wait/reap has a separate one-second cleanup budget;
|
||||
unconfirmed cleanup is refusal. Operations have a 30-second monotonic deadline
|
||||
and cannot return success once it has elapsed. Descendant tests verify ordinary
|
||||
same-group children with held and closed pipes. This is process-group cleanup,
|
||||
not a sandbox guarantee against a program escaping its group/session. Privileged
|
||||
runtime isolation is not established by this runner or its tests.
|
||||
|
||||
The three actual v1 remoteRef defaults (conversionStrategy=Default,
|
||||
decodingStrategy=None, metadataPolicy=None) compare equally when omitted or
|
||||
explicit. Other spec changes remain conflicts. The metadata-only contract helper
|
||||
is not itself the runtime freshness gate; r2 delivery adds the checks above.
|
||||
The fixed template bytes remain unchanged. Unknown mutation responses preserve
|
||||
pending reconciliation, including any valid returned UID/RV, and never gain a
|
||||
blind delete/retry path. Local one-shot state remains non-durable.
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
"""POSIX byte/time bounded subprocess collection; no credential/session authority."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import selectors
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
MAX_OUTPUT = 262144 # combined stdout + stderr; stderr is counted, never retained
|
||||
MAX_INPUT = 65536
|
||||
CLEANUP_TIMEOUT = 1
|
||||
|
||||
|
||||
class CommandRefused(Exception):
|
||||
"""Only fixed diagnostics escape this transport."""
|
||||
|
||||
|
||||
def command(argv, *, payload=None, timeout=6):
|
||||
if not isinstance(timeout, (float, int)) or not 0 < timeout <= 6:
|
||||
raise CommandRefused('command_budget_invalid')
|
||||
try:
|
||||
body = b'' if payload is None else json.dumps(payload).encode()
|
||||
if len(body) > MAX_INPUT:
|
||||
raise CommandRefused('command_input_bound')
|
||||
except (ValueError, TypeError):
|
||||
raise CommandRefused('command_input_invalid') from None
|
||||
process = None
|
||||
selector = selectors.DefaultSelector()
|
||||
output = bytearray()
|
||||
total = 0
|
||||
offset = 0
|
||||
failure = None
|
||||
code = None
|
||||
deadline = time.monotonic() + timeout
|
||||
try:
|
||||
process = subprocess.Popen(argv, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={},
|
||||
start_new_session=True, close_fds=True, shell=False)
|
||||
for stream in (process.stdout, process.stderr, process.stdin):
|
||||
os.set_blocking(stream.fileno(), False)
|
||||
selector.register(process.stdout, selectors.EVENT_READ, 'stdout')
|
||||
selector.register(process.stderr, selectors.EVENT_READ, 'stderr')
|
||||
if body:
|
||||
selector.register(process.stdin, selectors.EVENT_WRITE, 'stdin')
|
||||
else:
|
||||
process.stdin.close()
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise CommandRefused('command_timeout')
|
||||
# WNOWAIT deliberately leaves the leader unreaped so its PID/PGID
|
||||
# cannot be recycled before final process-group cleanup.
|
||||
state = os.waitid(os.P_PID, process.pid,
|
||||
os.WEXITED | os.WNOHANG | os.WNOWAIT)
|
||||
if state is not None and not selector.get_map():
|
||||
break
|
||||
for key, _ in selector.select(min(remaining, .05)):
|
||||
stream = key.fileobj
|
||||
if key.data == 'stdin':
|
||||
try:
|
||||
offset += os.write(stream.fileno(), body[offset:offset + 4096])
|
||||
except BlockingIOError:
|
||||
continue
|
||||
if offset == len(body):
|
||||
selector.unregister(stream); stream.close()
|
||||
continue
|
||||
try:
|
||||
block = os.read(stream.fileno(), 4096)
|
||||
except BlockingIOError:
|
||||
continue
|
||||
if not block:
|
||||
selector.unregister(stream); stream.close()
|
||||
continue
|
||||
total += len(block)
|
||||
if total > MAX_OUTPUT:
|
||||
raise CommandRefused('command_output_bound')
|
||||
if key.data == 'stdout':
|
||||
output.extend(block)
|
||||
except CommandRefused as error:
|
||||
failure = str(error)
|
||||
except Exception:
|
||||
failure = 'command_failed'
|
||||
finally:
|
||||
selector.close()
|
||||
if process is not None:
|
||||
# Also kills descendants that kept pipes open or closed all pipes.
|
||||
# The leader has not been reaped, avoiding PGID reuse races.
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except Exception:
|
||||
failure = 'command_cleanup_unconfirmed'
|
||||
for stream in (process.stdin, process.stdout, process.stderr):
|
||||
if stream is not None:
|
||||
stream.close()
|
||||
try:
|
||||
code = process.wait(timeout=CLEANUP_TIMEOUT)
|
||||
except Exception:
|
||||
failure = 'command_cleanup_unconfirmed'
|
||||
if failure is not None:
|
||||
raise CommandRefused(failure) from None
|
||||
if code != 0:
|
||||
raise CommandRefused('command_failed') from None
|
||||
return bytes(output)
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
"""RPF-1.3-T4 source mechanics; production admission deliberately unavailable."""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from keycape_issuer_eso import documents, require_fixed_store, Refused, canonical_external_spec
|
||||
from bounded_public_command import command, CommandRefused
|
||||
|
||||
POLL_COUNT = 10
|
||||
POLL_DELAY = 1
|
||||
ATTEMPT_TIMEOUT = 30
|
||||
API = '/apis/external-secrets.io/v1'
|
||||
|
||||
|
||||
class Uncertain(Refused):
|
||||
def __init__(self, reason, revision=None):
|
||||
super().__init__(reason)
|
||||
self.revision = deepcopy(revision)
|
||||
|
||||
|
||||
def production_bindings(*args, **kwargs):
|
||||
raise Refused('delivery_admission_exclusion_session_journal_evidence_revocation_unavailable')
|
||||
|
||||
|
||||
def _metadata(obj):
|
||||
if not isinstance(obj, dict) or not isinstance(obj.get('metadata'), dict):
|
||||
raise Refused('public_metadata_invalid')
|
||||
return obj['metadata']
|
||||
|
||||
|
||||
def _revision(obj):
|
||||
meta = _metadata(obj)
|
||||
result = {'uid': meta.get('uid'), 'resource_version': meta.get('resourceVersion')}
|
||||
if any(not isinstance(x, str) or not re.fullmatch(r'[A-Za-z0-9._:-]{1,192}', x)
|
||||
for x in result.values()):
|
||||
raise Refused('public_revision_invalid')
|
||||
return result
|
||||
|
||||
|
||||
def _generation(obj):
|
||||
value = _metadata(obj).get('generation')
|
||||
if type(value) is not int or not 1 <= value <= 2**63 - 1:
|
||||
raise Refused('public_generation_invalid')
|
||||
return value
|
||||
|
||||
|
||||
def _synced_version(obj):
|
||||
"""ESO v0.16.1 util.GetResourceVersion + utils.ObjectHash, not metadata RV.
|
||||
|
||||
Go fmt %+v on the actual two-field struct uses sorted string maps, raw
|
||||
keys/values, and map[] for both nil and empty maps. Tests compare this
|
||||
implementation to a standalone Go extraction of that exact upstream code.
|
||||
This MD5 is the controller's revision token, never authentication evidence.
|
||||
"""
|
||||
meta = _metadata(obj)
|
||||
maps = []
|
||||
for name in ('annotations', 'labels'):
|
||||
value = meta.get(name)
|
||||
if value is None:
|
||||
value = {}
|
||||
if not isinstance(value, dict) or any(
|
||||
not isinstance(k, str) or not isinstance(v, str) for k, v in value.items()):
|
||||
raise Refused('public_metadata_map_invalid')
|
||||
try:
|
||||
textual = 'map[' + ' '.join(k + ':' + value[k] for k in sorted(value)) + ']'
|
||||
textual.encode('utf-8', errors='strict')
|
||||
except UnicodeError:
|
||||
raise Refused('public_metadata_map_invalid') from None
|
||||
maps.append(name + ':' + textual)
|
||||
hashed = hashlib.md5(('{' + ' '.join(maps) + '}').encode(), usedforsecurity=False).hexdigest()
|
||||
return str(_generation(obj)) + '-' + hashed
|
||||
|
||||
|
||||
def _ready(obj):
|
||||
status = obj.get('status')
|
||||
if not isinstance(status, dict):
|
||||
return False
|
||||
conditions = status.get('conditions')
|
||||
if not isinstance(conditions, list) or any(not isinstance(c, dict) for c in conditions):
|
||||
return False
|
||||
ready = [c for c in conditions if c.get('type') == 'Ready']
|
||||
if len(ready) != 1 or any(ready[0].get(k) != v for k, v in {
|
||||
'status': 'True', 'reason': 'SecretSynced', 'message': 'secret synced'}.items()):
|
||||
return False
|
||||
if status.get('syncedResourceVersion') != _synced_version(obj):
|
||||
return False
|
||||
stamp = status.get('refreshTime')
|
||||
if not isinstance(stamp, str) or not re.fullmatch(
|
||||
r'\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)', stamp):
|
||||
return False
|
||||
try:
|
||||
refreshed = datetime.fromisoformat(stamp.replace('Z', '+00:00'))
|
||||
age = (datetime.now(timezone.utc) - refreshed).total_seconds()
|
||||
return 0 <= age <= 300
|
||||
except (ValueError, OverflowError):
|
||||
return False
|
||||
|
||||
|
||||
class _DeliveryMechanics:
|
||||
"""No authority boundary: injected calls in tests, unusable ambient transport.
|
||||
|
||||
Store observations detect drift; they cannot exclude an ABA race between
|
||||
observations or bind ESO's store lookup atomically. Production needs real
|
||||
owner exclusion/admission in addition to authenticated caller facilities.
|
||||
"""
|
||||
def __init__(self, client_id):
|
||||
self.client_id = client_id
|
||||
self.store, self.external = documents(client_id)
|
||||
self.created = None
|
||||
self.pending = False
|
||||
self.used = False
|
||||
self.deadline = None
|
||||
|
||||
def _call(self, args, payload=None):
|
||||
try:
|
||||
remaining = self.deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise Refused('attempt_deadline')
|
||||
raw = command(['kubectl', '--kubeconfig=/dev/null',
|
||||
'--request-timeout=5s', *args],
|
||||
payload=payload, timeout=min(6, remaining))
|
||||
if time.monotonic() >= self.deadline:
|
||||
raise Refused('attempt_deadline')
|
||||
# Runner enforces the same bound during collection, not just here.
|
||||
if not isinstance(raw, bytes) or len(raw) > 262144:
|
||||
raise Refused('public_response_bound')
|
||||
if not raw.strip():
|
||||
return None
|
||||
value = json.loads(raw)
|
||||
if not isinstance(value, dict):
|
||||
raise Refused('public_response_shape')
|
||||
return value
|
||||
except Refused:
|
||||
raise
|
||||
except Exception:
|
||||
raise Refused('public_command_failed') from None
|
||||
|
||||
def _get(self, store=False):
|
||||
if store:
|
||||
return self._call(['get', '--raw', API + '/clustersecretstores/' + self.store['metadata']['name']])
|
||||
# Raw GET fixes API version and avoids kubectl mapper fallback. A fixed
|
||||
# field-selected collection provides structured successful absence;
|
||||
# stderr/nonzero is never interpreted as NotFound. No Secret is listed.
|
||||
uri = (API + '/namespaces/sso/externalsecrets?fieldSelector=metadata.name%3D'
|
||||
+ self.external['metadata']['name'] + '&limit=2')
|
||||
listing = self._call(['get', '--raw', uri])
|
||||
if (not isinstance(listing, dict) or listing.get('apiVersion') != 'external-secrets.io/v1'
|
||||
or listing.get('kind') != 'ExternalSecretList'
|
||||
or not isinstance(listing.get('metadata'), dict)
|
||||
or listing['metadata'].get('continue') not in (None, '')
|
||||
or not isinstance(listing.get('items'), list) or len(listing['items']) > 1):
|
||||
raise Refused('public_collection_invalid')
|
||||
if not listing['items']:
|
||||
return None
|
||||
value = listing['items'][0]
|
||||
meta = _metadata(value)
|
||||
if (value.get('apiVersion') != 'external-secrets.io/v1'
|
||||
or value.get('kind') != 'ExternalSecret'
|
||||
or meta.get('name') != self.external['metadata']['name']
|
||||
or meta.get('namespace') != 'sso'):
|
||||
raise Refused('public_collection_identity_invalid')
|
||||
return value
|
||||
|
||||
def _store_revision(self, sealed=None):
|
||||
store = self._get(store=True)
|
||||
require_fixed_store(store, self.client_id)
|
||||
if _metadata(store).get('deletionTimestamp'):
|
||||
raise Refused('store_terminating')
|
||||
revision = _revision(store)
|
||||
# Actual Store status has no generation binding. Generation 1 excludes
|
||||
# a prior spec update under Kubernetes generation semantics. For any
|
||||
# later generation Ready cannot establish current-spec validation.
|
||||
if _generation(store) != 1:
|
||||
raise Refused('store_ready_generation_unverifiable')
|
||||
conditions = store.get('status', {}).get('conditions', [])
|
||||
ready = [c for c in conditions if isinstance(c, dict) and c.get('type') == 'Ready']
|
||||
if len(ready) != 1 or ready[0].get('reason') != 'Valid' or ready[0].get('message') != 'store validated':
|
||||
raise Refused('store_validation_unverified')
|
||||
if sealed is not None and sealed != revision:
|
||||
raise Refused('store_revision_changed')
|
||||
return revision
|
||||
|
||||
def _match(self, obj):
|
||||
metadata = _metadata(obj)
|
||||
if (obj.get('apiVersion') != self.external['apiVersion']
|
||||
or obj.get('kind') != 'ExternalSecret'
|
||||
or metadata.get('name') != self.external['metadata']['name']
|
||||
or metadata.get('namespace') != 'sso' or metadata.get('deletionTimestamp')
|
||||
or canonical_external_spec(obj.get('spec')) != canonical_external_spec(self.external['spec'])):
|
||||
raise Refused('delivery_conflict')
|
||||
_generation(obj)
|
||||
return _revision(obj)
|
||||
|
||||
def _receipt(self, operation, before, after):
|
||||
return {'schema': 'netkingdom.deliver-secret.v1', 'operation': operation,
|
||||
'namespace': 'sso', 'name': self.external['metadata']['name'],
|
||||
'custody_reference': 'platform/workloads/keycape/' + self.client_id,
|
||||
'key_names': ['client-secret'], 'values_emitted': False,
|
||||
'changed': before is None if operation == 'deliver' else True,
|
||||
'before': before, 'after': after}
|
||||
|
||||
def deliver(self):
|
||||
if self.used or self.pending or self.created is not None:
|
||||
raise Refused('attempt_already_used_or_unreconciled')
|
||||
self.used = True # local one-shot only, never a durable admission journal
|
||||
self.deadline = time.monotonic() + ATTEMPT_TIMEOUT
|
||||
sealed_store = self._store_revision()
|
||||
old = self._get()
|
||||
before = self._match(old) if old is not None else None
|
||||
uid = before['uid'] if before else None
|
||||
self._store_revision(sealed_store)
|
||||
if old is None:
|
||||
self.pending = True
|
||||
try:
|
||||
created = self._call(['-n', 'sso', 'create', '-f', '-', '-o', 'json'], self.external)
|
||||
# Retain response identity even if admission changed its spec.
|
||||
# Pending prevents deletion until exact verified delivery.
|
||||
self.created = _revision(created)
|
||||
self._match(created)
|
||||
uid = self.created['uid']
|
||||
except Exception:
|
||||
raise Uncertain('create_outcome_unverified', self.created) from None
|
||||
try:
|
||||
for index in range(POLL_COUNT):
|
||||
self._store_revision(sealed_store)
|
||||
current = self._get()
|
||||
after = self._match(current)
|
||||
if after['uid'] != uid:
|
||||
raise Refused('delivery_replaced')
|
||||
if _ready(current):
|
||||
self._store_revision(sealed_store)
|
||||
if self.created is not None:
|
||||
self.created = after
|
||||
self.pending = False
|
||||
return self._receipt('deliver', before, after)
|
||||
if index + 1 < POLL_COUNT:
|
||||
remaining = self.deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise Refused('attempt_deadline')
|
||||
time.sleep(min(POLL_DELAY, remaining))
|
||||
raise Refused('delivery_current_ready_timeout')
|
||||
except Exception:
|
||||
if self.pending or self.created is not None:
|
||||
raise Uncertain('created_delivery_requires_reconciliation', self.created) from None
|
||||
raise Refused('delivery_verification_failed') from None
|
||||
|
||||
def remove(self, revision):
|
||||
if self.pending or self.created is None or revision != self.created:
|
||||
raise Refused('no_verified_case_creation')
|
||||
self.deadline = time.monotonic() + ATTEMPT_TIMEOUT
|
||||
try:
|
||||
if self._match(self._get()) != self.created:
|
||||
raise Refused('created_revision_changed')
|
||||
except Exception:
|
||||
raise Uncertain('created_revision_changed', self.created) from None
|
||||
uri = API + '/namespaces/sso/externalsecrets/' + self.external['metadata']['name']
|
||||
options = {'apiVersion': 'v1', 'kind': 'DeleteOptions',
|
||||
'preconditions': {'uid': self.created['uid'],
|
||||
'resourceVersion': self.created['resource_version']}}
|
||||
prior = deepcopy(self.created)
|
||||
self.pending = True
|
||||
try:
|
||||
self._call(['delete', '--raw', uri, '-f', '-'], options)
|
||||
for index in range(POLL_COUNT):
|
||||
current = self._get()
|
||||
if current is None:
|
||||
self.created = None
|
||||
self.pending = False
|
||||
return self._receipt('remove', prior, None)
|
||||
if _revision(current)['uid'] != prior['uid']:
|
||||
raise Refused('replacement_preserved')
|
||||
if index + 1 < POLL_COUNT:
|
||||
remaining = self.deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise Refused('attempt_deadline')
|
||||
time.sleep(min(POLL_DELAY, remaining))
|
||||
raise Refused('delete_unconfirmed')
|
||||
except Exception:
|
||||
raise Uncertain('cleanup_requires_reconciliation', prior) from None
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
"""Proposed fixed issuer ESO source contract for RPF-1.3-T4 / NK48-R5-F2.
|
||||
|
||||
Pure public metadata only. No API/credential/Secret access or apply path. This
|
||||
new declaration requires independent Platform review before type integration.
|
||||
The delivery lane may use only external_secret(); store provisioning is a
|
||||
separate owner action and is never delegated by this module.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TEMPLATE = ROOT / 'manifests/templates/keycape-issuer-eso.yaml'
|
||||
|
||||
|
||||
class Refused(ValueError):
|
||||
"""Fixed issuer metadata differs; never return an owner delivery receipt."""
|
||||
|
||||
|
||||
def documents(client_id: str) -> tuple[dict, dict]:
|
||||
# Match the frozen type client_id pattern and existing adapter's derived bound.
|
||||
# No caller can supply a store name/kind/namespace or an OpenBao path.
|
||||
if not isinstance(client_id, str) or not re.fullmatch(r'[a-z0-9][a-z0-9-]{1,62}', client_id):
|
||||
raise Refused('bounded admitted client_id required')
|
||||
if len('keycape-' + client_id + '-client') > 63:
|
||||
raise Refused('derived issuer reference exceeds existing adapter bound')
|
||||
store, external = yaml.safe_load_all(TEMPLATE.read_text().format(client_id=client_id))
|
||||
return store, external
|
||||
|
||||
|
||||
def external_secret(client_id: str) -> dict:
|
||||
"""The ONLY document the proposed delivery capability may apply."""
|
||||
return deepcopy(documents(client_id)[1])
|
||||
|
||||
|
||||
def canonical_external_spec(spec):
|
||||
"""Normalize only the three pinned ESO v0.16.1 remoteRef defaults.
|
||||
|
||||
Omitted and explicit defaults are equivalent; all other fields remain exact.
|
||||
This says nothing about freshness, admission authority or Store continuity.
|
||||
"""
|
||||
value = deepcopy(spec)
|
||||
if isinstance(value, dict) and isinstance(value.get('data'), list):
|
||||
for item in value['data']:
|
||||
if isinstance(item, dict) and isinstance(item.get('remoteRef'), dict):
|
||||
for name, default in (('conversionStrategy', 'Default'),
|
||||
('decodingStrategy', 'None'),
|
||||
('metadataPolicy', 'None')):
|
||||
item['remoteRef'].setdefault(name, default)
|
||||
return value
|
||||
|
||||
|
||||
def _matching_public_resource(observed: dict, expected: dict) -> None:
|
||||
if not isinstance(observed, dict):
|
||||
raise Refused('public resource observation required')
|
||||
metadata = observed.get('metadata')
|
||||
if not isinstance(metadata, dict):
|
||||
raise Refused('resource metadata required')
|
||||
expected_namespace = expected['metadata'].get('namespace')
|
||||
namespace_matches = (metadata.get('namespace') == expected_namespace
|
||||
if expected_namespace else metadata.get('namespace') in (None, ''))
|
||||
if (observed.get('apiVersion') != expected['apiVersion']
|
||||
or observed.get('kind') != expected['kind']
|
||||
or metadata.get('name') != expected['metadata']['name']
|
||||
or not namespace_matches
|
||||
or (canonical_external_spec(observed.get('spec'))
|
||||
!= canonical_external_spec(expected['spec'])
|
||||
if expected['kind'] == 'ExternalSecret'
|
||||
else observed.get('spec') != expected['spec'])):
|
||||
raise Refused('fixed issuer resource differs')
|
||||
status = observed.get('status')
|
||||
conditions = status.get('conditions', []) if isinstance(status, dict) else []
|
||||
if not isinstance(conditions, list) or not any(
|
||||
isinstance(c, dict) and c.get('type') == 'Ready' and c.get('status') == 'True'
|
||||
for c in conditions):
|
||||
raise Refused('fixed issuer resource not ready')
|
||||
|
||||
|
||||
def require_fixed_store(observed: dict, client_id: str) -> None:
|
||||
"""Refuse a missing/drifted/not-ready store; never create it through delivery."""
|
||||
_matching_public_resource(observed, documents(client_id)[0])
|
||||
|
||||
|
||||
def require_ready_delivery(observed: dict, client_id: str) -> None:
|
||||
"""Spec equality and Ready precede a created/NO_CHANGE owner receipt."""
|
||||
_matching_public_resource(observed, documents(client_id)[1])
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
[
|
||||
{
|
||||
"metadata": {
|
||||
"generation": 1
|
||||
},
|
||||
"expected": "1-2f2c636d06132f67992c15a1daeb0a5f"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"generation": 2,
|
||||
"annotations": {},
|
||||
"labels": {}
|
||||
},
|
||||
"expected": "2-2f2c636d06132f67992c15a1daeb0a5f"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"generation": 3,
|
||||
"annotations": {
|
||||
"b": "2",
|
||||
"a": "1"
|
||||
},
|
||||
"labels": {
|
||||
"z": "last",
|
||||
"c": "first"
|
||||
}
|
||||
},
|
||||
"expected": "3-7ad4f86f5aba928cde01913c7b9d962a"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"generation": 4,
|
||||
"annotations": {
|
||||
"a:b": "c d",
|
||||
"a b": "[]:%+v"
|
||||
},
|
||||
"labels": {
|
||||
"newline\n": "tab\t"
|
||||
}
|
||||
},
|
||||
"expected": "4-48e35b520c38b2944d461645440cc3cd"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"generation": 9223372036854775807,
|
||||
"annotations": {
|
||||
"é": "好",
|
||||
"🦊": "😀"
|
||||
},
|
||||
"labels": {
|
||||
"": "",
|
||||
"null": "\u0000"
|
||||
}
|
||||
},
|
||||
"expected": "9223372036854775807-da6b2f7c16875cb96b3a00d83378eda3"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
import bounded_public_command as runner
|
||||
|
||||
# Every launched fixture denies socket creation in its own interpreter as well
|
||||
# as the parent. They only perform controlled local stdout/stderr/process work.
|
||||
PREFIX = "import socket; socket.socket=lambda *a,**k: (_ for _ in ()).throw(AssertionError('network forbidden')); "
|
||||
|
||||
|
||||
class BoundedCommand(unittest.TestCase):
|
||||
def setUp(self):
|
||||
p = patch.dict(os.environ, {}, clear=True); p.start(); self.addCleanup(p.stop)
|
||||
p = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
p.start(); self.addCleanup(p.stop)
|
||||
|
||||
def call(self, code, timeout=2, payload=None):
|
||||
return runner.command(['/usr/bin/python3', '-c', PREFIX + code], timeout=timeout, payload=payload)
|
||||
|
||||
def test_empty_environment_and_input_roundtrip(self):
|
||||
raw = self.call("import os,sys,json; b=sys.stdin.buffer.read(); print(json.dumps({'keys':sorted(os.environ),'body':json.loads(b)}))", payload={'fixed': 'metadata'})
|
||||
import json
|
||||
response = json.loads(raw)
|
||||
# CPython may inject LC_CTYPE during startup even though exec env={};
|
||||
# neither HOME nor credential variables are inherited.
|
||||
self.assertFalse(set(response['keys']) - {'LC_CTYPE'})
|
||||
self.assertEqual(response['body'], {'fixed': 'metadata'})
|
||||
|
||||
def test_stdout_flood_stopped_with_fixed_error(self):
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
self.call("import os; [os.write(1,b'PRIVATE'*4096) for _ in range(1000)]")
|
||||
|
||||
def test_stderr_flood_stopped_with_fixed_error(self):
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
self.call("import os; [os.write(2,b'PRIVATE'*4096) for _ in range(1000)]")
|
||||
|
||||
def test_combined_cap_not_independent_per_stream(self):
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
self.call("import os; os.write(1,b'x'*140000); os.write(2,b'y'*140000)")
|
||||
|
||||
def test_error_output_never_escapes(self):
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_failed$') as error:
|
||||
self.call("import sys; print('PRIVATE'); print('PRIVATE',file=sys.stderr); sys.exit(7)")
|
||||
self.assertNotIn('PRIVATE', str(error.exception))
|
||||
|
||||
def test_sleep_timeout_is_real_and_bounded(self):
|
||||
start = time.monotonic()
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_timeout$'):
|
||||
self.call('import time; time.sleep(60)', timeout=.15)
|
||||
self.assertLess(time.monotonic() - start, 2)
|
||||
|
||||
def test_descendant_holding_pipes_is_killed(self):
|
||||
with tempfile.TemporaryDirectory(prefix='delivery-runner-') as scratch:
|
||||
pidfile = Path(scratch) / 'descendant'
|
||||
code = ("import os,time; pid=os.fork(); "
|
||||
"(time.sleep(60) if pid==0 else "
|
||||
f"(open({str(pidfile)!r},'w').write(str(pid)),os._exit(0)))")
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_timeout$'):
|
||||
self.call(code, timeout=.2)
|
||||
self.assertTrue(pidfile.exists())
|
||||
pid = pidfile.read_text()
|
||||
status = Path('/proc') / pid / 'stat'
|
||||
for _ in range(20):
|
||||
if not status.exists() or status.read_text().split()[2] == 'Z': break
|
||||
time.sleep(.01)
|
||||
if status.exists(): self.assertEqual(status.read_text().split()[2], 'Z')
|
||||
|
||||
def test_descendant_with_closed_pipes_is_killed_on_success(self):
|
||||
with tempfile.TemporaryDirectory(prefix='delivery-runner-') as scratch:
|
||||
pidfile = Path(scratch) / 'descendant'
|
||||
code = ("import os,time; pid=os.fork(); "
|
||||
"(os.close(0),os.close(1),os.close(2),time.sleep(60)) if pid==0 else "
|
||||
f"(open({str(pidfile)!r},'w').write(str(pid)),print('ok'))")
|
||||
self.assertEqual(self.call(code), b'ok\n')
|
||||
status = Path('/proc') / pidfile.read_text() / 'stat'
|
||||
# Kill delivery may race the proc-state observation; wait boundedly.
|
||||
for _ in range(20):
|
||||
if not status.exists() or status.read_text().split()[2] == 'Z': break
|
||||
time.sleep(.01)
|
||||
if status.exists(): self.assertEqual(status.read_text().split()[2], 'Z')
|
||||
|
||||
def test_input_bound_and_spawn_failure_fixed(self):
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_input_bound$'):
|
||||
self.call('pass', payload={'data': 'x'*65536})
|
||||
with self.assertRaisesRegex(runner.CommandRefused, '^command_failed$'):
|
||||
runner.command(['/nonexistent-fixture-binary'], timeout=.1)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
import keycape_issuer_delivery as lane
|
||||
from bounded_public_command import CommandRefused
|
||||
|
||||
|
||||
class Mechanics(unittest.TestCase):
|
||||
def setUp(self):
|
||||
p = patch.dict(os.environ, {}, clear=True); p.start(); self.addCleanup(p.stop)
|
||||
p = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
p.start(); self.addCleanup(p.stop)
|
||||
self.m = lane._DeliveryMechanics('forgejo')
|
||||
self.store = self.resource(self.m.store, store=True)
|
||||
self.obj = None
|
||||
self.calls = []
|
||||
self.store_reads = 0
|
||||
self.hook = lambda args: None
|
||||
self.fail_create = False
|
||||
self.fail_delete = False
|
||||
self.create_mutate = lambda obj: obj
|
||||
self.force_list = None
|
||||
self.poll_object = None
|
||||
self.clock = 0
|
||||
self.clock_step = 0
|
||||
p = patch.object(lane, 'command', side_effect=self.command)
|
||||
p.start(); self.addCleanup(p.stop)
|
||||
p = patch.object(lane.time, 'sleep'); p.start(); self.addCleanup(p.stop)
|
||||
p = patch.object(lane.time, 'monotonic', side_effect=lambda: self.clock)
|
||||
p.start(); self.addCleanup(p.stop)
|
||||
|
||||
def resource(self, doc, store=False):
|
||||
value = deepcopy(doc)
|
||||
value['metadata'].update(uid='store-uid' if store else 'created-uid', resourceVersion='10', generation=1)
|
||||
if store:
|
||||
value['status'] = {'conditions': [{'type': 'Ready', 'status': 'True',
|
||||
'reason': 'Valid', 'message': 'store validated'}]}
|
||||
else:
|
||||
value['spec'] = lane.canonical_external_spec(value['spec'])
|
||||
value['status'] = {'conditions': [{'type': 'Ready', 'status': 'True',
|
||||
'reason': 'SecretSynced', 'message': 'secret synced'}],
|
||||
'syncedResourceVersion': lane._synced_version(value),
|
||||
'refreshTime': datetime.now(timezone.utc).isoformat()}
|
||||
return value
|
||||
|
||||
def command(self, args, *, payload=None, timeout=6):
|
||||
self.assertEqual(args[:3], ['kubectl', '--kubeconfig=/dev/null', '--request-timeout=5s'])
|
||||
self.assertGreater(timeout, 0); self.assertLessEqual(timeout, 6)
|
||||
self.calls.append((deepcopy(args), deepcopy(payload)))
|
||||
self.clock += self.clock_step
|
||||
self.hook(args)
|
||||
if 'create' in args:
|
||||
if self.fail_create:
|
||||
raise CommandRefused('PRIVATE PROVIDER VALUE')
|
||||
self.obj = self.create_mutate(self.resource(self.m.external))
|
||||
value = self.obj
|
||||
elif 'delete' in args:
|
||||
if self.fail_delete:
|
||||
raise CommandRefused('PRIVATE PROVIDER VALUE')
|
||||
revision = lane._revision(self.obj)
|
||||
if payload['preconditions'] != {'uid': revision['uid'], 'resourceVersion': revision['resource_version']}:
|
||||
raise CommandRefused('conditional conflict fixture')
|
||||
self.obj = None
|
||||
value = {'kind': 'Status', 'status': 'Success'}
|
||||
elif '/clustersecretstores/' in args[-1]:
|
||||
self.store_reads += 1
|
||||
value = self.store
|
||||
else:
|
||||
self.assertIn('get', args); self.assertIn('--raw', args)
|
||||
self.assertEqual(args[-1], lane.API + '/namespaces/sso/externalsecrets?fieldSelector=metadata.name%3Dkeycape-forgejo-client&limit=2')
|
||||
value = self.force_list if self.force_list is not None else {
|
||||
'apiVersion': 'external-secrets.io/v1', 'kind': 'ExternalSecretList',
|
||||
'metadata': {}, 'items': [] if self.obj is None else [self.poll_object or self.obj]}
|
||||
return json.dumps(deepcopy(value)).encode()
|
||||
|
||||
def test_actual_defaults_create_current_ready_and_conditional_delete(self):
|
||||
delivered = self.m.deliver()
|
||||
self.assertTrue(delivered['changed']); self.assertIsNone(delivered['before'])
|
||||
self.assertGreaterEqual(self.store_reads, 4)
|
||||
removed = self.m.remove(delivered['after'])
|
||||
self.assertEqual(removed['after'], None)
|
||||
args, payload = next(c for c in self.calls if 'delete' in c[0])
|
||||
self.assertEqual(args[5], lane.API + '/namespaces/sso/externalsecrets/keycape-forgejo-client')
|
||||
self.assertEqual(payload['preconditions'], {'uid': 'created-uid', 'resourceVersion': '10'})
|
||||
|
||||
def test_existing_no_change_cannot_be_deleted_or_reused(self):
|
||||
self.obj = self.resource(self.m.external)
|
||||
delivered = self.m.deliver()
|
||||
self.assertFalse(delivered['changed'])
|
||||
with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
self.assertFalse(any('create' in args or 'delete' in args for args, _ in self.calls))
|
||||
|
||||
def test_stale_current_spec_and_metadata_hash_never_ready(self):
|
||||
obj = self.resource(self.m.external)
|
||||
for field, value in [('generation', 2), ('labels', {'new': 'metadata'}), ('annotations', {'new': 'metadata'})]:
|
||||
with self.subTest(field=field):
|
||||
changed = deepcopy(obj); changed['metadata'][field] = value
|
||||
self.assertFalse(lane._ready(changed))
|
||||
|
||||
def test_exact_go_upstream_revision_vectors(self):
|
||||
vectors = json.loads((Path(__file__).parent / 'eso-hash-vectors.json').read_text())
|
||||
for vector in vectors:
|
||||
self.assertEqual(lane._synced_version({'metadata': vector['metadata']}), vector['expected'])
|
||||
|
||||
def test_wrong_default_strategies_and_identity_never_match(self):
|
||||
for field, value in [('conversionStrategy', 'Unicode'), ('decodingStrategy', 'Base64'),
|
||||
('metadataPolicy', 'Fetch'), ('key', 'elsewhere'), ('extra', 'value')]:
|
||||
obj = self.resource(self.m.external)
|
||||
obj['spec']['data'][0]['remoteRef'][field] = value
|
||||
with self.assertRaises(lane.Refused): self.m._match(obj)
|
||||
for field, value in [('namespace', 'default'), ('name', 'other'), ('generation', True)]:
|
||||
obj = self.resource(self.m.external); obj['metadata'][field] = value
|
||||
with self.assertRaises(lane.Refused): self.m._match(obj)
|
||||
|
||||
def test_invalid_metadata_maps_never_produce_hash(self):
|
||||
for value in ([], 'str', {'key': None}, {'key': '\ud800'}):
|
||||
obj = self.resource(self.m.external); obj['metadata']['labels'] = value
|
||||
with self.assertRaises(lane.Refused): lane._synced_version(obj)
|
||||
|
||||
def test_client_override_injection_refused_before_command(self):
|
||||
for client in ('x/../../secret', 'x;echo', 'A', None, 'a'*49):
|
||||
with self.assertRaises(lane.Refused): lane._DeliveryMechanics(client)
|
||||
self.assertEqual(self.calls, [])
|
||||
|
||||
def test_retained_missing_stale_future_duplicate_status_not_ready(self):
|
||||
for variant in ('retained', 'missing', 'old', 'future', 'duplicate', 'invalid_time'):
|
||||
with self.subTest(variant=variant):
|
||||
obj = self.resource(self.m.external)
|
||||
if variant == 'retained': obj['status']['conditions'][0]['message'] = 'secret retained due to DeletionPolicy=Retain'
|
||||
if variant == 'missing': obj['status'].pop('syncedResourceVersion')
|
||||
if variant in ('old', 'future'):
|
||||
delta = -301 if variant == 'old' else 60
|
||||
obj['status']['refreshTime'] = (datetime.now(timezone.utc) + timedelta(seconds=delta)).isoformat()
|
||||
if variant == 'duplicate': obj['status']['conditions'] *= 2
|
||||
if variant == 'invalid_time': obj['status']['refreshTime'] = 'PRIVATE MALFORMED'
|
||||
self.assertFalse(lane._ready(obj))
|
||||
|
||||
def test_stale_delivery_times_out_with_reconciliation_provenance(self):
|
||||
self.create_mutate = lambda obj: {**obj, 'status': {'conditions': [{'type': 'Ready', 'status': 'True'}]}}
|
||||
with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
self.assertEqual(error.exception.revision['uid'], 'created-uid')
|
||||
with self.assertRaises(lane.Refused): self.m.remove(error.exception.revision)
|
||||
self.assertLessEqual(len(self.calls), 2 * lane.POLL_COUNT + 4)
|
||||
|
||||
def test_store_prior_generation_refused_before_create(self):
|
||||
self.store['metadata']['generation'] = 2
|
||||
with self.assertRaisesRegex(lane.Refused, 'generation_unverifiable'): self.m.deliver()
|
||||
self.assertEqual(len(self.calls), 1)
|
||||
|
||||
def test_store_replaced_before_precreate_read_prevents_create(self):
|
||||
def hook(args):
|
||||
if '/externalsecrets?' in args[-1]: self.store['metadata']['uid'] = 'replacement'
|
||||
self.hook = hook
|
||||
with self.assertRaisesRegex(lane.Refused, 'store_revision_changed'): self.m.deliver()
|
||||
self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
|
||||
def test_store_replaced_inside_create_window_detected_after_mutation(self):
|
||||
def hook(args):
|
||||
if 'create' in args: self.store['metadata']['uid'] = 'replacement'
|
||||
self.hook = hook
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
self.assertIsNotNone(self.obj) # double-read did not exclude mutation
|
||||
self.assertTrue(self.m.pending)
|
||||
|
||||
def test_store_aba_race_is_not_claimed_excluded(self):
|
||||
# A mutation and restoration wholly between observations is invisible.
|
||||
seen = []
|
||||
def hook(args):
|
||||
if 'create' in args:
|
||||
original = deepcopy(self.store)
|
||||
self.store['metadata']['uid'] = 'transient-replacement'
|
||||
seen.append(self.store['metadata']['uid'])
|
||||
self.store = original
|
||||
seen.append(self.store['metadata']['uid'])
|
||||
self.hook = hook
|
||||
self.assertTrue(self.m.deliver()['changed'])
|
||||
self.assertEqual(len(seen), 2)
|
||||
self.assertEqual(seen, ['transient-replacement', 'store-uid'])
|
||||
|
||||
def test_malformed_create_preserves_fixed_unknown_outcome(self):
|
||||
for metadata in (None, [], {}, {'uid': 'created-uid', 'resourceVersion': None}):
|
||||
with self.subTest(metadata=metadata):
|
||||
self.m = lane._DeliveryMechanics('forgejo'); self.obj = None
|
||||
self.create_mutate = lambda obj: {**obj, 'metadata': metadata}
|
||||
with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
self.assertEqual(str(error.exception), 'create_outcome_unverified')
|
||||
self.assertTrue(self.m.pending)
|
||||
|
||||
def test_unknown_create_and_delete_never_blindly_retry(self):
|
||||
self.fail_create = True
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
self.fail_create = False; self.m = lane._DeliveryMechanics('forgejo')
|
||||
delivered = self.m.deliver(); self.fail_delete = True
|
||||
with self.assertRaises(lane.Uncertain) as error: self.m.remove(delivered['after'])
|
||||
self.assertNotIn('PRIVATE', str(error.exception))
|
||||
with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
|
||||
def test_delete_race_uses_server_preconditions_and_preserves_replacement(self):
|
||||
delivered = self.m.deliver()
|
||||
def hook(args):
|
||||
if 'delete' in args: self.obj['metadata']['uid'] = 'replacement'
|
||||
self.hook = hook
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
self.assertEqual(self.obj['metadata']['uid'], 'replacement')
|
||||
|
||||
def test_status_rv_changed_before_remove_no_delete(self):
|
||||
delivered = self.m.deliver(); self.obj['metadata']['resourceVersion'] = '11'
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
self.assertFalse(any('delete' in args for args, _ in self.calls))
|
||||
|
||||
def test_fresh_process_and_forged_revision_never_delete(self):
|
||||
delivered = self.m.deliver()
|
||||
with self.assertRaises(lane.Refused): lane._DeliveryMechanics('forgejo').remove(delivered['after'])
|
||||
with self.assertRaises(lane.Refused): self.m.remove({'uid': 'fake', 'resource_version': '10'})
|
||||
self.assertFalse(any('delete' in args for args, _ in self.calls))
|
||||
|
||||
def test_poll_rv_is_captured(self):
|
||||
reads = []
|
||||
def hook(args):
|
||||
if '/externalsecrets?' in args[-1] and self.obj is not None:
|
||||
self.obj['metadata']['resourceVersion'] = '11'
|
||||
reads.append(True)
|
||||
self.hook = hook
|
||||
delivered = self.m.deliver()
|
||||
self.assertEqual(delivered['after']['resource_version'], '11')
|
||||
self.assertTrue(reads)
|
||||
|
||||
def test_unconfirmed_delete_is_bounded_and_preserves_reconciliation(self):
|
||||
delivered = self.m.deliver()
|
||||
prior = deepcopy(self.obj)
|
||||
terminating = deepcopy(prior); terminating['metadata']['deletionTimestamp'] = 'now'
|
||||
with patch.object(self.m, '_get', side_effect=[prior] + [terminating]*lane.POLL_COUNT):
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
self.assertTrue(self.m.pending)
|
||||
with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
|
||||
def test_lost_delete_after_server_removal_is_still_uncertain(self):
|
||||
delivered = self.m.deliver(); self.fail_delete = True
|
||||
def hook(args):
|
||||
if 'delete' in args: self.obj = None
|
||||
self.hook = hook
|
||||
with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
self.assertIsNone(self.obj)
|
||||
self.assertIsNotNone(self.m.created)
|
||||
self.assertTrue(self.m.pending)
|
||||
|
||||
def test_collection_foreign_object_cannot_be_adopted(self):
|
||||
self.obj = self.resource(self.m.external); self.obj['metadata']['name'] = 'other'
|
||||
with self.assertRaisesRegex(lane.Refused, 'collection_identity'): self.m.deliver()
|
||||
self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
|
||||
def test_unknown_conflicting_or_unbounded_collection_never_absence(self):
|
||||
for listing in ({}, {'kind': 'Status'}, {'apiVersion': 'external-secrets.io/v1',
|
||||
'kind': 'ExternalSecretList', 'metadata': {'continue': 'more'}, 'items': []}):
|
||||
with self.subTest(listing=listing):
|
||||
self.m = lane._DeliveryMechanics('forgejo'); self.force_list = listing
|
||||
with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
|
||||
def test_total_attempt_deadline_blocks_late_success(self):
|
||||
self.clock_step = 6
|
||||
with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
self.assertLessEqual(len(self.calls), 6)
|
||||
|
||||
def test_no_secret_or_store_mutation_and_factory_refuses(self):
|
||||
self.m.deliver()
|
||||
for args, payload in self.calls:
|
||||
self.assertFalse(set(args) & {'secret', 'secrets'})
|
||||
if payload and 'create' in args: self.assertEqual(payload['kind'], 'ExternalSecret')
|
||||
before = len(self.calls)
|
||||
with self.assertRaises(lane.Refused): lane.production_bindings(authenticated=True)
|
||||
self.assertEqual(len(self.calls), before)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,993 @@
|
|||
--- a/docs/keycape-issuer-eso-contract.md
|
||||
+++ b/docs/keycape-issuer-eso-contract.md
|
||||
@@ -66,3 +66,110 @@
|
||||
RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical
|
||||
audience field placement differs from the issuer draft; this candidate uses
|
||||
the issuer draft's serviceAccountRef.audiences, not a claim of installed support.
|
||||
+
|
||||
+## Prepared delivery mechanics (still unavailable as a production lane)
|
||||
+
|
||||
+`scripts/keycape_issuer_delivery.py` now contains concrete command mechanics for
|
||||
+the existing RPF-1.3-T4 obligation. It reuses `keycape_issuer_eso.documents()` and
|
||||
+`require_fixed_store()` with the byte-bounded `bounded_public_command.command()`
|
||||
+transport. Only the fixed public ESO resources are read using raw v1 paths.
|
||||
+A successful field-selected collection response proves observed absence.
|
||||
+The helper creates only the ExternalSecret, compares its fixed spec with only
|
||||
+the three pinned defaults normalized, and polls at most ten times for a recent
|
||||
+current-metadata controller report with stable UID. Request, command and total
|
||||
+attempt deadlines apply as described below. Controller resourceVersion updates
|
||||
+during successful readiness are captured.
|
||||
+
|
||||
+Removal is permitted only for that instance's verified case-created revision,
|
||||
+rechecks spec/UID/resourceVersion, uses Kubernetes DeleteOptions with both UID
|
||||
+and resourceVersion preconditions, and confirms absence with bounded polling.
|
||||
+Existing matching objects return changed=false and are never rollback candidates.
|
||||
+Creation response loss, readiness failure after creation, replacement, delete
|
||||
+conflict or unconfirmed deletion raises a fixed uncertainty diagnostic and keeps
|
||||
+the last verified revision when known. No retry adopts an uncertain create; no
|
||||
+replacement is deleted. An unknown create outcome requires owner reconciliation
|
||||
+using the fixed resource name even when no UID could be recovered. Target Secret
|
||||
+garbage collection by ESO/Kubernetes is outside this helper; it never reads or
|
||||
+directly deletes Secret objects. There is no claim that deletion proves target
|
||||
+Secret disappearance.
|
||||
+
|
||||
+The callable `production_bindings()` **always refuses before any command**.
|
||||
+The private mechanics use empty environment and `/dev/null` kubeconfig and
|
||||
+therefore cannot borrow the caller's native session. They are source preparation,
|
||||
+not an adapter/evidence authority. Mechanics tests intercept command dispatch;
|
||||
+runner tests launch only synthetic local children. Neither provides production
|
||||
+callbacks or attestations. A real lane requires
|
||||
+an owner-reviewed authenticated human aal2 session, acknowledged case revision
|
||||
+and caller binding, scoped Kubernetes credentials, one-shot durable admission
|
||||
+journaling and authenticated evidence publication/verification, plus real
|
||||
+per-attempt session revocation and recovery. None exists for this lane at the
|
||||
+pinned source. The generic task_execution_journal module is real storage but
|
||||
+explicitly does not admit native work; it is not silently repurposed here.
|
||||
+The mint broker's case checker and OpenBao lookup-self are real for its existing
|
||||
+mint lane, not delivery authentication. The attended custody-projection SSH
|
||||
+transport and credential-change evidence writer likewise confer no delivery
|
||||
+lane authority. No unsigned receipt or `True` oracle replaces these facilities.
|
||||
+
|
||||
+Consequently RPF-1.3-T4 remains open for real capability binding and owner review;
|
||||
+this patch prepares concrete ESO command/state mechanics and regressions.
|
||||
+No ESO role, policy, Store, admission service, grant, catalog acceptance, native
|
||||
+permission or owner descriptor acceptance is created or inferred.
|
||||
+
|
||||
+
|
||||
+## r2 source mechanics: current controller report and containment
|
||||
+
|
||||
+This preparation is not production admission or owner acceptance. The factory
|
||||
+continues to refuse absent actual case/client authorization, scoped caller and
|
||||
+session, owner exclusion/admission, durable admission/recovery, authenticated
|
||||
+evidence and per-attempt revocation. Empty transport environment and /dev/null
|
||||
+kubeconfig deliberately remain unusable for ambient production access.
|
||||
+
|
||||
+The pinned ESO v0.16.1 controller (commit
|
||||
+8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af) sets syncedResourceVersion to
|
||||
+its metadata generation plus '-' plus the MD5 of its Go-formatted annotations
|
||||
+and labels struct. That token is not Kubernetes resourceVersion or authenticated
|
||||
+evidence. r2 compares the exact token, a unique Ready=True/SecretSynced condition
|
||||
+with the controller's successful 'secret synced' message, and a valid refreshTime
|
||||
+no older than the fixed five-minute refresh interval and not in the future.
|
||||
+The controller sets refreshTime to reconcile start. Its Retain/missing-provider
|
||||
+branch also sets Ready but uses a different message; r2 refuses that branch.
|
||||
+These checks prove a recent controller report for current object metadata under
|
||||
+the pinned implementation, not present Secret contents, provider availability,
|
||||
+a live credential outcome or runtime authentication. Deployed-version matching,
|
||||
+trusted API/status custody and a sane local clock remain production prerequisites.
|
||||
+
|
||||
+Store status has no generation binding. r2 accepts only generation 1 with the
|
||||
+fixed spec and unique Valid/'store validated' Ready condition, and records its
|
||||
+UID/resourceVersion. A later generation is refused with
|
||||
+store_ready_generation_unverifiable. Repeated exact Store observations before
|
||||
+create, during readiness and before a receipt detect observed drift; they cannot
|
||||
+atomically bind create or ESO's Store lookup to a Store revision. A wholly
|
||||
+interleaved change/restoration remains invisible. Actual owner-controlled
|
||||
+exclusion/admission is missing and remains a production blocker. Cleanup of a
|
||||
+verified ExternalSecret uses its own UID/RV preconditions and does not depend on
|
||||
+Store availability.
|
||||
+
|
||||
+All reads use explicit raw v1 paths. The fixed ExternalSecret collection query
|
||||
+includes an exact metadata.name field selector and limit 2 to obtain structured
|
||||
+successful absence. Its response must be a v1 ExternalSecretList with at most
|
||||
+one fixed namespace/name item and no continuation. Errors are never absence.
|
||||
+This needs field-selected ExternalSecret list access in any future real session;
|
||||
+no such access is granted by this code. No Kubernetes Secret is read or listed.
|
||||
+
|
||||
+The new POSIX runner bounds combined stdout+stderr during collection to 262144
|
||||
+bytes, discards stderr, uses a six-second maximum command deadline, sends input
|
||||
+without a pipe deadlock, and kills the process group before reaping its leader
|
||||
+on success or failure. Wait/reap has a separate one-second cleanup budget;
|
||||
+unconfirmed cleanup is refusal. Operations have a 30-second monotonic deadline
|
||||
+and cannot return success once it has elapsed. Descendant tests verify ordinary
|
||||
+same-group children with held and closed pipes. This is process-group cleanup,
|
||||
+not a sandbox guarantee against a program escaping its group/session. Privileged
|
||||
+runtime isolation is not established by this runner or its tests.
|
||||
+
|
||||
+The three actual v1 remoteRef defaults (conversionStrategy=Default,
|
||||
+decodingStrategy=None, metadataPolicy=None) compare equally when omitted or
|
||||
+explicit. Other spec changes remain conflicts. The metadata-only contract helper
|
||||
+is not itself the runtime freshness gate; r2 delivery adds the checks above.
|
||||
+The fixed template bytes remain unchanged. Unknown mutation responses preserve
|
||||
+pending reconciliation, including any valid returned UID/RV, and never gain a
|
||||
+blind delete/retry path. Local one-shot state remains non-durable.
|
||||
--- /dev/null
|
||||
+++ b/scripts/bounded_public_command.py
|
||||
@@ -0,0 +1,107 @@
|
||||
+"""POSIX byte/time bounded subprocess collection; no credential/session authority."""
|
||||
+from __future__ import annotations
|
||||
+
|
||||
+import json
|
||||
+import os
|
||||
+import selectors
|
||||
+import signal
|
||||
+import subprocess
|
||||
+import time
|
||||
+
|
||||
+MAX_OUTPUT = 262144 # combined stdout + stderr; stderr is counted, never retained
|
||||
+MAX_INPUT = 65536
|
||||
+CLEANUP_TIMEOUT = 1
|
||||
+
|
||||
+
|
||||
+class CommandRefused(Exception):
|
||||
+ """Only fixed diagnostics escape this transport."""
|
||||
+
|
||||
+
|
||||
+def command(argv, *, payload=None, timeout=6):
|
||||
+ if not isinstance(timeout, (float, int)) or not 0 < timeout <= 6:
|
||||
+ raise CommandRefused('command_budget_invalid')
|
||||
+ try:
|
||||
+ body = b'' if payload is None else json.dumps(payload).encode()
|
||||
+ if len(body) > MAX_INPUT:
|
||||
+ raise CommandRefused('command_input_bound')
|
||||
+ except (ValueError, TypeError):
|
||||
+ raise CommandRefused('command_input_invalid') from None
|
||||
+ process = None
|
||||
+ selector = selectors.DefaultSelector()
|
||||
+ output = bytearray()
|
||||
+ total = 0
|
||||
+ offset = 0
|
||||
+ failure = None
|
||||
+ code = None
|
||||
+ deadline = time.monotonic() + timeout
|
||||
+ try:
|
||||
+ process = subprocess.Popen(argv, stdin=subprocess.PIPE,
|
||||
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={},
|
||||
+ start_new_session=True, close_fds=True, shell=False)
|
||||
+ for stream in (process.stdout, process.stderr, process.stdin):
|
||||
+ os.set_blocking(stream.fileno(), False)
|
||||
+ selector.register(process.stdout, selectors.EVENT_READ, 'stdout')
|
||||
+ selector.register(process.stderr, selectors.EVENT_READ, 'stderr')
|
||||
+ if body:
|
||||
+ selector.register(process.stdin, selectors.EVENT_WRITE, 'stdin')
|
||||
+ else:
|
||||
+ process.stdin.close()
|
||||
+ while True:
|
||||
+ remaining = deadline - time.monotonic()
|
||||
+ if remaining <= 0:
|
||||
+ raise CommandRefused('command_timeout')
|
||||
+ # WNOWAIT deliberately leaves the leader unreaped so its PID/PGID
|
||||
+ # cannot be recycled before final process-group cleanup.
|
||||
+ state = os.waitid(os.P_PID, process.pid,
|
||||
+ os.WEXITED | os.WNOHANG | os.WNOWAIT)
|
||||
+ if state is not None and not selector.get_map():
|
||||
+ break
|
||||
+ for key, _ in selector.select(min(remaining, .05)):
|
||||
+ stream = key.fileobj
|
||||
+ if key.data == 'stdin':
|
||||
+ try:
|
||||
+ offset += os.write(stream.fileno(), body[offset:offset + 4096])
|
||||
+ except BlockingIOError:
|
||||
+ continue
|
||||
+ if offset == len(body):
|
||||
+ selector.unregister(stream); stream.close()
|
||||
+ continue
|
||||
+ try:
|
||||
+ block = os.read(stream.fileno(), 4096)
|
||||
+ except BlockingIOError:
|
||||
+ continue
|
||||
+ if not block:
|
||||
+ selector.unregister(stream); stream.close()
|
||||
+ continue
|
||||
+ total += len(block)
|
||||
+ if total > MAX_OUTPUT:
|
||||
+ raise CommandRefused('command_output_bound')
|
||||
+ if key.data == 'stdout':
|
||||
+ output.extend(block)
|
||||
+ except CommandRefused as error:
|
||||
+ failure = str(error)
|
||||
+ except Exception:
|
||||
+ failure = 'command_failed'
|
||||
+ finally:
|
||||
+ selector.close()
|
||||
+ if process is not None:
|
||||
+ # Also kills descendants that kept pipes open or closed all pipes.
|
||||
+ # The leader has not been reaped, avoiding PGID reuse races.
|
||||
+ try:
|
||||
+ os.killpg(process.pid, signal.SIGKILL)
|
||||
+ except ProcessLookupError:
|
||||
+ pass
|
||||
+ except Exception:
|
||||
+ failure = 'command_cleanup_unconfirmed'
|
||||
+ for stream in (process.stdin, process.stdout, process.stderr):
|
||||
+ if stream is not None:
|
||||
+ stream.close()
|
||||
+ try:
|
||||
+ code = process.wait(timeout=CLEANUP_TIMEOUT)
|
||||
+ except Exception:
|
||||
+ failure = 'command_cleanup_unconfirmed'
|
||||
+ if failure is not None:
|
||||
+ raise CommandRefused(failure) from None
|
||||
+ if code != 0:
|
||||
+ raise CommandRefused('command_failed') from None
|
||||
+ return bytes(output)
|
||||
--- /dev/null
|
||||
+++ b/scripts/keycape_issuer_delivery.py
|
||||
@@ -0,0 +1,284 @@
|
||||
+"""RPF-1.3-T4 source mechanics; production admission deliberately unavailable."""
|
||||
+from __future__ import annotations
|
||||
+
|
||||
+from copy import deepcopy
|
||||
+from datetime import datetime, timezone
|
||||
+import hashlib
|
||||
+import json
|
||||
+import re
|
||||
+import time
|
||||
+
|
||||
+from keycape_issuer_eso import documents, require_fixed_store, Refused, canonical_external_spec
|
||||
+from bounded_public_command import command, CommandRefused
|
||||
+
|
||||
+POLL_COUNT = 10
|
||||
+POLL_DELAY = 1
|
||||
+ATTEMPT_TIMEOUT = 30
|
||||
+API = '/apis/external-secrets.io/v1'
|
||||
+
|
||||
+
|
||||
+class Uncertain(Refused):
|
||||
+ def __init__(self, reason, revision=None):
|
||||
+ super().__init__(reason)
|
||||
+ self.revision = deepcopy(revision)
|
||||
+
|
||||
+
|
||||
+def production_bindings(*args, **kwargs):
|
||||
+ raise Refused('delivery_admission_exclusion_session_journal_evidence_revocation_unavailable')
|
||||
+
|
||||
+
|
||||
+def _metadata(obj):
|
||||
+ if not isinstance(obj, dict) or not isinstance(obj.get('metadata'), dict):
|
||||
+ raise Refused('public_metadata_invalid')
|
||||
+ return obj['metadata']
|
||||
+
|
||||
+
|
||||
+def _revision(obj):
|
||||
+ meta = _metadata(obj)
|
||||
+ result = {'uid': meta.get('uid'), 'resource_version': meta.get('resourceVersion')}
|
||||
+ if any(not isinstance(x, str) or not re.fullmatch(r'[A-Za-z0-9._:-]{1,192}', x)
|
||||
+ for x in result.values()):
|
||||
+ raise Refused('public_revision_invalid')
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
+def _generation(obj):
|
||||
+ value = _metadata(obj).get('generation')
|
||||
+ if type(value) is not int or not 1 <= value <= 2**63 - 1:
|
||||
+ raise Refused('public_generation_invalid')
|
||||
+ return value
|
||||
+
|
||||
+
|
||||
+def _synced_version(obj):
|
||||
+ """ESO v0.16.1 util.GetResourceVersion + utils.ObjectHash, not metadata RV.
|
||||
+
|
||||
+ Go fmt %+v on the actual two-field struct uses sorted string maps, raw
|
||||
+ keys/values, and map[] for both nil and empty maps. Tests compare this
|
||||
+ implementation to a standalone Go extraction of that exact upstream code.
|
||||
+ This MD5 is the controller's revision token, never authentication evidence.
|
||||
+ """
|
||||
+ meta = _metadata(obj)
|
||||
+ maps = []
|
||||
+ for name in ('annotations', 'labels'):
|
||||
+ value = meta.get(name)
|
||||
+ if value is None:
|
||||
+ value = {}
|
||||
+ if not isinstance(value, dict) or any(
|
||||
+ not isinstance(k, str) or not isinstance(v, str) for k, v in value.items()):
|
||||
+ raise Refused('public_metadata_map_invalid')
|
||||
+ try:
|
||||
+ textual = 'map[' + ' '.join(k + ':' + value[k] for k in sorted(value)) + ']'
|
||||
+ textual.encode('utf-8', errors='strict')
|
||||
+ except UnicodeError:
|
||||
+ raise Refused('public_metadata_map_invalid') from None
|
||||
+ maps.append(name + ':' + textual)
|
||||
+ hashed = hashlib.md5(('{' + ' '.join(maps) + '}').encode(), usedforsecurity=False).hexdigest()
|
||||
+ return str(_generation(obj)) + '-' + hashed
|
||||
+
|
||||
+
|
||||
+def _ready(obj):
|
||||
+ status = obj.get('status')
|
||||
+ if not isinstance(status, dict):
|
||||
+ return False
|
||||
+ conditions = status.get('conditions')
|
||||
+ if not isinstance(conditions, list) or any(not isinstance(c, dict) for c in conditions):
|
||||
+ return False
|
||||
+ ready = [c for c in conditions if c.get('type') == 'Ready']
|
||||
+ if len(ready) != 1 or any(ready[0].get(k) != v for k, v in {
|
||||
+ 'status': 'True', 'reason': 'SecretSynced', 'message': 'secret synced'}.items()):
|
||||
+ return False
|
||||
+ if status.get('syncedResourceVersion') != _synced_version(obj):
|
||||
+ return False
|
||||
+ stamp = status.get('refreshTime')
|
||||
+ if not isinstance(stamp, str) or not re.fullmatch(
|
||||
+ r'\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)', stamp):
|
||||
+ return False
|
||||
+ try:
|
||||
+ refreshed = datetime.fromisoformat(stamp.replace('Z', '+00:00'))
|
||||
+ age = (datetime.now(timezone.utc) - refreshed).total_seconds()
|
||||
+ return 0 <= age <= 300
|
||||
+ except (ValueError, OverflowError):
|
||||
+ return False
|
||||
+
|
||||
+
|
||||
+class _DeliveryMechanics:
|
||||
+ """No authority boundary: injected calls in tests, unusable ambient transport.
|
||||
+
|
||||
+ Store observations detect drift; they cannot exclude an ABA race between
|
||||
+ observations or bind ESO's store lookup atomically. Production needs real
|
||||
+ owner exclusion/admission in addition to authenticated caller facilities.
|
||||
+ """
|
||||
+ def __init__(self, client_id):
|
||||
+ self.client_id = client_id
|
||||
+ self.store, self.external = documents(client_id)
|
||||
+ self.created = None
|
||||
+ self.pending = False
|
||||
+ self.used = False
|
||||
+ self.deadline = None
|
||||
+
|
||||
+ def _call(self, args, payload=None):
|
||||
+ try:
|
||||
+ remaining = self.deadline - time.monotonic()
|
||||
+ if remaining <= 0:
|
||||
+ raise Refused('attempt_deadline')
|
||||
+ raw = command(['kubectl', '--kubeconfig=/dev/null',
|
||||
+ '--request-timeout=5s', *args],
|
||||
+ payload=payload, timeout=min(6, remaining))
|
||||
+ if time.monotonic() >= self.deadline:
|
||||
+ raise Refused('attempt_deadline')
|
||||
+ # Runner enforces the same bound during collection, not just here.
|
||||
+ if not isinstance(raw, bytes) or len(raw) > 262144:
|
||||
+ raise Refused('public_response_bound')
|
||||
+ if not raw.strip():
|
||||
+ return None
|
||||
+ value = json.loads(raw)
|
||||
+ if not isinstance(value, dict):
|
||||
+ raise Refused('public_response_shape')
|
||||
+ return value
|
||||
+ except Refused:
|
||||
+ raise
|
||||
+ except Exception:
|
||||
+ raise Refused('public_command_failed') from None
|
||||
+
|
||||
+ def _get(self, store=False):
|
||||
+ if store:
|
||||
+ return self._call(['get', '--raw', API + '/clustersecretstores/' + self.store['metadata']['name']])
|
||||
+ # Raw GET fixes API version and avoids kubectl mapper fallback. A fixed
|
||||
+ # field-selected collection provides structured successful absence;
|
||||
+ # stderr/nonzero is never interpreted as NotFound. No Secret is listed.
|
||||
+ uri = (API + '/namespaces/sso/externalsecrets?fieldSelector=metadata.name%3D'
|
||||
+ + self.external['metadata']['name'] + '&limit=2')
|
||||
+ listing = self._call(['get', '--raw', uri])
|
||||
+ if (not isinstance(listing, dict) or listing.get('apiVersion') != 'external-secrets.io/v1'
|
||||
+ or listing.get('kind') != 'ExternalSecretList'
|
||||
+ or not isinstance(listing.get('metadata'), dict)
|
||||
+ or listing['metadata'].get('continue') not in (None, '')
|
||||
+ or not isinstance(listing.get('items'), list) or len(listing['items']) > 1):
|
||||
+ raise Refused('public_collection_invalid')
|
||||
+ if not listing['items']:
|
||||
+ return None
|
||||
+ value = listing['items'][0]
|
||||
+ meta = _metadata(value)
|
||||
+ if (value.get('apiVersion') != 'external-secrets.io/v1'
|
||||
+ or value.get('kind') != 'ExternalSecret'
|
||||
+ or meta.get('name') != self.external['metadata']['name']
|
||||
+ or meta.get('namespace') != 'sso'):
|
||||
+ raise Refused('public_collection_identity_invalid')
|
||||
+ return value
|
||||
+
|
||||
+ def _store_revision(self, sealed=None):
|
||||
+ store = self._get(store=True)
|
||||
+ require_fixed_store(store, self.client_id)
|
||||
+ if _metadata(store).get('deletionTimestamp'):
|
||||
+ raise Refused('store_terminating')
|
||||
+ revision = _revision(store)
|
||||
+ # Actual Store status has no generation binding. Generation 1 excludes
|
||||
+ # a prior spec update under Kubernetes generation semantics. For any
|
||||
+ # later generation Ready cannot establish current-spec validation.
|
||||
+ if _generation(store) != 1:
|
||||
+ raise Refused('store_ready_generation_unverifiable')
|
||||
+ conditions = store.get('status', {}).get('conditions', [])
|
||||
+ ready = [c for c in conditions if isinstance(c, dict) and c.get('type') == 'Ready']
|
||||
+ if len(ready) != 1 or ready[0].get('reason') != 'Valid' or ready[0].get('message') != 'store validated':
|
||||
+ raise Refused('store_validation_unverified')
|
||||
+ if sealed is not None and sealed != revision:
|
||||
+ raise Refused('store_revision_changed')
|
||||
+ return revision
|
||||
+
|
||||
+ def _match(self, obj):
|
||||
+ metadata = _metadata(obj)
|
||||
+ if (obj.get('apiVersion') != self.external['apiVersion']
|
||||
+ or obj.get('kind') != 'ExternalSecret'
|
||||
+ or metadata.get('name') != self.external['metadata']['name']
|
||||
+ or metadata.get('namespace') != 'sso' or metadata.get('deletionTimestamp')
|
||||
+ or canonical_external_spec(obj.get('spec')) != canonical_external_spec(self.external['spec'])):
|
||||
+ raise Refused('delivery_conflict')
|
||||
+ _generation(obj)
|
||||
+ return _revision(obj)
|
||||
+
|
||||
+ def _receipt(self, operation, before, after):
|
||||
+ return {'schema': 'netkingdom.deliver-secret.v1', 'operation': operation,
|
||||
+ 'namespace': 'sso', 'name': self.external['metadata']['name'],
|
||||
+ 'custody_reference': 'platform/workloads/keycape/' + self.client_id,
|
||||
+ 'key_names': ['client-secret'], 'values_emitted': False,
|
||||
+ 'changed': before is None if operation == 'deliver' else True,
|
||||
+ 'before': before, 'after': after}
|
||||
+
|
||||
+ def deliver(self):
|
||||
+ if self.used or self.pending or self.created is not None:
|
||||
+ raise Refused('attempt_already_used_or_unreconciled')
|
||||
+ self.used = True # local one-shot only, never a durable admission journal
|
||||
+ self.deadline = time.monotonic() + ATTEMPT_TIMEOUT
|
||||
+ sealed_store = self._store_revision()
|
||||
+ old = self._get()
|
||||
+ before = self._match(old) if old is not None else None
|
||||
+ uid = before['uid'] if before else None
|
||||
+ self._store_revision(sealed_store)
|
||||
+ if old is None:
|
||||
+ self.pending = True
|
||||
+ try:
|
||||
+ created = self._call(['-n', 'sso', 'create', '-f', '-', '-o', 'json'], self.external)
|
||||
+ # Retain response identity even if admission changed its spec.
|
||||
+ # Pending prevents deletion until exact verified delivery.
|
||||
+ self.created = _revision(created)
|
||||
+ self._match(created)
|
||||
+ uid = self.created['uid']
|
||||
+ except Exception:
|
||||
+ raise Uncertain('create_outcome_unverified', self.created) from None
|
||||
+ try:
|
||||
+ for index in range(POLL_COUNT):
|
||||
+ self._store_revision(sealed_store)
|
||||
+ current = self._get()
|
||||
+ after = self._match(current)
|
||||
+ if after['uid'] != uid:
|
||||
+ raise Refused('delivery_replaced')
|
||||
+ if _ready(current):
|
||||
+ self._store_revision(sealed_store)
|
||||
+ if self.created is not None:
|
||||
+ self.created = after
|
||||
+ self.pending = False
|
||||
+ return self._receipt('deliver', before, after)
|
||||
+ if index + 1 < POLL_COUNT:
|
||||
+ remaining = self.deadline - time.monotonic()
|
||||
+ if remaining <= 0:
|
||||
+ raise Refused('attempt_deadline')
|
||||
+ time.sleep(min(POLL_DELAY, remaining))
|
||||
+ raise Refused('delivery_current_ready_timeout')
|
||||
+ except Exception:
|
||||
+ if self.pending or self.created is not None:
|
||||
+ raise Uncertain('created_delivery_requires_reconciliation', self.created) from None
|
||||
+ raise Refused('delivery_verification_failed') from None
|
||||
+
|
||||
+ def remove(self, revision):
|
||||
+ if self.pending or self.created is None or revision != self.created:
|
||||
+ raise Refused('no_verified_case_creation')
|
||||
+ self.deadline = time.monotonic() + ATTEMPT_TIMEOUT
|
||||
+ try:
|
||||
+ if self._match(self._get()) != self.created:
|
||||
+ raise Refused('created_revision_changed')
|
||||
+ except Exception:
|
||||
+ raise Uncertain('created_revision_changed', self.created) from None
|
||||
+ uri = API + '/namespaces/sso/externalsecrets/' + self.external['metadata']['name']
|
||||
+ options = {'apiVersion': 'v1', 'kind': 'DeleteOptions',
|
||||
+ 'preconditions': {'uid': self.created['uid'],
|
||||
+ 'resourceVersion': self.created['resource_version']}}
|
||||
+ prior = deepcopy(self.created)
|
||||
+ self.pending = True
|
||||
+ try:
|
||||
+ self._call(['delete', '--raw', uri, '-f', '-'], options)
|
||||
+ for index in range(POLL_COUNT):
|
||||
+ current = self._get()
|
||||
+ if current is None:
|
||||
+ self.created = None
|
||||
+ self.pending = False
|
||||
+ return self._receipt('remove', prior, None)
|
||||
+ if _revision(current)['uid'] != prior['uid']:
|
||||
+ raise Refused('replacement_preserved')
|
||||
+ if index + 1 < POLL_COUNT:
|
||||
+ remaining = self.deadline - time.monotonic()
|
||||
+ if remaining <= 0:
|
||||
+ raise Refused('attempt_deadline')
|
||||
+ time.sleep(min(POLL_DELAY, remaining))
|
||||
+ raise Refused('delete_unconfirmed')
|
||||
+ except Exception:
|
||||
+ raise Uncertain('cleanup_requires_reconciliation', prior) from None
|
||||
--- a/scripts/keycape_issuer_eso.py
|
||||
+++ b/scripts/keycape_issuer_eso.py
|
||||
@@ -37,6 +37,23 @@
|
||||
return deepcopy(documents(client_id)[1])
|
||||
|
||||
|
||||
+def canonical_external_spec(spec):
|
||||
+ """Normalize only the three pinned ESO v0.16.1 remoteRef defaults.
|
||||
+
|
||||
+ Omitted and explicit defaults are equivalent; all other fields remain exact.
|
||||
+ This says nothing about freshness, admission authority or Store continuity.
|
||||
+ """
|
||||
+ value = deepcopy(spec)
|
||||
+ if isinstance(value, dict) and isinstance(value.get('data'), list):
|
||||
+ for item in value['data']:
|
||||
+ if isinstance(item, dict) and isinstance(item.get('remoteRef'), dict):
|
||||
+ for name, default in (('conversionStrategy', 'Default'),
|
||||
+ ('decodingStrategy', 'None'),
|
||||
+ ('metadataPolicy', 'None')):
|
||||
+ item['remoteRef'].setdefault(name, default)
|
||||
+ return value
|
||||
+
|
||||
+
|
||||
def _matching_public_resource(observed: dict, expected: dict) -> None:
|
||||
if not isinstance(observed, dict):
|
||||
raise Refused('public resource observation required')
|
||||
@@ -50,7 +67,10 @@
|
||||
or observed.get('kind') != expected['kind']
|
||||
or metadata.get('name') != expected['metadata']['name']
|
||||
or not namespace_matches
|
||||
- or observed.get('spec') != expected['spec']):
|
||||
+ or (canonical_external_spec(observed.get('spec'))
|
||||
+ != canonical_external_spec(expected['spec'])
|
||||
+ if expected['kind'] == 'ExternalSecret'
|
||||
+ else observed.get('spec') != expected['spec'])):
|
||||
raise Refused('fixed issuer resource differs')
|
||||
status = observed.get('status')
|
||||
conditions = status.get('conditions', []) if isinstance(status, dict) else []
|
||||
--- /dev/null
|
||||
+++ b/tests/eso-hash-vectors.json
|
||||
@@ -0,0 +1,57 @@
|
||||
+[
|
||||
+ {
|
||||
+ "metadata": {
|
||||
+ "generation": 1
|
||||
+ },
|
||||
+ "expected": "1-2f2c636d06132f67992c15a1daeb0a5f"
|
||||
+ },
|
||||
+ {
|
||||
+ "metadata": {
|
||||
+ "generation": 2,
|
||||
+ "annotations": {},
|
||||
+ "labels": {}
|
||||
+ },
|
||||
+ "expected": "2-2f2c636d06132f67992c15a1daeb0a5f"
|
||||
+ },
|
||||
+ {
|
||||
+ "metadata": {
|
||||
+ "generation": 3,
|
||||
+ "annotations": {
|
||||
+ "b": "2",
|
||||
+ "a": "1"
|
||||
+ },
|
||||
+ "labels": {
|
||||
+ "z": "last",
|
||||
+ "c": "first"
|
||||
+ }
|
||||
+ },
|
||||
+ "expected": "3-7ad4f86f5aba928cde01913c7b9d962a"
|
||||
+ },
|
||||
+ {
|
||||
+ "metadata": {
|
||||
+ "generation": 4,
|
||||
+ "annotations": {
|
||||
+ "a:b": "c d",
|
||||
+ "a b": "[]:%+v"
|
||||
+ },
|
||||
+ "labels": {
|
||||
+ "newline\n": "tab\t"
|
||||
+ }
|
||||
+ },
|
||||
+ "expected": "4-48e35b520c38b2944d461645440cc3cd"
|
||||
+ },
|
||||
+ {
|
||||
+ "metadata": {
|
||||
+ "generation": 9223372036854775807,
|
||||
+ "annotations": {
|
||||
+ "é": "好",
|
||||
+ "🦊": "😀"
|
||||
+ },
|
||||
+ "labels": {
|
||||
+ "": "",
|
||||
+ "null": "\u0000"
|
||||
+ }
|
||||
+ },
|
||||
+ "expected": "9223372036854775807-da6b2f7c16875cb96b3a00d83378eda3"
|
||||
+ }
|
||||
+]
|
||||
--- /dev/null
|
||||
+++ b/tests/test_bounded_public_command.py
|
||||
@@ -0,0 +1,95 @@
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import sys
|
||||
+import tempfile
|
||||
+import time
|
||||
+import unittest
|
||||
+from unittest.mock import patch
|
||||
+
|
||||
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
+import bounded_public_command as runner
|
||||
+
|
||||
+# Every launched fixture denies socket creation in its own interpreter as well
|
||||
+# as the parent. They only perform controlled local stdout/stderr/process work.
|
||||
+PREFIX = "import socket; socket.socket=lambda *a,**k: (_ for _ in ()).throw(AssertionError('network forbidden')); "
|
||||
+
|
||||
+
|
||||
+class BoundedCommand(unittest.TestCase):
|
||||
+ def setUp(self):
|
||||
+ p = patch.dict(os.environ, {}, clear=True); p.start(); self.addCleanup(p.stop)
|
||||
+ p = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
+ p.start(); self.addCleanup(p.stop)
|
||||
+
|
||||
+ def call(self, code, timeout=2, payload=None):
|
||||
+ return runner.command(['/usr/bin/python3', '-c', PREFIX + code], timeout=timeout, payload=payload)
|
||||
+
|
||||
+ def test_empty_environment_and_input_roundtrip(self):
|
||||
+ raw = self.call("import os,sys,json; b=sys.stdin.buffer.read(); print(json.dumps({'keys':sorted(os.environ),'body':json.loads(b)}))", payload={'fixed': 'metadata'})
|
||||
+ import json
|
||||
+ response = json.loads(raw)
|
||||
+ # CPython may inject LC_CTYPE during startup even though exec env={};
|
||||
+ # neither HOME nor credential variables are inherited.
|
||||
+ self.assertFalse(set(response['keys']) - {'LC_CTYPE'})
|
||||
+ self.assertEqual(response['body'], {'fixed': 'metadata'})
|
||||
+
|
||||
+ def test_stdout_flood_stopped_with_fixed_error(self):
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
+ self.call("import os; [os.write(1,b'PRIVATE'*4096) for _ in range(1000)]")
|
||||
+
|
||||
+ def test_stderr_flood_stopped_with_fixed_error(self):
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
+ self.call("import os; [os.write(2,b'PRIVATE'*4096) for _ in range(1000)]")
|
||||
+
|
||||
+ def test_combined_cap_not_independent_per_stream(self):
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_output_bound$'):
|
||||
+ self.call("import os; os.write(1,b'x'*140000); os.write(2,b'y'*140000)")
|
||||
+
|
||||
+ def test_error_output_never_escapes(self):
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_failed$') as error:
|
||||
+ self.call("import sys; print('PRIVATE'); print('PRIVATE',file=sys.stderr); sys.exit(7)")
|
||||
+ self.assertNotIn('PRIVATE', str(error.exception))
|
||||
+
|
||||
+ def test_sleep_timeout_is_real_and_bounded(self):
|
||||
+ start = time.monotonic()
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_timeout$'):
|
||||
+ self.call('import time; time.sleep(60)', timeout=.15)
|
||||
+ self.assertLess(time.monotonic() - start, 2)
|
||||
+
|
||||
+ def test_descendant_holding_pipes_is_killed(self):
|
||||
+ with tempfile.TemporaryDirectory(prefix='delivery-runner-') as scratch:
|
||||
+ pidfile = Path(scratch) / 'descendant'
|
||||
+ code = ("import os,time; pid=os.fork(); "
|
||||
+ "(time.sleep(60) if pid==0 else "
|
||||
+ f"(open({str(pidfile)!r},'w').write(str(pid)),os._exit(0)))")
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_timeout$'):
|
||||
+ self.call(code, timeout=.2)
|
||||
+ self.assertTrue(pidfile.exists())
|
||||
+ pid = pidfile.read_text()
|
||||
+ status = Path('/proc') / pid / 'stat'
|
||||
+ for _ in range(20):
|
||||
+ if not status.exists() or status.read_text().split()[2] == 'Z': break
|
||||
+ time.sleep(.01)
|
||||
+ if status.exists(): self.assertEqual(status.read_text().split()[2], 'Z')
|
||||
+
|
||||
+ def test_descendant_with_closed_pipes_is_killed_on_success(self):
|
||||
+ with tempfile.TemporaryDirectory(prefix='delivery-runner-') as scratch:
|
||||
+ pidfile = Path(scratch) / 'descendant'
|
||||
+ code = ("import os,time; pid=os.fork(); "
|
||||
+ "(os.close(0),os.close(1),os.close(2),time.sleep(60)) if pid==0 else "
|
||||
+ f"(open({str(pidfile)!r},'w').write(str(pid)),print('ok'))")
|
||||
+ self.assertEqual(self.call(code), b'ok\n')
|
||||
+ status = Path('/proc') / pidfile.read_text() / 'stat'
|
||||
+ # Kill delivery may race the proc-state observation; wait boundedly.
|
||||
+ for _ in range(20):
|
||||
+ if not status.exists() or status.read_text().split()[2] == 'Z': break
|
||||
+ time.sleep(.01)
|
||||
+ if status.exists(): self.assertEqual(status.read_text().split()[2], 'Z')
|
||||
+
|
||||
+ def test_input_bound_and_spawn_failure_fixed(self):
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_input_bound$'):
|
||||
+ self.call('pass', payload={'data': 'x'*65536})
|
||||
+ with self.assertRaisesRegex(runner.CommandRefused, '^command_failed$'):
|
||||
+ runner.command(['/nonexistent-fixture-binary'], timeout=.1)
|
||||
+
|
||||
+
|
||||
+if __name__ == '__main__': unittest.main()
|
||||
--- /dev/null
|
||||
+++ b/tests/test_keycape_issuer_delivery.py
|
||||
@@ -0,0 +1,284 @@
|
||||
+from copy import deepcopy
|
||||
+from datetime import datetime, timedelta, timezone
|
||||
+import json
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import sys
|
||||
+import unittest
|
||||
+from unittest.mock import patch
|
||||
+
|
||||
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
+import keycape_issuer_delivery as lane
|
||||
+from bounded_public_command import CommandRefused
|
||||
+
|
||||
+
|
||||
+class Mechanics(unittest.TestCase):
|
||||
+ def setUp(self):
|
||||
+ p = patch.dict(os.environ, {}, clear=True); p.start(); self.addCleanup(p.stop)
|
||||
+ p = patch('socket.socket', side_effect=AssertionError('network forbidden'))
|
||||
+ p.start(); self.addCleanup(p.stop)
|
||||
+ self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ self.store = self.resource(self.m.store, store=True)
|
||||
+ self.obj = None
|
||||
+ self.calls = []
|
||||
+ self.store_reads = 0
|
||||
+ self.hook = lambda args: None
|
||||
+ self.fail_create = False
|
||||
+ self.fail_delete = False
|
||||
+ self.create_mutate = lambda obj: obj
|
||||
+ self.force_list = None
|
||||
+ self.poll_object = None
|
||||
+ self.clock = 0
|
||||
+ self.clock_step = 0
|
||||
+ p = patch.object(lane, 'command', side_effect=self.command)
|
||||
+ p.start(); self.addCleanup(p.stop)
|
||||
+ p = patch.object(lane.time, 'sleep'); p.start(); self.addCleanup(p.stop)
|
||||
+ p = patch.object(lane.time, 'monotonic', side_effect=lambda: self.clock)
|
||||
+ p.start(); self.addCleanup(p.stop)
|
||||
+
|
||||
+ def resource(self, doc, store=False):
|
||||
+ value = deepcopy(doc)
|
||||
+ value['metadata'].update(uid='store-uid' if store else 'created-uid', resourceVersion='10', generation=1)
|
||||
+ if store:
|
||||
+ value['status'] = {'conditions': [{'type': 'Ready', 'status': 'True',
|
||||
+ 'reason': 'Valid', 'message': 'store validated'}]}
|
||||
+ else:
|
||||
+ value['spec'] = lane.canonical_external_spec(value['spec'])
|
||||
+ value['status'] = {'conditions': [{'type': 'Ready', 'status': 'True',
|
||||
+ 'reason': 'SecretSynced', 'message': 'secret synced'}],
|
||||
+ 'syncedResourceVersion': lane._synced_version(value),
|
||||
+ 'refreshTime': datetime.now(timezone.utc).isoformat()}
|
||||
+ return value
|
||||
+
|
||||
+ def command(self, args, *, payload=None, timeout=6):
|
||||
+ self.assertEqual(args[:3], ['kubectl', '--kubeconfig=/dev/null', '--request-timeout=5s'])
|
||||
+ self.assertGreater(timeout, 0); self.assertLessEqual(timeout, 6)
|
||||
+ self.calls.append((deepcopy(args), deepcopy(payload)))
|
||||
+ self.clock += self.clock_step
|
||||
+ self.hook(args)
|
||||
+ if 'create' in args:
|
||||
+ if self.fail_create:
|
||||
+ raise CommandRefused('PRIVATE PROVIDER VALUE')
|
||||
+ self.obj = self.create_mutate(self.resource(self.m.external))
|
||||
+ value = self.obj
|
||||
+ elif 'delete' in args:
|
||||
+ if self.fail_delete:
|
||||
+ raise CommandRefused('PRIVATE PROVIDER VALUE')
|
||||
+ revision = lane._revision(self.obj)
|
||||
+ if payload['preconditions'] != {'uid': revision['uid'], 'resourceVersion': revision['resource_version']}:
|
||||
+ raise CommandRefused('conditional conflict fixture')
|
||||
+ self.obj = None
|
||||
+ value = {'kind': 'Status', 'status': 'Success'}
|
||||
+ elif '/clustersecretstores/' in args[-1]:
|
||||
+ self.store_reads += 1
|
||||
+ value = self.store
|
||||
+ else:
|
||||
+ self.assertIn('get', args); self.assertIn('--raw', args)
|
||||
+ self.assertEqual(args[-1], lane.API + '/namespaces/sso/externalsecrets?fieldSelector=metadata.name%3Dkeycape-forgejo-client&limit=2')
|
||||
+ value = self.force_list if self.force_list is not None else {
|
||||
+ 'apiVersion': 'external-secrets.io/v1', 'kind': 'ExternalSecretList',
|
||||
+ 'metadata': {}, 'items': [] if self.obj is None else [self.poll_object or self.obj]}
|
||||
+ return json.dumps(deepcopy(value)).encode()
|
||||
+
|
||||
+ def test_actual_defaults_create_current_ready_and_conditional_delete(self):
|
||||
+ delivered = self.m.deliver()
|
||||
+ self.assertTrue(delivered['changed']); self.assertIsNone(delivered['before'])
|
||||
+ self.assertGreaterEqual(self.store_reads, 4)
|
||||
+ removed = self.m.remove(delivered['after'])
|
||||
+ self.assertEqual(removed['after'], None)
|
||||
+ args, payload = next(c for c in self.calls if 'delete' in c[0])
|
||||
+ self.assertEqual(args[5], lane.API + '/namespaces/sso/externalsecrets/keycape-forgejo-client')
|
||||
+ self.assertEqual(payload['preconditions'], {'uid': 'created-uid', 'resourceVersion': '10'})
|
||||
+
|
||||
+ def test_existing_no_change_cannot_be_deleted_or_reused(self):
|
||||
+ self.obj = self.resource(self.m.external)
|
||||
+ delivered = self.m.deliver()
|
||||
+ self.assertFalse(delivered['changed'])
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
+ with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
+ self.assertFalse(any('create' in args or 'delete' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_stale_current_spec_and_metadata_hash_never_ready(self):
|
||||
+ obj = self.resource(self.m.external)
|
||||
+ for field, value in [('generation', 2), ('labels', {'new': 'metadata'}), ('annotations', {'new': 'metadata'})]:
|
||||
+ with self.subTest(field=field):
|
||||
+ changed = deepcopy(obj); changed['metadata'][field] = value
|
||||
+ self.assertFalse(lane._ready(changed))
|
||||
+
|
||||
+ def test_exact_go_upstream_revision_vectors(self):
|
||||
+ vectors = json.loads((Path(__file__).parent / 'eso-hash-vectors.json').read_text())
|
||||
+ for vector in vectors:
|
||||
+ self.assertEqual(lane._synced_version({'metadata': vector['metadata']}), vector['expected'])
|
||||
+
|
||||
+ def test_wrong_default_strategies_and_identity_never_match(self):
|
||||
+ for field, value in [('conversionStrategy', 'Unicode'), ('decodingStrategy', 'Base64'),
|
||||
+ ('metadataPolicy', 'Fetch'), ('key', 'elsewhere'), ('extra', 'value')]:
|
||||
+ obj = self.resource(self.m.external)
|
||||
+ obj['spec']['data'][0]['remoteRef'][field] = value
|
||||
+ with self.assertRaises(lane.Refused): self.m._match(obj)
|
||||
+ for field, value in [('namespace', 'default'), ('name', 'other'), ('generation', True)]:
|
||||
+ obj = self.resource(self.m.external); obj['metadata'][field] = value
|
||||
+ with self.assertRaises(lane.Refused): self.m._match(obj)
|
||||
+
|
||||
+ def test_invalid_metadata_maps_never_produce_hash(self):
|
||||
+ for value in ([], 'str', {'key': None}, {'key': '\ud800'}):
|
||||
+ obj = self.resource(self.m.external); obj['metadata']['labels'] = value
|
||||
+ with self.assertRaises(lane.Refused): lane._synced_version(obj)
|
||||
+
|
||||
+ def test_client_override_injection_refused_before_command(self):
|
||||
+ for client in ('x/../../secret', 'x;echo', 'A', None, 'a'*49):
|
||||
+ with self.assertRaises(lane.Refused): lane._DeliveryMechanics(client)
|
||||
+ self.assertEqual(self.calls, [])
|
||||
+
|
||||
+ def test_retained_missing_stale_future_duplicate_status_not_ready(self):
|
||||
+ for variant in ('retained', 'missing', 'old', 'future', 'duplicate', 'invalid_time'):
|
||||
+ with self.subTest(variant=variant):
|
||||
+ obj = self.resource(self.m.external)
|
||||
+ if variant == 'retained': obj['status']['conditions'][0]['message'] = 'secret retained due to DeletionPolicy=Retain'
|
||||
+ if variant == 'missing': obj['status'].pop('syncedResourceVersion')
|
||||
+ if variant in ('old', 'future'):
|
||||
+ delta = -301 if variant == 'old' else 60
|
||||
+ obj['status']['refreshTime'] = (datetime.now(timezone.utc) + timedelta(seconds=delta)).isoformat()
|
||||
+ if variant == 'duplicate': obj['status']['conditions'] *= 2
|
||||
+ if variant == 'invalid_time': obj['status']['refreshTime'] = 'PRIVATE MALFORMED'
|
||||
+ self.assertFalse(lane._ready(obj))
|
||||
+
|
||||
+ def test_stale_delivery_times_out_with_reconciliation_provenance(self):
|
||||
+ self.create_mutate = lambda obj: {**obj, 'status': {'conditions': [{'type': 'Ready', 'status': 'True'}]}}
|
||||
+ with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
+ self.assertEqual(error.exception.revision['uid'], 'created-uid')
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(error.exception.revision)
|
||||
+ self.assertLessEqual(len(self.calls), 2 * lane.POLL_COUNT + 4)
|
||||
+
|
||||
+ def test_store_prior_generation_refused_before_create(self):
|
||||
+ self.store['metadata']['generation'] = 2
|
||||
+ with self.assertRaisesRegex(lane.Refused, 'generation_unverifiable'): self.m.deliver()
|
||||
+ self.assertEqual(len(self.calls), 1)
|
||||
+
|
||||
+ def test_store_replaced_before_precreate_read_prevents_create(self):
|
||||
+ def hook(args):
|
||||
+ if '/externalsecrets?' in args[-1]: self.store['metadata']['uid'] = 'replacement'
|
||||
+ self.hook = hook
|
||||
+ with self.assertRaisesRegex(lane.Refused, 'store_revision_changed'): self.m.deliver()
|
||||
+ self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_store_replaced_inside_create_window_detected_after_mutation(self):
|
||||
+ def hook(args):
|
||||
+ if 'create' in args: self.store['metadata']['uid'] = 'replacement'
|
||||
+ self.hook = hook
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+ self.assertIsNotNone(self.obj) # double-read did not exclude mutation
|
||||
+ self.assertTrue(self.m.pending)
|
||||
+
|
||||
+ def test_store_aba_race_is_not_claimed_excluded(self):
|
||||
+ # A mutation and restoration wholly between observations is invisible.
|
||||
+ seen = []
|
||||
+ def hook(args):
|
||||
+ if 'create' in args:
|
||||
+ original = deepcopy(self.store)
|
||||
+ self.store['metadata']['uid'] = 'transient-replacement'
|
||||
+ seen.append(self.store['metadata']['uid'])
|
||||
+ self.store = original
|
||||
+ seen.append(self.store['metadata']['uid'])
|
||||
+ self.hook = hook
|
||||
+ self.assertTrue(self.m.deliver()['changed'])
|
||||
+ self.assertEqual(len(seen), 2)
|
||||
+ self.assertEqual(seen, ['transient-replacement', 'store-uid'])
|
||||
+
|
||||
+ def test_malformed_create_preserves_fixed_unknown_outcome(self):
|
||||
+ for metadata in (None, [], {}, {'uid': 'created-uid', 'resourceVersion': None}):
|
||||
+ with self.subTest(metadata=metadata):
|
||||
+ self.m = lane._DeliveryMechanics('forgejo'); self.obj = None
|
||||
+ self.create_mutate = lambda obj: {**obj, 'metadata': metadata}
|
||||
+ with self.assertRaises(lane.Uncertain) as error: self.m.deliver()
|
||||
+ self.assertEqual(str(error.exception), 'create_outcome_unverified')
|
||||
+ self.assertTrue(self.m.pending)
|
||||
+
|
||||
+ def test_unknown_create_and_delete_never_blindly_retry(self):
|
||||
+ self.fail_create = True
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+ with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
+ self.fail_create = False; self.m = lane._DeliveryMechanics('forgejo')
|
||||
+ delivered = self.m.deliver(); self.fail_delete = True
|
||||
+ with self.assertRaises(lane.Uncertain) as error: self.m.remove(delivered['after'])
|
||||
+ self.assertNotIn('PRIVATE', str(error.exception))
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
+
|
||||
+ def test_delete_race_uses_server_preconditions_and_preserves_replacement(self):
|
||||
+ delivered = self.m.deliver()
|
||||
+ def hook(args):
|
||||
+ if 'delete' in args: self.obj['metadata']['uid'] = 'replacement'
|
||||
+ self.hook = hook
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
+ self.assertEqual(self.obj['metadata']['uid'], 'replacement')
|
||||
+
|
||||
+ def test_status_rv_changed_before_remove_no_delete(self):
|
||||
+ delivered = self.m.deliver(); self.obj['metadata']['resourceVersion'] = '11'
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
+ self.assertFalse(any('delete' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_fresh_process_and_forged_revision_never_delete(self):
|
||||
+ delivered = self.m.deliver()
|
||||
+ with self.assertRaises(lane.Refused): lane._DeliveryMechanics('forgejo').remove(delivered['after'])
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove({'uid': 'fake', 'resource_version': '10'})
|
||||
+ self.assertFalse(any('delete' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_poll_rv_is_captured(self):
|
||||
+ reads = []
|
||||
+ def hook(args):
|
||||
+ if '/externalsecrets?' in args[-1] and self.obj is not None:
|
||||
+ self.obj['metadata']['resourceVersion'] = '11'
|
||||
+ reads.append(True)
|
||||
+ self.hook = hook
|
||||
+ delivered = self.m.deliver()
|
||||
+ self.assertEqual(delivered['after']['resource_version'], '11')
|
||||
+ self.assertTrue(reads)
|
||||
+
|
||||
+ def test_unconfirmed_delete_is_bounded_and_preserves_reconciliation(self):
|
||||
+ delivered = self.m.deliver()
|
||||
+ prior = deepcopy(self.obj)
|
||||
+ terminating = deepcopy(prior); terminating['metadata']['deletionTimestamp'] = 'now'
|
||||
+ with patch.object(self.m, '_get', side_effect=[prior] + [terminating]*lane.POLL_COUNT):
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
+ self.assertTrue(self.m.pending)
|
||||
+ with self.assertRaises(lane.Refused): self.m.remove(delivered['after'])
|
||||
+
|
||||
+ def test_lost_delete_after_server_removal_is_still_uncertain(self):
|
||||
+ delivered = self.m.deliver(); self.fail_delete = True
|
||||
+ def hook(args):
|
||||
+ if 'delete' in args: self.obj = None
|
||||
+ self.hook = hook
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.remove(delivered['after'])
|
||||
+ self.assertIsNone(self.obj)
|
||||
+ self.assertIsNotNone(self.m.created)
|
||||
+ self.assertTrue(self.m.pending)
|
||||
+
|
||||
+ def test_collection_foreign_object_cannot_be_adopted(self):
|
||||
+ self.obj = self.resource(self.m.external); self.obj['metadata']['name'] = 'other'
|
||||
+ with self.assertRaisesRegex(lane.Refused, 'collection_identity'): self.m.deliver()
|
||||
+ self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_unknown_conflicting_or_unbounded_collection_never_absence(self):
|
||||
+ for listing in ({}, {'kind': 'Status'}, {'apiVersion': 'external-secrets.io/v1',
|
||||
+ 'kind': 'ExternalSecretList', 'metadata': {'continue': 'more'}, 'items': []}):
|
||||
+ with self.subTest(listing=listing):
|
||||
+ self.m = lane._DeliveryMechanics('forgejo'); self.force_list = listing
|
||||
+ with self.assertRaises(lane.Refused): self.m.deliver()
|
||||
+ self.assertFalse(any('create' in args for args, _ in self.calls))
|
||||
+
|
||||
+ def test_total_attempt_deadline_blocks_late_success(self):
|
||||
+ self.clock_step = 6
|
||||
+ with self.assertRaises(lane.Uncertain): self.m.deliver()
|
||||
+ self.assertLessEqual(len(self.calls), 6)
|
||||
+
|
||||
+ def test_no_secret_or_store_mutation_and_factory_refuses(self):
|
||||
+ self.m.deliver()
|
||||
+ for args, payload in self.calls:
|
||||
+ self.assertFalse(set(args) & {'secret', 'secrets'})
|
||||
+ if payload and 'create' in args: self.assertEqual(payload['kind'], 'ExternalSecret')
|
||||
+ before = len(self.calls)
|
||||
+ with self.assertRaises(lane.Refused): lane.production_bindings(authenticated=True)
|
||||
+ self.assertEqual(len(self.calls), before)
|
||||
+
|
||||
+
|
||||
+if __name__ == '__main__': unittest.main()
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Reproduce locally from pinned Git objects. No network or native owner actions."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PLATFORM = Path('/home/worsch/railiance-platform')
|
||||
BASE = '104ed90f42a9a86bf9fda343b49b65a85c9ee4b9'
|
||||
ENV = {'PATH': '/usr/bin:/bin', 'PYTHONDONTWRITEBYTECODE': '1'}
|
||||
|
||||
|
||||
def base_file(name):
|
||||
return subprocess.check_output(['git', '-C', str(PLATFORM), 'show', BASE + ':' + name],
|
||||
env=ENV, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory(prefix='platform-delivery-r2-') as scratch:
|
||||
scratch = Path(scratch)
|
||||
home = scratch / 'empty-home'; home.mkdir()
|
||||
env = {**ENV, 'HOME': str(home)}
|
||||
tree = scratch / 'corrected'
|
||||
shutil.copytree(HERE / 'prepared-source', tree)
|
||||
for name in ('manifests/templates/keycape-issuer-eso.yaml', 'tests/test_keycape_issuer_eso.py'):
|
||||
target = tree / name; target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(base_file(name))
|
||||
runs = []
|
||||
commands = [
|
||||
('corrected-regressions', ['/usr/bin/python3', '-m', 'unittest', 'discover', '-s', str(tree / 'tests'), '-v']),
|
||||
('corrected-predecessor-assertions', ['/usr/bin/python3', str(HERE / 'assert_corrected_regressions.py'), str(tree)]),
|
||||
]
|
||||
for label, argv in commands:
|
||||
result = subprocess.run(argv, env=env, capture_output=True)
|
||||
print(label + ': ' + str(result.returncode))
|
||||
print((result.stdout + result.stderr).decode())
|
||||
runs.append(result.returncode)
|
||||
if any(runs): raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"eso_tag": "v0.16.1",
|
||||
"eso_commit": "8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af",
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/externalsecret/externalsecret_controller.go",
|
||||
"file": "upstream/eso-pkg_controllers_externalsecret_externalsecret_controller.go",
|
||||
"sha256": "e6b18f869361227a19b74fb5d617014e7d8964fbfb6dc47402d128a35cd7bbf1"
|
||||
},
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/externalsecret/util.go",
|
||||
"file": "upstream/eso-pkg_controllers_externalsecret_util.go",
|
||||
"sha256": "350cbef4a8fd56080003a141b3f6bb19af89de7c33999e77159c4f85f11adeef"
|
||||
},
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/util/util.go",
|
||||
"file": "upstream/eso-pkg_controllers_util_util.go",
|
||||
"sha256": "d0337f622f83127cc7798fe493b11f26e9e1c0cb2bde7bfdecc57931edece67e"
|
||||
},
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/controllers/secretstore/common.go",
|
||||
"file": "upstream/eso-pkg_controllers_secretstore_common.go",
|
||||
"sha256": "4f2d8ed21d9dd271f839a3d03fb91c1ad1e2ee695ce944770745dd2b52c0117a"
|
||||
},
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/pkg/utils/utils.go",
|
||||
"file": "upstream/eso-pkg_utils_utils.go",
|
||||
"sha256": "17c722af9a16d07b1e8088b94783ce63ab6d86207b5b6039b1e4daebe0021764"
|
||||
},
|
||||
{
|
||||
"url": "https://raw.githubusercontent.com/external-secrets/external-secrets/8d037d197c88eeb9c4fe489e906c9cb3ab9ee6af/apis/externalsecrets/v1/secretstore_types.go",
|
||||
"file": "upstream/eso-apis_externalsecrets_v1_secretstore_types.go",
|
||||
"sha256": "573f93672c18301c85d978b6184fe0286a8beb6fd3c3620057720ea95196a67d"
|
||||
}
|
||||
],
|
||||
"readiness_limit": "These are source pins, not proof of deployed ESO version or native outcome."
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// SecretStoreSpec defines the desired state of SecretStore.
|
||||
type SecretStoreSpec struct {
|
||||
// Used to select the correct ESO controller (think: ingress.ingressClassName)
|
||||
// The ESO controller is instantiated with a specific controller name and filters ES based on this property
|
||||
// +optional
|
||||
Controller string `json:"controller,omitempty"`
|
||||
|
||||
// Used to configure the provider. Only one provider may be set
|
||||
Provider *SecretStoreProvider `json:"provider"`
|
||||
|
||||
// Used to configure http retries if failed
|
||||
// +optional
|
||||
RetrySettings *SecretStoreRetrySettings `json:"retrySettings,omitempty"`
|
||||
|
||||
// Used to configure store refresh interval in seconds. Empty or 0 will default to the controller config.
|
||||
// +optional
|
||||
RefreshInterval int `json:"refreshInterval,omitempty"`
|
||||
|
||||
// Used to constraint a ClusterSecretStore to specific namespaces. Relevant only to ClusterSecretStore
|
||||
// +optional
|
||||
Conditions []ClusterSecretStoreCondition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in
|
||||
// for a ClusterSecretStore instance.
|
||||
type ClusterSecretStoreCondition struct {
|
||||
// Choose namespace using a labelSelector
|
||||
// +optional
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
|
||||
// Choose namespaces by name
|
||||
// +optional
|
||||
// +kubebuilder:validation:items:MinLength:=1
|
||||
// +kubebuilder:validation:items:MaxLength:=63
|
||||
// +kubebuilder:validation:items:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
|
||||
// Choose namespaces by using regex matching
|
||||
// +optional
|
||||
NamespaceRegexes []string `json:"namespaceRegexes,omitempty"`
|
||||
}
|
||||
|
||||
// SecretStoreProvider contains the provider-specific configuration.
|
||||
// +kubebuilder:validation:MinProperties=1
|
||||
// +kubebuilder:validation:MaxProperties=1
|
||||
type SecretStoreProvider struct {
|
||||
// AWS configures this store to sync secrets using AWS Secret Manager provider
|
||||
// +optional
|
||||
AWS *AWSProvider `json:"aws,omitempty"`
|
||||
|
||||
// AzureKV configures this store to sync secrets using Azure Key Vault provider
|
||||
// +optional
|
||||
AzureKV *AzureKVProvider `json:"azurekv,omitempty"`
|
||||
|
||||
// Akeyless configures this store to sync secrets using Akeyless Vault provider
|
||||
// +optional
|
||||
Akeyless *AkeylessProvider `json:"akeyless,omitempty"`
|
||||
|
||||
// BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider
|
||||
// +optional
|
||||
BitwardenSecretsManager *BitwardenSecretsManagerProvider `json:"bitwardensecretsmanager,omitempty"`
|
||||
|
||||
// Vault configures this store to sync secrets using Hashi provider
|
||||
// +optional
|
||||
Vault *VaultProvider `json:"vault,omitempty"`
|
||||
|
||||
// GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
|
||||
// +optional
|
||||
GCPSM *GCPSMProvider `json:"gcpsm,omitempty"`
|
||||
|
||||
// Oracle configures this store to sync secrets using Oracle Vault provider
|
||||
// +optional
|
||||
Oracle *OracleProvider `json:"oracle,omitempty"`
|
||||
|
||||
// IBM configures this store to sync secrets using IBM Cloud provider
|
||||
// +optional
|
||||
IBM *IBMProvider `json:"ibm,omitempty"`
|
||||
|
||||
// YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
|
||||
// +optional
|
||||
YandexCertificateManager *YandexCertificateManagerProvider `json:"yandexcertificatemanager,omitempty"`
|
||||
|
||||
// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
|
||||
// +optional
|
||||
YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
|
||||
|
||||
// Github configures this store to push Github Action secrets using Github API provider
|
||||
// +optional
|
||||
Github *GithubProvider `json:"github,omitempty"`
|
||||
|
||||
// GitLab configures this store to sync secrets using GitLab Variables provider
|
||||
// +optional
|
||||
Gitlab *GitlabProvider `json:"gitlab,omitempty"`
|
||||
|
||||
// Alibaba configures this store to sync secrets using Alibaba Cloud provider
|
||||
// +optional
|
||||
Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
|
||||
|
||||
// OnePassword configures this store to sync secrets using the 1Password Cloud provider
|
||||
// +optional
|
||||
OnePassword *OnePasswordProvider `json:"onepassword,omitempty"`
|
||||
|
||||
// Webhook configures this store to sync secrets using a generic templated webhook
|
||||
// +optional
|
||||
Webhook *WebhookProvider `json:"webhook,omitempty"`
|
||||
|
||||
// Kubernetes configures this store to sync secrets using a Kubernetes cluster provider
|
||||
// +optional
|
||||
Kubernetes *KubernetesProvider `json:"kubernetes,omitempty"`
|
||||
|
||||
// Fake configures a store with static key/value pairs
|
||||
// +optional
|
||||
Fake *FakeProvider `json:"fake,omitempty"`
|
||||
|
||||
// Senhasegura configures this store to sync secrets using senhasegura provider
|
||||
// +optional
|
||||
Senhasegura *SenhaseguraProvider `json:"senhasegura,omitempty"`
|
||||
|
||||
// Scaleway
|
||||
// +optional
|
||||
Scaleway *ScalewayProvider `json:"scaleway,omitempty"`
|
||||
|
||||
// Doppler configures this store to sync secrets using the Doppler provider
|
||||
// +optional
|
||||
Doppler *DopplerProvider `json:"doppler,omitempty"`
|
||||
|
||||
// Previder configures this store to sync secrets using the Previder provider
|
||||
// +optional
|
||||
Previder *PreviderProvider `json:"previder,omitempty"`
|
||||
|
||||
// Onboardbase configures this store to sync secrets using the Onboardbase provider
|
||||
// +optional
|
||||
Onboardbase *OnboardbaseProvider `json:"onboardbase,omitempty"`
|
||||
|
||||
// KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
|
||||
// +optional
|
||||
KeeperSecurity *KeeperSecurityProvider `json:"keepersecurity,omitempty"`
|
||||
|
||||
// Conjur configures this store to sync secrets using conjur provider
|
||||
// +optional
|
||||
Conjur *ConjurProvider `json:"conjur,omitempty"`
|
||||
|
||||
// Delinea DevOps Secrets Vault
|
||||
// https://docs.delinea.com/online-help/products/devops-secrets-vault/current
|
||||
// +optional
|
||||
Delinea *DelineaProvider `json:"delinea,omitempty"`
|
||||
|
||||
// SecretServer configures this store to sync secrets using SecretServer provider
|
||||
// https://docs.delinea.com/online-help/secret-server/start.htm
|
||||
// +optional
|
||||
SecretServer *SecretServerProvider `json:"secretserver,omitempty"`
|
||||
|
||||
// Chef configures this store to sync secrets with chef server
|
||||
// +optional
|
||||
Chef *ChefProvider `json:"chef,omitempty"`
|
||||
|
||||
// Pulumi configures this store to sync secrets using the Pulumi provider
|
||||
// +optional
|
||||
Pulumi *PulumiProvider `json:"pulumi,omitempty"`
|
||||
|
||||
// Fortanix configures this store to sync secrets using the Fortanix provider
|
||||
// +optional
|
||||
Fortanix *FortanixProvider `json:"fortanix,omitempty"`
|
||||
|
||||
// +optional
|
||||
PasswordDepot *PasswordDepotProvider `json:"passworddepot,omitempty"`
|
||||
|
||||
// +optional
|
||||
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
|
||||
|
||||
// Device42 configures this store to sync secrets using the Device42 provider
|
||||
// +optional
|
||||
Device42 *Device42Provider `json:"device42,omitempty"`
|
||||
|
||||
// Infisical configures this store to sync secrets using the Infisical provider
|
||||
// +optional
|
||||
Infisical *InfisicalProvider `json:"infisical,omitempty"`
|
||||
|
||||
// Beyondtrust configures this store to sync secrets using Password Safe provider.
|
||||
// +optional
|
||||
Beyondtrust *BeyondtrustProvider `json:"beyondtrust,omitempty"`
|
||||
|
||||
// CloudruSM configures this store to sync secrets using the Cloud.ru Secret Manager provider
|
||||
// +optional
|
||||
CloudruSM *CloudruSMProvider `json:"cloudrusm,omitempty"`
|
||||
}
|
||||
|
||||
type CAProviderType string
|
||||
|
||||
const (
|
||||
CAProviderTypeSecret CAProviderType = "Secret"
|
||||
CAProviderTypeConfigMap CAProviderType = "ConfigMap"
|
||||
)
|
||||
|
||||
// Used to provide custom certificate authority (CA) certificates
|
||||
// for a secret store. The CAProvider points to a Secret or ConfigMap resource
|
||||
// that contains a PEM-encoded certificate.
|
||||
type CAProvider struct {
|
||||
// The type of provider to use such as "Secret", or "ConfigMap".
|
||||
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
|
||||
Type CAProviderType `json:"type"`
|
||||
|
||||
// The name of the object located at the provider type.
|
||||
// +kubebuilder:validation:MinLength:=1
|
||||
// +kubebuilder:validation:MaxLength:=253
|
||||
// +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
Name string `json:"name"`
|
||||
|
||||
// The key where the CA certificate can be found in the Secret or ConfigMap.
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:validation:MinLength:=1
|
||||
// +kubebuilder:validation:MaxLength:=253
|
||||
// +kubebuilder:validation:Pattern:=^[-._a-zA-Z0-9]+$
|
||||
Key string `json:"key,omitempty"`
|
||||
|
||||
// The namespace the Provider type is in.
|
||||
// Can only be defined when used in a ClusterSecretStore.
|
||||
// +optional
|
||||
// +kubebuilder:validation:MinLength:=1
|
||||
// +kubebuilder:validation:MaxLength:=63
|
||||
// +kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
Namespace *string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
type SecretStoreRetrySettings struct {
|
||||
MaxRetries *int32 `json:"maxRetries,omitempty"`
|
||||
RetryInterval *string `json:"retryInterval,omitempty"`
|
||||
}
|
||||
|
||||
type SecretStoreConditionType string
|
||||
|
||||
const (
|
||||
SecretStoreReady SecretStoreConditionType = "Ready"
|
||||
|
||||
ReasonInvalidStore = "InvalidStoreConfiguration"
|
||||
ReasonInvalidProviderConfig = "InvalidProviderConfig"
|
||||
ReasonValidationFailed = "ValidationFailed"
|
||||
ReasonStoreValid = "Valid"
|
||||
StoreUnmaintained = "StoreUnmaintained"
|
||||
)
|
||||
|
||||
type SecretStoreStatusCondition struct {
|
||||
Type SecretStoreConditionType `json:"type"`
|
||||
Status corev1.ConditionStatus `json:"status"`
|
||||
|
||||
// +optional
|
||||
Reason string `json:"reason,omitempty"`
|
||||
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// +optional
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
|
||||
}
|
||||
|
||||
// SecretStoreCapabilities defines the possible operations a SecretStore can do.
|
||||
type SecretStoreCapabilities string
|
||||
|
||||
const (
|
||||
SecretStoreReadOnly SecretStoreCapabilities = "ReadOnly"
|
||||
SecretStoreWriteOnly SecretStoreCapabilities = "WriteOnly"
|
||||
SecretStoreReadWrite SecretStoreCapabilities = "ReadWrite"
|
||||
)
|
||||
|
||||
// SecretStoreStatus defines the observed state of the SecretStore.
|
||||
type SecretStoreStatus struct {
|
||||
// +optional
|
||||
Conditions []SecretStoreStatusCondition `json:"conditions,omitempty"`
|
||||
// +optional
|
||||
Capabilities SecretStoreCapabilities `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:storageversion
|
||||
|
||||
// SecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
|
||||
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
|
||||
// +kubebuilder:printcolumn:name="Capabilities",type=string,JSONPath=`.status.capabilities`
|
||||
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:metadata:labels="external-secrets.io/component=controller"
|
||||
// +kubebuilder:resource:scope=Namespaced,categories={external-secrets},shortName=ss
|
||||
type SecretStore struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec SecretStoreSpec `json:"spec,omitempty"`
|
||||
Status SecretStoreStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// SecretStoreList contains a list of SecretStore resources.
|
||||
type SecretStoreList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []SecretStore `json:"items"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:storageversion
|
||||
|
||||
// ClusterSecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
|
||||
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
|
||||
// +kubebuilder:printcolumn:name="Capabilities",type=string,JSONPath=`.status.capabilities`
|
||||
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:metadata:labels="external-secrets.io/component=controller"
|
||||
// +kubebuilder:resource:scope=Cluster,categories={external-secrets},shortName=css
|
||||
type ClusterSecretStore struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec SecretStoreSpec `json:"spec,omitempty"`
|
||||
Status SecretStoreStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ClusterSecretStoreList contains a list of ClusterSecretStore resources.
|
||||
type ClusterSecretStoreList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []ClusterSecretStore `json:"items"`
|
||||
}
|
||||
|
|
@ -0,0 +1,985 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package externalsecret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/ptr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
||||
// Metrics.
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
|
||||
ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/util"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
||||
|
||||
// Loading registered generators.
|
||||
_ "github.com/external-secrets/external-secrets/pkg/generator/register"
|
||||
// Loading registered providers.
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
|
||||
|
||||
// condition messages for "SecretSynced" reason.
|
||||
msgSynced = "secret synced"
|
||||
msgSyncedRetain = "secret retained due to DeletionPolicy=Retain"
|
||||
|
||||
// condition messages for "SecretDeleted" reason.
|
||||
msgDeleted = "secret deleted due to DeletionPolicy=Delete"
|
||||
|
||||
// condition messages for "SecretMissing" reason.
|
||||
msgMissing = "secret will not be created due to CreationPolicy=Merge"
|
||||
|
||||
// condition messages for "SecretSyncedError" reason.
|
||||
msgErrorGetSecretData = "could not get secret data from provider"
|
||||
msgErrorDeleteSecret = "could not delete secret"
|
||||
msgErrorDeleteOrphaned = "could not delete orphaned secrets"
|
||||
msgErrorUpdateSecret = "could not update secret"
|
||||
msgErrorUpdateImmutable = "could not update secret, target is immutable"
|
||||
msgErrorBecomeOwner = "failed to take ownership of target secret"
|
||||
msgErrorIsOwned = "target is owned by another ExternalSecret"
|
||||
|
||||
// log messages.
|
||||
logErrorGetES = "unable to get ExternalSecret"
|
||||
logErrorUpdateESStatus = "unable to update ExternalSecret status"
|
||||
logErrorGetSecret = "unable to get Secret"
|
||||
logErrorPatchSecret = "unable to patch Secret"
|
||||
logErrorSecretCacheNotSynced = "controller caches for Secret are not in sync"
|
||||
logErrorUnmanagedStore = "unable to determine if store is managed"
|
||||
|
||||
// error formats.
|
||||
errConvert = "error applying conversion strategy %s to keys: %w"
|
||||
errRewrite = "error applying rewrite to keys: %w"
|
||||
errDecode = "error applying decoding strategy %s to data: %w"
|
||||
errGenerate = "error using generator: %w"
|
||||
errInvalidKeys = "invalid secret keys (TIP: use rewrite or conversionStrategy to change keys): %w"
|
||||
errFetchTplFrom = "error fetching templateFrom data: %w"
|
||||
errApplyTemplate = "could not apply template: %w"
|
||||
errExecTpl = "could not execute template: %w"
|
||||
errMutate = "unable to mutate secret %s: %w"
|
||||
errUpdate = "unable to update secret %s: %w"
|
||||
errUpdateNotFound = "unable to update secret %s: not found"
|
||||
errDeleteCreatePolicy = "unable to delete secret %s: creationPolicy=%s is not Owner"
|
||||
errSecretCachesNotSynced = "controller caches for secret %s are not in sync"
|
||||
|
||||
// event messages.
|
||||
eventCreated = "secret created"
|
||||
eventUpdated = "secret updated"
|
||||
eventDeleted = "secret deleted due to DeletionPolicy=Delete"
|
||||
eventDeletedOrphaned = "secret deleted because it was orphaned"
|
||||
eventMissingProviderSecret = "secret does not exist at provider using spec.dataFrom[%d]"
|
||||
eventMissingProviderSecretKey = "secret does not exist at provider using spec.dataFrom[%d] (key=%s)"
|
||||
)
|
||||
|
||||
// these errors are explicitly defined so we can detect them with `errors.Is()`.
|
||||
var (
|
||||
ErrSecretImmutable = fmt.Errorf("secret is immutable")
|
||||
ErrSecretIsOwned = fmt.Errorf("secret is owned by another ExternalSecret")
|
||||
ErrSecretSetCtrlRef = fmt.Errorf("could not set controller reference on secret")
|
||||
ErrSecretRemoveCtrlRef = fmt.Errorf("could not remove controller reference on secret")
|
||||
)
|
||||
|
||||
const indexESTargetSecretNameField = ".metadata.targetSecretName"
|
||||
|
||||
// Reconciler reconciles a ExternalSecret object.
|
||||
type Reconciler struct {
|
||||
client.Client
|
||||
SecretClient client.Client
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
RestConfig *rest.Config
|
||||
ControllerClass string
|
||||
RequeueInterval time.Duration
|
||||
ClusterSecretStoreEnabled bool
|
||||
EnableFloodGate bool
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
// Reconcile implements the main reconciliation loop
|
||||
// for watched objects (ExternalSecret, ClusterSecretStore and SecretStore),
|
||||
// and updates/creates a Kubernetes secret based on them.
|
||||
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
|
||||
log := r.Log.WithValues("ExternalSecret", req.NamespacedName)
|
||||
|
||||
resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
|
||||
start := time.Now()
|
||||
|
||||
syncCallsError := esmetrics.GetCounterVec(esmetrics.SyncCallsErrorKey)
|
||||
|
||||
// use closures to dynamically update resourceLabels
|
||||
defer func() {
|
||||
esmetrics.GetGaugeVec(esmetrics.ExternalSecretReconcileDurationKey).With(resourceLabels).Set(float64(time.Since(start)))
|
||||
esmetrics.GetCounterVec(esmetrics.SyncCallsKey).With(resourceLabels).Inc()
|
||||
}()
|
||||
|
||||
externalSecret := &esv1.ExternalSecret{}
|
||||
err = r.Get(ctx, req.NamespacedName, externalSecret)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// NOTE: this does not actually set the condition on the ExternalSecret, because it does not exist
|
||||
// this is a hack to disable metrics for deleted ExternalSecrets, see:
|
||||
// https://github.com/external-secrets/external-secrets/pull/612
|
||||
conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretDeleted, v1.ConditionFalse, esv1.ConditionReasonSecretDeleted, "Secret was deleted")
|
||||
SetExternalSecretCondition(&esv1.ExternalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: req.Namespace,
|
||||
},
|
||||
}, *conditionSynced)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
log.Error(err, logErrorGetES)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// skip reconciliation if deletion timestamp is set on external secret
|
||||
if !externalSecret.GetDeletionTimestamp().IsZero() {
|
||||
log.V(1).Info("skipping ExternalSecret, it is marked for deletion")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// if extended metrics is enabled, refine the time series vector
|
||||
resourceLabels = ctrlmetrics.RefineLabels(resourceLabels, externalSecret.Labels)
|
||||
|
||||
// skip this ExternalSecret if it uses a ClusterSecretStore and the feature is disabled
|
||||
if shouldSkipClusterSecretStore(r, externalSecret) {
|
||||
log.V(1).Info("skipping ExternalSecret, ClusterSecretStore feature is disabled")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// skip this ExternalSecret if it uses any SecretStore not managed by this controller
|
||||
skip, err := shouldSkipUnmanagedStore(ctx, req.Namespace, r, externalSecret)
|
||||
if err != nil {
|
||||
log.Error(err, logErrorUnmanagedStore)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if skip {
|
||||
log.V(1).Info("skipping ExternalSecret, uses unmanaged SecretStore")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// the target secret name defaults to the ExternalSecret name, if not explicitly set
|
||||
secretName := externalSecret.Spec.Target.Name
|
||||
if secretName == "" {
|
||||
secretName = externalSecret.Name
|
||||
}
|
||||
|
||||
// fetch the existing secret (from the partial cache)
|
||||
// - please note that the ~partial cache~ is different from the ~full cache~
|
||||
// so there can be race conditions between the two caches
|
||||
// - the WatchesMetadata(v1.Secret{}) in SetupWithManager() is using the partial cache
|
||||
// so we might receive a reconcile request before the full cache is updated
|
||||
// - furthermore, when `--enable-managed-secrets-caching` is true, the full cache
|
||||
// will ONLY include secrets with the "managed" label, so we cant use the full cache
|
||||
// to reliably determine if a secret exists or not
|
||||
secretPartial := &metav1.PartialObjectMetadata{}
|
||||
secretPartial.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret"))
|
||||
err = r.Get(ctx, client.ObjectKey{Name: secretName, Namespace: externalSecret.Namespace}, secretPartial)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
log.Error(err, logErrorGetSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// if the secret exists but does not have the "managed" label, add the label
|
||||
// using a PATCH so it is visible in the cache, then requeue immediately
|
||||
if secretPartial.UID != "" && secretPartial.Labels[esv1.LabelManaged] != esv1.LabelManagedValue {
|
||||
fqdn := fmt.Sprintf(fieldOwnerTemplate, externalSecret.Name)
|
||||
patch := client.MergeFrom(secretPartial.DeepCopy())
|
||||
if secretPartial.Labels == nil {
|
||||
secretPartial.Labels = make(map[string]string)
|
||||
}
|
||||
secretPartial.Labels[esv1.LabelManaged] = esv1.LabelManagedValue
|
||||
err = r.Patch(ctx, secretPartial, patch, client.FieldOwner(fqdn))
|
||||
if err != nil {
|
||||
log.Error(err, logErrorPatchSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// fetch existing secret (from the full cache)
|
||||
// NOTE: we are using the `r.SecretClient` which we only use for managed secrets.
|
||||
// when `enableManagedSecretsCache` is true, this is a cached client that only sees our managed secrets,
|
||||
// otherwise it will be the normal controller-runtime client which may be cached or make direct API calls,
|
||||
// depending on if `enabledSecretCache` is true or false.
|
||||
existingSecret := &v1.Secret{}
|
||||
err = r.SecretClient.Get(ctx, client.ObjectKey{Name: secretName, Namespace: externalSecret.Namespace}, existingSecret)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
log.Error(err, logErrorGetSecret, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// ensure the full cache is up-to-date
|
||||
// NOTE: this prevents race conditions between the partial and full cache.
|
||||
// we return an error so we get an exponential backoff if we end up looping,
|
||||
// for example, during high cluster load and frequent updates to the target secret by other controllers.
|
||||
if secretPartial.UID != existingSecret.UID || secretPartial.ResourceVersion != existingSecret.ResourceVersion {
|
||||
err = fmt.Errorf(errSecretCachesNotSynced, secretName)
|
||||
log.Error(err, logErrorSecretCacheNotSynced, "secretName", secretName, "secretNamespace", externalSecret.Namespace)
|
||||
syncCallsError.With(resourceLabels).Inc()
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// refresh will be skipped if ALL the following conditions are met:
|
||||
// 1. refresh interval is not 0
|
||||
// 2. resource generation of the ExternalSecret has not changed
|
||||
// 3. the last refresh time of the ExternalSecret is within the refresh interval
|
||||
// 4. the target secret is valid:
|
||||
// - it exists
|
||||
// - it has the correct "managed" label
|
||||
// - it has the correct "data-hash" annotation
|
||||
if !shouldRefresh(externalSecret) && isSecretValid(existingSecret) {
|
||||
log.V(1).Info("skipping refresh")
|
||||
return r.getRequeueResult(externalSecret), nil
|
||||
}
|
||||
|
||||
// update status of the ExternalSecret when this function returns, if needed.
|
||||
// NOTE: we use the ability of deferred functions to update named return values `result` and `err`
|
||||
// NOTE: we dereference the DeepCopy of the status field because status fields are NOT pointers,
|
||||
// so otherwise the `equality.Semantic.DeepEqual` will always return false.
|
||||
currentStatus := *externalSecret.Status.DeepCopy()
|
||||
defer func() {
|
||||
// if the status has not changed, we don't need to update it
|
||||
if equality.Semantic.DeepEqual(currentStatus, externalSecret.Status) {
|
||||
return
|
||||
}
|
||||
|
||||
// update the status of the ExternalSecret, storing any error in a new variable
|
||||
// if there was no new error, we don't need to change the `result` or `err` values
|
||||
updateErr := r.Status().Update(ctx, externalSecret)
|
||||
if updateErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if we got an update conflict, we should requeue immediately
|
||||
if apierrors.IsConflict(updateErr) {
|
||||
log.V(1).Info("conflict while updating status, will requeue")
|
||||
|
||||
// we only explicitly request a requeue if the main function did not return an `err`.
|
||||
// otherwise, we get an annoying log saying that results are ignored when there is an error,
|
||||
// as errors are always retried.
|
||||
if err == nil {
|
||||
result = ctrl.Result{Requeue: true}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// for other errors, log and update the `err` variable if there is no error already
|
||||
// so the reconciler will requeue the request
|
||||
log.Error(updateErr, logErrorUpdateESStatus)
|
||||
if err == nil {
|
||||
err = updateErr
|
||||
}
|
||||
}()
|
||||
|
||||
// retrieve the provider secret data.
|
||||
dataMap, err := r.getProviderSecretData(ctx, externalSecret)
|
||||
if err != nil {
|
||||
r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// if no data was found we can delete the secret if needed.
|
||||
if len(dataMap) == 0 {
|
||||
switch externalSecret.Spec.Target.DeletionPolicy {
|
||||
// delete secret and return early.
|
||||
case esv1.DeletionPolicyDelete:
|
||||
// safeguard that we only can delete secrets we own.
|
||||
// this is also implemented in the es validation webhook.
|
||||
// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
|
||||
creationPolicy := externalSecret.Spec.Target.CreationPolicy
|
||||
if creationPolicy != esv1.CreatePolicyOwner {
|
||||
err = fmt.Errorf(errDeleteCreatePolicy, secretName, creationPolicy)
|
||||
r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// delete the secret, if it exists
|
||||
if existingSecret.UID != "" {
|
||||
err = r.Delete(ctx, existingSecret)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1.ReasonDeleted, eventDeleted)
|
||||
}
|
||||
|
||||
r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretDeleted, msgDeleted)
|
||||
return r.getRequeueResult(externalSecret), nil
|
||||
// In case provider secrets don't exist the kubernetes secret will be kept as-is.
|
||||
case esv1.DeletionPolicyRetain:
|
||||
r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretSynced, msgSyncedRetain)
|
||||
return r.getRequeueResult(externalSecret), nil
|
||||
// noop, handled below
|
||||
case esv1.DeletionPolicyMerge:
|
||||
}
|
||||
}
|
||||
|
||||
// mutationFunc is a function which can be applied to a secret to make it match the desired state.
|
||||
mutationFunc := func(secret *v1.Secret) error {
|
||||
// get information about the current owner of the secret
|
||||
// - we ignore the API version as it can change over time
|
||||
// - we ignore the UID for consistency with the SetControllerReference function
|
||||
currentOwner := metav1.GetControllerOf(secret)
|
||||
ownerIsESKind := false
|
||||
ownerIsCurrentES := false
|
||||
if currentOwner != nil {
|
||||
currentOwnerGK := schema.FromAPIVersionAndKind(currentOwner.APIVersion, currentOwner.Kind).GroupKind()
|
||||
ownerIsESKind = currentOwnerGK.String() == esv1.ExtSecretGroupKind
|
||||
ownerIsCurrentES = ownerIsESKind && currentOwner.Name == externalSecret.Name
|
||||
}
|
||||
|
||||
// if another ExternalSecret is the owner, we should return an error
|
||||
// otherwise the controller will fight with itself to update the secret.
|
||||
// note, this does not prevent other controllers from owning the secret.
|
||||
if ownerIsESKind && !ownerIsCurrentES {
|
||||
return fmt.Errorf("%w: %s", ErrSecretIsOwned, currentOwner.Name)
|
||||
}
|
||||
|
||||
// if the CreationPolicy is Owner, we should set ourselves as the owner of the secret
|
||||
if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
|
||||
err = controllerutil.SetControllerReference(externalSecret, secret, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrSecretSetCtrlRef, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if the creation policy is not Owner, we should remove ourselves as the owner
|
||||
// this could happen if the creation policy was changed after the secret was created
|
||||
if externalSecret.Spec.Target.CreationPolicy != esv1.CreatePolicyOwner && ownerIsCurrentES {
|
||||
err = controllerutil.RemoveControllerReference(externalSecret, secret, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrSecretRemoveCtrlRef, err)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize maps within the secret so it's safe to set values
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
if secret.Labels == nil {
|
||||
secret.Labels = make(map[string]string)
|
||||
}
|
||||
if secret.Data == nil {
|
||||
secret.Data = make(map[string][]byte)
|
||||
}
|
||||
|
||||
// get the list of keys that are managed by this ExternalSecret
|
||||
keys, err := getManagedDataKeys(secret, externalSecret.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove any data keys that are managed by this ExternalSecret, so we can re-add them
|
||||
// this ensures keys added by templates are not left behind when they are removed from the template
|
||||
for _, key := range keys {
|
||||
delete(secret.Data, key)
|
||||
}
|
||||
|
||||
// WARNING: this will remove any labels or annotations managed by this ExternalSecret
|
||||
// so any updates to labels and annotations should be done AFTER this point
|
||||
err = r.applyTemplate(ctx, externalSecret, secret, dataMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errApplyTemplate, err)
|
||||
}
|
||||
|
||||
// set the immutable flag on the secret if requested by the ExternalSecret
|
||||
if externalSecret.Spec.Target.Immutable {
|
||||
secret.Immutable = ptr.To(true)
|
||||
}
|
||||
|
||||
// we also use a label to keep track of the owner of the secret
|
||||
// this lets us remove secrets that are no longer needed if the target secret name changes
|
||||
if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
|
||||
lblValue := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
|
||||
secret.Labels[esv1.LabelOwner] = lblValue
|
||||
} else {
|
||||
// the label should not be set if the creation policy is not Owner
|
||||
delete(secret.Labels, esv1.LabelOwner)
|
||||
}
|
||||
|
||||
secret.Labels[esv1.LabelManaged] = esv1.LabelManagedValue
|
||||
secret.Annotations[esv1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
switch externalSecret.Spec.Target.CreationPolicy {
|
||||
case esv1.CreatePolicyNone:
|
||||
log.V(1).Info("secret creation skipped due to CreationPolicy=None")
|
||||
err = nil
|
||||
case esv1.CreatePolicyMerge:
|
||||
// update the secret, if it exists
|
||||
if existingSecret.UID != "" {
|
||||
err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
|
||||
} else {
|
||||
// if the secret does not exist, we wait until the next refresh interval
|
||||
// rather than returning an error which would requeue immediately
|
||||
r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretMissing, msgMissing)
|
||||
return r.getRequeueResult(externalSecret), nil
|
||||
}
|
||||
case esv1.CreatePolicyOrphan:
|
||||
// create the secret, if it does not exist
|
||||
if existingSecret.UID == "" {
|
||||
err = r.createSecret(ctx, mutationFunc, externalSecret, secretName)
|
||||
} else {
|
||||
// if the secret exists, we should update it
|
||||
err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
|
||||
}
|
||||
case esv1.CreatePolicyOwner:
|
||||
// we may have orphaned secrets to clean up,
|
||||
// for example, if the target secret name was changed
|
||||
err = r.deleteOrphanedSecrets(ctx, externalSecret, secretName)
|
||||
if err != nil {
|
||||
r.markAsFailed(msgErrorDeleteOrphaned, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// create the secret, if it does not exist
|
||||
if existingSecret.UID == "" {
|
||||
err = r.createSecret(ctx, mutationFunc, externalSecret, secretName)
|
||||
} else {
|
||||
// if the secret exists, we should update it
|
||||
err = r.updateSecret(ctx, existingSecret, mutationFunc, externalSecret, secretName)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// if we got an update conflict, we should requeue immediately
|
||||
if apierrors.IsConflict(err) {
|
||||
log.V(1).Info("conflict while updating secret, will requeue")
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// detect errors indicating that we failed to set ourselves as the owner of the secret
|
||||
// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
|
||||
if errors.Is(err, ErrSecretSetCtrlRef) {
|
||||
r.markAsFailed(msgErrorBecomeOwner, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// detect errors indicating that the secret has another ExternalSecret as owner
|
||||
// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
|
||||
if errors.Is(err, ErrSecretIsOwned) {
|
||||
r.markAsFailed(msgErrorIsOwned, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// detect errors indicating that the secret is immutable
|
||||
// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
|
||||
if errors.Is(err, ErrSecretImmutable) {
|
||||
r.markAsFailed(msgErrorUpdateImmutable, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.markAsDone(externalSecret, start, log, esv1.ConditionReasonSecretSynced, msgSynced)
|
||||
return r.getRequeueResult(externalSecret), nil
|
||||
}
|
||||
|
||||
// getRequeueResult create a result with requeueAfter based on the ExternalSecret refresh interval.
|
||||
func (r *Reconciler) getRequeueResult(externalSecret *esv1.ExternalSecret) ctrl.Result {
|
||||
// default to the global requeue interval
|
||||
// note, this will never be used because the CRD has a default value of 1 hour
|
||||
refreshInterval := r.RequeueInterval
|
||||
if externalSecret.Spec.RefreshInterval != nil {
|
||||
refreshInterval = externalSecret.Spec.RefreshInterval.Duration
|
||||
}
|
||||
|
||||
// if the refresh interval is <= 0, we should not requeue
|
||||
if refreshInterval <= 0 {
|
||||
return ctrl.Result{}
|
||||
}
|
||||
|
||||
// if the last refresh time is not set, requeue after the refresh interval
|
||||
// note, this should not happen, as we only call this function on ExternalSecrets
|
||||
// that have been reconciled at least once
|
||||
if externalSecret.Status.RefreshTime.IsZero() {
|
||||
return ctrl.Result{RequeueAfter: refreshInterval}
|
||||
}
|
||||
|
||||
timeSinceLastRefresh := time.Since(externalSecret.Status.RefreshTime.Time)
|
||||
|
||||
// if the last refresh time is in the future, we should requeue immediately
|
||||
// note, this should not happen, as we always refresh an ExternalSecret
|
||||
// that has a last refresh time in the future
|
||||
if timeSinceLastRefresh < 0 {
|
||||
return ctrl.Result{Requeue: true}
|
||||
}
|
||||
|
||||
// if there is time remaining, requeue after the remaining time
|
||||
if timeSinceLastRefresh < refreshInterval {
|
||||
return ctrl.Result{RequeueAfter: refreshInterval - timeSinceLastRefresh}
|
||||
}
|
||||
|
||||
// otherwise, requeue immediately
|
||||
return ctrl.Result{Requeue: true}
|
||||
}
|
||||
|
||||
func (r *Reconciler) markAsDone(externalSecret *esv1.ExternalSecret, start time.Time, log logr.Logger, reason, msg string) {
|
||||
oldReadyCondition := GetExternalSecretCondition(externalSecret.Status, esv1.ExternalSecretReady)
|
||||
newReadyCondition := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionTrue, reason, msg)
|
||||
SetExternalSecretCondition(externalSecret, *newReadyCondition)
|
||||
|
||||
externalSecret.Status.RefreshTime = metav1.NewTime(start)
|
||||
externalSecret.Status.SyncedResourceVersion = util.GetResourceVersion(externalSecret.ObjectMeta)
|
||||
|
||||
// if the status or reason has changed, log at the appropriate verbosity level
|
||||
if oldReadyCondition == nil || oldReadyCondition.Status != newReadyCondition.Status || oldReadyCondition.Reason != newReadyCondition.Reason {
|
||||
if newReadyCondition.Reason == esv1.ConditionReasonSecretDeleted {
|
||||
log.Info("deleted secret")
|
||||
} else {
|
||||
log.Info("reconciled secret")
|
||||
}
|
||||
} else {
|
||||
log.V(1).Info("reconciled secret")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reconciler) markAsFailed(msg string, err error, externalSecret *esv1.ExternalSecret, counter prometheus.Counter) {
|
||||
r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1.ReasonUpdateFailed, err.Error())
|
||||
conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionFalse, esv1.ConditionReasonSecretSyncedError, msg)
|
||||
SetExternalSecretCondition(externalSecret, *conditionSynced)
|
||||
counter.Inc()
|
||||
}
|
||||
|
||||
func (r *Reconciler) deleteOrphanedSecrets(ctx context.Context, externalSecret *esv1.ExternalSecret, secretName string) error {
|
||||
ownerLabel := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
|
||||
|
||||
// we use a PartialObjectMetadataList to avoid loading the full secret objects
|
||||
// and because the Secrets partials are always cached due to WatchesMetadata() in SetupWithManager()
|
||||
secretListPartial := &metav1.PartialObjectMetadataList{}
|
||||
secretListPartial.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("SecretList"))
|
||||
listOpts := &client.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(map[string]string{
|
||||
esv1.LabelOwner: ownerLabel,
|
||||
}),
|
||||
Namespace: externalSecret.Namespace,
|
||||
}
|
||||
if err := r.List(ctx, secretListPartial, listOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete all secrets that are not the target secret
|
||||
for _, secretPartial := range secretListPartial.Items {
|
||||
if secretPartial.GetName() != secretName {
|
||||
err := r.Delete(ctx, &secretPartial)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1.ReasonDeleted, eventDeletedOrphaned)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSecret creates a new secret with the given mutation function.
|
||||
func (r *Reconciler) createSecret(ctx context.Context, mutationFunc func(secret *v1.Secret) error, es *esv1.ExternalSecret, secretName string) error {
|
||||
fqdn := fmt.Sprintf(fieldOwnerTemplate, es.Name)
|
||||
|
||||
// define and mutate the new secret
|
||||
newSecret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: es.Namespace,
|
||||
},
|
||||
Data: make(map[string][]byte),
|
||||
}
|
||||
if err := mutationFunc(newSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// note, we set field owner even for Create
|
||||
if err := r.Create(ctx, newSecret, client.FieldOwner(fqdn)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the binding reference to the secret
|
||||
// https://github.com/external-secrets/external-secrets/pull/2263
|
||||
es.Status.Binding = v1.LocalObjectReference{Name: newSecret.Name}
|
||||
|
||||
r.recorder.Event(es, v1.EventTypeNormal, esv1.ReasonCreated, eventCreated)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) updateSecret(ctx context.Context, existingSecret *v1.Secret, mutationFunc func(secret *v1.Secret) error, es *esv1.ExternalSecret, secretName string) error {
|
||||
fqdn := fmt.Sprintf(fieldOwnerTemplate, es.Name)
|
||||
|
||||
// fail if the secret does not exist
|
||||
// this should never happen because we check this before calling this function
|
||||
if existingSecret.UID == "" {
|
||||
return fmt.Errorf(errUpdateNotFound, secretName)
|
||||
}
|
||||
|
||||
// set the binding reference to the secret
|
||||
// https://github.com/external-secrets/external-secrets/pull/2263
|
||||
es.Status.Binding = v1.LocalObjectReference{Name: secretName}
|
||||
|
||||
// mutate a copy of the existing secret with the mutation function
|
||||
updatedSecret := existingSecret.DeepCopy()
|
||||
if err := mutationFunc(updatedSecret); err != nil {
|
||||
return fmt.Errorf(errMutate, updatedSecret.Name, err)
|
||||
}
|
||||
|
||||
// if the secret does not need to be updated, return early
|
||||
if equality.Semantic.DeepEqual(existingSecret, updatedSecret) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the existing secret is immutable, we can only update the object metadata
|
||||
if ptr.Deref(existingSecret.Immutable, false) {
|
||||
// check if the metadata was changed
|
||||
metadataChanged := !equality.Semantic.DeepEqual(existingSecret.ObjectMeta, updatedSecret.ObjectMeta)
|
||||
|
||||
// check if the immutable data/type was changed
|
||||
var dataChanged bool
|
||||
if metadataChanged {
|
||||
// update the `existingSecret` object with the metadata from `updatedSecret`
|
||||
// this lets us compare the objects to see if the immutable data/type was changed
|
||||
existingSecret.ObjectMeta = *updatedSecret.ObjectMeta.DeepCopy()
|
||||
dataChanged = !equality.Semantic.DeepEqual(existingSecret, updatedSecret)
|
||||
|
||||
// because we use labels and annotations to keep track of the secret,
|
||||
// we need to update the metadata, regardless of if the immutable data was changed
|
||||
// NOTE: we are using the `existingSecret` object here, as we ONLY want to update the metadata,
|
||||
// and we previously copied the metadata from the `updatedSecret` object
|
||||
if err := r.Update(ctx, existingSecret, client.FieldOwner(fqdn)); err != nil {
|
||||
// if we get a conflict, we should return early to requeue immediately
|
||||
// note, we don't wrap this error so we can handle it in the caller
|
||||
if apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf(errUpdate, existingSecret.Name, err)
|
||||
}
|
||||
} else {
|
||||
// we know there was some change in the secret (or we would have returned early)
|
||||
// we know the metadata was NOT changed (metadataChanged == false)
|
||||
// so, the only thing that could have changed is the immutable data/type fields
|
||||
dataChanged = true
|
||||
}
|
||||
|
||||
// if the immutable data was changed, we should return an error
|
||||
if dataChanged {
|
||||
return fmt.Errorf(errUpdate, existingSecret.Name, ErrSecretImmutable)
|
||||
}
|
||||
}
|
||||
|
||||
// update the secret
|
||||
if err := r.Update(ctx, updatedSecret, client.FieldOwner(fqdn)); err != nil {
|
||||
// if we get a conflict, we should return early to requeue immediately
|
||||
// note, we don't wrap this error so we can handle it in the caller
|
||||
if apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf(errUpdate, updatedSecret.Name, err)
|
||||
}
|
||||
|
||||
r.recorder.Event(es, v1.EventTypeNormal, esv1.ReasonUpdated, eventUpdated)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getManagedDataKeys returns the list of data keys in a secret which are managed by a specified owner.
|
||||
func getManagedDataKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
|
||||
return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]any) []string {
|
||||
dataFields := fields["f:data"]
|
||||
if dataFields == nil {
|
||||
return nil
|
||||
}
|
||||
df, ok := dataFields.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return slices.Collect(maps.Keys(df))
|
||||
})
|
||||
}
|
||||
|
||||
func getManagedFieldKeys(
|
||||
secret *v1.Secret,
|
||||
fieldOwner string,
|
||||
process func(fields map[string]any) []string,
|
||||
) ([]string, error) {
|
||||
fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
|
||||
var keys []string
|
||||
for _, v := range secret.ObjectMeta.ManagedFields {
|
||||
if v.Manager != fqdn {
|
||||
continue
|
||||
}
|
||||
fields := make(map[string]any)
|
||||
err := json.Unmarshal(v.FieldsV1.Raw, &fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
|
||||
}
|
||||
for _, key := range process(fields) {
|
||||
if key == "." {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, strings.TrimPrefix(key, "f:"))
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func shouldSkipClusterSecretStore(r *Reconciler, es *esv1.ExternalSecret) bool {
|
||||
return !r.ClusterSecretStoreEnabled && es.Spec.SecretStoreRef.Kind == esv1.ClusterSecretStoreKind
|
||||
}
|
||||
|
||||
// shouldSkipUnmanagedStore iterates over all secretStore references in the externalSecret spec,
|
||||
// fetches the store and evaluates the controllerClass property.
|
||||
// Returns true if any storeRef points to store with a non-matching controllerClass.
|
||||
func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconciler, es *esv1.ExternalSecret) (bool, error) {
|
||||
var storeList []esv1.SecretStoreRef
|
||||
|
||||
if es.Spec.SecretStoreRef.Name != "" {
|
||||
storeList = append(storeList, es.Spec.SecretStoreRef)
|
||||
}
|
||||
|
||||
for _, ref := range es.Spec.Data {
|
||||
if ref.SourceRef != nil {
|
||||
storeList = append(storeList, ref.SourceRef.SecretStoreRef)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ref := range es.Spec.DataFrom {
|
||||
if ref.SourceRef != nil && ref.SourceRef.SecretStoreRef != nil {
|
||||
storeList = append(storeList, *ref.SourceRef.SecretStoreRef)
|
||||
}
|
||||
|
||||
// verify that generator's controllerClass matches
|
||||
if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
|
||||
_, obj, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, ref.SourceRef.GeneratorRef)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// skip non-existent generators
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, resolvers.ErrUnableToGetGenerator) {
|
||||
// skip generators that we can't get (e.g. due to being invalid)
|
||||
continue
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
skipGenerator, err := shouldSkipGenerator(r, obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if skipGenerator {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ref := range storeList {
|
||||
var store esv1.GenericStore
|
||||
|
||||
switch ref.Kind {
|
||||
case esv1.SecretStoreKind, "":
|
||||
store = &esv1.SecretStore{}
|
||||
case esv1.ClusterSecretStoreKind:
|
||||
store = &esv1.ClusterSecretStore{}
|
||||
namespace = ""
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported secret store kind: %s", ref.Kind)
|
||||
}
|
||||
|
||||
err := r.Get(ctx, types.NamespacedName{
|
||||
Name: ref.Name,
|
||||
Namespace: namespace,
|
||||
}, store)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// skip non-existent stores
|
||||
continue
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
class := store.GetSpec().Controller
|
||||
if class != "" && class != r.ControllerClass {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func shouldRefresh(es *esv1.ExternalSecret) bool {
|
||||
switch es.Spec.RefreshPolicy {
|
||||
case esv1.RefreshPolicyCreatedOnce:
|
||||
if es.Status.SyncedResourceVersion == "" || es.Status.RefreshTime.IsZero() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case esv1.RefreshPolicyOnChange:
|
||||
if es.Status.SyncedResourceVersion == "" || es.Status.RefreshTime.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
return es.Status.SyncedResourceVersion != util.GetResourceVersion(es.ObjectMeta)
|
||||
|
||||
case esv1.RefreshPolicyPeriodic:
|
||||
return shouldRefreshPeriodic(es)
|
||||
|
||||
default:
|
||||
return shouldRefreshPeriodic(es)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRefreshPeriodic(es *esv1.ExternalSecret) bool {
|
||||
// if the refresh interval is 0, and we have synced previously, we should not refresh
|
||||
if es.Spec.RefreshInterval.Duration <= 0 && es.Status.SyncedResourceVersion != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the ExternalSecret has been updated, we should refresh
|
||||
if es.Status.SyncedResourceVersion != util.GetResourceVersion(es.ObjectMeta) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the last refresh time is zero, we should refresh
|
||||
if es.Status.RefreshTime.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the last refresh time is in the future, we should refresh
|
||||
if es.Status.RefreshTime.Time.After(time.Now()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the last refresh time + refresh interval is before now, we should refresh
|
||||
return es.Status.RefreshTime.Add(es.Spec.RefreshInterval.Duration).Before(time.Now())
|
||||
}
|
||||
|
||||
// isSecretValid checks if the secret exists, and it's data is consistent with the calculated hash.
|
||||
func isSecretValid(existingSecret *v1.Secret) bool {
|
||||
// if target secret doesn't exist, we need to refresh
|
||||
if existingSecret.UID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the managed label is missing or incorrect, then it's invalid
|
||||
if existingSecret.Labels[esv1.LabelManaged] != esv1.LabelManagedValue {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the data-hash annotation is missing or incorrect, then it's invalid
|
||||
// this is how we know if the data has chanced since we last updated the secret
|
||||
if existingSecret.Annotations[esv1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetupWithManager returns a new controller builder that will be started by the provided Manager.
|
||||
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
|
||||
r.recorder = mgr.GetEventRecorderFor("external-secrets")
|
||||
|
||||
// index ExternalSecrets based on the target secret name,
|
||||
// this lets us quickly find all ExternalSecrets which target a specific Secret
|
||||
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &esv1.ExternalSecret{}, indexESTargetSecretNameField, func(obj client.Object) []string {
|
||||
es := obj.(*esv1.ExternalSecret)
|
||||
// if the target name is set, use that as the index
|
||||
if es.Spec.Target.Name != "" {
|
||||
return []string{es.Spec.Target.Name}
|
||||
}
|
||||
// otherwise, use the ExternalSecret name
|
||||
return []string{es.Name}
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// predicate function to ignore secret events unless they have the "managed" label
|
||||
secretHasESLabel := predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
value, hasLabel := object.GetLabels()[esv1.LabelManaged]
|
||||
return hasLabel && value == esv1.LabelManagedValue
|
||||
})
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
WithOptions(opts).
|
||||
For(&esv1.ExternalSecret{}).
|
||||
// we cant use Owns(), as we don't set ownerReferences when the creationPolicy is not Owner.
|
||||
// we use WatchesMetadata() to reduce memory usage, as otherwise we have to process full secret objects.
|
||||
WatchesMetadata(
|
||||
&v1.Secret{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret),
|
||||
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, secretHasESLabel),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *Reconciler) findObjectsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {
|
||||
externalSecretsList := &esv1.ExternalSecretList{}
|
||||
listOps := &client.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector(indexESTargetSecretNameField, secret.GetName()),
|
||||
Namespace: secret.GetNamespace(),
|
||||
}
|
||||
err := r.List(ctx, externalSecretsList, listOps)
|
||||
if err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := make([]reconcile.Request, len(externalSecretsList.Items))
|
||||
for i := range externalSecretsList.Items {
|
||||
requests[i] = reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: externalSecretsList.Items[i].GetName(),
|
||||
Namespace: externalSecretsList.Items[i].GetNamespace(),
|
||||
},
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package externalsecret
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
|
||||
)
|
||||
|
||||
// NewExternalSecretCondition a set of default options for creating an External Secret Condition.
|
||||
func NewExternalSecretCondition(condType esv1.ExternalSecretConditionType, status v1.ConditionStatus, reason, message string) *esv1.ExternalSecretStatusCondition {
|
||||
return &esv1.ExternalSecretStatusCondition{
|
||||
Type: condType,
|
||||
Status: status,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExternalSecretCondition returns the condition with the provided type.
|
||||
func GetExternalSecretCondition(status esv1.ExternalSecretStatus, condType esv1.ExternalSecretConditionType) *esv1.ExternalSecretStatusCondition {
|
||||
for _, c := range status.Conditions {
|
||||
if c.Type == condType {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExternalSecretCondition updates the external secret to include the provided
|
||||
// condition.
|
||||
func SetExternalSecretCondition(es *esv1.ExternalSecret, condition esv1.ExternalSecretStatusCondition) {
|
||||
currentCond := GetExternalSecretCondition(es.Status, condition.Type)
|
||||
|
||||
if currentCond != nil && currentCond.Status == condition.Status &&
|
||||
currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
|
||||
esmetrics.UpdateExternalSecretCondition(es, &condition, 1.0)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not update lastTransitionTime if the status of the condition doesn't change.
|
||||
if currentCond != nil && currentCond.Status == condition.Status {
|
||||
condition.LastTransitionTime = currentCond.LastTransitionTime
|
||||
}
|
||||
|
||||
es.Status.Conditions = append(filterOutCondition(es.Status.Conditions, condition.Type), condition)
|
||||
|
||||
if currentCond != nil {
|
||||
esmetrics.UpdateExternalSecretCondition(es, currentCond, 0.0)
|
||||
}
|
||||
|
||||
esmetrics.UpdateExternalSecretCondition(es, &condition, 1.0)
|
||||
}
|
||||
|
||||
// filterOutCondition returns an empty set of conditions with the provided type.
|
||||
func filterOutCondition(conditions []esv1.ExternalSecretStatusCondition, condType esv1.ExternalSecretConditionType) []esv1.ExternalSecretStatusCondition {
|
||||
newConditions := make([]esv1.ExternalSecretStatusCondition, 0, len(conditions))
|
||||
for _, c := range conditions {
|
||||
if c.Type == condType {
|
||||
continue
|
||||
}
|
||||
newConditions = append(newConditions, c)
|
||||
}
|
||||
return newConditions
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secretstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
errStoreClient = "could not get provider client: %w"
|
||||
errValidationFailed = "could not validate provider: %w"
|
||||
errPatchStatus = "unable to patch status: %w"
|
||||
errUnableCreateClient = "unable to create client"
|
||||
errUnableValidateStore = "unable to validate store: %s"
|
||||
|
||||
msgStoreValidated = "store validated"
|
||||
msgStoreNotMaintained = "store isn't currently maintained. Please plan and prepare accordingly."
|
||||
)
|
||||
|
||||
type Opts struct {
|
||||
ControllerClass string
|
||||
GaugeVecGetter metrics.GaugeVevGetter
|
||||
Recorder record.EventRecorder
|
||||
RequeueInterval time.Duration
|
||||
}
|
||||
|
||||
func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl client.Client, log logr.Logger, opts Opts) (ctrl.Result, error) {
|
||||
if !ShouldProcessStore(ss, opts.ControllerClass) {
|
||||
log.V(1).Info("skip store")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
requeueInterval := opts.RequeueInterval
|
||||
|
||||
if ss.GetSpec().RefreshInterval != 0 {
|
||||
requeueInterval = time.Second * time.Duration(ss.GetSpec().RefreshInterval)
|
||||
}
|
||||
|
||||
// patch status when done processing
|
||||
p := client.MergeFrom(ss.Copy())
|
||||
defer func() {
|
||||
err := cl.Status().Patch(ctx, ss, p)
|
||||
if err != nil {
|
||||
log.Error(err, errPatchStatus)
|
||||
}
|
||||
}()
|
||||
|
||||
// validateStore modifies the store conditions
|
||||
// we have to patch the status
|
||||
log.V(1).Info("validating")
|
||||
err := validateStore(ctx, req.Namespace, opts.ControllerClass, ss, cl, opts.GaugeVecGetter, opts.Recorder)
|
||||
if err != nil {
|
||||
log.Error(err, "unable to validate store")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
storeProvider, err := esapi.GetProvider(ss)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
isMaintained, err := esapi.GetMaintenanceStatus(ss)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
annotations := ss.GetAnnotations()
|
||||
_, ok := annotations["external-secrets.io/ignore-maintenance-checks"]
|
||||
|
||||
if !bool(isMaintained) && !ok {
|
||||
opts.Recorder.Event(ss, v1.EventTypeWarning, esapi.StoreUnmaintained, msgStoreNotMaintained)
|
||||
}
|
||||
|
||||
capStatus := esapi.SecretStoreStatus{
|
||||
Capabilities: storeProvider.Capabilities(),
|
||||
Conditions: ss.GetStatus().Conditions,
|
||||
}
|
||||
ss.SetStatus(capStatus)
|
||||
|
||||
opts.Recorder.Event(ss, v1.EventTypeNormal, esapi.ReasonStoreValid, msgStoreValidated)
|
||||
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonStoreValid, msgStoreValidated)
|
||||
SetExternalSecretCondition(ss, *cond, opts.GaugeVecGetter)
|
||||
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueInterval,
|
||||
}, err
|
||||
}
|
||||
|
||||
// validateStore tries to construct a new client
|
||||
// if it fails sets a condition and writes events.
|
||||
func validateStore(ctx context.Context, namespace, controllerClass string, store esapi.GenericStore,
|
||||
client client.Client, gaugeVecGetter metrics.GaugeVevGetter, recorder record.EventRecorder) error {
|
||||
mgr := NewManager(client, controllerClass, false)
|
||||
defer mgr.Close(ctx)
|
||||
cl, err := mgr.GetFromStore(ctx, store, namespace)
|
||||
if err != nil {
|
||||
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient)
|
||||
SetExternalSecretCondition(store, *cond, gaugeVecGetter)
|
||||
recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
|
||||
return fmt.Errorf(errStoreClient, err)
|
||||
}
|
||||
validationResult, err := cl.Validate()
|
||||
if err != nil && validationResult != esapi.ValidationResultUnknown {
|
||||
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonValidationFailed, fmt.Sprintf(errUnableValidateStore, err))
|
||||
SetExternalSecretCondition(store, *cond, gaugeVecGetter)
|
||||
recorder.Event(store, v1.EventTypeWarning, esapi.ReasonValidationFailed, err.Error())
|
||||
return fmt.Errorf(errValidationFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldProcessStore returns true if the store should be processed.
|
||||
func ShouldProcessStore(store esapi.GenericStore, class string) bool {
|
||||
if store == nil || store.GetSpec().Controller == "" || store.GetSpec().Controller == class {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
func GetResourceVersion(meta metav1.ObjectMeta) string {
|
||||
return fmt.Sprintf("%d-%s", meta.GetGeneration(), HashMeta(meta))
|
||||
}
|
||||
|
||||
func HashMeta(m metav1.ObjectMeta) string {
|
||||
type meta struct {
|
||||
annotations map[string]string
|
||||
labels map[string]string
|
||||
}
|
||||
return utils.ObjectHash(meta{
|
||||
annotations: m.Annotations,
|
||||
labels: m.Labels,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue