Adopt staged generation and prepare weekly sizing and credential isolation
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 11s
Python Tests / pytest (push) Failing after 28s

This commit is contained in:
codex 2026-10-19 12:42:11 +02:00
parent fd4a3ba00e
commit 02e69269c8
31 changed files with 1439 additions and 26 deletions

View file

@ -0,0 +1 @@
{"version": 1, "repo_slug": "the-custodian"}

View file

@ -0,0 +1,8 @@
# Explicit opt-in recipe; no environment files or external make includes.
CUSTODIAN_SOURCE ?= /home/worsch/the-custodian
GENERATED_REPO_SLUG ?= the-custodian
.PHONY: generated-work-records generated-work-records-check
generated-work-records:
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)"
generated-work-records-check:
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" --check

View file

@ -232,8 +232,9 @@ artifact-adr008:
--subtitle "A framework for describing, holding and improving multi-tenancy — including where we are not there yet."
.PHONY: generated-work-records generated-work-records-check
GENERATED_REPO_SLUG ?= the-custodian
generated-work-records: ## Regenerate and stage index from staged sources; preserve unstaged edits
python3 scripts/generate_staged_work_records.py --repo . --repo-slug the-custodian
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records GENERATED_REPO_SLUG="$(GENERATED_REPO_SLUG)"
generated-work-records-check: ## Check staged index without committing, pushing or changing files
python3 scripts/generate_staged_work_records.py --repo . --repo-slug the-custodian --check
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records-check GENERATED_REPO_SLUG="$(GENERATED_REPO_SLUG)"

View file

@ -218,7 +218,7 @@ keywords: [git, generated, hooks, work-records, validation]
```capability
type: tool
title: Staged work-record index generation
description: make generated-work-records reuses the State Hub renderer on staged Markdown, with explicit repository identity, and stages only WORK-RECORDS.md while refusing unstaged generated-file edits. make generated-work-records-check is read-only. Source adapter and tests are implemented; shared-hook activation and fleet adoption are not established. See docs/staged-work-record-generation.md.
description: make generated-work-records reuses the State Hub renderer on staged Markdown, with explicit repository identity, and stages only WORK-RECORDS.md while refusing unstaged generated-file edits. make generated-work-records-check is read-only. Source adapter and tests are implemented; the shared/local hook supports explicit per-repo adoption, with activity-core, policy-nexus and repo-manager committed. Remaining fleet and other index adoption are still open. See docs/staged-work-record-generation.md.
keywords: [git, generated, work-records, staged-sources, validation]
```
@ -241,6 +241,13 @@ These local discovery descriptions are not publication grants. The federated
and repository classification tooling. Confidential canon, memory, operational
records and estate-specific execution configuration stay outside that boundary.
```capability
type: tool
title: Retained weekly allocation evidence report
description: scripts/weekly_allocation_report.py joins existing cluster, allocation, telemetry and resource-control evidence into an immutable weekly report. It preserves missing measurements, namespace-only aggregates, comparable metric windows and stable recommendation identities. The disabled activity definition still requires accepted input collection, durable executor storage, delivery and recovery before unattended operation.
keywords: [capacity, weekly, telemetry, retention, recommendations, resource-control]
```
---
## Notes

View file

@ -79,8 +79,10 @@
| 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-0079 | active | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| workplan | CUST-WP-0080 | finished | — | 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 | CUST-WP-0082 | backlog | — | workplans/CUST-WP-0082-netkingdom-security-hardening.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 |
@ -501,13 +503,82 @@
| 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-T05 | progress | — | 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-T03 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T04 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T05 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T06 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T07 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T08 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T09 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T10 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T11 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T12 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T13 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T14 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T15 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T16 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T17 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T18 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T19 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T20 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T21 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T22 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T23 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T24 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T25 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T26 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T27 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T28 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T29 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T30 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T31 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T32 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T33 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T34 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T35 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T36 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T37 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T38 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T39 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T40 | cancel | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T41 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T42 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T43 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T44 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T45 | done | — | 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 | CUST-WP-0082-T01 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T02 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T03 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T04 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T05 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T06 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T07 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T08 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T09 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T10 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T11 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T12 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T13 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T14 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T15 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T16 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T17 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T18 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T19 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T20 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T21 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T22 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T23 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T24 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.md |
| task | CUST-WP-0082-T25 | todo | — | workplans/CUST-WP-0082-netkingdom-security-hardening.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 |
@ -527,3 +598,4 @@
| 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 |

View file

@ -0,0 +1,13 @@
{
"schema": "custodian.weekly-allocation-request.v1",
"binding_status": "unbound; CUST-WP-0071-T05 executor owner must provide real retained inputs before enabling",
"cluster": null,
"allocation": null,
"telemetry": null,
"inventory": null,
"provenance": null,
"previous": null,
"reviews": null,
"as_of": null,
"output": null
}

View file

@ -0,0 +1,92 @@
# CUST-WP-0073 credential boundary — source proposal revision 2
Status: preparation; no execution authorization. Supervisor: Bernd Worsch.
Assignment: custodian-codex; supervised, autopilot disabled.
Stable proposal ID: CUST73-CREDENTIAL-BOUNDARY-R1. Original source inputs are
pinned in source-pins.json. SHA256SUMS binds the current review packet; original-revision-1.SHA256SUMS
retains the original digest inventory. Revision 2 fixes the negative verifier
after review found aggregate namespace denial insufficient. This is refinement
of the same proposal, not another success or any execution approval.
This packet narrows the remaining implementation choices. It is not executable
as a complete cutover: exact current host configuration, workstation assignment
launch and approved privileged executor inputs remain owner review gates.
## Prepared source and owners
- Platform: identity-rbac.yaml proposes a dedicated ServiceAccount and explicit
GET-only version/health observation allowlist. No raw resource observation is
granted. Workload observations must use sanitized supervisor-returned results.
Issue short-lived credentials through the existing owner custody path; do not
create legacy static ServiceAccount token Secrets. Other existing RBAC bindings
must be tested; this manifest alone cannot prove effective denial.
- Cluster: cluster-bootstrap.patch removes mode 644 and unconditional transfer
of the admin kubeconfig into the controller account. It changes future install
flags to 600 and converges file ownership/mode. The actual source owner is
railiance-cluster, not railiance-enablement. The existing installer has a
creates guard: on an installed node it does NOT update the systemd launch
flags. The owner must capture sanitized effective mode/flag evidence and supply
an exact persistent override/restart diff; applying this patch alone would
permit the old 644 unit flag to restore exposure on restart. Do not run the
broad bootstrap playbook for this narrow cutover.
- Infra: host-identity.candidate.yaml specifies a separate locked-password
custodian-agt account and agt-custodian-codex principal. Use the existing
ssh_ca_host inventory mechanism, never map this principal to tegwick. No sudo,
privileged groups, agent forwarding, port forwarding, user RC/env or raw keys.
Existing agt-task-bridge and agt-interhub-bootstrap mappings have other consumers;
do not remove them blindly. Prove the assignment cannot use their identities.
- Warden: issue only the scoped new SSH principal under its existing issuance
contract after host mapping review. No secret retrieval through Warden.
- Harness: reuse installed f599 and successful revision-3 offline rehearsal;
finish existing native admission gates. The actual interactive session must
enter the restricted profile before any model/tool process can observe the
host account, external connectors or inherited credentials. A child sandbox
inside the current admin-capable parent does not migrate this assignment.
## Exact cutover packet still required before approval
Record the concrete host and workstation account IDs; resolved SSH policy;
public principal mapping; sanitized effective sudo/group/ACL decisions; pinned
restricted kubeconfig context and API endpoint; trusted executor identity and
allowlisted exact command digest; issuer TTL/audience; existing credential/file,
agent socket, GitOps and external-tool inventory; safe staging and retirement
order. Bind all of these plus owner reviews and verification/rollback digests
into a new exact revision, preserving this original packet. Do not ask for a
blanket approval of placeholders.
Kubernetes and SSH identity issuance do not solve workstation credential reach:
old admin kubeconfigs, SSH agent sockets, writable trusted executor/runtime
files, GitOps write credentials, container sockets, OpenBao tokens and inherited
connector authority must all be absent from the actual agent account/profile.
Privileged executor must live outside the same writable account and bind exact
approved action/target/revision; a readable approval JSON is not enforcement.
## Verification and rollback
1. Owner renders/reviews manifests and source diff without activation. Check
effective existing RBAC and host override sources before finalizing.
2. Supervisor retains separate attended recovery access, then stages the new
identity. Never remove the last working human admin path.
3. Run verify_agent_denials.py inside the actual new account/profile with its
explicit context and supervisor-supplied complete current namespace inventory.
Inventory JSON requires cluster="railiance01", complete=true, a nonempty unique
namespaces list and supervisor_receipt_ref; its canonical SHA256 is bound in
the result. Its authenticity/completeness must be checked by the supervisor.
Aggregate --all-namespaces checks are retained, with every namespaced permission
also checked in every listed namespace; cluster-scoped privileges are checked
separately. Empty inventory refuses before any command. It refuses the old account/identity, uses only auth reviews
and access checks, suppresses raw command output, and reports partial proof.
4. Supervisor independently verifies sudoers/ACLs, old admin SSH refusal, complete
workstation/external-tool isolation, and positive sanitized execution of one
approved action plus refusal of changed, unapproved, expired and replayed
actions. Bind results to the actual runtime and assignment, not a fixture.
5. Only then document the admitted T02/T04 path. On any incomplete/failed probe,
stop assignment admission, revoke only newly issued identities, and use the
retained human path for recovery. Restore exact predecessor configuration
only under the scoped recovery decision; never silently restore world-readable
admin credentials. Preserve failed receipts. No automatic privilege fallback.
Cost estimate: no new infrastructure allocation; owner review and one attended
window required. Risk estimate: potential loss of administrative access and
single-node interruption during k3s restart; monetary loss is not quantified.
This is not an autopilot grant. T05 rotation deferral remains unchanged.

View file

@ -0,0 +1,9 @@
1d60bf9acf066baff954348159c2213bba14d51131bf17e98d5fe4db165dbd7f PROPOSAL.md
67f71a470f9a877d4f8415af3d627bef16d44c20e5add5d17e30518e327d4ff8 cluster-bootstrap.patch
a8f83d8e1bb5ae65cbdfdab3e710a02f47f395b157c278768700b515ed343f9f host-identity.candidate.yaml
591d893a8f0b1d1b30235900b1a5bd828e01e2163a7bb48d8ba4de963dba80d1 identity-rbac.yaml
aa99050e3c446faf773a60701e5d1215f90ebca66a20edcb95975611e8683a5f original-revision-1.SHA256SUMS
c3815aea14fe0b1ba6e8b5e4b339a82496adb511a2ea587d38c797c9513e86f4 source-pins.json
3077831ba2efdfe53ec643c4a925801850edeecf9f1f52a43d12c3ae923da4e5 test_negative_verifier.py
88da0faf33b532acbec16e5f28acfb87278ec66328effb97ff9404891a818b65 validation.json
7f8cfb9340873acf508072f19e8476603a61a329041e381f0464000f2e5d795f verify_agent_denials.py

View file

@ -0,0 +1,45 @@
--- a/ansible/bootstrap.yml
+++ b/ansible/bootstrap.yml
@@ -9,7 +9,6 @@
vars:
k3s_version: "v1.35.1+k3s1"
helm_version: "v3.17.3"
- kubeconfig_local_path: "~/.kube/config-hosteurope"
tasks:
# ── Base packages ────────────────────────────────────────────────────────
@@ -27,7 +26,7 @@
shell: |
curl -sfL https://get.k3s.io | \
INSTALL_K3S_VERSION="{{ k3s_version }}" \
- INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644" \
+ INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=600" \
sh -
args:
creates: /usr/local/bin/k3s
@@ -77,17 +76,11 @@
- debug: var=helm_ver.stdout
- # ── Kubeconfig ───────────────────────────────────────────────────────────
- - name: Fetch kubeconfig to control node
- fetch:
- src: /etc/rancher/k3s/k3s.yaml
- dest: "{{ kubeconfig_local_path }}"
- flat: yes
-
- - name: Fix server address in fetched kubeconfig
- delegate_to: localhost
- become: false
- replace:
- path: "{{ kubeconfig_local_path }}"
- regexp: 'server: https://127\.0\.0\.1:6443'
- replace: "server: https://{{ ansible_host }}:6443"
+ # Admin kubeconfig stays on the host. Attended administration uses
+ # sudo k3s kubectl; agent identity issuance belongs to railiance-platform.
+ - name: Restrict existing admin kubeconfig permissions
+ ansible.builtin.file:
+ path: /etc/rancher/k3s/k3s.yaml
+ owner: root
+ group: root
+ mode: "0600"

View file

@ -0,0 +1,25 @@
# Proposed additions only, not a runnable estate playbook.
account:
name: custodian-agt
password_locked: true
home_mode: '0700'
supplementary_groups: []
sudo_rules: []
ssh_certificate_principal: agt-custodian-codex
authorized_keys: []
ssh_match:
User: custodian-agt
PasswordAuthentication: 'no'
KbdInteractiveAuthentication: 'no'
AuthenticationMethods: publickey
AuthorizedKeysFile: none
AllowAgentForwarding: 'no'
AllowTcpForwarding: 'no'
X11Forwarding: 'no'
PermitTunnel: 'no'
PermitUserRC: 'no'
PermitUserEnvironment: 'no'
principals:
# Add this user to Railiance01 only; never map the new principal to tegwick.
Railiance01:
custodian-agt: [agt-custodian-codex]

View file

@ -0,0 +1,34 @@
# REVIEW CANDIDATE ONLY. No credential issuance or activation.
apiVersion: v1
kind: Namespace
metadata:
name: custodian-agents
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: custodian-codex
namespace: custodian-agents
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: custodian-supervised-observer
rules:
# Health/version only. No raw workload specs, logs, ConfigMaps or Secrets.
- nonResourceURLs: ["/version", "/healthz", "/livez", "/readyz"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: custodian-codex-supervised-observer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: custodian-supervised-observer
subjects:
- kind: ServiceAccount
name: custodian-codex
namespace: custodian-agents

View file

@ -0,0 +1,8 @@
e981c883a0d9f0618717d5da9c1c9016845643d8a3fb96f64894877d69e0d2e7 PROPOSAL.md
67f71a470f9a877d4f8415af3d627bef16d44c20e5add5d17e30518e327d4ff8 cluster-bootstrap.patch
a8f83d8e1bb5ae65cbdfdab3e710a02f47f395b157c278768700b515ed343f9f host-identity.candidate.yaml
591d893a8f0b1d1b30235900b1a5bd828e01e2163a7bb48d8ba4de963dba80d1 identity-rbac.yaml
c3815aea14fe0b1ba6e8b5e4b339a82496adb511a2ea587d38c797c9513e86f4 source-pins.json
11a4f287cbe2148db511704f88a04d60416e6b7ece1ee6c4fe02e8e7b39eb155 test_negative_verifier.py
868551696f425366b3053ba16ec505c98ec60dc899b1187b4b9f6ed2f86b51af validation.json
aae332f01e1e788f19fd653b6ed9c850d1bc62e5721cd11cd750c79c68be5e62 verify_agent_denials.py

View file

@ -0,0 +1,27 @@
{
"railiance-cluster": {
"head": "07ce43f33c6128f4caf1f3251a18b3bf10bab061",
"files": {
"ansible/bootstrap.yml": "82d58148582e33cc195bf4041ce7cb42f7a6a1a562e383d7a7e4b93e0756071d"
}
},
"railiance-infra": {
"head": "32f64b54fd94a5e6a145ef7c2c8faf051f721a71",
"files": {
"ansible/inventory/ssh_principals.yaml": "4d8577d9c91ee2a0c1fc94944ef633f1866fd0b70c6967def210df991d0bcdd0",
"ansible/roles/ssh_ca_host/tasks/main.yml": "c840ccb53d351ca6a1bb8b7a8406efae4451bb651707b03353f5145aa8f21fd3"
}
},
"glas-harness": {
"head": "136d532a02b87adcf36213a29478954d9dc5ff7d",
"files": {
"SCOPE.md": "882c972bec13f04d91a9a0cd3e38cc496a491a2fde1b800e617e1fd250ff643c"
}
},
"rein-aharness": {
"head": "eca0a5891c78fbda2422b39d4a594cec1704c860",
"files": {
"workplans/REINAH-WP-0008-simpler-metered-retry.md": "1c7a9f3ef10fb0d945e7ca8c473377bcf778fe08a404d7b87db90e529fc541a3"
}
}
}

View file

@ -0,0 +1,71 @@
import importlib.util
import json
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('verifier', Path(__file__).with_name('verify_agent_denials.py'))
v = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v)
INVENTORY = {'cluster': 'railiance01', 'namespaces': ['default', 'sso'], 'complete': True, 'supervisor_receipt_ref': 'synthetic-test-only'}
class VerificationTest(unittest.TestCase):
def test_old_account_refuses_before_any_command(self):
with patch.object(v.os, 'geteuid', return_value=0), patch.object(v, 'run') as run:
self.assertFalse(v.probe('/kubectl', 'agent', 'custodian-agt', INVENTORY)['passed'])
run.assert_not_called()
def simulate(self, identity, denied_code=1, denied_output=b'no', allow_sso=False):
payload = json.dumps({'status': {'userInfo': identity}}).encode()
def response(argv):
if 'whoami' in argv:
return 0, payload
if allow_sso and '--namespace' in argv and argv[-1] == 'sso' and 'secrets' in argv:
return 0, b'yes'
return denied_code, denied_output
with patch.object(v.os, 'geteuid', return_value=1234), \
patch.object(v.pwd, 'getpwuid', return_value=SimpleNamespace(pw_name='custodian-agt')), \
patch.object(v.os, 'getgroups', return_value=[1234]), \
patch.object(v.os, 'getegid', return_value=1234), \
patch.object(v.grp, 'getgrgid', return_value=SimpleNamespace(gr_name='custodian-agt')), \
patch.object(v.os, 'access', return_value=False), \
patch.object(v, 'run', side_effect=response):
return v.probe('/kubectl', 'agent', 'custodian-agt', INVENTORY)
def test_admin_membership_refuses(self):
result = self.simulate({'username': v.IDENTITY, 'groups': ['system:masters']})
self.assertEqual(result['status'], 'refused_wrong_identity')
def test_timeout_is_not_denial(self):
result = self.simulate({'username': v.IDENTITY}, None, b'')
self.assertFalse(result['passed'])
def test_forbidden_text_is_not_valid_denial(self):
result = self.simulate({'username': v.IDENTITY}, 1, b'Forbidden secret material')
self.assertFalse(result['passed'])
self.assertNotIn('secret material', json.dumps(result))
def test_namespaced_allow_fails_even_when_all_namespace_query_would_deny(self):
result = self.simulate({'username': v.IDENTITY}, allow_sso=True)
self.assertFalse(result['passed'])
self.assertFalse(result['checks']['sso:get:secrets'])
def test_empty_inventory_refuses_before_commands(self):
with patch.object(v, 'run') as run:
result = v.probe('/kubectl', 'agent', 'custodian-agt', {**INVENTORY, 'namespaces': []})
self.assertFalse(result['passed'])
run.assert_not_called()
def test_negative_success_cannot_admit_assignment(self):
result = self.simulate({'username': v.IDENTITY})
self.assertTrue(result['passed'])
self.assertFalse(result['assignment_admitted'])
self.assertTrue(result['remaining'])
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,9 @@
{
"source_patch_applies": true,
"negative_verifier_tests": 7,
"live_probes": false,
"live_mutations": false,
"assignment_admitted": false,
"proposal_revision": 2,
"retained_refinement": "Revision1 aggregate denial insufficient for namespace RoleBinding; no live use; revision2 adds complete inventoried per-namespace and separate cluster probes."
}

View file

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Read-only actual-account probes. Never prints command output or credentials."""
import argparse
import grp
import hashlib
import re
import json
import os
import pwd
import subprocess
IDENTITY = 'system:serviceaccount:custodian-agents:custodian-codex'
DENIALS = [
('get', 'secrets'), ('list', 'secrets'), ('watch', 'secrets'),
('get', 'configmaps'), ('get', 'pods'), ('get', 'pods/log'),
('create', 'pods/exec'), ('create', 'pods/attach'), ('create', 'pods'),
('patch', 'deployments.apps'), ('create', 'serviceaccounts/token'),
('patch', 'applications.argoproj.io'),
]
CLUSTER_DENIALS = [('impersonate', 'users'),
('bind', 'clusterroles.rbac.authorization.k8s.io'),
('escalate', 'clusterroles.rbac.authorization.k8s.io')]
def inventory_valid(inventory):
if not isinstance(inventory, dict):
return False
namespaces = inventory.get('namespaces')
return (inventory.get('cluster') == 'railiance01'
and inventory.get('complete') is True
and isinstance(inventory.get('supervisor_receipt_ref'), str)
and bool(inventory['supervisor_receipt_ref'].strip())
and isinstance(namespaces, list) and bool(namespaces)
and all(isinstance(n, str) and len(n) <= 63
and re.fullmatch(r'[a-z0-9](?:[a-z0-9-]*[a-z0-9])?', n)
for n in namespaces)
and len(set(namespaces)) == len(namespaces))
def run(argv):
try:
p = subprocess.run(argv, capture_output=True, timeout=20, check=False)
return p.returncode, p.stdout
except (OSError, subprocess.TimeoutExpired):
return None, b''
def probe(kubectl, context, account, inventory):
if not inventory_valid(inventory):
return {"status": "refused_namespace_inventory", "passed": False}
# Abort before even auth-only Kubernetes requests under the old account.
if os.geteuid() == 0 or pwd.getpwuid(os.geteuid()).pw_name != account:
return {'status': 'refused_wrong_account', 'passed': False}
base = [kubectl, '--context', context, '--request-timeout=10s', 'auth']
code, output = run(base + ['whoami', '-o', 'json'])
try:
info = json.loads(output)['status']['userInfo']
identity = code == 0 and info['username'] == IDENTITY and 'system:masters' not in info.get('groups', [])
except (KeyError, TypeError, ValueError):
identity = False
if not identity:
return {'status': 'refused_wrong_identity', 'passed': False}
checks = {}
for verb, resource in DENIALS:
code, output = run(base + ['can-i', verb, resource, '--all-namespaces'])
checks[f'all-namespaces:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
# An all-namespaces denial can coexist with a namespaced RoleBinding grant.
# Probe each namespace supplied in the separately supervised complete inventory.
for namespace in inventory['namespaces']:
for verb, resource in DENIALS:
code, output = run(base + ['can-i', verb, resource, '--namespace', namespace])
checks[f'{namespace}:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
for verb, resource in CLUSTER_DENIALS:
code, output = run(base + ['can-i', verb, resource])
checks[f'cluster:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
dangerous = {'sudo', 'wheel', 'docker', 'lxd', 'incus-admin', 'disk', 'libvirt'}
groups = {grp.getgrgid(gid).gr_name for gid in set(os.getgroups()) | {os.getegid()}}
checks['no_privileged_groups'] = not (groups & dangerous)
checks['host_admin_kubeconfig_unreadable'] = not os.access('/etc/rancher/k3s/k3s.yaml', os.R_OK)
checks['legacy_admin_home_unsearchable'] = not os.access('/home/tegwick', os.X_OK)
checks['root_home_unsearchable'] = not os.access('/root', os.X_OK)
for label, path in [('docker', '/var/run/docker.sock'), ('containerd', '/run/k3s/containerd/containerd.sock')]:
checks[f'{label}_socket_not_writable'] = not os.access(path, os.W_OK)
# This is a partial negative proof, never assignment admission on its own.
return {'status': 'negative_probes_passed' if all(checks.values()) else 'negative_probes_failed',
'passed': all(checks.values()), 'checks': checks, 'assignment_admitted': False,
'namespace_inventory_sha256': hashlib.sha256(json.dumps(inventory, sort_keys=True, separators=(',', ':')).encode()).hexdigest(),
'namespace_count': len(inventory['namespaces']),
'supervisor_receipt_ref': inventory['supervisor_receipt_ref'],
'remaining': ['supervisor_verified_complete_current_namespace_inventory', 'supervisor_verified_sudoers_and_ACLs', 'all_external_tool_and_credential_paths',
'old_admin_SSH_denial', 'exact_approved_action_and_changed_action_refusal',
'actual_interactive_runtime_admission']}
if __name__ == '__main__':
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--kubectl', default='/usr/local/bin/kubectl')
p.add_argument('--context', required=True)
p.add_argument('--namespace-inventory', required=True, help='Supervisor-supplied complete current namespace inventory JSON')
p.add_argument('--account', default='custodian-agt', choices=['custodian-agt'])
a = p.parse_args()
try:
with open(a.namespace_inventory, encoding='utf-8') as stream:
inventory = json.load(stream)
except (OSError, ValueError):
inventory = None
receipt = probe(a.kubectl, a.context, a.account, inventory)
print(json.dumps(receipt, sort_keys=True))
raise SystemExit(0 if receipt['passed'] else 1)

View file

@ -33,10 +33,33 @@ reuse a different owner checkout. The adapter does not duplicate the parser or
call the auto-publishing consistency CLI. It retains the owner's generated-file
format, including its existing regeneration attribution.
This implementation has not been activated in shared hooks or adopted across
the fleet. A future reviewed pre-commit integration must run generation beside
the existing generated-only guard and secret scan, retain their rejection
behavior, and preserve unrelated hooks and changes. The existing guard still
rejects generated-only commits. Source inventories and publication indexes
The shared repo-manager hook and Custodian local hook now run the existing
generated-only guard first, then the opt-in generator, then their existing secret
scan. Activity Core, Policy Nexus and Repo Manager have committed opt-ins;
Custodian source includes its opt-in. The guard still rejects generated-only
commits. Other repositories without a staged opt-in keep their prior behavior.
The tracked `.generated-work-records.json` supplies version 1 and canonical
`repo_slug`. Its dedicated `.generated-work-records.mk` avoids loading unrelated
Makefiles or environment-file includes. The hook verifies that both files match
the staged blobs before invoking the fixed target, with Make override variables
removed from its environment. The recipe is reviewed, trusted repository source;
this is not a sandbox for arbitrary untrusted Make code.
`git commit --only` and other alternate-index commits must not need generation:
Git otherwise retains a stale normal index after a hook changes the temporary
commit index. The adapter refuses such writes before mutation. Stage the selected
sources and run `make generated-work-records` first, then include both those
source paths and `WORK-RECORDS.md` in `git commit --only`. A regression test proves
that unrelated staged briefs remain staged and the generated index is coherent.
Upgrade the exact reviewed secret-scan hook composition reproducibly with:
```bash
python3 scripts/install_shared_generated_guard.py --repo . --install --enable-generation
```
Unknown hook compositions are refused; existing other hooks and routing remain
unchanged. The option does not opt any repository into generation on its own. Source inventories and publication indexes
retain their respective owner generators; this adapter generates only the
work-record index. Fleet adoption remains an open CUST-WP-0079-T05 obligation.

View file

@ -0,0 +1,110 @@
# Retained weekly allocation assessment
`scripts/weekly_allocation_report.py` implements the offline evidence join for
CUST-WP-0071-T05. It reads the existing cluster collector, Custodian allocation
reconcile, Prometheus query-response capture and resource-control inventory.
It performs no collection, deployment, network calls, task writes or delivery.
It is suitable for an accepted durable executor; it does not install one.
## Inputs and retained evidence
The required inputs are:
- `--cluster`: `railiance.cluster-resource-observation.v1`, produced by
railiance-cluster's `make cluster-observe REMOTE=railiance01`.
- `--allocation`: `custodian.allocation-reconcile.v1` from
`scripts/reconcile_railiance_allocation.py`, matching the same capture and
workload inventory. Run both against the same retained observation.
- `--telemetry`: the existing September 28 query-capture shape: `captured_at`,
`window: 7d`, `step: 5m`, and `queries` mapping each metric name to its exact
`promql` and Prometheus `response`. Preserve original sample evaluation times;
do not restamp historical query results. These describe the query window,
not independently verified raw scrape completeness.
- `--inventory`: resource-control's `data/resources/reef-railiance-k3s.json`.
Dated portfolio capacity is retained as portfolio context, never summed with
the live cluster or treated as current scheduler evidence.
- `--provenance`: exact source collector revisions and `source_observation`
matching the cluster capture, as in the September 28 allocation provenance.
- `--as-of`: explicit evaluation timestamp; identical retries use the same value.
- `--output`: a new retained JSON path. Exact identical retries succeed; changed
content at that path fails and requires an explicit revision path. Writes use
a complete temporary file and atomic create-without-replacement publication.
Optional `--previous` selects the previous retained report. Deltas require the
same resource, exact query texts, sample step, seven-day window and consecutive
capture endpoints within one sample step. Other comparisons stay unknown; the
report still records disappeared workloads. Recommendation identity depends on
resource/namespace/workload/review purpose, not the date or recommendation text.
Existing receiving-task links survive retries and subsequent reports.
Metric names are `cpu_p95_m`, `cpu_peak_m`, `memory_p95_bytes`,
`memory_peak_bytes`, `cpu_throttle_ratio`, `latency_p95_seconds`, `errors`,
`request_count`, `restarts`, `oom_events`, `cpu_sample_count` and
`memory_sample_count`. Metrics are nonnegative, finite instant-vector aggregates.
Workload queries must carry exactly `namespace` and `workload` identity labels
(an optional Prometheus `__name__` is allowed). Extra labels or duplicate series
are not silently aggregated. Namespace-only results are retained separately;
they never become per-workload values, even for a currently single-workload
namespace. Namespace peaks and percentiles are not additive.
Missing series, failed queries and non-finite values remain null with explicit
reasons. Observed zero remains zero. Evaluation counts never prove raw scrape
coverage. CPU/memory limits and instantaneous observations come from the cluster
snapshot; historical latency, request/errors, OOM or throttling are not inferred
from that snapshot. Unknown operating margin remains null; reservation residual
does not establish processing capacity or release acceptance.
## Owner decisions
Without a complete current owner review the result is `investigate`, with low
confidence and explicit evidence gaps. `--reviews` accepts a JSON mapping keyed
by the stable recommendation ID. A review provides `decision` (keep, increase,
decrease or investigate), `owner`, `period_end` matching telemetry capture,
`reviewed_at`, `evidence_refs`, `receiving_task`, `configuration`, `rollback_ref`,
`coverage_ref`, `service_acceptance_ref`, and `margin_ref`. Optional `outcome`
records what happened to the preceding recommendation. These references are
owner assertions to be checked during acceptance, not proof generated by this
adapter. Wrong-owner/window reviews, missing workload metrics, zero CPU requests,
unknown owners and stale/future evidence keep the result at investigate and
retain the proposal for review. No idle sample automatically recommends a cut.
Only new observation evidence and verified owner disposition can complete T02–T04.
In particular, the historical Vergabe sample does not establish the attended
two-user p95/error acceptance target or incremental database attribution.
## Disabled execution contract
The owner-source candidate is
`activity-core/activity-definitions/weekly-railiance-allocation-review.md`.
Its fixed command is:
```sh
python3 scripts/weekly_allocation_report.py --request config/weekly-allocation-request.json
```
The request is intentionally unbound and the command fails until the accepted
executor supplies real input paths, a fixed `as_of`, and a durable output path.
Paths resolve relative to the request file. The executor must snapshot this
request per run, keep it stable for retries, bind a real previous report, and
retain reports beyond the seven-day raw-metric retention. The committed null
template is not a production storage location or collection implementation.
Activity-core must select a scoped durable Railiance executor/profile before
projection or activation; no workstation profile is asserted here. It owns the
Monday 08:00 Europe/Berlin schedule and operations-run retry/lease evidence.
The adapter's matching recommendation IDs are the downstream upsert keys for
existing owner work records; it does not itself create those records. Delivery,
explicit owner acknowledgment, independent missed/failed-run detection and
recovery remain unbound. A report file or successful ops_run alone proves none
of them. T05 needs the first unattended schedule receipt and exercised recovery.
## Verification
```sh
python3 -m unittest discover -s tests -p test_weekly_allocation_report.py -v
```
The tests cover historical namespace-only input, missing/zero/NaN, invalid
aggregation, query failure, compatible/incompatible periods, stable identities
and owner-task links, stale/future evidence, owner review, resource/capture
mismatch and retained retry/collision behavior.

View file

@ -14,7 +14,10 @@ def generated(path):
name = parsed.name
return (name in {"work-records.md", ".custodian-brief.md", "source-index.md",
"publication-index.md", "index.generated.md"}
or name.startswith("source-inventory")
# The root config is authored generator input, not its output.
# Keep similarly named evidence and generated-directory copies gated.
or (name.startswith("source-inventory")
and str(parsed) != "source-inventory.config.json")
or name.startswith("decision-packets")
or name.startswith("publication-index") or name.startswith("projection-refresh")
or str(parsed).startswith("generated/")

View file

@ -5,6 +5,7 @@ Uses the existing State Hub renderer. No hub access, commit or push. Intended
for a Make target invoked by pre-commit, before the unchanged generated guard.
"""
import argparse
import os
from pathlib import Path
import re
import subprocess
@ -75,6 +76,14 @@ def generate(repo, renderer_root, *, repo_slug, check=False):
return False
if check:
raise ValueError('staged WORK-RECORDS.md is stale')
index_env = os.environ.get('GIT_INDEX_FILE')
if index_env:
selected = Path(index_env)
if not selected.is_absolute():
selected = repo / selected
normal = Path(git(repo, 'rev-parse', '--absolute-git-dir').decode().strip()) / 'index'
if selected.resolve() != normal.resolve():
raise ValueError('alternate index/partial commit needs regeneration; stage selected sources, run make generated-work-records, then include WORK-RECORDS.md in the partial commit')
if staged_entries != git(repo, 'ls-files', '--stage', '-z'):
raise ValueError('Git index changed during rendering; retry')
if (destination.read_bytes() if destination.exists() else None) != existing:

View file

@ -2,4 +2,5 @@
# Install as .git/hooks/pre-commit; rejects generated/evidence-only auto commits too.
set -eu
REPO_ROOT="$(git rev-parse --show-toplevel)"
exec python3 "$REPO_ROOT/scripts/check_generated_only_commit.py" --staged
python3 "$REPO_ROOT/scripts/check_generated_only_commit.py" --staged
exec python3 "$REPO_ROOT/scripts/run_generated_work_records_hook.py"

View file

@ -17,10 +17,12 @@ import subprocess
# CUST-WP-0080 / RMGR-WP-0021; unknown hooks still require owner integration.
SCANNED_HOOK_SHA256 = "d50cf763dc9814383c6886e30b6650bbbdcb49ddd78889082e3fda3626a2cbfd"
ADOPTED_HOOK_SHA256 = "1a8a8b2a54092311a355a3a51cf6a696bd05e146f2f4a25455ae5c30c502929d"
CHECKER = Path(__file__).with_name("check_generated_only_commit.py").resolve()
def install(repo, execute=False):
def install(repo, execute=False, enable_generation=False):
repo = Path(repo).resolve()
configured = subprocess.check_output(
["git", "-C", str(repo), "config", "--get", "core.hooksPath"], text=True).strip()
@ -36,9 +38,25 @@ def install(repo, execute=False):
raise ValueError("refuse redirected existing pre-commit hook")
if target.exists():
if (target.read_text() != content
and hashlib.sha256(target.read_bytes()).hexdigest() != SCANNED_HOOK_SHA256):
and hashlib.sha256(target.read_bytes()).hexdigest() not in {SCANNED_HOOK_SHA256, ADOPTED_HOOK_SHA256}):
raise ValueError("existing pre-commit hook differs; owner integration required")
return {"repo": str(repo), "hook": str(target), "status": "installed", "changed": False}
changed = False
if enable_generation and hashlib.sha256(target.read_bytes()).hexdigest() == SCANNED_HOOK_SHA256:
marker = "python3 " + shlex.quote(str(CHECKER)) + " --staged\n"
existing = target.read_text()
if existing.count(marker) != 1:
raise ValueError("reviewed hook checker path differs")
revised = existing.replace(marker, marker + "python3 " + shlex.quote(str(CHECKER.with_name("run_generated_work_records_hook.py"))) + "\n")
if hashlib.sha256(revised.encode()).hexdigest() != ADOPTED_HOOK_SHA256:
raise ValueError("adopted composition differs from reviewed hook")
if execute:
fd = os.open(target, os.O_WRONLY | os.O_TRUNC | os.O_NOFOLLOW)
with os.fdopen(fd, "w") as handle:
handle.write(revised)
handle.flush()
os.fsync(handle.fileno())
changed = True
return {"repo": str(repo), "hook": str(target), "status": "installed", "changed": changed}
if execute:
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o755)
with os.fdopen(fd, "w") as handle:
@ -53,10 +71,14 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", type=Path, required=True)
parser.add_argument("--install", action="store_true")
parser.add_argument("--enable-generation", action="store_true",
help="upgrade only the reviewed scanner composition with opt-in generation")
args = parser.parse_args()
import json
try:
print(json.dumps(install(args.repo, args.install), sort_keys=True))
print(json.dumps(install(args.repo, args.install, args.enable_generation), sort_keys=True))
except PermissionError:
parser.exit(1, "Permission denied updating the reviewed hook; rerun through the permitted filesystem path.\n")
except (ValueError, OSError, subprocess.SubprocessError):
parser.exit(1, "Shared hook unverified; existing files and routing are not replaced.\n")

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Run explicitly adopted staged generation without executing unstaged Make input."""
import json
from pathlib import Path
import re
import subprocess
import sys
CONFIG = '.generated-work-records.json'
def run(repo):
repo = Path(repo)
config = subprocess.run(['git', '-C', str(repo), 'show', ':' + CONFIG], capture_output=True)
if config.returncode:
# An unstaged opt-in never changes hook behavior.
return False
record = json.loads(config.stdout)
if (set(record) != {'version', 'repo_slug'} or record['version'] != 1
or not isinstance(record['repo_slug'], str)
or not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', record['repo_slug'])):
raise ValueError('invalid generated-work-record opt-in')
for name in (CONFIG, '.generated-work-records.mk'):
path = repo / name
staged = subprocess.check_output(['git', '-C', str(repo), 'show', ':' + name])
if path.is_symlink() or path.read_bytes() != staged:
raise ValueError(f'{name} has unstaged/redirected content; refusing hook execution')
# Make includes and environment overrides could run unstaged recipe inputs.
# Only invoke the reviewed target without implicit makefiles or user MAKEFLAGS.
import os
env = {k: v for k, v in os.environ.items() if k not in ('MAKEFILES', 'MAKEFLAGS', 'MFLAGS', 'GNUMAKEFLAGS')}
subprocess.run(['make', '--no-print-directory', '-f', '.generated-work-records.mk',
'generated-work-records', 'GENERATED_REPO_SLUG=' + record['repo_slug'],
'CUSTODIAN_SOURCE=' + str(Path(__file__).resolve().parents[1])],
cwd=repo, env=env, check=True)
return True
def main():
try:
root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()
run(root)
except (ValueError, OSError, subprocess.SubprocessError) as exc:
sys.exit(str(exc))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,321 @@
#!/usr/bin/env python3
"""Retain CUST-WP-0071 sizing evidence; no network, resizing or task emission."""
from __future__ import annotations
import argparse
from datetime import datetime, timedelta, timezone
import hashlib
import json
import math
import os
from pathlib import Path
import re
import tempfile
SCHEMA = "custodian.weekly-allocation.v1"
METRICS = (
"cpu_p95_m", "cpu_peak_m", "memory_p95_bytes", "memory_peak_bytes",
"cpu_throttle_ratio", "latency_p95_seconds", "errors", "request_count",
"restarts", "oom_events", "cpu_sample_count", "memory_sample_count",
)
REQUIRED = tuple(m for m in METRICS if not m.endswith("sample_count"))
def timestamp(value):
result = datetime.fromisoformat(value.replace("Z", "+00:00"))
if result.tzinfo is None:
raise ValueError("timestamps must include timezone")
return result.astimezone(timezone.utc)
def duration(value):
match = re.fullmatch(r"([1-9][0-9]*)([smhdw])", value)
if not match:
raise ValueError(f"unsupported duration: {value}")
return int(match[1]) * {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}[match[2]]
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, allow_nan=False).encode()).hexdigest()
def finite(value):
number = float(value)
if not math.isfinite(number) or number < 0:
raise ValueError("metric must be finite and nonnegative")
return number
def metric_series(telemetry):
"""Do not distribute namespace aggregates over workloads, or sum percentiles."""
series, queries = {}, {}
seconds = duration(telemetry["window"])
for name, query in sorted(telemetry.get("queries", {}).items()):
response = query.get("response", {})
data = response.get("data", {})
queries[name] = {
"promql": query.get("promql"), "response_status": response.get("status"),
"result_type": data.get("resultType"), "warnings": response.get("warnings", []),
"error": response.get("error"), "response_sha256": digest(response),
"evaluation_times": [],
}
if name not in METRICS or response.get("status") != "success" or data.get("resultType") != "vector":
continue
for sample in data.get("result", []):
labels = sample.get("metric", {})
namespace, workload = labels.get("namespace"), labels.get("workload")
# Additional dimensions are ambiguous: do not silently collapse pod/job series.
if not namespace or set(labels) - {"namespace", "workload", "__name__"}:
queries[name].setdefault("rejected", []).append("unaggregated or absent identity labels")
continue
key = (namespace, workload)
values = series.setdefault(key, {})
if name in values:
values[name] = {"value": None, "reason": "ambiguous duplicate series"}
continue
try:
epoch, raw = sample["value"]
end = datetime.fromtimestamp(finite(epoch), timezone.utc)
number = finite(raw)
except (KeyError, ValueError, TypeError, OverflowError):
values[name] = {"value": None, "reason": "invalid sample"}
continue
queries[name]["evaluation_times"].append(end.isoformat())
values[name] = {
"value": number, "window_start": (end - timedelta(seconds=seconds)).isoformat(),
"window_end": end.isoformat(), "query": name,
"capture_alignment_seconds": (end - timestamp(telemetry["captured_at"])).total_seconds(),
}
return series, queries
def recommendation_id(resource, namespace, workload):
return "allocation-" + digest([resource, namespace, workload, "sizing-review"])[:24]
def build_report(cluster, allocation, telemetry, inventory, provenance, *, as_of,
previous=None, reviews=None, max_age_hours=48):
now = timestamp(as_of)
if cluster.get("schema_version") != "railiance.cluster-resource-observation.v1":
raise ValueError("unsupported cluster schema")
if allocation.get("schema") != "custodian.allocation-reconcile.v1":
raise ValueError("unsupported allocation schema")
resource = cluster["resource_id"]
if inventory.get("id") != resource or allocation.get("host", {}).get("cluster_resource_id") != resource:
raise ValueError("resource identity mismatch")
if allocation.get("count_host_and_cluster_cpus_once") is not True:
raise ValueError("allocation must deduplicate host and cluster CPUs")
if cluster["captured_at"] != allocation["captured_at"]:
raise ValueError("cluster and reconcile snapshots differ")
if provenance.get("source_observation") != cluster["captured_at"]:
raise ValueError("provenance does not identify cluster snapshot")
seconds = duration(telemetry["window"])
if seconds != 604800:
raise ValueError("weekly report requires a seven-day telemetry window")
step = duration(telemetry["step"])
series, queries = metric_series(telemetry)
captured = {"cluster": cluster["captured_at"], "telemetry": telemetry["captured_at"]}
freshness = {}
for name, value in captured.items():
age = (now - timestamp(value)).total_seconds()
freshness[name] = {"age_seconds": age, "status": "future" if age < 0 else "stale" if age > max_age_hours * 3600 else "fresh"}
query_signature = digest({name: q["promql"] for name, q in queries.items()})
end = timestamp(telemetry["captured_at"])
comparison_reason = "no previous retained report"
compatible = False
if previous:
prev_end = timestamp(previous["period"]["capture_end"])
compatible = (
previous.get("schema") == SCHEMA and previous.get("resource_id") == resource
and previous["period"].get("seconds") == seconds
and previous["period"].get("step_seconds") == step
and previous.get("query_signature") == query_signature
and abs((end - prev_end).total_seconds() - seconds) <= step
)
comparison_reason = "comparable consecutive capture windows" if compatible else "nonconsecutive windows or changed query/resource/step contract"
previous_rows = {(r["namespace"], r["workload"]): r for r in (previous or {}).get("workloads", [])}
owners = {(r["namespace"], r["workload"]): r for r in allocation.get("workloads", [])}
if len(owners) != len(allocation.get("workloads", [])):
raise ValueError("duplicate allocation workload identity")
reviews = reviews or {}
rows, recommendations = [], []
seen = set()
for workload in sorted(cluster.get("utilization", {}).get("workloads", []), key=lambda x: (x["namespace"], x["workload"])):
key = workload["namespace"], workload["workload"]
if key in seen:
raise ValueError("duplicate cluster workload identity")
seen.add(key)
owner = owners.get(key, {})
measurements = {m: series.get(key, {}).get(m, {"value": None, "reason": "no workload series"}) for m in METRICS}
gaps = [m for m in REQUIRED if measurements[m]["value"] is None]
metric_stale = [m for m, v in measurements.items() if v["value"] is not None and not 0 <= (now - timestamp(v["window_end"])).total_seconds() <= max_age_hours * 3600]
metric_unaligned = [m for m, v in measurements.items() if v["value"] is not None and abs(v["capture_alignment_seconds"]) > step]
rid = recommendation_id(resource, *key)
reasons = []
if gaps:
reasons.append("missing workload metrics: " + ", ".join(gaps))
if metric_stale or any(v["status"] != "fresh" for v in freshness.values()):
reasons.append("stale or future observation")
if metric_unaligned:
reasons.append("metric evaluation windows differ from capture by more than one step")
if owner.get("owner", "unknown") == "unknown":
reasons.append("unknown owner")
if workload.get("requests", {}).get("cpu_millicores") == 0:
reasons.append("zero CPU request requires owner review")
review = reviews.get(rid)
decision, confidence, evidence = "investigate", "low", []
if review:
if review.get("decision") not in {"keep", "increase", "decrease", "investigate"}:
raise ValueError("invalid owner review decision")
required = ("owner", "evidence_refs", "receiving_task", "configuration", "rollback_ref", "reviewed_at", "period_end", "coverage_ref", "service_acceptance_ref", "margin_ref")
if any(not review.get(k) for k in required):
raise ValueError("owner review missing evidence, receiving task or configuration/rollback")
if review["owner"] != owner.get("owner") or review["period_end"] != telemetry["captured_at"]:
reasons.append("owner review does not match this owner/window")
elif timestamp(review["reviewed_at"]) > now:
reasons.append("owner review is future-dated")
elif not reasons:
decision, confidence, evidence = review["decision"], "owner-reviewed", review["evidence_refs"]
else:
reasons.append("owner proposal retained; incomplete evidence prevents accepted sizing recommendation")
else:
reasons.append("no owner-reviewed sizing decision")
row = {
"namespace": key[0], "workload": key[1], "owner": owner.get("owner", "unknown"),
"service": owner.get("service", "unknown"), "tenant": owner.get("tenant", "unknown"),
"pods": workload.get("pods"), "requests": workload.get("requests"),
"limits": workload.get("limits"), "instantaneous_observed": workload.get("observed"),
"metrics": measurements, "missing_metrics": gaps,
"namespace_context_ref": key[0] if (key[0], None) in series else None,
"raw_scrape_coverage": "unknown; evaluation counts do not prove source coverage",
"recommendation_id": rid, "week_on_week": {}, "comparison_reasons": {},
}
prior = previous_rows.get(key)
for metric in METRICS:
current = measurements[metric]["value"]
old_measurement = (prior or {}).get("metrics", {}).get(metric, {})
old = old_measurement.get("value")
metric_compatible = False
reason = comparison_reason
if compatible and current is not None and old is not None:
try:
old_end, old_start = timestamp(old_measurement["window_end"]), timestamp(old_measurement["window_start"])
new_end, new_start = timestamp(measurements[metric]["window_end"]), timestamp(measurements[metric]["window_start"])
metric_compatible = (
(old_end - old_start).total_seconds() == seconds
and (new_end - new_start).total_seconds() == seconds
and abs((new_end - old_end).total_seconds() - seconds) <= step
and abs((new_start - old_end).total_seconds()) <= step
and abs(measurements[metric]["capture_alignment_seconds"]) <= step
and abs((old_end - timestamp(previous["period"]["capture_end"])).total_seconds()) <= step
)
reason = "comparable consecutive metric windows" if metric_compatible else "metric evaluation windows are not consecutive or capture-aligned"
except (KeyError, ValueError, TypeError):
reason = "missing or invalid previous metric window"
elif compatible:
reason = "missing current or previous metric"
row["week_on_week"][metric] = current - old if metric_compatible else None
row["comparison_reasons"][metric] = reason
rows.append(row)
prior_recommendation = next((r for r in (previous or {}).get("recommendations", []) if r["id"] == rid), {})
recommendations.append({
"id": rid, "decision": decision, "confidence": confidence, "owner": row["owner"],
"reasons": reasons, "evidence_refs": evidence, "owner_proposal": review,
"receiving_task": review.get("receiving_task") if review else prior_recommendation.get("receiving_task"),
"previous_decision": prior_recommendation.get("decision"),
"owner_outcome": review.get("outcome") if review else None,
"delivery": "not emitted", "acknowledgment": None,
})
if seen != set(owners):
raise ValueError("cluster and allocation workload inventories differ")
return {
"schema": SCHEMA, "workplan": "CUST-WP-0071-T05", "as_of": as_of,
"adapter_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"resource_id": resource, "period": {"capture_end": telemetry["captured_at"], "seconds": seconds, "step_seconds": step,
"note": "Actual per-query windows are retained on samples; capture time is not a common query evaluation time."},
"freshness": freshness, "max_age_hours": max_age_hours, "query_signature": query_signature,
"queries": queries, "collector_provenance": provenance,
"source_sha256": {"cluster": digest(cluster), "allocation": digest(allocation), "telemetry": digest(telemetry), "inventory": digest(inventory)},
"node": {"capacity": cluster.get("capacity"), "allocation": cluster.get("allocation"),
"scheduled_request_cpu_m": allocation.get("scheduled_request_cpu_m"),
"pending_unscheduled_cpu_m": allocation.get("pending_unscheduled_cpu_m"),
"reservation_residual_cpu_m": allocation.get("residual_cpu_m"),
"release_preflight": allocation.get("state_hub_preflight_observation"),
"operating_margin": None, "release_margin_acceptance": "not established by reservation residual",
"host_cluster_counted_once": True},
"portfolio": {k: inventory.get(k) for k in ("id", "schema_version", "ownership", "capacity", "relationships", "evidence")},
"workloads": rows,
"namespace_context": {ns: values for (ns, workload), values in sorted(series.items(), key=lambda x: str(x[0])) if workload is None},
"comparison": {"compatible": compatible, "reason": comparison_reason,
"previous_report_sha256": digest(previous) if previous else None,
"disappeared_workloads": [list(key) for key in sorted(set(previous_rows) - seen)]},
"measurement_gaps": cluster.get("measurement_gaps", []) + allocation.get("measurement_gaps", []),
"recommendations": recommendations,
"limitations": ["No representative pilot acceptance inferred from telemetry.",
"Namespace percentiles and peaks are not workload metrics or additive node peaks.",
"Portfolio capacity is dated evidence, not additional host capacity.",
"No procurement, sizing changes, owner-task creation, delivery or acknowledgment performed.",
"Installed executor, scoped reads, independent missed-run handling and first unattended receipt remain T05 gates."],
}
def retain(path, report):
"""Never overwrite a retained week; identical retries succeed."""
encoded = json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n"
path.parent.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as stream:
temporary = Path(stream.name)
stream.write(encoded)
stream.flush()
os.fsync(stream.fileno())
try:
os.link(temporary, path)
except FileExistsError:
if path.is_symlink() or path.read_text() != encoded:
raise ValueError("retained report differs; choose a new explicit revision path")
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--request", type=Path, help="Executor-owned JSON request; paths relative to its directory")
for name in ("cluster", "allocation", "telemetry", "inventory", "provenance"):
parser.add_argument("--" + name, type=Path)
parser.add_argument("--previous", type=Path)
parser.add_argument("--reviews", type=Path)
parser.add_argument("--as-of")
parser.add_argument("--max-age-hours", type=float, default=48)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
try:
if args.request:
request = json.loads(args.request.read_text())
if request.get("schema") != "custodian.weekly-allocation-request.v1":
raise ValueError("unsupported executor request schema")
if any(getattr(args, key) for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "previous", "reviews", "as_of", "output")):
raise ValueError("request cannot be mixed with input/output overrides")
for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "previous", "reviews", "output"):
value = request.get(key)
setattr(args, key, args.request.parent / value if value else None)
args.as_of = request.get("as_of")
if any(not getattr(args, key) for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "as_of", "output")):
raise ValueError("executor input paths, as_of and retention output must be explicitly bound")
if not math.isfinite(args.max_age_hours) or args.max_age_hours <= 0:
raise ValueError("max-age-hours must be positive and finite")
inputs = {name: json.loads(getattr(args, name).read_text()) for name in ("cluster", "allocation", "telemetry", "inventory", "provenance")}
report = build_report(**inputs, as_of=args.as_of, max_age_hours=args.max_age_hours,
previous=json.loads(args.previous.read_text()) if args.previous else None,
reviews=json.loads(args.reviews.read_text()) if args.reviews else None)
retain(args.output, report)
except (OSError, ValueError, KeyError, TypeError) as exc:
parser.exit(2, f"weekly allocation report refused: {exc}\n")
print(json.dumps({"status": "retained", "path": str(args.output), "sha256": digest(report),
"workloads": len(report["workloads"]), "delivery": "not emitted"}))
if __name__ == "__main__":
main()

View file

@ -27,6 +27,15 @@ def test_evidence_packets_and_generated_brief_need_source_change():
assert not guard.standalone(["docs/decisions/authorization.md"])
def test_source_inventory_configuration_is_authored_input():
assert not guard.standalone(["source-inventory.config.json"])
assert not guard.standalone(["source-inventory.config.json", "source-inventory.json"])
assert guard.standalone(["source-inventory.json"])
assert guard.standalone(["source-inventory.config.json.generated"])
assert guard.standalone(["docs/evidence/source-inventory.config.json"])
assert guard.standalone(["generated/source-inventory.config.json"])
def test_ci_checks_each_commit_in_range(tmp_path):
def git(*args):
return subprocess.check_output(["git", *args], cwd=tmp_path).decode().strip()

View file

@ -95,3 +95,18 @@ def test_make_install_recognizes_existing_shared_guard(tmp_path, existing_local)
subprocess.run(["git", "-C", str(repo), "add", "WORK-RECORDS.md"], check=True)
assert subprocess.run([str(hooks / "pre-commit")], cwd=repo,
capture_output=True).returncode == 1
def test_reviewed_hook_generation_upgrade_preserves_scanner(tmp_path):
repo, hooks = repository(tmp_path)
original = (SCRIPT.parents[1] / 'docs/changes/CUST-WP-0080/secret-hooks-pre-commit').read_bytes()
hook = hooks / 'pre-commit'
hook.write_bytes(original)
hook.chmod(0o755)
assert guard.install(repo, True, enable_generation=True)['changed']
marker = b'python3 /home/worsch/the-custodian/scripts/run_generated_work_records_hook.py\n'
assert hook.read_bytes().replace(marker, b'') == original
assert not guard.install(repo, True, enable_generation=True)['changed']
(repo / 'WORK-RECORDS.md').write_text('generated only')
subprocess.run(['git', '-C', str(repo), 'add', 'WORK-RECORDS.md'], check=True)
assert subprocess.run([str(hook)], cwd=repo, capture_output=True).returncode == 1

View file

@ -101,3 +101,69 @@ def test_explicit_renderer_roots_do_not_share_import_cache(tmp_path):
'def _work_record_index_document(slug, rows): return "unused"\n'
)
assert module.render(tmp_path, 'canonical-repo', root) == (label + 'canonical-repo').encode()
def hook_module():
spec = importlib.util.spec_from_file_location('record_hook', Path(__file__).parents[1] / 'scripts/run_generated_work_records_hook.py')
loaded = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded)
return loaded
def test_opt_in_hook_ignores_unstaged_opt_in(repo):
(repo / '.generated-work-records.json').write_text('{"version":1,"repo_slug":"canonical-repo"}')
assert not hook_module().run(repo)
def test_opt_in_hook_rejects_unstaged_recipe(repo):
(repo / '.generated-work-records.json').write_text('{"version":1,"repo_slug":"canonical-repo"}')
(repo / '.generated-work-records.mk').write_text('generated-work-records:\n\t@true\n')
git(repo, 'add', '.generated-work-records.json', '.generated-work-records.mk')
(repo / '.generated-work-records.mk').write_text('unstaged change')
with pytest.raises(ValueError, match='unstaged/redirected'):
hook_module().run(repo)
def test_adopted_real_hook_preserves_partial_staging(repo):
root = Path(__file__).parents[1]
(repo / '.generated-work-records.json').write_text('{"version":1,"repo_slug":"canonical-repo"}')
(repo / '.generated-work-records.mk').write_bytes((root / '.generated-work-records.mk').read_bytes())
plan = repo / 'workplans/plan.md'
plan.write_text('---\nid: TEST-WP-0001\nstatus: active\n---\n')
git(repo, 'add', '.generated-work-records.json', '.generated-work-records.mk', 'workplans/plan.md')
plan.write_text('unstaged source')
# The ordinary Makefile must not be parsed by this fixed recipe invocation.
(repo / 'Makefile').write_text('$(error should never execute)\n')
assert hook_module().run(repo)
assert b'TEST-WP-0001 | active' in git(repo, 'show', ':WORK-RECORDS.md')
assert plan.read_text() == 'unstaged source'
def test_real_commit_only_includes_derived_index_and_preserves_other_staging(repo):
root = Path(__file__).parents[1]
git(repo, 'config', 'user.name', 'Test')
git(repo, 'config', 'user.email', 'test@example.invalid')
git(repo, 'config', 'core.hooksPath', str(repo / '.git/hooks'))
(repo / '.generated-work-records.json').write_text('{"version":1,"repo_slug":"canonical-repo"}')
(repo / '.generated-work-records.mk').write_bytes((root / '.generated-work-records.mk').read_bytes())
git(repo, 'add', '.')
git(repo, 'commit', '-qm', 'initial')
(repo / '.custodian-brief.md').write_text('unrelated staged brief')
git(repo, 'add', '.custodian-brief.md')
plan = repo / 'workplans/plan.md'
plan.write_text('---\nid: TEST-WP-0001\nstatus: active\n---\n')
hook = repo / '.git/hooks/pre-commit'
hook.write_text('#!/bin/sh\nset -eu\npython3 "' + str(root / 'scripts/check_generated_only_commit.py') + '" --staged\npython3 "' + str(root / 'scripts/run_generated_work_records_hook.py') + '"\n')
hook.chmod(0o755)
before = git(repo, 'ls-files', '--stage')
failed = subprocess.run(['git', '-C', str(repo), 'commit', '--only', 'workplans/plan.md', '-qm', 'blocked partial'], capture_output=True)
assert failed.returncode != 0
assert b'alternate index/partial commit' in failed.stderr
assert git(repo, 'ls-files', '--stage') == before
assert (repo / 'WORK-RECORDS.md').read_text() == 'old index'
git(repo, 'add', 'workplans/plan.md')
assert hook_module().run(repo)
git(repo, 'commit', '--only', 'workplans/plan.md', 'WORK-RECORDS.md', '-qm', 'source change')
assert b'TEST-WP-0001 | active' in git(repo, 'show', 'HEAD:WORK-RECORDS.md')
assert git(repo, 'diff', '--cached', '--name-only').decode().splitlines() == ['.custodian-brief.md']
assert (repo / 'WORK-RECORDS.md').read_bytes() == git(repo, 'show', 'HEAD:WORK-RECORDS.md')

View file

@ -0,0 +1,150 @@
import copy
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("weekly", ROOT / "scripts/weekly_allocation_report.py")
weekly = importlib.util.module_from_spec(spec)
spec.loader.exec_module(weekly)
class WeeklyAllocationTests(unittest.TestCase):
def setUp(self):
self.end = "2026-10-19T06:00:00+00:00"
self.resource = "resource:railiance:reef-railiance:k3s"
self.cluster = {
"schema_version": "railiance.cluster-resource-observation.v1",
"resource_id": self.resource, "captured_at": self.end,
"utilization": {"workloads": [{"namespace": "pilot", "workload": "app", "pods": 1,
"requests": {"cpu_millicores": 60, "memory_bytes": 256},
"limits": {"cpu_millicores": 1000, "memory_bytes": 1024}}]},
}
self.allocation = {
"schema": "custodian.allocation-reconcile.v1", "captured_at": self.end,
"host": {"cluster_resource_id": self.resource}, "count_host_and_cluster_cpus_once": True,
"workloads": [{"namespace": "pilot", "workload": "app", "owner": "railiance-apps"}],
}
self.telemetry = {"captured_at": self.end, "window": "7d", "step": "5m", "queries": {}}
self.inventory = {"id": self.resource}
self.provenance = {"source_observation": self.end, "revisions": {"collector": "a" * 40}}
def add_metric(self, name="cpu_p95_m", value="1.5", workload=True):
labels = {"namespace": "pilot"}
if workload:
labels["workload"] = "app"
self.telemetry["queries"][name] = {"promql": f"aggregate_{name}[7d:5m]",
"response": {"status": "success", "data": {"resultType": "vector", "result": [
{"metric": labels, "value": [weekly.timestamp(self.end).timestamp(), value]}]}}}
def report(self, **kwargs):
return weekly.build_report(self.cluster, self.allocation, self.telemetry, self.inventory,
self.provenance, as_of=kwargs.pop("as_of", self.end), **kwargs)
def test_namespace_is_not_workload_demand(self):
self.add_metric(workload=False)
report = self.report()
self.assertEqual(report["namespace_context"]["pilot"]["cpu_p95_m"]["value"], 1.5)
self.assertIsNone(report["workloads"][0]["metrics"]["cpu_p95_m"]["value"])
self.assertEqual(report["recommendations"][0]["decision"], "investigate")
def test_missing_zero_and_nonfinite_are_distinct(self):
self.add_metric(value="0")
self.add_metric("memory_peak_bytes", value="NaN")
row = self.report()["workloads"][0]
self.assertEqual(row["metrics"]["cpu_p95_m"]["value"], 0)
self.assertIsNone(row["metrics"]["memory_peak_bytes"]["value"])
self.assertIsNone(row["metrics"]["errors"]["value"])
def test_duplicate_or_extra_labels_do_not_aggregate(self):
self.add_metric()
samples = self.telemetry["queries"]["cpu_p95_m"]["response"]["data"]["result"]
samples.append(copy.deepcopy(samples[0]))
self.assertIsNone(self.report()["workloads"][0]["metrics"]["cpu_p95_m"]["value"])
samples.pop()
samples[0]["metric"]["pod"] = "app-123"
self.assertIsNone(self.report()["workloads"][0]["metrics"]["cpu_p95_m"]["value"])
def test_failure_response_is_missing(self):
self.add_metric()
self.telemetry["queries"]["cpu_p95_m"]["response"]["status"] = "error"
self.assertIsNone(self.report()["workloads"][0]["metrics"]["cpu_p95_m"]["value"])
def test_stable_recommendation_and_only_comparable_weeks(self):
self.add_metric(value="2")
current = self.report()
previous = copy.deepcopy(current)
previous["period"]["capture_end"] = "2026-10-12T06:00:00+00:00"
previous["workloads"][0]["metrics"]["cpu_p95_m"]["value"] = 1
previous["workloads"][0]["metrics"]["cpu_p95_m"].update(
window_end="2026-10-12T06:00:00+00:00", window_start="2026-10-05T06:00:00+00:00")
previous["recommendations"][0]["receiving_task"] = "OWNER-WP-0001-T01"
compared = self.report(previous=previous)
self.assertEqual(compared["workloads"][0]["week_on_week"]["cpu_p95_m"], 1)
self.assertEqual(compared["recommendations"][0]["id"], previous["recommendations"][0]["id"])
self.assertEqual(compared["recommendations"][0]["receiving_task"], "OWNER-WP-0001-T01")
previous["query_signature"] = "changed"
self.assertIsNone(self.report(previous=previous)["workloads"][0]["week_on_week"]["cpu_p95_m"])
def test_capture_consecutive_but_metric_windows_not_consecutive(self):
self.add_metric(value="2")
previous = copy.deepcopy(self.report())
previous["period"]["capture_end"] = "2026-10-12T06:00:00+00:00"
previous["workloads"][0]["metrics"]["cpu_p95_m"].update(
window_end="2026-10-14T06:00:00+00:00", window_start="2026-10-07T06:00:00+00:00")
row = self.report(previous=previous)["workloads"][0]
self.assertIsNone(row["week_on_week"]["cpu_p95_m"])
self.assertIn("not consecutive", row["comparison_reasons"]["cpu_p95_m"])
def test_future_and_stale_observations_stay_unaccepted(self):
for time, state in [("2026-10-18T06:00:00+00:00", "future"), ("2026-10-22T06:00:00+00:00", "stale")]:
report = self.report(as_of=time)
self.assertEqual(report["freshness"]["telemetry"]["status"], state)
self.assertEqual(report["recommendations"][0]["decision"], "investigate")
def test_matching_owner_review_required_for_keep(self):
for metric in weekly.METRICS:
self.add_metric(metric)
rid = self.report()["recommendations"][0]["id"]
review = {"decision": "keep", "owner": "railiance-apps", "evidence_refs": ["run:pilot"],
"receiving_task": "CUST-WP-0071-T04", "configuration": {"cpu_request_m": 60},
"rollback_ref": "unchanged-config", "reviewed_at": self.end, "period_end": self.end,
"coverage_ref": "coverage:pilot", "service_acceptance_ref": "acceptance:pilot", "margin_ref": "budget:pilot"}
self.assertEqual(self.report(reviews={rid: review})["recommendations"][0]["decision"], "keep")
review["owner"] = "other"
self.assertEqual(self.report(reviews={rid: review})["recommendations"][0]["decision"], "investigate")
def test_identity_and_snapshot_mismatch_refused(self):
self.inventory["id"] = "another-cluster"
with self.assertRaises(ValueError):
self.report()
self.inventory["id"] = self.resource
self.allocation["captured_at"] = "2026-10-12T06:00:00+00:00"
with self.assertRaises(ValueError):
self.report()
def test_retention_retries_and_revisions(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "week.json"
report = self.report()
weekly.retain(output, report)
weekly.retain(output, report)
report["as_of"] = "different"
with self.assertRaises(ValueError):
weekly.retain(output, report)
self.assertEqual(json.loads(output.read_text())["as_of"], self.end)
self.assertEqual(list(Path(directory).iterdir()), [output])
def test_historical_fixture_preserves_known_limits(self):
load = lambda suffix: json.loads((ROOT / f"docs/evidence/2026-09-28-{suffix}.json").read_text())
report = weekly.build_report(load("cluster-observation"), load("allocation-reconcile"),
load("allocation-telemetry"), self.inventory, load("allocation-provenance"), as_of=self.end)
self.assertEqual(report["node"]["reservation_residual_cpu_m"], 580)
self.assertEqual(report["namespace_context"]["forgejo"]["cpu_p95_m"]["value"], 1454.3502378724597)
self.assertTrue(all(r["decision"] == "investigate" for r in report["recommendations"]))
if __name__ == "__main__":
unittest.main()

View file

@ -352,3 +352,43 @@ validation is not green. The required `rmgr sync --path . --push` returned an
applied primary receipt with expected and derived commit both
`2b2c6f08005905a50dd394b137c4c010251b13ba`. This is repository projection evidence,
not live telemetry or reuse-surface ingestion proof.
## Offline assessment implementation — 2026-10-19
T05 preparation now includes `scripts/weekly_allocation_report.py`, the input
contract in `docs/weekly-allocation-report.md`, and an explicitly unbound
`config/weekly-allocation-request.json`. The adapter consumes retained cluster,
allocation, exact PromQL responses, provenance and resource-control inventory;
it never collects credentials or changes allocations. It retains collector/query
provenance, unknown coverage, stale/future flags, workload request/limit snapshots,
namespace context without false workload attribution, stable recommendation IDs
and previous owner-task links. Week-on-week deltas require compatible query,
resource and sample contracts plus consecutive actual metric evaluation windows;
capture timestamps alone do not suffice. Historical namespace-only measurements
therefore remain insufficient for an accepted workload recommendation.
Report publication uses atomic create-without-replacement and exact idempotent
retries; changed content requires an explicit revision. Owner proposals require
named evidence, coverage, service acceptance, margin, configuration and rollback
references; missing/stale evidence keeps the recommendation at investigate.
The adapter does not claim those references were independently accepted, create
owner tasks, deliver messages or acknowledge reports.
The actual disabled owner definition is committed in activity-core at `7490eb8`:
`activity-definitions/weekly-railiance-allocation-review.md`. Its native parser
accepts Monday 08:00 Europe/Berlin with skip misfires, `enabled: false`.
The fixed command uses the null request template and refuses execution until
real collector/retention inputs are bound. No workstation profile, production
storage path or durable-executor acceptance is asserted. No schedule was
projected, installed or enabled. The first unattended run, durable retention,
owner delivery/acknowledgment and independent missed/failed-run recovery remain
T05 acceptance gates, after T04 and executor-owner acceptance.
Validation: eleven unit tests pass, covering the historical September 28 input,
namespace/workload isolation, zero/missing/NaN and duplicate series, query
failure, stale/future evidence, owner decisions, snapshot identity, exact retry
retention, stable task links, and misleading consecutive capture timestamps
whose actual metric windows are only five days apart. The null executor request
fails closed as intended. T02–T05 remain waiting for their existing acceptance
returns; this source implementation does not close them.

View file

@ -87,11 +87,11 @@ id: CUST-WP-0073-T02
status: wait
priority: high
depends_on: [GLAS-WP-0012]
blocking_reason: "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."
blocking_reason: "Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-cluster persistent k3s config, railiance-infra principal/account mapping and ops-warden certificate issuance."
state_hub_task_id: "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35"
```
Owner: railiance-platform (RBAC), railiance-enablement (k3s install),
Owner: railiance-platform (RBAC), railiance-cluster (k3s install; ownership corrected 2026-10-19),
railiance-infra (host principal mapping), ops-warden (certificate issuance).
- Bind the agent's stable identity and `supervised-mode` to its existing
@ -107,7 +107,10 @@ railiance-infra (host principal mapping), ops-warden (certificate issuance).
issuance through an attended login).
- Proof: as the agent identity, `kubectl auth can-i get secrets -A` and
`can-i create pods/exec -A` both answer `no`, and `kubectl auth whoami`
shows no `system:masters`. Record the output as evidence. Also verify that
shows no `system:masters`. Retain these aggregate checks and additionally
test every namespaced forbidden permission in each namespace from a supervisor-verified
complete current inventory; aggregate denial does not exclude a namespaced grant.
Test cluster-scoped privileges separately. Record sanitized output as evidence. Also verify that
an exact supervisor-approved action can execute through the selected privileged
path and return sanitized outcome evidence, while an unapproved/revised action
cannot use that approval. Test from the actual agent account, including denial
@ -356,3 +359,31 @@ and enablement clean. Glas's generated change matches GLAS-WP-0016-T02 already
done in its source workplan; use the owning generator workflow to commit those
projections. No privileged change, runtime retry, rotation or autonomy grant
was performed by this review.
## Concrete boundary source packet — 2026-10-19
Prepared `docs/changes/CUST-WP-0073/credential-boundary-r1/PROPOSAL.md` with
SHA256SUMS-bound RBAC/identity candidate, host principal/account specification,
cluster installer diff, source pins and a safe actual-account negative verifier.
Seven counterexample/partial-proof tests pass after the same proposal revision-2
namespace-inventory correction; revision-1 digest inventory and refinement are
retained. Installer patch applies to its
pinned source. No live probe, identity issuance, account change or cutover ran.
Source ownership correction: `railiance-cluster/ansible/bootstrap.yml` owns the
k3s installer and currently combines mode644 with unconditional admin kubeconfig
fetch to the controller. `railiance-enablement` is S4, not this S2 installer
owner. Route the persistent mode600 change to cluster; enablement is consulted
only if distribution tooling requires it. The proposed installer diff also
removes admin kubeconfig export. Existing hosts need an exact reviewed effective
unit/config override because the installer `creates` guard skips their flags;
chmod alone can be reverted by the old flag at restart.
Infra's current SSH inventory maps generic agent principals into tegwick. The
packet proposes a new dedicated account/principal without altering existing
bridge consumers. Actual assignment isolation must additionally exclude old
principal credentials, external tools/connectors and writable privileged paths.
The negative verifier uses auth reviews and access checks only, refuses the
old account/identity and never upgrades a passing subset to assignment admission.
Exact effective host/workstation inputs and privileged execution binding remain
required before an execution approval packet is complete.

View file

@ -4,7 +4,7 @@ type: workplan
title: "Deterministic security flow: standing authorizations, flow report, and agent re-engagement"
domain: infotech
repo: the-custodian
status: blocked
status: active
owner: claude-code
topic_slug: custodian
created: "2026-10-11"
@ -113,9 +113,8 @@ Canon source written 2026-10-11: `canon/standards/build-mode-change-authorizatio
```task
id: CUST-WP-0079-T05
status: wait
blocked_on: message-from:repo-manager
blocking_reason: "Local/shared guards are implemented and the 2026-10-12 to 2026-10-19 local history audit found zero generated-only commits across 15 security repos (498 commits). Fleet generator adoption remains unverified; shared hooks reject but do not regenerate indexes."
status: progress
blocking_reason: "Staged WORK-RECORDS generation adopted in activity-core, policy-nexus and repo-manager with Custodian activation underway. Remaining security fleet adoption and other owner index generators are still open; the 498-commit local seven-day history check passed."
priority: medium
state_hub_task_id: "8a3dc219-8982-5903-9407-e1b549d4544c"
```
@ -259,3 +258,37 @@ in a fresh subprocess, preventing cross-checkout module-cache reuse. Tests prove
both boundaries. Usage and adoption limitations are documented in
`docs/staged-work-record-generation.md` and the local SCOPE capability; no shared
hook routing was changed.
### T05 generator adoption — 2026-10-19
T05 is progress and the workplan active because authorized source adoption is
underway. The shared repo-manager hook now composes the original generated-only
guard, opt-in staged generator and unchanged exact-scope secret scanner. The
Custodian's separate local scanner hook was upgraded with the reviewed installer
(`changed: true`); its routing and other hooks were preserved. Opt-in is explicit
in `.generated-work-records.json`, with a dedicated staged Make recipe. Ordinary
Makefiles and `.env` includes are not loaded by the hook. Recipe content remains
trusted source, not an arbitrary-code sandbox.
Actual owner commits exercised the hook: activity-core `8a7275c` found its index
current; policy-nexus `c89364d` and repo-manager `05046fb` regenerated and included
WORK-RECORDS with the adoption source. All three checkouts were clean after their
commits, and the latter two retained successful secret scans. Subsequent owner
work may change those checkouts independently.
Twenty-one adapter/shared-hook/guard tests pass, including preservation of
partially staged source and unrelated staged briefs. Real `git commit --only`
exposed Git's temporary-index behavior; the adapter now refuses regeneration in
an alternate index before mutation. Pre-generating against normally staged
sources and including WORK-RECORDS in the partial commit passes with unrelated
staging intact. Instructions are in `docs/staged-work-record-generation.md`.
This is WORK-RECORDS adoption in three provider repos plus Custodian preparation,
not fleet completion. Remaining surfaces include the other security repositories,
hub-derived `.custodian-brief.md`, policy-nexus source inventory (whose authored
configuration/dispositions must be retained), and net-kingdom's generated
coordination-frame receiver through `tools/coordination-frame/render.py`.
Historic evidence inventories are not recurring generator outputs. T05 stays
open until those applicable owner generation paths and fleet acceptance are
verified; the independent schedule, publication and attended gates remain open.