Adopt staged generation and prepare weekly sizing and credential isolation
This commit is contained in:
parent
fd4a3ba00e
commit
02e69269c8
31 changed files with 1439 additions and 26 deletions
1
.generated-work-records.json
Normal file
1
.generated-work-records.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version": 1, "repo_slug": "the-custodian"}
|
||||
8
.generated-work-records.mk
Normal file
8
.generated-work-records.mk
Normal 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
|
||||
5
Makefile
5
Makefile
|
|
@ -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)"
|
||||
|
|
|
|||
9
SCOPE.md
9
SCOPE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
13
config/weekly-allocation-request.json
Normal file
13
config/weekly-allocation-request.json
Normal 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
|
||||
}
|
||||
92
docs/changes/CUST-WP-0073/credential-boundary-r1/PROPOSAL.md
Normal file
92
docs/changes/CUST-WP-0073/credential-boundary-r1/PROPOSAL.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
110
docs/weekly-allocation-report.md
Normal file
110
docs/weekly-allocation-report.md
Normal 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.
|
||||
|
|
@ -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/")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
48
scripts/run_generated_work_records_hook.py
Normal file
48
scripts/run_generated_work_records_hook.py
Normal 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()
|
||||
321
scripts/weekly_allocation_report.py
Normal file
321
scripts/weekly_allocation_report.py
Normal 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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
150
tests/test_weekly_allocation_report.py
Normal file
150
tests/test_weekly_allocation_report.py
Normal 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()
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue