docs: preserve reviewed NetKingdom session artifacts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 11s

This commit is contained in:
codex 2026-11-01 22:58:27 +01:00
parent 2f18be7880
commit 014bbd2893
50 changed files with 6134 additions and 0 deletions

View file

@ -0,0 +1,275 @@
---
id: RAIL-BS-WP-0004
type: workplan
title: "Integrated Backup — S2 Kubernetes Runtime Layer"
domain: financials
repo: railiance-cluster
status: finished
owner: tegwick
topic_slug: railiance
state_hub_workstream_id: "7e8b0c20-51eb-40c9-9e3b-85dd380d7625"
created: "2026-02-25"
updated: "2026-03-26"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# Integrated Backup — S2 Kubernetes Runtime Layer
## Goal
Implement the Q3 (Operability & Resilience) integrated backup for
railiance-cluster (S2). Backs up what S2 owns — the Kubernetes runtime state —
encrypted with age, written to a local directory on the server. No external
dependencies required.
## Architecture (Decision D4)
Each railiance repo implements its own backup for what it owns. No central
backup service. See `DECISIONS.md` D4 for full rationale.
**Standard interface every railiance repo must provide:**
```bash
make backup # encrypt + write to /opt/backup/railiance/<layer>/
make restore # restore from most recent local backup
```
Encryption: age, same key pair as SOPS secrets (`.sops.yaml` public key).
Output: `/opt/backup/railiance/cluster/` on the server.
## What S2 (railiance-cluster) owns and must back up
| Asset | Why it matters |
|---|---|
| k3s etcd snapshots | Full cluster state — all workloads, configs, secrets |
| Helm release values | Runtime values not in git (any manually applied overrides) |
| kubeconfig | Admin access to the cluster |
**Not S2's responsibility:**
- Custodian State Hub DB → the-custodian owns this
- Operator workstation config (`.claude/`, `.gitconfig`) → operator's own concern
- Application data (Gitea repos, uploads) → S5 (railiance-apps) owns this
- PostgreSQL data volumes → S3 (railiance-platform) owns this
## Encryption
Reuse the age public key from `.sops.yaml`:
```bash
AGE_PUBLIC_KEY=$(grep 'age:' .sops.yaml | awk '{print $2}')
tar -czf - <assets> | age -r "${AGE_PUBLIC_KEY}" -o backup.tar.gz.age
```
Decryption requires the private key at `~/.config/sops/age/keys.txt`
(same key used for `sops -d`). No additional key management needed.
## Extension Point EP-RAIL-005
Once all five OAS layers implement this interface, the custodian can
orchestrate a full-stack backup with:
```bash
for repo in railiance-infra railiance-cluster railiance-platform \
railiance-enablement railiance-apps; do
make -C ~/$repo backup
done
```
No special protocol needed — just the standard interface.
---
## Tasks
### T01 — Define backup directory and encryption wrapper
```task
id: T01
status: done
priority: high
state_hub_task_id: "4526a842-ea31-4874-9231-92ab556cfe7b"
```
Create `tools/cmd/railiance-backup-s2` (replacing the old `railiance-backup`):
- Backup dir: `/opt/backup/railiance/cluster/` (create with `mkdir -p`)
- Encrypt each artifact with age using public key from `.sops.yaml`
- Write timestamp-named files: `etcd-<ts>.snap.age`, `helm-values-<ts>.tar.gz.age`, `kubeconfig-<ts>.yaml.age`
- Keep last 7 of each type
- Write `.last-backup` stamp
- Exit 0 on success, non-zero on any failure
- No network required
Also remove the old `tools/cmd/railiance-backup` (backed up Docker-based
custodian DB — wrong scope, not applicable to this server).
**Done when:** `make backup` runs on COULOMBCORE without error and files
appear in `/opt/backup/railiance/cluster/`.
---
### T02 — Back up k3s state (SQLite hot backup)
```task
id: T02
status: done
priority: high
state_hub_task_id: "a6313e06-1976-46a7-8e31-df4eb2eca880"
```
k3s has built-in etcd snapshot support:
```bash
sudo k3s etcd-snapshot save --name railiance-$(date -u +%Y%m%dT%H%M%SZ)
# Default location: /var/lib/rancher/k3s/server/db/snapshots/
```
Add to the backup script: take a fresh snapshot, encrypt with age,
copy to `/opt/backup/railiance/cluster/`.
> **Note — verify etcd is in use before implementing:**
> `k3s etcd-snapshot` only works if k3s was started with `--cluster-init`.
> Without it, k3s uses SQLite and this command will fail.
> Verify first: `sudo k3s etcd-snapshot ls 2>&1`
> **Note — sudo required:** etcd snapshot requires root. See T06 for how
> this is resolved (backup runs under root's crontab).
**Done when:** backup includes a current etcd snapshot.
---
### T03 — Back up Helm release values
```task
id: T03
status: done
priority: medium
state_hub_task_id: "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3"
```
Capture current runtime Helm values for all releases:
```bash
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm list -A -o json | \
jq -r '.[].name + " " + .namespace' | \
while read name ns; do
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm get values "$name" -n "$ns" -o yaml
done
```
Tar and age-encrypt into `helm-values-<ts>.tar.gz.age`.
> **Note — kubeconfig permissions:** `/etc/rancher/k3s/k3s.yaml` is root-readable
> only by default. The backup script must either run as root (see T06) or k3s
> must be configured with `--write-kubeconfig-mode=644`. Running as root
> (via root crontab) is the chosen approach — no config change needed.
**Done when:** backup includes a snapshot of all Helm release values.
---
### T04 — Back up kubeconfig
```task
id: T04
status: done
priority: medium
state_hub_task_id: "08233868-d522-4117-bc4e-6c0f52545665"
```
Age-encrypt `~/.kube/config-hosteurope` (or `/etc/rancher/k3s/k3s.yaml`)
into `kubeconfig-<ts>.yaml.age` in the backup directory.
**Done when:** backup includes the encrypted kubeconfig.
---
### T05 — make restore target
```task
id: T05
status: done
priority: medium
state_hub_task_id: "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8"
```
Add `tools/cmd/railiance-restore-s2` that decrypts and lists available
backups, with guided restore for the etcd snapshot case.
Restore of etcd from snapshot:
```bash
sudo k3s server --cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/<name>
```
**Done when:** `make restore` prints available backups and a restore guide.
---
### T06 — Install cron job and run restore drill
```task
id: T06
status: done
priority: medium
state_hub_task_id: "f8e4a094-c367-40eb-b895-da17bc144b07"
```
#### Solving the sudo problem
The backup script needs root for two reasons:
- `k3s etcd-snapshot save` requires root
- `/etc/rancher/k3s/k3s.yaml` (kubeconfig) is root-readable only
**Solution: run the cron under root's crontab.**
This is the correct pattern for system-level backup jobs. It avoids a
proliferating sudoers whitelist (one entry per command, brittle to maintain)
and matches how tools like `rsnapshot`, `bacula`, and `borgbackup` work in
production. The backup writes to `/opt/backup/` which is root-owned anyway.
Install the cron as root:
```bash
sudo crontab -e
# Add:
0 2 * * * make -C /home/tegwick/railiance-cluster backup >> /opt/backup/railiance/cluster/backup.log 2>&1
```
Note: use the absolute path to the repo — `~` does not expand reliably in
root's crontab unless HOME is set.
Verify it is installed:
```bash
sudo crontab -l | grep railiance
```
#### Restore drill
Once T01–T04 are done, run a decrypt-and-verify drill:
```bash
# Decrypt the etcd snapshot and verify it is a valid snapshot file
sudo age -d -i ~/.config/sops/age/keys.txt \
/opt/backup/railiance/cluster/etcd-$(ls /opt/backup/railiance/cluster/etcd-*.snap.age | sort -r | head -1 | xargs basename | sed 's/etcd-//;s/.snap.age//').snap.age \
| file -
# Record the drill
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) restore drill OK" \
>> /opt/backup/railiance/cluster/restore-drill.log
```
**Done when:** cron installed under root, drill completes without error,
log entry written.
---
## References
- Decision D4: Integrated backup per capability (`DECISIONS.md`)
- Decision D2: Nextcloud as optional offsite extension (still valid, not a requirement)
- OAS Q3: Operability & Resilience
- Extension point EP-RAIL-005: Custodian full-stack backup orchestration
- k3s etcd snapshots: https://docs.k3s.io/datastore/backup-restore

View file

@ -0,0 +1,177 @@
---
id: RAIL-BS-WP-0002
type: workplan
title: "k3s and Kubernetes Platform Baseline"
domain: financials
repo: railiance-cluster
status: finished
owner: railiance
topic_slug: railiance
repo_goal_id: "70ab2379-fb9d-4fec-a09d-b2a717e4ace8"
state_hub_workstream_id: "4c63dfc6-9eac-4e79-9f77-8f644ad7147d"
created: "2026-03-09"
updated: "2026-03-10"
completed: "2026-03-10"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# k3s and Kubernetes Platform Baseline
## Goal
Install k3s, Helm, and the baseline Kubernetes services on the converged
HostEurope node. This workplan picks up exactly where `railiance-hosts`
leaves off: a hardened, verified OS node that is ready for Kubernetes.
## Pre-condition
`railiance-infra` converge + Goss verify must pass before any task here
is executed:
```bash
# In railiance-infra/
make converge
make verify # must exit 0
```
## Boundary
This repo owns everything from k3s upward. It must not re-configure items
defined in `railiance-infra/spec/server-baseline.yaml`. See ADR-003:
`railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`.
**Out of scope here:** platform services (PostgreSQL, storage, identity)
→ `railiance-platform`. Application deployments (Gitea, coulomb services)
→ `railiance-apps`.
---
## Tasks
### T01 — Ansible playbook: install k3s (server mode)
```task
id: T01
status: done
priority: high
state_hub_task_id: "3f042630-eab0-4c6a-9167-e2b28ff20e40"
completed: "2026-03-10"
```
Harden `ansible/bootstrap.yml` to a production-ready k3s install:
- Use the official k3s install script pinned to a specific version
(`INSTALL_K3S_VERSION=v1.35.1+k3s1`)
- `INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644"`
(cluster-init enables embedded etcd for future HA expansion)
- Wait for node `Ready` before proceeding:
```bash
k3s kubectl wait node --all --for=condition=Ready --timeout=120s
```
- Fetch kubeconfig to the control node as `~/.kube/config-hosteurope`
**Done when:** `k3s kubectl get nodes` returns `Ready` from both the server
and the control node (via kubeconfig).
---
### T02 — Helm installation
```task
id: T02
status: done
priority: high
state_hub_task_id: "e8510646-46ed-4697-a345-f3d3009eea78"
completed: "2026-03-10"
```
Add a task (or a role `roles/helm/`) that:
1. Downloads the Helm binary (pinned version) to `/usr/local/bin/helm`
2. Verifies the checksum
3. Confirms `helm version` succeeds
**Done when:** `helm version` succeeds on the HostEurope node.
---
### T03 — Smoke test: k3s + Helm
```task
id: T03
status: done
priority: high
state_hub_task_id: "dab2c07f-8aa0-4635-8df6-857e87e93fc5"
completed: "2026-03-10"
```
Extend `tests/smoke_kube.sh` to assert:
- `k3s kubectl get nodes` → node in Ready state
- `helm version` exits 0
- CoreDNS pod running in `kube-system`
- Traefik ingress controller pod running (default in k3s)
Run via:
```bash
ansible-playbook -i ansible/hosts.ini ansible/smoke.yml
```
or directly over SSH if the kubeconfig is available locally.
**Done when:** all assertions pass and the script exits 0.
---
### T04 — Commit kubeconfig management notes
```task
id: T04
status: done
priority: medium
state_hub_task_id: "5c3d40e4-239b-488e-9519-6f7a38d2325f"
completed: "2026-03-10"
```
Document in `docs/kubeconfig.md`:
- Where the kubeconfig is fetched to (`~/.kube/config-hosteurope`)
- How to merge it into `~/.kube/config`
- How to switch context: `kubectl config use-context default`
- Security note: kubeconfig is gitignored (contains cluster CA + client cert)
**Done when:** doc written and committed.
---
### T05 — Add `make k3s-install` and `make smoke` targets
```task
id: T05
status: done
priority: medium
state_hub_task_id: "7f9e0e58-a130-467a-a2d0-b3f2564e496f"
completed: "2026-03-10"
```
Add to Makefile (create one if none exists):
```makefile
k3s-install: ## Install k3s and Helm on all inventory hosts
ansible-playbook -i ansible/hosts.ini ansible/bootstrap.yml
smoke: ## Run Kubernetes smoke tests
bash tests/smoke_kube.sh
```
**Done when:** both targets work and are listed in `make help`.
---
## References
- Repo goal: `70ab2379-fb9d-4fec-a09d-b2a717e4ace8` (Install k3s and Kubernetes Baseline)
- Domain goal: `6f96c712-60e6-4ea9-ab06-168878eafbce` (Three-Phoenix Secure Kubernetes Infrastructure)
- Pre-condition: railiance-infra WP-0001 (Secure Single-Server Bootstrap) — completed 2026-03-09
- Boundary ADR: `railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md`
- k3s releases: https://github.com/k3s-io/k3s/releases

View file

@ -0,0 +1,196 @@
---
id: RAIL-BS-WP-0003
type: workplan
title: "pgpool CrashLoopBackOff on PostgreSQL HA failover — missing secret key"
domain: financials
repo: railiance-cluster
status: finished
owner: tegwick
created: "2026-03-10"
updated: "2026-03-10"
state_hub_workstream_id: "7ee9ee22-1fae-4567-9194-8d70a9e0f45b"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# Bug Report: pgpool CrashLoopBackOff on PostgreSQL HA failover
## Summary
On 2026-03-10 a PostgreSQL HA failover caused all three postgresql pods to
restart. pgpool — the connection pooler between Gitea and PostgreSQL — then
entered CrashLoopBackOff and produced no logs. As a result Gitea's login
and all write operations hung indefinitely. The root page was still served
(from Valkey cache) which masked the failure.
The fix was to patch a missing key in a Kubernetes secret. The root cause is
that the `gitea-12.2.0` Helm chart (postgresql-ha subchart v16.2.2) does not
populate the `pgpool-password` key in the `gitea-postgresql-ha-postgresql`
secret, even though the pgpool pod requires it at startup.
---
## Timeline
| Time (UTC) | Event |
|---|---|
| ~09:45 | postgresql-0, postgresql-2 pods restarted (repmgr failover) |
| ~09:45 | pgpool pod restarted and entered CrashLoopBackOff |
| ~11:00 | User noticed Gitea login hanging; home page still loading |
| ~13:00 | Root cause identified: missing `pgpool-password` secret key |
| ~13:10 | Secret patched; pgpool pod deleted and restarted cleanly |
| ~13:15 | Gitea fully operational |
---
## Root Cause
The Bitnami `pgpool` container startup script reads the file
`/opt/bitnami/pgpool/secrets/pgpool-password`, which is mounted from the
`gitea-postgresql-ha-postgresql` Kubernetes Secret via a `subPath` volume
mount. That secret key was never created by the Helm chart, so the file did
not exist. The container exited immediately with no logs.
The pod had been running for 20 days without a restart, so this gap was
never discovered during initial deployment.
---
## Evidence
```bash
# Secret was missing the pgpool-password key
sudo k3s kubectl get secret -n default gitea-postgresql-ha-postgresql -o yaml
# data: keys were password, postgres-password, repmgr-password only
# pgpool-password was absent
# pgpool pod describe showed 824 back-off restarts over 173 minutes
# No logs in either current or --previous output
sudo k3s kubectl logs -n default <pgpool-pod> --previous
# (empty)
# Gitea process had zero TCP connections to PostgreSQL port 5432
# but many connections to Valkey port 6379
cat /proc/<gitea-pid>/net/tcp | grep 1538 # 1538 = 5432 hex — no results
```
---
## Immediate Fix Applied
```bash
# Add the missing key (value = sr-check-password = changeme4 = base64: Y2hhbmdlbWU0)
sudo k3s kubectl patch secret -n default gitea-postgresql-ha-postgresql \
--type='json' \
-p='[{"op":"add","path":"/data/pgpool-password","value":"Y2hhbmdlbWU0"}]'
# Restart pgpool
sudo k3s kubectl delete pod -n default <pgpool-pod-name>
```
---
## Risk: Fix Will Be Lost on helm upgrade
The patched secret is managed by Helm (annotation:
`meta.helm.sh/release-name: gitea`). A `helm upgrade` will regenerate the
secret from the chart template, which does not include `pgpool-password`,
and the bug will recur.
---
## Tasks
### T01 — Add pgpool-password to Helm values
```task
id: T01
status: done
priority: high
state_hub_task_id: "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc"
```
Create or update `helm/gitea-values.yaml` (or equivalent) to permanently
include the pgpool-password so it survives `helm upgrade`:
```yaml
postgresql-ha:
postgresql:
pgpoolPassword: <value matching sr-check-password>
```
**Done when:** `helm upgrade gitea` completes and pgpool starts cleanly
without manual secret patching.
---
### T02 — Add pgpool health check to smoke test
```task
id: T02
status: done
priority: high
state_hub_task_id: "ab166073-30a7-4702-a037-4091e8706e20"
```
Extend `tests/smoke_kube.sh` to assert:
```bash
# All postgresql-ha pods Running
kubectl get pods -n default | grep gitea-postgresql-ha | grep -v Running && exit 1
# pgpool specifically not in CrashLoopBackOff
kubectl get pod -n default -l app.kubernetes.io/component=pgpool \
-o jsonpath='{.items[0].status.containerStatuses[0].state}' | grep -v crash
```
**Done when:** the smoke test catches a pgpool failure within 5 minutes.
---
### T03 — Add HA failover test
```task
id: T03
status: done
priority: high
state_hub_task_id: "140da396-8e30-4f4d-b88c-c42c0cd46c01"
```
Create `tests/test_ha_failover.sh` that:
1. Records Gitea login response time (baseline)
2. Kills the primary PostgreSQL pod: `kubectl delete pod gitea-postgresql-ha-postgresql-0 -n default`
3. Waits for repmgr to promote a replica (max 60s)
4. Asserts Gitea login POST still succeeds within 10s
5. Asserts pgpool pod is Running (not CrashLoopBackOff)
6. Asserts all postgresql pods return to Running
This test must pass before any PostgreSQL HA deployment is considered done.
**Done when:** script exits 0 against a live cluster.
---
### T04 — Document the incident in docs/
```task
id: T04
status: done
priority: medium
state_hub_task_id: "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d"
```
Add `docs/incidents/2026-03-10-pgpool-missing-secret.md` with the full
timeline, root cause, and fix, so future operators understand what happened
and how to recover.
**Done when:** doc committed and linked from `docs/README.md`.
---
## References
- Bitnami postgresql-ha chart v16.2.2
- Gitea Helm chart v12.2.0
- Related decision: D3 (HA testing policy) in `DECISIONS.md`

View file

@ -0,0 +1,275 @@
---
id: RAIL-BS-WP-0004
type: workplan
title: "Integrated Backup — S2 Kubernetes Runtime Layer"
domain: financials
repo: railiance-cluster
status: finished
owner: tegwick
topic_slug: railiance
state_hub_workstream_id: "7e8b0c20-51eb-40c9-9e3b-85dd380d7625"
created: "2026-02-25"
updated: "2026-03-26"
---
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
# Integrated Backup — S2 Kubernetes Runtime Layer
## Goal
Implement the Q3 (Operability & Resilience) integrated backup for
railiance-cluster (S2). Backs up what S2 owns — the Kubernetes runtime state —
encrypted with age, written to a local directory on the server. No external
dependencies required.
## Architecture (Decision D4)
Each railiance repo implements its own backup for what it owns. No central
backup service. See `DECISIONS.md` D4 for full rationale.
**Standard interface every railiance repo must provide:**
```bash
make backup # encrypt + write to /opt/backup/railiance/<layer>/
make restore # restore from most recent local backup
```
Encryption: age, same key pair as SOPS secrets (`.sops.yaml` public key).
Output: `/opt/backup/railiance/cluster/` on the server.
## What S2 (railiance-cluster) owns and must back up
| Asset | Why it matters |
|---|---|
| k3s etcd snapshots | Full cluster state — all workloads, configs, secrets |
| Helm release values | Runtime values not in git (any manually applied overrides) |
| kubeconfig | Admin access to the cluster |
**Not S2's responsibility:**
- Custodian State Hub DB → the-custodian owns this
- Operator workstation config (`.claude/`, `.gitconfig`) → operator's own concern
- Application data (Gitea repos, uploads) → S5 (railiance-apps) owns this
- PostgreSQL data volumes → S3 (railiance-platform) owns this
## Encryption
Reuse the age public key from `.sops.yaml`:
```bash
AGE_PUBLIC_KEY=$(grep 'age:' .sops.yaml | awk '{print $2}')
tar -czf - <assets> | age -r "${AGE_PUBLIC_KEY}" -o backup.tar.gz.age
```
Decryption requires the private key at `~/.config/sops/age/keys.txt`
(same key used for `sops -d`). No additional key management needed.
## Extension Point EP-RAIL-005
Once all five OAS layers implement this interface, the custodian can
orchestrate a full-stack backup with:
```bash
for repo in railiance-infra railiance-cluster railiance-platform \
railiance-enablement railiance-apps; do
make -C ~/$repo backup
done
```
No special protocol needed — just the standard interface.
---
## Tasks
### T01 — Define backup directory and encryption wrapper
```task
id: T01
status: done
priority: high
state_hub_task_id: "4526a842-ea31-4874-9231-92ab556cfe7b"
```
Create `tools/cmd/railiance-backup-s2` (replacing the old `railiance-backup`):
- Backup dir: `/opt/backup/railiance/cluster/` (create with `mkdir -p`)
- Encrypt each artifact with age using public key from `.sops.yaml`
- Write timestamp-named files: `etcd-<ts>.snap.age`, `helm-values-<ts>.tar.gz.age`, `kubeconfig-<ts>.yaml.age`
- Keep last 7 of each type
- Write `.last-backup` stamp
- Exit 0 on success, non-zero on any failure
- No network required
Also remove the old `tools/cmd/railiance-backup` (backed up Docker-based
custodian DB — wrong scope, not applicable to this server).
**Done when:** `make backup` runs on COULOMBCORE without error and files
appear in `/opt/backup/railiance/cluster/`.
---
### T02 — Back up k3s state (SQLite hot backup)
```task
id: T02
status: done
priority: high
state_hub_task_id: "a6313e06-1976-46a7-8e31-df4eb2eca880"
```
k3s has built-in etcd snapshot support:
```bash
sudo k3s etcd-snapshot save --name railiance-$(date -u +%Y%m%dT%H%M%SZ)
# Default location: /var/lib/rancher/k3s/server/db/snapshots/
```
Add to the backup script: take a fresh snapshot, encrypt with age,
copy to `/opt/backup/railiance/cluster/`.
> **Note — verify etcd is in use before implementing:**
> `k3s etcd-snapshot` only works if k3s was started with `--cluster-init`.
> Without it, k3s uses SQLite and this command will fail.
> Verify first: `sudo k3s etcd-snapshot ls 2>&1`
> **Note — sudo required:** etcd snapshot requires root. See T06 for how
> this is resolved (backup runs under root's crontab).
**Done when:** backup includes a current etcd snapshot.
---
### T03 — Back up Helm release values
```task
id: T03
status: done
priority: medium
state_hub_task_id: "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3"
```
Capture current runtime Helm values for all releases:
```bash
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm list -A -o json | \
jq -r '.[].name + " " + .namespace' | \
while read name ns; do
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm get values "$name" -n "$ns" -o yaml
done
```
Tar and age-encrypt into `helm-values-<ts>.tar.gz.age`.
> **Note — kubeconfig permissions:** `/etc/rancher/k3s/k3s.yaml` is root-readable
> only by default. The backup script must either run as root (see T06) or k3s
> must be configured with `--write-kubeconfig-mode=644`. Running as root
> (via root crontab) is the chosen approach — no config change needed.
**Done when:** backup includes a snapshot of all Helm release values.
---
### T04 — Back up kubeconfig
```task
id: T04
status: done
priority: medium
state_hub_task_id: "08233868-d522-4117-bc4e-6c0f52545665"
```
Age-encrypt `~/.kube/config-hosteurope` (or `/etc/rancher/k3s/k3s.yaml`)
into `kubeconfig-<ts>.yaml.age` in the backup directory.
**Done when:** backup includes the encrypted kubeconfig.
---
### T05 — make restore target
```task
id: T05
status: done
priority: medium
state_hub_task_id: "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8"
```
Add `tools/cmd/railiance-restore-s2` that decrypts and lists available
backups, with guided restore for the etcd snapshot case.
Restore of etcd from snapshot:
```bash
sudo k3s server --cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/<name>
```
**Done when:** `make restore` prints available backups and a restore guide.
---
### T06 — Install cron job and run restore drill
```task
id: T06
status: done
priority: medium
state_hub_task_id: "f8e4a094-c367-40eb-b895-da17bc144b07"
```
#### Solving the sudo problem
The backup script needs root for two reasons:
- `k3s etcd-snapshot save` requires root
- `/etc/rancher/k3s/k3s.yaml` (kubeconfig) is root-readable only
**Solution: run the cron under root's crontab.**
This is the correct pattern for system-level backup jobs. It avoids a
proliferating sudoers whitelist (one entry per command, brittle to maintain)
and matches how tools like `rsnapshot`, `bacula`, and `borgbackup` work in
production. The backup writes to `/opt/backup/` which is root-owned anyway.
Install the cron as root:
```bash
sudo crontab -e
# Add:
0 2 * * * make -C /home/tegwick/railiance-cluster backup >> /opt/backup/railiance/cluster/backup.log 2>&1
```
Note: use the absolute path to the repo — `~` does not expand reliably in
root's crontab unless HOME is set.
Verify it is installed:
```bash
sudo crontab -l | grep railiance
```
#### Restore drill
Once T01–T04 are done, run a decrypt-and-verify drill:
```bash
# Decrypt the etcd snapshot and verify it is a valid snapshot file
sudo age -d -i ~/.config/sops/age/keys.txt \
/opt/backup/railiance/cluster/etcd-$(ls /opt/backup/railiance/cluster/etcd-*.snap.age | sort -r | head -1 | xargs basename | sed 's/etcd-//;s/.snap.age//').snap.age \
| file -
# Record the drill
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) restore drill OK" \
>> /opt/backup/railiance/cluster/restore-drill.log
```
**Done when:** cron installed under root, drill completes without error,
log entry written.
---
## References
- Decision D4: Integrated backup per capability (`DECISIONS.md`)
- Decision D2: Nextcloud as optional offsite extension (still valid, not a requirement)
- OAS Q3: Operability & Resilience
- Extension point EP-RAIL-005: Custodian full-stack backup orchestration
- k3s etcd snapshots: https://docs.k3s.io/datastore/backup-restore

View file

@ -0,0 +1,67 @@
import shutil
import uuid
from pathlib import Path
import pytest
from api.services import forge_projection as fp
from api.services import historical_projection as hp
from tests.test_forge_projection import _FakeSession,_Repo,_Row,_TaskRow
FIXTURE=Path(__file__).parent/'fixtures/railiance-cluster'
@pytest.fixture
def bound(tmp_path):
root=tmp_path/'railiance-cluster';shutil.copytree(FIXTURE,root)
derived=fp.derive_from_checkout(root,'railiance-cluster','candidate')
assert len(derived.workplans)==3
rows=[];tasks=[]
for parent in derived.workplans:
row=_Row(parent.record_id.lower(),'finished',parent.relative_path);row.id=uuid.UUID(parent.uuid);rows.append(row)
for source in parent.tasks:
task=_TaskRow(source.record_id,status='done',workplan_id=row.id);task.id=uuid.UUID(source.uuid);tasks.append(task)
assert len(tasks)==15
return root,derived,rows,tasks
def test_exact_three_sources_derive_only_frozen_15_task_uuids(bound):
root,derived,rows,tasks=bound
assert all(parent.historical_preservation for parent in derived.workplans)
assert all(parent.uuid!=fp.derived_record_uuid(parent.record_id) for parent in derived.workplans)
expected={item['uuid'] for entry in hp.BINDINGS if entry['repo']=='railiance-cluster' for item in entry['tasks']}
assert {str(t.id) for t in tasks}==expected
@pytest.mark.asyncio
async def test_existing_rows_are_entirely_unchanged_on_normal_receiving(bound):
root,derived,rows,tasks=bound;before=[dict(x.__dict__) for x in rows+tasks]
session=_FakeSession(_Repo(),rows,task_rows=tasks)
outcome=await fp.reset_repository_projection(session,'railiance-cluster',derived=derived)
assert outcome.status=='noop' and not outcome.refused
assert [dict(x.__dict__) for x in rows+tasks]==before
assert not session.added and not session.deleted
@pytest.mark.parametrize('defect',['missing-parent','missing-task','foreign-holder','nonterminal-task','nonterminal-parent','duplicate-current-label'])
@pytest.mark.asyncio
async def test_unsupported_actual_binding_refuses_before_any_mutation(bound,defect):
root,derived,rows,tasks=bound
if defect=='missing-parent':rows.pop()
elif defect=='missing-task':tasks.pop()
elif defect=='foreign-holder':tasks[0].workplan_id=uuid.uuid4()
elif defect=='nonterminal-task':tasks[0].status='progress'
elif defect=='duplicate-current-label':tasks.append(_TaskRow(tasks[0].record_id,status='done',workplan_id=tasks[0].workplan_id))
else:rows[0].status='active'
before=[dict(x.__dict__) for x in rows+tasks];session=_FakeSession(_Repo(),rows,task_rows=tasks)
outcome=await fp.reset_repository_projection(session,'railiance-cluster',derived=derived)
assert outcome.status=='refused';assert [dict(x.__dict__) for x in rows+tasks]==before
assert not session.added and not session.deleted
@pytest.mark.parametrize('change',['hash-drift','missing','symlink','live-source','missing-source-uuid'])
def test_frozen_source_drift_refuses_derivation(bound,change):
root,*_=bound;p=next((root/'workplans/archived').glob('*.md'));raw=p.read_bytes()
if change=='missing':p.unlink()
elif change=='symlink':p.unlink();target=root/'saved';target.write_bytes(raw);p.symlink_to(target)
elif change=='live-source':p.write_bytes(raw.replace(b'status: finished',b'status: active',1))
elif change=='missing-source-uuid':p.write_bytes(raw.replace(b'state_hub_task_id:',b'old_task_id:',1))
else:p.write_bytes(raw+b'\nunreviewed\n')
with pytest.raises(fp.ForgeDeriveError,match='Historical source binding'):fp.derive_from_checkout(root,'railiance-cluster','candidate')
def test_unknown_repository_never_trusts_frozen_source_uuids(bound):
root,derived,*_=bound;other=fp.derive_from_checkout(root,'different-repo','candidate')
assert not any(parent.historical_preservation for parent in other.workplans)
assert not {parent.uuid for parent in derived.workplans}&{parent.uuid for parent in other.workplans}

View file

@ -0,0 +1,135 @@
"""Closed historical views preserve identity; unknown sources never gain authority."""
import hashlib
import uuid
from pathlib import Path
import pytest
from api.services import forge_projection as fp
from api.services import historical_projection as hp
from tests.test_forge_projection import _FakeSession, _Repo, _Row, _TaskRow, TestExistingWorkplanTasks as ExistingTaskTests
def _fixture(tmp_path, monkeypatch):
parent = str(uuid.uuid4()); task = str(uuid.uuid4())
paths = ['workplans/archived/old.md', 'workplans/split.md']
entries = []
for index, path in enumerate(paths):
rid = 'custodian-WP-0003' + ('a' if index else '')
data = (f'---\nid: {rid}\ntype: workplan\nrepo: activity-core\nstatus: done\n'
f'state_hub_workstream_id: {parent}\ntasks:\n'
f'- id: T34\n title: Closed task\n status: done\n state_hub_task_id: {task}\n---\n').encode()
p = tmp_path/path;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data)
entries.append(dict(repo='activity-core',path=path,sha256=hashlib.sha256(data).hexdigest(),
record_id=rid,uuid=parent,parent_record_id='custodian-WP-0003'))
monkeypatch.setattr(hp,'BINDINGS',entries)
return parent,task,paths
def test_exact_shared_views_derive_one_preserved_parent(tmp_path, monkeypatch):
parent,task,_ = _fixture(tmp_path,monkeypatch)
derived = fp.derive_from_checkout(tmp_path,'activity-core','abc')
assert len(derived.workplans)==1
w = derived.workplans[0]
assert w.uuid == parent and w.historical_preservation
assert len(w.tasks)==1 and w.tasks[0].uuid==task
assert w.tasks[0].record_id=='T34'
assert w.uuid != fp.derived_record_uuid(w.record_id)
@pytest.mark.parametrize('change',['bytes','missing','symlink'])
def test_changed_bound_history_refuses_derive(tmp_path,monkeypatch,change):
_,_,paths = _fixture(tmp_path,monkeypatch)
p=tmp_path/paths[1]
if change=='bytes':p.write_text(p.read_text().replace('status: done','status: active'))
elif change=='missing':p.unlink()
else:
data=p.read_bytes();p.unlink();other=tmp_path/'other.md';other.write_bytes(data);p.symlink_to(other)
with pytest.raises(fp.ForgeDeriveError,match='Historical source binding'):
fp.derive_from_checkout(tmp_path,'activity-core','abc')
def test_unknown_repo_cannot_trust_existing_binding(tmp_path,monkeypatch):
parent,_,_ = _fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'other-repo','abc')
assert all(not w.historical_preservation and w.uuid != parent for w in derived.workplans)
@pytest.mark.asyncio
async def test_historical_receiving_preserves_terminal_tombstone_and_null_task_label(tmp_path,monkeypatch):
parent,task,_=_fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'activity-core','abc')
row=_Row(slug='old@retired-20260827',status='archived',path='workplans/archived/old.md')
row.id=uuid.UUID(parent)
from datetime import datetime,timezone
row.projection_retired_at=datetime.now(timezone.utc)
t=_TaskRow(None,status='done',workplan_id=row.id);t.id=uuid.UUID(task)
before_row=dict(row.__dict__);before_task=dict(t.__dict__)
session=_FakeSession(_Repo(),[row],task_rows=[t])
outcome=await fp.reset_repository_projection(session,'activity-core',derived=derived)
assert outcome.status=='noop' and outcome.refused==[]
assert row.__dict__==before_row and t.__dict__==before_task
assert not session.added and not session.deleted
@pytest.mark.asyncio
async def test_missing_historical_task_refuses_before_mutation(tmp_path,monkeypatch):
parent,_,_=_fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'activity-core','abc')
row=_Row(slug='old',status='archived',path='workplans/archived/old.md');row.id=uuid.UUID(parent)
session=_FakeSession(_Repo(),[row],task_rows=[])
outcome=await fp.reset_repository_projection(session,'activity-core',derived=derived)
assert outcome.status=='refused'
assert outcome.refused[0]['reason'].startswith('historical binding')
assert not session.added and not session.deleted
@pytest.mark.asyncio
async def test_task_holder_query_includes_retired_and_stale_parents():
derived=ExistingTaskTests()._derived(('DEMO-WP-0001-T01','Task','todo'))
current=_Row(slug='demo-wp-0001',status='active',path='workplans/a.md')
holder=_TaskRow('DEMO-WP-0001-T01',workplan_id=uuid.uuid4())
holder.id=uuid.UUID(derived.workplans[0].tasks[0].uuid)
class Session(_FakeSession):
statements=[]
async def execute(self,statement,*args,**kwargs):
self.statements.append(str(statement))
return await super().execute(statement,*args,**kwargs)
session=Session(_Repo(),[current],task_rows=[],task_collision_rows=[holder])
outcome=await fp.reset_repository_projection(session,'demo',derived=derived)
assert outcome.status=='refused' and not session.added
query=session.statements[-1]
assert 'tasks.id IN' in query
assert 'projection_retired_at' not in query and 'NOT IN' not in query and 'JOIN' not in query
@pytest.mark.asyncio
async def test_missing_historical_parent_never_recreates(tmp_path,monkeypatch):
_fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'activity-core','abc')
session=_FakeSession(_Repo(),[])
outcome=await fp.reset_repository_projection(session,'activity-core',derived=derived)
assert outcome.status=='refused' and not session.added and not session.deleted
@pytest.mark.asyncio
@pytest.mark.parametrize('field',['parent','task'])
async def test_historical_nonterminal_hub_record_requires_owner_review(tmp_path,monkeypatch,field):
parent,task,_=_fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'activity-core','abc')
row=_Row(slug='old',status='active' if field=='parent' else 'finished',path='workplans/archived/old.md')
row.id=uuid.UUID(parent)
t=_TaskRow('preserved-old-label',status='wait' if field=='task' else 'done',workplan_id=row.id)
t.id=uuid.UUID(task)
session=_FakeSession(_Repo(),[row],task_rows=[t])
outcome=await fp.reset_repository_projection(session,'activity-core',derived=derived)
assert outcome.status=='refused' and not session.added and not session.deleted
def test_historical_diff_uses_uuid_without_relabeling(tmp_path,monkeypatch):
parent,task,_=_fixture(tmp_path,monkeypatch)
derived=fp.derive_from_checkout(tmp_path,'activity-core','abc')
diff=fp.diff_against_hub(derived,[{'id':parent,'slug':'historic@retired-20260827',
'status':'archived','backing_relative_path':'old-path.md'}],
{parent:[{'id':task,'record_id':None,'title':'Old title','status':'done'}]})
assert not diff.missing and not diff.stale and not diff.differing

View file

@ -0,0 +1,231 @@
---
id: STATE-WP-0095
type: workplan
title: "Preserve exact closed historical source bindings in Forge receiving"
domain: infotech
repo: state-hub
status: active
owner: codex
created: "2026-10-04"
updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "79579572-8120-5d3b-aca5-dd4aff60d058"
---
## Goal
Normal Forge receiving must preserve the seven bound Activity legacy source
views and the Custodian retired ADHOC source without new identities, source
rewrites, task movement, deletion, relabeling or unretirement. This implements
receiving support before RMGR-WP-0019 registrar admission. CUST-WP-0077 owns
cross-owner coordination; deployment and replay remain explicit owner returns.
## Verify source bindings and shared views
```task
id: STATE-WP-0095-T01
status: done
priority: high
state_hub_task_id: "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7"
```
Reviewed seven exact Activity files: four existing workplans and 64 UUID-bound
closed tasks. The archived custodian-WP-0003 parent and three split_from views
share one existing parent UUID; every repeated task binding/content agrees.
Custodian ADHOC-2026-08-25-retired retains parent
1c53d3db-6633-5e2d-987f-10706d73be9e and task
d8af3e6b-26ae-580c-bbeb-caa8ad419bec. No identity migration is authorized.
## Implement preservation receiving and global collision refusal
```task
id: STATE-WP-0095-T02
status: done
priority: high
state_hub_task_id: "ebb24194-e4cc-5130-aaa6-663f2ea2c822"
```
Prepared exact source digest manifest, existing-parent/task verification,
immutable receiving and UUID-based historical diff. Ordinary canonical sources
continue deriving their identities. Missing/changed/symlinked source, absent
bound rows or nonterminal hub histories refuse. Check task UUID holders globally,
including retired and acknowledged-stale parent workplans, before INSERT.
Offline preview tests and snapshot proof are evidence of source behavior only;
finish after applying reviewed source and its focused tests to the owning repo.
## Deliver receiving through normal deployment and verify owner return
```task
id: STATE-WP-0095-T03
status: done
priority: high
depends_on: [STATE-WP-0095-T02]
state_hub_task_id: "e8566341-cf6e-5b9c-ad80-8564d14cd52a"
decision_id: "36772026-f43d-4331-b466-5f4b1afe4eca"
```
No direct live code patch. This task is a deployment return, not policy authority
or permission to resolve identity decisions. Exact receiving fixture acceptance
and global collision rejection must hold in the delivered version.
## Verify normal historical replay preserves existing identities
```task
id: STATE-WP-0095-T04
status: done
priority: high
depends_on: [STATE-WP-0095-T03, RMGR-WP-0019-T03]
state_hub_task_id: "3bbb311f-194e-5862-8aed-358a580f22b4"
```
Verify Activity four historical parent UUIDs/64 task UUIDs and Custodian retired
parent/task unchanged; no historical row creation, movement, deletion,
relabeling or unretirement. Unrelated stale projections remain separate decisions.
2026-10-05: Source exact-set retirement support now exists. Repository projection
reconcile accepts `expected_retirement_uuids`; that set must equal the stale
projections, and any task or dependency edge on a stale row refuses the call.
Live apply still needs a reviewed rollout. On 2026-10-05 the founder accepted
review `review-1ece4f3e-d0e3-4fd6-abc8-272f1b1876f4`, which names only the three
empty projections and preserves parent `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`.
That acceptance is not a live retirement and does not resolve
`91b7c633-a66f-5d43-9f4b-6565aefb9356`.
## Offline evidence, 2026-10-04
Prepared receiver suite: Forge projection regression plus historical preservation,
missing/changed source, terminality, immutable UUID diff, and global holder query.
Real-source offline snapshot proof preserves four Activity parents/64 tasks and
one Custodian parent/task without mutations; no API writes or deployment occurred.
Exact original bindings are maintained in api/services/historical_projection.py.
2026-10-04 actual owning-source validation: 155 focused receiver/qualifier/planner
cases passed; the independent offline current-source/current-hub-snapshot proof
retained five existing parents and 65 task UUIDs, no mutations, clean historical
diff. T02 is source-complete; T03/T04 remain wait for reviewed deployment and
normal registrar/replay receiving, respectively. Evidence: docs/evidence/2026-10-04-historical-receiving-source.json.
## Verified receiving rollout and remaining replay gate, 2026-10-04
T03 is done on actual founder approval of proposal
STATE-WP-0095-T03-historical-receiving revision 1, SHA256
`d6789359b3fe7988a6201fff6f367cb46948aa197e1cc0ca66c31f45e1ea9981`,
approval receipt reference `user_reply:call_o87uEfFs3AtXfxO5YqUnSpcg:0`.
The normal owner Helm path delivered `main-5dd3df9`, commit
`5dd3df9c69eb564af10ea5ea01e349786212501a`, revision 72; API and MCP are Ready,
primary/railiance01 health and schema a0b1c2d3e4f5 verified, running source
hashes match. Rollback remains revision 71; no receipt-table downgrade is needed.
Actual historical preservation and global task-holder refusal were verified in
the delivered version. Durable receipt:
[verified historical receiving rollout](../docs/evidence/2026-10-04-historical-receiving-rollout.json).
Custodian immutable replay applied while preserving the retired historical
parent/task UUIDs. Activity immutable replay retained all four historical parent
and 64 task UUIDs and historical row contents, but refused transactionally on
three unrelated stale projections: custodian-wp-0003 (archived),
custodian-wp-0003a (done), custodian-wp-0003b (done). No retirement acknowledgement
was issued and that refusal is not represented as normal replay success.
Registrar source `c41d36c2` has 38 actual passing tests and Repo Manager's own
normal sync succeeded; registrar admission does not authorize retiring Activity
projections. T04 therefore remains wait, retaining RMGR-WP-0019-T03 and awaiting
Activity Core's precise reviewed disposition of those three stale rows. No
rename, identity migration, new paid native trial or further live action is
included in this source status update.
## Activity Core return, 2026-10-06
Activity Core returned the exact disposition (message
`cf2d5847-8b3f-4653-9d9f-d5fcfbe3f1e4`, ACTIVITY-WP-0045, activity-core commit
`941fac0`). `expected_retirement_uuids`:
`683724d9-4249-5e0e-88e0-7baff4330d58` (custodian-wp-0003),
`644017c7-c5f2-5a64-81c3-d8ee806f8251` (custodian-wp-0003a),
`a5806df9-5b9a-50da-81c1-7e606076bed9` (custodian-wp-0003b). Each has zero
tasks and zero dependency edges. None is preserved parent
`b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`. Repo Manager independently verified the
set (message `916841f6`, repo-manager commit `498682c`). Nothing was retired.
T04 now waits on decision `91b7c633` and a reviewed live rollout. After that,
Repo Manager re-runs the Activity owner sync and closes RMGR-WP-0019-T03.
## Retirement rollout and T04 verification, 2026-10-06
The founder resolved decision `91b7c633` (retire exactly the three projections)
and approved the rollout. The founder ran the normal Helm upgrade to
`main-968a811` (revision 74; rollback revision 73, `main-fad9b3f`). API and MCP
are Ready, health is ok on primary/railiance01, and schema `b3c4d5e6f7a8` is
unchanged; no migrations. The activity-core reconcile at commit
`67a92d2ff8e033baf2c36998d24187fe83edec84` with `expected_retirement_uuids` set
to the three UUIDs applied: 0 created, 3 retired (tombstoned with an
`@retired-20261006` slug; UUIDs and statuses kept), 0 refused, 0 tasks
cancelled. All 7 bound Activity source files still match their digests. The four
historical parents and 64 task UUIDs are present under their bound parents with
unchanged fields. The Custodian retired parent and task are unchanged. Activity
had 54 workplans and 288 tasks before and after. Evidence:
[activity stale retirement rollout](../docs/evidence/2026-10-06-activity-stale-retirement-rollout.json).
Repo Manager's re-run of the Activity owner sync (RMGR-WP-0019-T03) and
ACTIVITY-WP-0045-T02 remain with their owners.
## Cluster exact historical receiving successor, 2026-10-17
This scoped extension retains the completed Activity/Custodian outcomes above.
Three frozen cluster archives contain three existing parent UUIDs and 15 existing
closed task UUIDs. Extend the existing exact historical receiving path before
registrar admission; never derive replacement identities or modify those
sources. Cross-owner observation attaches to existing SHR-WP-0001, with
RMGR-WP-0019 as obligor/beneficiary and NK-WP-0049-T35 benefiting indirectly.
The local derived case is waiting; no live worker injection is authorized.
## Prepare exact cluster source preservation and refusal cases
```task
id: STATE-WP-0095-T05
status: done
priority: high
```
Prepared the same full-file SHA256 and 15 task binding manifest as Repo Manager.
Candidate receiving tests pass 26: existing rows remain entirely unchanged;
missing/foreign/nonterminal rows and unknown, changed, missing or symlinked
source refuse. Read-only public metadata confirms all 18 existing rows have the
expected ownership and terminal status. These are source and metadata checks,
not delivered receiving or a committed native trial.
## Deliver reviewed cluster receiving successor before registrar admission
```task
id: STATE-WP-0095-T06
status: wait
priority: high
depends_on: [STATE-WP-0095-T05]
needs_human: true
intervention_note: "Review exact committed source, CI image and owner deployment proposal before rollout."
```
Use the normal CI image and Helm owner path after exact revision/image pin,
fresh headroom and reviewed dry-run/diff. API/MCP currently run main-7d99282;
Helm revision75 is the observed rollback baseline, requiring freshness before
execution. No deployment assent exists for this successor. Verify delivered
module hash, health, both readiness checks and unchanged schema; verify exact
three parents/15 tasks and rejection behavior without committed probe changes.
Only the verified return unblocks RMGR-WP-0019-T05. Costs and monetary risk are
unknown pending exact rollout proposal; no zero-cost or autonomy grant is inferred.
## Verify ordinary cluster receiving and immutable historical rows
```task
id: STATE-WP-0095-T07
status: wait
priority: high
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06]
```
Capture normal cluster owner publication/receiving receipts and compare entire
existing row snapshots and exact source bytes. No creation, movement, relabel,
unretirement or retirement is authorized for these histories. A rollback first
fences cluster sync and restores registrar refusal before removing receiving
support; it never rewrites archived IDs or UUIDs. This work does not establish
original TLS recovery or owner-native admission.

View file

@ -0,0 +1,27 @@
{
"schema": "cluster-historical-source-validation.v1",
"canonical_mutations": false,
"production_mutations": false,
"repos": {
"repo-manager": {
"tests": 47,
"result": "pass",
"command": "PYTHONPATH=repo-manager/src /home/worsch/repo-manager/.venv/bin/python -m pytest -q --tb=short repo-manager/tests"
},
"state-hub": {
"tests": 26,
"result": "pass",
"command": "PYTHONPATH=state-hub /home/worsch/state-hub/.venv/bin/python -m pytest -q --tb=short state-hub/tests/test_cluster_historical_projection.py state-hub/tests/test_historical_projection.py"
}
},
"checks": {
"receiving_registrar_manifests_equal": true,
"frozen_archives": 3,
"existing_task_uuids": 15,
"read_only_existing_terminal_rows": 18,
"actual_allocator_assignments": 7,
"actual_allocator_expected_files": 2,
"strict_invalid_labels_retained": 15,
"strict_collisions_retained": 5
}
}

View file

@ -0,0 +1,28 @@
# Secrets Engine responder authentication source handoff
Apply/review `implementation-r2.patch` using `integration-manifest.json` and
`source-provenance.json`. R1 is retained unchanged for audit; its initial fixture
failures and subsequent fixes are in the retained logs. Original dirty patches
are provenance only and must not be applied as implementation.
R2 makes the actual production Check/authorization path require independently
admitted Ed25519 keys, exact signed evaluator/mode and policy pin/artifact digest.
It processes original response bytes using unchanged pinned Access Engine Go
producer types/verifier, then consumes only the authenticated typed output.
Unsigned/forged/tampered/downgraded responses refuse. The default Check API is
also authenticated; synthetic transport tests explicitly name their opt-out.
The subprocess receives an empty environment. Existing request/claim/lifetime/CAS
bindings stay enforced; native Go signing drives the migrated consumer tests.
Verification: 158 focused pass; full suite 685 pass/16 fail/1 skip. All sixteen
failure ids exactly match untouched canonical baseline failures caused by the
sandbox temporary-path ownership check. `verification-r2.json` retains the exact
comparison. No source guard was weakened to make that environment pass.
Deployment must separately install the reviewed Go helper; a Python wheel alone
is insufficient. Key admission/rotation and native rollout still require exact
owner review and authorization. No production keys, credentials, backend rights,
CCR38 scope or trust settings were changed. WP14 T02 remains progress and T03
wait. WP13 and its native acceptance are unchanged. Root performs canonical
integration and consistency sync; this worker changed only isolated source and
the Custodian handoff artifacts.

View file

@ -0,0 +1,77 @@
diff --git a/.custodian-brief.md b/.custodian-brief.md
index d3d8eba..b392557 100644
--- a/.custodian-brief.md
+++ b/.custodian-brief.md
@@ -2,7 +2,7 @@
# Custodian Brief — access-engine
**Domain:** infotech
-**Last synced:** 2026-10-11 18:38 UTC
+**Last synced:** 2026-10-11 21:31 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
@@ -12,7 +12,7 @@ Progress: 5/9 done | workplan_id: `84f5d9fe-b4c9-584a-b964-efe3e48af095`
**Open tasks:**
- ! 2. Register flex-auth as an audit-core sender `89661908`
- *(wait: AUDIT-IN-0006 sender rows and governed token delivery remain pending; gate-house GH-WP-0004-T12 must rule on the load-bearing failure-path exception.)*
+ *(wait: GH-WP-0004-T12 refused the load-bearing failure-path exception (GH-DEC-2026-024). AUDIT-IN-0006 sender rows and governed token delivery remain pending.)*
- ! 4. Close G2 `423b3090`
*(wait: G2 needs T08 native heartbeat/drain and silence-finding evidence plus T06 reconciliation; local tests cannot close the delivery gap.)*
- ! 6. Reconciliation, profile check, and storage `bf92a951`
@@ -24,12 +24,12 @@ Progress: 5/9 done | workplan_id: `84f5d9fe-b4c9-584a-b964-efe3e48af095`
Progress: 0/8 done | workplan_id: `af0f8d77-af04-569b-8e79-6c47eb8f1087`
**Open tasks:**
+- ! Make the PDP accept both names `487525e5`
+- ! Stand up the access-engine namespace next to flex-auth `13b93d7a`
+- ! Consumers switch over, one at a time `f0470e5c`
- ► Decide the target names and supersede the retain table `4f9a796d`
- ► Inventory every consumer reference and open handoffs `8f597899`
- ► Rename the source `ff7fd369`
-- · Make the PDP accept both names `487525e5`
-- · Stand up the access-engine namespace next to flex-auth `13b93d7a`
-- · Consumers switch over, one at a time `f0470e5c`
- · Retire the flex-auth runtime `e46c9c8e`
- … and 1 more open tasks
@@ -38,14 +38,14 @@ Progress: 10/11 done | workplan_id: `99a661a8-b36c-5c1c-b78b-1e8930bcd0a9`
**Open tasks:**
- ! 11. Soak, hand off residuals, and clean up the old checkout `46d8318b`
- *(wait: Needs SBOM-IN-0001 catch-up receipt, secrets-engine coordinate correction and railiance-fabric residual disposition, observed soak evidence, and exact checkout cleanup approval under e071b34e-c4d4-49d0-824d-2c58bb0ae496.)*
+ *(wait: Cleanup approved conditionally (e071b34e): runs once the listed soak evidence, SBOM-IN-0001 receipt and residual handoffs exist.)*
### 2026-10-03 secret-read evaluator for ops-warden fetch checks
Progress: 1/2 done | workplan_id: `60941288-d1f8-58fe-ad38-d508b7d5eac2`
**Open tasks:**
- ! Deploy the pin (founder go required) `aa00b29f`
- *(wait: Live railiance01 change; founder go needed for CI image digest, helm install and tunnel.)*
+ *(wait: Install accepted retroactively (877c268b); steps 6-7 follow WARDEN-WP-0039-T03 token receipts and fetch URL.)*
## Inbox Hygiene
diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 62ffebf..2268973 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -214,12 +214,12 @@
| task | FLEX-WP-0035-T03 | done | — | workplans/FLEX-WP-0035-per-identity-application-readback.md |
| task | FLEX-WP-0035-T04 | done | — | workplans/FLEX-WP-0035-per-identity-application-readback.md |
| task | FLEX-WP-0035-T05 | done | — | workplans/FLEX-WP-0035-per-identity-application-readback.md |
-| task | FLEX-WP-0036-T01 | progress | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
+| task | FLEX-WP-0036-T01 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
| task | FLEX-WP-0036-T02 | progress | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
-| task | FLEX-WP-0036-T03 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
-| task | FLEX-WP-0036-T04 | progress | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
-| task | FLEX-WP-0036-T05 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
-| task | FLEX-WP-0036-T06 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
+| task | FLEX-WP-0036-T03 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
+| task | FLEX-WP-0036-T04 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
+| task | FLEX-WP-0036-T05 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
+| task | FLEX-WP-0036-T06 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
| task | FLEX-WP-0036-T07 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
| task | FLEX-WP-0036-T08 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md |
| intake | FLEX-IN-0001 | closed | — | intakes/intakes.md |

View file

@ -0,0 +1,57 @@
{
"schema": "netkingdom.source-integration.v1",
"owner": "secrets-engine",
"checkout": "/tmp/netkingdom-secrets-decision-auth",
"source_base": "6bd7129eac94131b4cf689e47cbdc9899418beb3",
"patch": "implementation.patch",
"patch_sha256": "1cd7adab482c1e359786ed403e1bb209e76956c62ee045d85bb8a59a13fe55b7",
"paths": [
"SCOPE.md",
"docs/pdp-access-path.md",
"scripts/check_pdp_producer_source.py",
"src/secrets_engine/approval_consume.py",
"src/secrets_engine/config.py",
"src/secrets_engine/decision_check.py",
"src/secrets_engine/decision_signature.py",
"tests/fixtures/pdp-signature/decision_rotate_signed.json",
"tests/fixtures/pdp-signature/decision_rotate_signed_tampered.json",
"tests/signature_support.py",
"tests/test_approval_auth.py",
"tests/test_consume_binding_join.py",
"tests/test_decision_signature.py",
"tests/test_decision_transport.py",
"verifier/LICENSE.access-engine",
"verifier/README.md",
"verifier/go.mod",
"verifier/internal/sign/sign.go",
"verifier/main.go",
"verifier/pkg/api/canonical.go",
"verifier/pkg/api/caring.go",
"verifier/pkg/api/doc.go",
"verifier/pkg/api/resource_manifest.go",
"verifier/producer-source.json",
"verifier/testsupport/signer/main.go",
"workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md"
],
"workplan": "SECRETS-WP-0014",
"task_status": {
"T02": "progress: implementation prepared; independent review/key admission pending",
"T03": "wait: no native rollout/trust admission"
},
"verification": [
"89 focused exact-source tests passed",
"20 initial native producer/consumer tests passed",
"Access producer go test ./internal/sign ./pkg/api passed",
"producer source/fixture hash check against current Access owner passed"
],
"native_acceptance": false,
"canonical_owner_modified": false,
"secrets_read": false,
"integration_instructions": "Review/apply implementation.patch to current source after base/dirty provenance check; do not apply original dirty provenance patches. Install reviewed verifier separately; do not deploy/admit keys automatically. Root handles canonical integration/fix-consistency.",
"limitations": [
"Python wheel alone lacks installed Go helper; deployment package must install it",
"Signer plus signed evaluator/mode and policy artifact digest binds issuer/pin; producer has no distinct instance issuer field",
"Trust file must be independently admitted; no production keys/config changed",
"Full suite initial collection lacked optional clock import; broader rerun remains under investigation, focused suite green"
]
}

View file

@ -0,0 +1,43 @@
{
"schema": "netkingdom.source-integration.v1",
"revision": "r3",
"base_patch": "implementation-r2.patch",
"base_patch_sha256": "98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9",
"delta": "implementation-r3-delta.patch",
"delta_sha256": "b6c944fee615298a29dbd50ba0ad437ff76e6eab190f6168427e4ae5dbbfc59c",
"files": {
"src/secrets_engine/config.py": {
"r2_sha256": "be74993577237d52523ead470fad167e0c5834df3b064a3d0fca95235cc6ddb1",
"r3_sha256": "228f548822620d538ad0c025eb68d88091be542819ed571be06c9e69c5e912c6"
},
"tests/test_consume_binding_join.py": {
"r2_sha256": "c3fed0d17cd42acd23798eec37adc20287b4b285fd0e001eb75769e9acb05b78",
"r3_sha256": "eb5f033057caebc470a6fad4fcfa2b30fbc92ff41217ad7ace6f329dae7e9af3"
},
"tests/test_execution_context.py": {
"r2_sha256": "7a173c50a58bc8fe95decb936ba7c181b5c0279e51ecea937588d76c84cdc88a",
"r3_sha256": "7b405dd87a796b142cf0a4bbf48b9df074e99be1c985fe622386796e6ff491a1"
},
"tests/test_human_control.py": {
"r2_sha256": "90c27818e17b78772d7699064c52759b3709ff4bf8fed21b1f63647905e46c84",
"r3_sha256": "edc9b464fe0aff4b05a4ce5123ff715b6ae53898f0f001c28c3815aaa462b301"
}
},
"purpose": "Name public coordination/approval IDs explicitly; values and scanner policy unchanged",
"canonical_owner_modified": false,
"verification": {
"scanner": {
"blocked_blob_ids": [],
"scanned_blobs": 21,
"scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509",
"scanner_version": "8.30.1",
"scope": "changed-staged-blobs",
"status": "clean"
},
"scanned_files_match_r3_hashes": true,
"focused_tests": "84 passed in 9.07s",
"config": "default topic and explicit SECRETS_ENGINE_TOPIC_ID override preserved",
"diff_whitespace": "pass",
"dependent_import_tests": "2 passed in 3.41s"
}
}

View file

@ -0,0 +1,68 @@
{
"schema": "netkingdom.source-integration.v1",
"owner": "secrets-engine",
"checkout": "/tmp/netkingdom-secrets-decision-auth",
"source_base": "6bd7129eac94131b4cf689e47cbdc9899418beb3",
"patch": "implementation-r2.patch",
"patch_sha256": "98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9",
"paths": [
"SCOPE.md",
"docs/pdp-access-path.md",
"scripts/check_pdp_producer_source.py",
"src/secrets_engine/approval_consume.py",
"src/secrets_engine/config.py",
"src/secrets_engine/decision_check.py",
"src/secrets_engine/decision_signature.py",
"tests/fixtures/pdp-signature/decision_rotate_signed.json",
"tests/fixtures/pdp-signature/decision_rotate_signed_tampered.json",
"tests/signature_support.py",
"tests/test_approval_auth.py",
"tests/test_consume_binding_join.py",
"tests/test_decision_replay.py",
"tests/test_decision_signature.py",
"tests/test_decision_transport.py",
"tests/test_execution_context.py",
"tests/test_human_control.py",
"tests/test_integration_authorization.py",
"verifier/LICENSE.access-engine",
"verifier/README.md",
"verifier/go.mod",
"verifier/internal/sign/sign.go",
"verifier/main.go",
"verifier/pkg/api/canonical.go",
"verifier/pkg/api/caring.go",
"verifier/pkg/api/doc.go",
"verifier/pkg/api/resource_manifest.go",
"verifier/producer-source.json",
"verifier/testsupport/signer/main.go",
"workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md"
],
"workplan": "SECRETS-WP-0014",
"task_status": {
"T02": "progress: implementation prepared; independent review/key admission pending",
"T03": "wait: no native rollout/trust admission"
},
"verification": [
"158 focused tests passed in 12.42s",
"Full candidate: 685 passed, 16 failed, 1 skipped in 51.58s",
"All 16 candidate failures exactly match unchanged canonical baseline failed-file run (120 passed/16 failed); zero new failure ids",
"Expanded execution/human/disposable OpenBao: 71 passed, 1 matching baseline failure",
"Real Access producer go test ./internal/sign ./pkg/api passed",
"Exact producer/fixture hashes and git diff --check pass"
],
"native_acceptance": false,
"canonical_owner_modified": false,
"secrets_read": false,
"integration_instructions": "Review/apply implementation.patch to current source after base/dirty provenance check; do not apply original dirty provenance patches. Install reviewed verifier separately; do not deploy/admit keys automatically. Root handles canonical integration/fix-consistency.",
"limitations": [
"Python wheel alone lacks installed Go helper; deployment package must install it",
"Signer plus signed evaluator/mode and policy artifact digest binds issuer/pin; producer has no distinct instance issuer field",
"Trust file must be independently admitted; no production keys/config changed",
"Sixteen pre-existing sandbox tmp ancestry ownership failures prevent claiming full green; exact baseline/candidate ids retained in verification-r2.json. No new failure ids remain."
],
"revision": "r2",
"supersedes": {
"patch": "implementation.patch",
"sha256": "1cd7adab482c1e359786ed403e1bb209e76956c62ee045d85bb8a59a13fe55b7"
}
}

View file

@ -0,0 +1 @@
{"blocked_blob_ids": [], "scanned_blobs": 21, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}

View file

@ -0,0 +1,92 @@
diff --git a/.custodian-brief.md b/.custodian-brief.md
index 0cb912a..122e79b 100644
--- a/.custodian-brief.md
+++ b/.custodian-brief.md
@@ -2,17 +2,33 @@
# Custodian Brief — secrets-engine
**Domain:** infotech
-**Last synced:** 2026-10-11 17:52 UTC
+**Last synced:** 2026-10-12 06:27 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
+### Constrain PDP transport and authenticate decision responses
+Progress: 1/3 done | workplan_id: `7252469d-d7a5-5c18-8911-b511b8c6a736`
+
+**Open tasks:**
+- ! Verify the admitted native consumer and rotation path `85aa4c78`
+ *(wait: Requires reviewed verification-key admission, source enforcement and exact native rollout receipt; no production trust change is proposed yet.)*
+- · Implement independently anchored responder authentication `877782e5`
+
### Design and prove task-scoped protected OpenBao execution
Progress: 1/3 done | workplan_id: `5e38aecc-85ca-5f55-94c6-9fbb08604819`
**Open tasks:**
+- ! Define the exact mediated execution contract `b2f0d462`
+ *(wait: Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred.)*
- ! Implement and prove one native metadata action `5f8f7c41`
-- ► Define the exact mediated execution contract `b2f0d462`
+
+### Production-safe provisioning, authorization, and lifecycle hardening
+Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
+
+**Open tasks:**
+- ! Resume native production lane adoption `a0a1dd92`
+ *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
### Activate native Claude credential delivery for Glas
Progress: 2/3 done | workplan_id: `40ccc3b4-d046-5a58-8649-e7935f45c974`
@@ -26,23 +42,16 @@ Progress: 4/6 done | workplan_id: `31f7f8ea-7f73-516c-8877-f03a13f1db82`
**Open tasks:**
- ! Stage live apply and verification `fb103f1e`
- *(wait: OpenRouter key-check native acceptance is recorded in SECRETS-WP-0010-T03. Other lanes still need exact approvals, scoped attended apply and per-lane positive/negative/health/revocation evidence.)*
+ *(wait: Owner work first: publish a reviewed scoped-apply command per lane. Per-lane approvals are no longer needed (D1 standing rule); each lane with a command enters the attended window.)*
- ! Reconcile routing ownership and retire interim proxies `1431edae`
*(wait: Needs owner-agreed routing/proxy retirement for each verified lane and custody disposition of the legacy npm pointer; a single approved OpenRouter key-check does not authorize broader routing cutover.)*
-### Production-safe provisioning, authorization, and lifecycle hardening
-Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
-
-**Open tasks:**
-- ! Resume native production lane adoption `a0a1dd92`
- *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
-
### Evolve the Lifecycle engine to the accepted security layer model
Progress: 5/6 done | workplan_id: `9c9e5164-b2f5-5ea2-a557-5368d65e9fe0`
**Open tasks:**
- ! No standing engine credential `d7bc8bdc`
- *(wait: Positive service-JWT login proof passed 2026-10-10 (SECRETS-WP-0012-T03, docs/evidence/2026-10-10-keycape-openbao-probe.json). Waits on railiance-platform closing RPF-WP-0035-T02 with the CCR-2026-0038 negative checks, then on routing live engine actions through service-jwt instead of bootstrap/env.)*
+ *(wait: Published JWT contract and positive login/revocation are delivered. Native KEY-WP-0039-T02 claim negatives, CCR-2026-0038 effective-policy/capability/expiry/use checks and MASON-WP-0009-T05 remain. Login-self-only grants no protected-operation authority: agree and implement separately reviewed task-scoped backend execution entitlement, then prove functional steady-state service-identity adoption without bootstrap/env fallback.)*
## Inbox Hygiene
diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 70e5831..a467a0e 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -22,6 +22,7 @@
| workplan | SECRETS-WP-0011 | finished | — | workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md |
| workplan | SECRETS-WP-0012 | finished | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| workplan | SECRETS-WP-0013 | proposed | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| workplan | SECRETS-WP-0014 | active | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| task | SECRETS-WP-ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md |
| task | SECRETS-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md |
| task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
@@ -83,9 +84,12 @@
| task | SECRETS-WP-0012-T02 | cancel | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T03 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T04 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
-| task | SECRETS-WP-0013-T01 | progress | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0013-T01 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T02 | done | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T03 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0014-T01 | done | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T02 | todo | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T03 | wait | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| intake | SECRETS-IN-0001 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0002 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0003 | closed | — | intakes/intakes.md |

View file

@ -0,0 +1,12 @@
{
"secrets-engine": {
"head": "6bd7129eac94131b4cf689e47cbdc9899418beb3",
"dirty_patch_sha256": "499b8b4cd583b097d225dfe6e33dde9cbf72329054796a8762a535908c7af3dc",
"status": "M .custodian-brief.md\n M WORK-RECORDS.md\n"
},
"access-engine": {
"head": "62ac2b4e99dec2005c7c407fe633612578a7907d",
"dirty_patch_sha256": "a20bc75c88e2861e37df6b793c2346efbd8c3fc4a46c7fa2ad0e5aa8c0144130",
"status": " M .custodian-brief.md\n M WORK-RECORDS.md\n"
}
}

View file

@ -0,0 +1,42 @@
{
"schema": "netkingdom.verification.v1",
"patch": "implementation-r2.patch",
"patch_sha256": "98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9",
"full_candidate": {
"passed": 685,
"failed": 16,
"skipped": 1,
"seconds": 51.58
},
"focused_candidate": {
"passed": 158,
"failed": 0,
"seconds": 12.42
},
"unchanged_owner_baseline_failed_files": {
"passed": 120,
"failed": 16,
"seconds": 14.08
},
"new_failures": [],
"baseline_failure_test_ids": [
"tests/test_exec_owner.py::test_actual_bound_child_gets_only_selected_secret_and_fixed_environment",
"tests/test_exec_owner.py::test_binding_changed_after_approval_refuses_before_fetch",
"tests/test_exec_owner.py::test_config_changed_during_fetch_does_not_launch",
"tests/test_exec_owner_companions.py::test_companion_fetch_failure_starts_no_child",
"tests/test_exec_owner_companions.py::test_companion_value_reaches_bound_child_and_is_redacted",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[consent]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[env]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[extra]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[field]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[lane]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[missing]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[stage]",
"tests/test_exec_owner_companions.py::test_each_lane_is_gated_separately_before_backend",
"tests/test_exec_owner_companions.py::test_unresolvable_companion_refuses_before_any_gate",
"tests/test_human_control.py::test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child",
"tests/test_integration_companions.py::test_two_lanes_reach_one_bound_owner"
],
"reason": "Every candidate failure exactly matches a failure reproduced against unchanged canonical owner source; all refuse on exec_owner._check_path sandbox /tmp ancestry ownership. No guard bypass or ownership relaxation was added.",
"native_acceptance": false
}

View file

@ -0,0 +1,29 @@
# Strict Secrets task handoff source package
Review `implementation-r1.patch` on c80a944 independently of the CLI packaging
revision. Exact source hashes, public API and tests are in the integration manifest.
The module calls the real existing serving claim/signed Check validators, retains
immutable served artifacts, verifies exact descriptor/request/native approval and
enforced caller joins, then exposes a process-local one-shot strict CAS handoff.
It refuses missing/mismatched/coerced response fields and historical idempotence.
No raw mapping/boolean/authorizer callback supplies authorization. Platform must
bind its owner Plan, reserve durable journal, call this consume, independently
GET-confirm actual original Approval consumption and journal it before native.
Focused tests: 246 pass. Full: 736 pass/16 fail/1 skip; exact16 failure IDs match the
unchanged canonical baseline's temporary-path ownership refusals, with zero new
failure IDs. All failing initial observations are retained; no guard was weakened.
Pinned scanner is clean5 blobs and whitespace checks pass. Real producer Go
signatures and local HTTP are exercised; the fixture caller/approval are synthetic.
No live key/trust, policy admission, server mapping, service binding propagation,
backend entitlement or protected effect is claimed. WP13 native work remains open.
Platform owns the independent actual-provider/Access-serve whole-stack proof.
R2 supersedes R1 with an exactness repair: independently recompute the served
native act digest, because Python dict equality can equate True and integer1 in
registry enrichment. The retained R1 counterexample accepted that malformed fact;
R2 refuses it. Public interface and all source gates are unchanged. Review full
`implementation-r2.patch` or tiny `r1-to-r2.patch`; R1 is retained unchanged.
R2:247 focused pass; full737 pass/16 baseline failures/1skip; scanner clean5.

View file

@ -0,0 +1,600 @@
diff --git a/docs/task-execution-handoff.md b/docs/task-execution-handoff.md
new file mode 100644
index 0000000..5821175
--- /dev/null
+++ b/docs/task-execution-handoff.md
@@ -0,0 +1,78 @@
+# Strict Secrets service task-execution handoff
+
+This is a narrow source seam for existing SECRETS-WP-0013, not a native grant or
+an execute-ccr CLI. Both Access runtime-deny policy variants remain unchanged.
+The proposed service subject/transport mapping and the complete native executor
+still need independent owner admission. Positive source fixtures do not admit
+that mapping, a policy, a caller, a production key or an OpenBao entitlement.
+
+Platform's trusted owner process calls:
+
+```python
+from secrets_engine.task_execution_handoff import (
+ authorize_task_execution, consume_task_execution,
+)
+authorization = authorize_task_execution(cfg, entry, execution_contract=contract)
+record = authorization.as_owner_handoff()
+# Independently bind exact owner Plan/descriptor; reserve durable one-shot journal.
+# Persist pending strict-CAS intent before calling:
+consumption = consume_task_execution(cfg, authorization)
+# Independently GET-confirm exact original consumption with Approval Engine's
+# ExecutionConfirmationClient before journaling consume-confirmed/native work.
+```
+
+Preparation uses the existing empty fields/policy_targets/auth_targets defaults
+and existing build_action_request/claim/Check validators. No caller-selected
+transport, authorizer callback, policy result or raw handoff mapping is accepted.
+The normal authorizer retains detached serialized request, claim and authenticated
+Go-typed envelope snapshots; its existing evidence output does not expose them.
+The narrow adapter reuses the validators, rather than evaluating a policy.
+
+The adapter checks the exact descriptor in the signed context, full evaluated and
+submitted request digests, approval-free digest, exact five-field native act and
+native binding digest, service subject/resource/tenant correspondence and original
+decision/approval/deadline bounds. Initiating/represented subject is strictly
+`secrets-engine` of type `service`; the execution actor is `service:secrets-engine`.
+Signed caller provenance must report `enforce`, the proposed exact Secrets
+ServiceAccount principal, `flex-auth` audience and live expiry bounding the
+operation deadline. This consumes the signed PDP's caller attestation; it does
+not independently perform TokenReview or promote the proposed server mapping.
+Human/delegation/projected-identity flags cannot substitute for this service seam.
+No service_actor_binding request propagation or source-policy activation is added.
+
+`as_owner_handoff()` returns a detached non-secret mapping with:
+
+- execution_contract
+- approval_id, request_digest, decision_id
+- native_binding, approval_binding_digest, submitted_request_digest
+- decision_not_before, decision_expires_at, approval_expires_at
+
+The exact native binding is derived from the signed evaluated request using the
+published Approval five-field mapping, and is compared to the served claim.
+Descriptor/CCR/grant/source/catalog/target correspondence is retained end to end;
+Platform still independently compiles its owner Plan and grant from owner bytes.
+The mapping is evidence for that join, never a transferable bearer authorization.
+
+Consumption accepts only the module-created opaque prepared object in that same
+trusted owner process. It fixes source/config coordinates, reauthenticates using
+currently admitted responder keys, and rechecks claim/decision/descriptor/caller
+freshness before reading the CAS credential. Each object is reserved once before
+CAS/revalidation and cannot retry after any failure or unknown outcome. Its
+process-local guard complements Platform's durable cross-process journal; it is
+not a replacement for that journal or process isolation.
+
+The strict response path uses the existing CAS transport but rejects duplicate,
+nonfinite or excessively deep JSON, missing/mismatched approval and decision IDs,
+wrong digest/status, missing or nonboolean idempotence, historical idempotent
+success, verdict fields and absent/naive/invalid consumption timestamps. The
+consumption instant and post-CAS freshness must remain inside the original
+bounds. No field is coerced or filled from the submitted binding. A late/uncertain
+CAS may have occurred; it never licenses retry or protected native work.
+
+The returned immutable TaskExecutionConsumption contains exact approval_id,
+request_digest, decision_id, consumed_at and explicit false idempotent. It proves
+only the strict CAS response observed by Secrets. Platform must independently
+confirm original durable consumption using Approval's real provider client,
+journal it, then recheck its exact Plan, owner sources, grant and deadlines before
+any native operation. Neither a dataclass nor a caller-supplied boolean admits
+execution. Existing generic CLI CAS behavior is otherwise unchanged.
diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py
index bb1e766..1cf4140 100644
--- a/src/secrets_engine/approval_consume.py
+++ b/src/secrets_engine/approval_consume.py
@@ -23,7 +23,7 @@ from secrets_engine.execution_contract import ExecutionContract, require_executi
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
-from secrets_engine.decision_signature import load_trust
+from secrets_engine.decision_signature import load_trust, strict_json
from secrets_engine.authorization import (
build_action_request,
validate_decision_envelope,
@@ -70,6 +70,8 @@ class AuthorizedAction:
binding: ConsumeBinding
decision_id: str
expires_at: str
+ # Detached original served artifacts for the narrow task-execution handoff.
+ artifacts: str = field(default="", repr=False, compare=False)
@property
def id(self) -> str:
@@ -380,6 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
+ artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
+ sort_keys=True,separators=(",",":"),allow_nan=False),
)
@@ -391,6 +395,7 @@ def consume_approval(
binding: ConsumeBinding,
timeout_seconds: float = 3,
opener: Callable[..., Any] = credential_urlopen,
+ strict_response: bool = False,
) -> ConsumedApproval:
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
if not base_url or not base_url.startswith(("http://", "https://")):
@@ -438,8 +443,8 @@ def consume_approval(
if status != 200 or len(raw) > _MAX_BODY:
raise DecisionError(_status_message(status if status != 200 else 502))
try:
- payload = json.loads(raw.decode("utf-8"))
- except (UnicodeDecodeError, json.JSONDecodeError) as exc:
+ payload = (strict_json if strict_response else json.loads)(raw.decode("utf-8"))
+ except (UnicodeDecodeError, ValueError, RecursionError) as exc:
raise DecisionError("approval consume returned invalid JSON") from exc
if not isinstance(payload, dict):
raise DecisionError("approval consume returned invalid payload")
@@ -447,6 +452,19 @@ def consume_approval(
raise DecisionError("approval consumption was not confirmed")
if payload.get("request_digest") != binding.request_digest:
raise DecisionError("approval consume digest does not match the request")
+ if strict_response:
+ from datetime import datetime
+ if (payload.get("approval_id") != approval_id
+ or payload.get("decision_id") != binding.decision_id
+ or payload.get("idempotent") is not False
+ or any(key in payload for key in ("effect","allow","deny","decision"))):
+ raise DecisionError("strict task consumption correspondence refused")
+ try:
+ stamp = datetime.fromisoformat(payload["consumed_at"].replace("Z","+00:00"))
+ if stamp.tzinfo is None or stamp.utcoffset() is None:
+ raise ValueError("timezone required")
+ except (KeyError,TypeError,AttributeError,ValueError):
+ raise DecisionError("strict task consumption timestamp refused") from None
decision_id = payload.get("decision_id")
if decision_id is not None and (
not isinstance(decision_id, str) or not decision_id
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
new file mode 100644
index 0000000..deab6e2
--- /dev/null
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -0,0 +1,191 @@
+"""Narrow service-task PEP handoff; records never grant native execution.
+
+Platform calls preparation itself, binds its independently compiled owner Plan,
+reserves/journals once, then calls strict consumption and independent Approval
+GET confirmation. No inbound mapping/boolean or constructor creates authority.
+Actual policy/server input mapping and runtime entitlement remain unadmitted.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+import json
+from threading import Lock
+
+from secrets_engine.approval_auth import approval_token, credential_urlopen
+from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest, validate_approval_claim
+from secrets_engine.approval_consume import authorize_action, consume_approval
+from secrets_engine.application_time import read_window, validity_bounds
+from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest
+from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
+from secrets_engine.errors import DecisionError
+from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
+
+_CALLER = 'system:serviceaccount:secrets-engine:secrets-engine'
+_NATIVE_FIELDS = ('action','actor','principal','purpose','target')
+_COORDINATES = ('approval_url','approval_token_file','approval_client_secret_file','keycape_token_url','keycape_issuer',
+ 'pdp_url','pdp_token_file','pdp_trust_file','pdp_verifier','clock_trust_file',
+ 'authorization_subject_id','authorization_subject_type','authorization_policy_package','authorization_policy_version')
+_ISSUED = object()
+
+
+def _refuse():
+ raise DecisionError('strict task execution handoff correspondence refused')
+
+
+def _coordinates(cfg):
+ return tuple(str(getattr(cfg,name,'') or '') for name in _COORDINATES)
+
+
+def _stamp(value):
+ try:
+ parsed=datetime.fromisoformat(value.replace('Z','+00:00'))
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
+ _refuse()
+ return parsed
+ except (AttributeError,TypeError,ValueError):
+ _refuse()
+
+
+class TaskExecutionAuthorization:
+ """Opaque process-local prepared state; not a bearer credential or allow."""
+ __slots__=('_action','_contract','_coordinates','_handoff','_used','_lock')
+
+ def __init__(self, issuer=None, *, action=None, contract=None, coordinates=None, handoff=None):
+ if issuer is not _ISSUED:
+ _refuse()
+ object.__setattr__(self,'_action',action)
+ object.__setattr__(self,'_contract',contract)
+ object.__setattr__(self,'_coordinates',coordinates)
+ object.__setattr__(self,'_handoff',json.dumps(handoff,sort_keys=True,separators=(',',':'),allow_nan=False))
+ object.__setattr__(self,'_used',False)
+ object.__setattr__(self,'_lock',Lock())
+
+ def __setattr__(self,name,value):
+ raise AttributeError('prepared task handoff is immutable')
+
+ def as_owner_handoff(self):
+ return strict_json(self._handoff)
+
+
+@dataclass(frozen=True)
+class TaskExecutionConsumption:
+ approval_id: str
+ request_digest: str
+ decision_id: str
+ consumed_at: str
+ idempotent: bool
+
+
+def _validate(cfg, action, contract):
+ if type(contract) is not ExecutionContract or not action.artifacts:
+ _refuse()
+ descriptor=contract.as_dict()
+ artifacts=strict_json(action.artifacts)
+ request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
+ # Rotation/removal and installed-helper changes cannot hide behind a cached
+ # prepared label. Reauthenticate before each strict CAS, without networking.
+ authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
+ trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
+ verifier=getattr(cfg,'pdp_verifier',None))
+ if authenticated != envelope:
+ _refuse()
+ window=read_window(cfg)
+ lower,upper=validity_bounds(window=window)
+ validated=validate_decision_envelope(envelope,request,
+ accepted_policy_packages={str(cfg.authorization_policy_package)},
+ accepted_policy_versions={str(cfg.authorization_policy_version)},
+ expected_approval_binding_digest=claim['binding']['pdp_digest'],time_window=window)
+ validate_approval_claim(claim,approval_id=action.binding.approval_id,
+ expected_pdp_digest=envelope['binding']['approval_binding_digest'],
+ require_human_control=True,time_window=window)
+ require_execution_deadline(request,time_window=window,
+ bounds=(claim['validity']['expires_at'],envelope['lifetime']['expires_at']))
+ binding=envelope['binding']
+ if (envelope['lifetime'].get('kind')!='ttl'
+ or _stamp(envelope['lifetime'].get('not_before'))>lower):
+ _refuse()
+ if (request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
+ or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
+ or binding.get('context')!=request['context']
+ or request['context'].get('execution_contract')!=descriptor
+ or binding.get('request_digest')!=request_digest(binding)
+ or binding.get('approval_binding_digest')!=approval_binding_digest(binding)
+ or action.binding.request_digest!=validated.request_digest
+ or action.binding.decision_id!=validated.decision_id):
+ _refuse()
+ subject=binding['subject']; resource=binding['resource']
+ if (request['subject']!={'id':'secrets-engine','type':'service'}
+ or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
+ or subject.get('tenant') not in (None,'tenant:platform')
+ or 'principal' in subject.get('attributes',{})
+ or envelope['subject']!=subject or envelope['resource']!=resource
+ or descriptor['execution_actor']!='service:secrets-engine'
+ or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
+ or resource.get('tenant') not in (None,'tenant:platform')
+ or any(resource.get('attributes',{}).get(key)!=value for key,value in request['resource']['attributes'].items())
+ or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
+ _refuse()
+ caller=envelope['provenance'].get('caller')
+ if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
+ or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
+ or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
+ or upper >= _stamp(caller.get('not_after'))):
+ _refuse()
+ native=binding_from_check_request(binding)
+ supplied=claim['binding']
+ if (any(supplied.get(key)!=native[key] for key in _NATIVE_FIELDS)
+ or supplied.get('digest')!=claim_binding_digest(**native)):
+ _refuse()
+ return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
+ 'request_digest':validated.request_digest,'decision_id':validated.decision_id,
+ 'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
+ 'submitted_request_digest':validated.submitted_request_digest,
+ 'decision_not_before':envelope['lifetime'].get('not_before') or '',
+ 'decision_expires_at':envelope['lifetime']['expires_at'],
+ 'approval_expires_at':claim['validity']['expires_at']}
+
+
+def authorize_task_execution(cfg, entry, *, execution_contract):
+ if type(execution_contract) is not ExecutionContract:
+ _refuse()
+ # No injected transports or policy callbacks: use the actual existing PIP/PDP
+ # clients and mandatory signature validators in the owner process.
+ action=authorize_action(cfg,entry,'execute-ccr',execution_contract=execution_contract)
+ if action is None:
+ _refuse()
+ try:
+ handoff=_validate(cfg,action,execution_contract)
+ return TaskExecutionAuthorization(_ISSUED,action=action,contract=ExecutionContract.parse(execution_contract.as_dict()),
+ coordinates=_coordinates(cfg),handoff=handoff)
+ except (ValueError,KeyError,TypeError,RecursionError) as exc:
+ raise DecisionError('strict task execution handoff malformed') from exc
+
+
+def consume_task_execution(cfg, authorization):
+ if type(authorization) is not TaskExecutionAuthorization:
+ _refuse()
+ with authorization._lock:
+ if authorization._used or _coordinates(cfg)!=authorization._coordinates:
+ _refuse()
+ # Reserve once before all work; failures/unknown outcomes never retry this
+ # prepared object. Platform's durable journal is the cross-process guard.
+ object.__setattr__(authorization,'_used',True)
+ try:
+ handoff=_validate(cfg,authorization._action,authorization._contract)
+ if handoff!=authorization.as_owner_handoff():
+ _refuse()
+ result=consume_approval(base_url=str(cfg.approval_url),
+ token_provider=lambda:approval_token(cfg,scope='approval:consume'),
+ binding=authorization._action.binding,opener=credential_urlopen,strict_response=True)
+ stamp=_stamp(result.consumed_at)
+ window=read_window(cfg);_,upper=validity_bounds(window=window)
+ if (stamp>upper or (handoff['decision_not_before'] and stamp<_stamp(handoff['decision_not_before']))
+ or any(stamp>=_stamp(value) for value in (handoff['decision_expires_at'],handoff['approval_expires_at'],
+ handoff['execution_contract']['operation_deadline']))):
+ _refuse()
+ # Late CAS completion is historical evidence only; refuse native dispatch.
+ _validate(cfg,authorization._action,authorization._contract)
+ return TaskExecutionConsumption(result.approval_id,result.request_digest,result.decision_id,result.consumed_at,result.idempotent)
+ except (ValueError,KeyError,TypeError,RecursionError) as exc:
+ raise DecisionError('strict task execution consumption malformed') from exc
diff --git a/tests/test_task_execution_handoff.py b/tests/test_task_execution_handoff.py
new file mode 100644
index 0000000..b0961fc
--- /dev/null
+++ b/tests/test_task_execution_handoff.py
@@ -0,0 +1,212 @@
+"""Real local HTTP/producer signatures, synthetic approval/caller only.
+
+These source fixtures never enable Access's runtime-deny policy or admit a real
+caller, key, owner grant or native backend. Whole-provider integration is separate.
+"""
+import copy
+from datetime import datetime, timedelta, timezone
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+from pathlib import Path
+from threading import Thread
+
+import pytest
+
+from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest
+from secrets_engine.approval_consume import _expected_request
+from secrets_engine.authorization import request_digest, approval_binding_digest
+from secrets_engine.errors import DecisionError
+from secrets_engine.execution_contract import ExecutionContract
+from secrets_engine.task_execution_handoff import (authorize_task_execution,consume_task_execution,
+ TaskExecutionAuthorization)
+from tests.authorization_stub import AuthorizationStub
+from tests.signature_support import signature_tools, admit_tools, sign_envelope
+from tests.test_consume_binding_join import _Cfg,_token,APPROVAL_ID
+from tests.test_execution_context import entry,descriptor
+
+
+def stamp(moment):
+ return moment.isoformat()
+
+
+@pytest.fixture
+def lane(tmp_path,signature_tools):
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
+ admit_tools(cfg,tmp_path,signature_tools)
+ original=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
+ stub=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
+ stub.bind_request(original)
+ state={'calls':[],'claim_change':None,'decision_change':None,'response_change':None,'raw_response':None}
+ claim=stub.claim();native=binding_from_check_request(original)
+ claim['binding'].update(native,digest=claim_binding_digest(**native),human_control=True)
+ state['claim']=claim
+
+ class Handler(BaseHTTPRequestHandler):
+ def log_message(self,*_args):pass
+ def reply(self,payload):
+ data=json.dumps(payload).encode() if isinstance(payload,dict) else payload
+ self.send_response(200);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
+ def do_GET(self):
+ state['calls'].append('claim')
+ value=copy.deepcopy(state['claim'])
+ if state['claim_change']:state['claim_change'](value)
+ self.reply(value)
+ def do_POST(self):
+ submitted=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
+ if self.path=='/v1/check':
+ state['calls'].append('check')
+ envelope=stub.decision(submitted)
+ envelope['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'audience':'flex-auth','not_after':stamp(datetime.now(timezone.utc)+timedelta(minutes=20))}
+ if state['decision_change']:state['decision_change'](envelope)
+ self.reply(sign_envelope(envelope,signature_tools))
+ else:
+ state['calls'].append('consume')
+ payload={'approval_id':APPROVAL_ID,'status':'consumed','request_digest':submitted['request_digest'],
+ 'decision_id':submitted['decision_id'],'consumed_at':stamp(datetime.now(timezone.utc)),'idempotent':False}
+ if state['response_change']:state['response_change'](payload)
+ self.reply(state['raw_response'] if state['raw_response'] is not None else payload)
+ httpd=ThreadingHTTPServer(('127.0.0.1',0),Handler)
+ thread=Thread(target=httpd.serve_forever,kwargs={'poll_interval':0.01},daemon=True);thread.start()
+ cfg.approval_url=cfg.pdp_url=f'http://127.0.0.1:{httpd.server_port}'
+ try:yield cfg,e,contract,state
+ finally:httpd.shutdown();httpd.server_close();thread.join(2)
+
+
+def prepared(lane):
+ cfg,e,contract,state=lane
+ return authorize_task_execution(cfg,e,execution_contract=contract)
+
+
+def test_exact_preparation_then_one_strict_cas(lane):
+ cfg,e,contract,state=lane
+ result=prepared(lane)
+ assert state['calls']==['claim','check']
+ handoff=result.as_owner_handoff()
+ assert set(handoff)=={'execution_contract','approval_id','request_digest','decision_id','native_binding','approval_binding_digest',
+ 'submitted_request_digest','decision_not_before','decision_expires_at','approval_expires_at'}
+ assert handoff['execution_contract']==contract.as_dict()
+ assert handoff['native_binding']==binding_from_check_request(_expected_request(cfg,e,'execute-ccr',execution_contract=contract))
+ changed=result.as_owner_handoff();changed['execution_contract']['ccr_id']='changed'
+ assert result.as_owner_handoff()==handoff
+ with pytest.raises(AttributeError):result._handoff='changed'
+ receipt=consume_task_execution(cfg,result)
+ assert receipt.approval_id==handoff['approval_id'] and receipt.request_digest==handoff['request_digest']
+ assert receipt.decision_id==handoff['decision_id'] and receipt.idempotent is False
+ assert state['calls']==['claim','check','consume']
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+@pytest.mark.parametrize('field',['action','actor','principal','purpose','target','digest'])
+def test_changed_native_claim_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(claim):claim['binding'][field]={'id':'other'} if field=='target' else 'other'
+ state['claim_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['mode','principal','audience','not_after'])
+def test_signed_wrong_caller_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda envelope:envelope['provenance']['caller'].__setitem__(field,'wrong')
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['execution_actor','execution_grant_ccr_id','grant_immutable_sha256','ccr_id','backend_origin'])
+def test_signed_changed_descriptor_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(envelope):envelope['binding']['context']['execution_contract'][field]='wrong'
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['subject','resource','request_digest','submitted_request_digest','approval_binding_digest'])
+def test_signed_changed_request_correspondence_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(envelope):
+ envelope['binding'][field]={'id':'other'} if field in ('subject','resource') else 'sha256:'+'0'*64
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['approval_id','decision_id','request_digest','status','consumed_at','idempotent'])
+def test_cas_correspondence_never_coerces_or_falls_back(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.__setitem__(field,'wrong')
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+@pytest.mark.parametrize('value',[True,1,0,'false',None])
+def test_nonfresh_or_nonboolean_idempotence_refuses(lane,value):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.__setitem__('idempotent',value)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+
+
+@pytest.mark.parametrize('body',[b'{"status":"consumed","status":"consumed"}',b'{"status":NaN}',b'['*2000+b'0'+b']'*2000])
+def test_duplicate_nonfinite_or_deep_consume_body_refuses(lane,body):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['raw_response']=body
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+
+
+@pytest.mark.parametrize('field',['pdp_url','approval_url','authorization_subject_id','authorization_policy_version','pdp_verifier'])
+def test_changed_owner_coordinates_refuse_before_cas(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane);setattr(cfg,field,'changed')
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+def test_removed_signer_refuses_before_cas(lane):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ trust=json.loads(cfg.pdp_trust_file.read_text());trust['keys']={'other':next(iter(trust['keys'].values()))}
+ cfg.pdp_trust_file.write_text(json.dumps(trust))
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+def test_mapping_boolean_and_public_constructor_are_not_authority(lane):
+ cfg,e,contract,state=lane
+ for value in [True,{},object()]:
+ with pytest.raises(DecisionError):consume_task_execution(cfg,value)
+ with pytest.raises(DecisionError):TaskExecutionAuthorization()
+ assert not state['calls']
+
+
+@pytest.mark.parametrize('field',['approval_id','decision_id','consumed_at','idempotent'])
+def test_absent_cas_fields_cannot_fall_back(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.pop(field)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+def test_expired_preparation_refuses_before_cas(lane,monkeypatch):
+ from types import SimpleNamespace
+ import secrets_engine.task_execution_handoff as module
+ cfg,e,contract,state=lane;result=prepared(lane)
+ later=int((datetime.now(timezone.utc)+timedelta(minutes=2)).timestamp()*1_000_000_000)
+ monkeypatch.setattr(module,'read_window',lambda cfg:SimpleNamespace(lower_ns=later,upper_ns=later))
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('change',['deny','expired','missing-caller','missing-start'])
+def test_signed_decision_missing_authority_refuses_before_cas(lane,change):
+ cfg,e,contract,state=lane
+ def alter(envelope):
+ if change=='deny':envelope['effect']='deny'
+ elif change=='expired':envelope['lifetime']['expires_at']=stamp(datetime.now(timezone.utc)-timedelta(seconds=1))
+ elif change=='missing-start':envelope['lifetime'].pop('not_before')
+ else:envelope['provenance'].pop('caller')
+ state['decision_change']=alter
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
diff --git a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
index 01be511..a8a3845 100644
--- a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
+++ b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
@@ -9,7 +9,7 @@ flavor: planning
owner: codex
topic_slug: netkingdom
created: "2026-10-11"
-updated: "2026-10-11"
+updated: "2026-10-16"
depends_on: []
state_hub_workstream_id: "5e38aecc-85ca-5f55-94c6-9fbb08604819"
---
@@ -110,3 +110,17 @@ This is source preparation awaiting independent technical review/publication,
not a formal owner assent, policy deployment or native execution milestone. T01
remains progress; T03 and SECRETS-WP-0008-T06 remain open. Detailed limitations
and retained failed test observations are in the source preparation receipt.
+
+
+2026-10-16 bounded source handoff preparation: `task_execution_handoff.py` uses
+the actual serving approval/mandatory signed Check path and retained detached
+artifacts to check exact descriptor/request/native approval/caller/lifetime joins,
+then performs one strict exact-response CAS only after Platform journal admission.
+It accepts no inbound boolean/mapping/authorizer callback and grants no native
+execution. Platform independently confirms durable consumption with the actual
+Approval provider client before native dispatch. Focused local HTTP/native Go
+producer-signature source checks cover altered joins/caller/grant, missing fields,
+coercion/idempotence, expiry, signer removal and replay refusal. No service binding
+propagation, policy gate flip, identity mapping admission or production trust/key
+change was made. Independent source integration/review and actual whole-provider
+proof remain required. T01/T03 and WP8-T06 retain their open native obligations.

View file

@ -0,0 +1,622 @@
diff --git a/docs/task-execution-handoff.md b/docs/task-execution-handoff.md
new file mode 100644
index 0000000..5821175
--- /dev/null
+++ b/docs/task-execution-handoff.md
@@ -0,0 +1,78 @@
+# Strict Secrets service task-execution handoff
+
+This is a narrow source seam for existing SECRETS-WP-0013, not a native grant or
+an execute-ccr CLI. Both Access runtime-deny policy variants remain unchanged.
+The proposed service subject/transport mapping and the complete native executor
+still need independent owner admission. Positive source fixtures do not admit
+that mapping, a policy, a caller, a production key or an OpenBao entitlement.
+
+Platform's trusted owner process calls:
+
+```python
+from secrets_engine.task_execution_handoff import (
+ authorize_task_execution, consume_task_execution,
+)
+authorization = authorize_task_execution(cfg, entry, execution_contract=contract)
+record = authorization.as_owner_handoff()
+# Independently bind exact owner Plan/descriptor; reserve durable one-shot journal.
+# Persist pending strict-CAS intent before calling:
+consumption = consume_task_execution(cfg, authorization)
+# Independently GET-confirm exact original consumption with Approval Engine's
+# ExecutionConfirmationClient before journaling consume-confirmed/native work.
+```
+
+Preparation uses the existing empty fields/policy_targets/auth_targets defaults
+and existing build_action_request/claim/Check validators. No caller-selected
+transport, authorizer callback, policy result or raw handoff mapping is accepted.
+The normal authorizer retains detached serialized request, claim and authenticated
+Go-typed envelope snapshots; its existing evidence output does not expose them.
+The narrow adapter reuses the validators, rather than evaluating a policy.
+
+The adapter checks the exact descriptor in the signed context, full evaluated and
+submitted request digests, approval-free digest, exact five-field native act and
+native binding digest, service subject/resource/tenant correspondence and original
+decision/approval/deadline bounds. Initiating/represented subject is strictly
+`secrets-engine` of type `service`; the execution actor is `service:secrets-engine`.
+Signed caller provenance must report `enforce`, the proposed exact Secrets
+ServiceAccount principal, `flex-auth` audience and live expiry bounding the
+operation deadline. This consumes the signed PDP's caller attestation; it does
+not independently perform TokenReview or promote the proposed server mapping.
+Human/delegation/projected-identity flags cannot substitute for this service seam.
+No service_actor_binding request propagation or source-policy activation is added.
+
+`as_owner_handoff()` returns a detached non-secret mapping with:
+
+- execution_contract
+- approval_id, request_digest, decision_id
+- native_binding, approval_binding_digest, submitted_request_digest
+- decision_not_before, decision_expires_at, approval_expires_at
+
+The exact native binding is derived from the signed evaluated request using the
+published Approval five-field mapping, and is compared to the served claim.
+Descriptor/CCR/grant/source/catalog/target correspondence is retained end to end;
+Platform still independently compiles its owner Plan and grant from owner bytes.
+The mapping is evidence for that join, never a transferable bearer authorization.
+
+Consumption accepts only the module-created opaque prepared object in that same
+trusted owner process. It fixes source/config coordinates, reauthenticates using
+currently admitted responder keys, and rechecks claim/decision/descriptor/caller
+freshness before reading the CAS credential. Each object is reserved once before
+CAS/revalidation and cannot retry after any failure or unknown outcome. Its
+process-local guard complements Platform's durable cross-process journal; it is
+not a replacement for that journal or process isolation.
+
+The strict response path uses the existing CAS transport but rejects duplicate,
+nonfinite or excessively deep JSON, missing/mismatched approval and decision IDs,
+wrong digest/status, missing or nonboolean idempotence, historical idempotent
+success, verdict fields and absent/naive/invalid consumption timestamps. The
+consumption instant and post-CAS freshness must remain inside the original
+bounds. No field is coerced or filled from the submitted binding. A late/uncertain
+CAS may have occurred; it never licenses retry or protected native work.
+
+The returned immutable TaskExecutionConsumption contains exact approval_id,
+request_digest, decision_id, consumed_at and explicit false idempotent. It proves
+only the strict CAS response observed by Secrets. Platform must independently
+confirm original durable consumption using Approval's real provider client,
+journal it, then recheck its exact Plan, owner sources, grant and deadlines before
+any native operation. Neither a dataclass nor a caller-supplied boolean admits
+execution. Existing generic CLI CAS behavior is otherwise unchanged.
diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py
index bb1e766..1cf4140 100644
--- a/src/secrets_engine/approval_consume.py
+++ b/src/secrets_engine/approval_consume.py
@@ -23,7 +23,7 @@ from secrets_engine.execution_contract import ExecutionContract, require_executi
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
-from secrets_engine.decision_signature import load_trust
+from secrets_engine.decision_signature import load_trust, strict_json
from secrets_engine.authorization import (
build_action_request,
validate_decision_envelope,
@@ -70,6 +70,8 @@ class AuthorizedAction:
binding: ConsumeBinding
decision_id: str
expires_at: str
+ # Detached original served artifacts for the narrow task-execution handoff.
+ artifacts: str = field(default="", repr=False, compare=False)
@property
def id(self) -> str:
@@ -380,6 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
+ artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
+ sort_keys=True,separators=(",",":"),allow_nan=False),
)
@@ -391,6 +395,7 @@ def consume_approval(
binding: ConsumeBinding,
timeout_seconds: float = 3,
opener: Callable[..., Any] = credential_urlopen,
+ strict_response: bool = False,
) -> ConsumedApproval:
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
if not base_url or not base_url.startswith(("http://", "https://")):
@@ -438,8 +443,8 @@ def consume_approval(
if status != 200 or len(raw) > _MAX_BODY:
raise DecisionError(_status_message(status if status != 200 else 502))
try:
- payload = json.loads(raw.decode("utf-8"))
- except (UnicodeDecodeError, json.JSONDecodeError) as exc:
+ payload = (strict_json if strict_response else json.loads)(raw.decode("utf-8"))
+ except (UnicodeDecodeError, ValueError, RecursionError) as exc:
raise DecisionError("approval consume returned invalid JSON") from exc
if not isinstance(payload, dict):
raise DecisionError("approval consume returned invalid payload")
@@ -447,6 +452,19 @@ def consume_approval(
raise DecisionError("approval consumption was not confirmed")
if payload.get("request_digest") != binding.request_digest:
raise DecisionError("approval consume digest does not match the request")
+ if strict_response:
+ from datetime import datetime
+ if (payload.get("approval_id") != approval_id
+ or payload.get("decision_id") != binding.decision_id
+ or payload.get("idempotent") is not False
+ or any(key in payload for key in ("effect","allow","deny","decision"))):
+ raise DecisionError("strict task consumption correspondence refused")
+ try:
+ stamp = datetime.fromisoformat(payload["consumed_at"].replace("Z","+00:00"))
+ if stamp.tzinfo is None or stamp.utcoffset() is None:
+ raise ValueError("timezone required")
+ except (KeyError,TypeError,AttributeError,ValueError):
+ raise DecisionError("strict task consumption timestamp refused") from None
decision_id = payload.get("decision_id")
if decision_id is not None and (
not isinstance(decision_id, str) or not decision_id
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
new file mode 100644
index 0000000..9772861
--- /dev/null
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -0,0 +1,194 @@
+"""Narrow service-task PEP handoff; records never grant native execution.
+
+Platform calls preparation itself, binds its independently compiled owner Plan,
+reserves/journals once, then calls strict consumption and independent Approval
+GET confirmation. No inbound mapping/boolean or constructor creates authority.
+Actual policy/server input mapping and runtime entitlement remain unadmitted.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+import json
+from threading import Lock
+
+from secrets_engine.approval_auth import approval_token, credential_urlopen
+from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest, validate_approval_claim
+from secrets_engine.approval_consume import authorize_action, consume_approval
+from secrets_engine.application_time import read_window, validity_bounds
+from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest
+from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
+from secrets_engine.errors import DecisionError
+from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
+
+_CALLER = 'system:serviceaccount:secrets-engine:secrets-engine'
+_NATIVE_FIELDS = ('action','actor','principal','purpose','target')
+_COORDINATES = ('approval_url','approval_token_file','approval_client_secret_file','keycape_token_url','keycape_issuer',
+ 'pdp_url','pdp_token_file','pdp_trust_file','pdp_verifier','clock_trust_file',
+ 'authorization_subject_id','authorization_subject_type','authorization_policy_package','authorization_policy_version')
+_ISSUED = object()
+
+
+def _refuse():
+ raise DecisionError('strict task execution handoff correspondence refused')
+
+
+def _coordinates(cfg):
+ return tuple(str(getattr(cfg,name,'') or '') for name in _COORDINATES)
+
+
+def _stamp(value):
+ try:
+ parsed=datetime.fromisoformat(value.replace('Z','+00:00'))
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
+ _refuse()
+ return parsed
+ except (AttributeError,TypeError,ValueError):
+ _refuse()
+
+
+class TaskExecutionAuthorization:
+ """Opaque process-local prepared state; not a bearer credential or allow."""
+ __slots__=('_action','_contract','_coordinates','_handoff','_used','_lock')
+
+ def __init__(self, issuer=None, *, action=None, contract=None, coordinates=None, handoff=None):
+ if issuer is not _ISSUED:
+ _refuse()
+ object.__setattr__(self,'_action',action)
+ object.__setattr__(self,'_contract',contract)
+ object.__setattr__(self,'_coordinates',coordinates)
+ object.__setattr__(self,'_handoff',json.dumps(handoff,sort_keys=True,separators=(',',':'),allow_nan=False))
+ object.__setattr__(self,'_used',False)
+ object.__setattr__(self,'_lock',Lock())
+
+ def __setattr__(self,name,value):
+ raise AttributeError('prepared task handoff is immutable')
+
+ def as_owner_handoff(self):
+ return strict_json(self._handoff)
+
+
+@dataclass(frozen=True)
+class TaskExecutionConsumption:
+ approval_id: str
+ request_digest: str
+ decision_id: str
+ consumed_at: str
+ idempotent: bool
+
+
+def _validate(cfg, action, contract):
+ if type(contract) is not ExecutionContract or not action.artifacts:
+ _refuse()
+ descriptor=contract.as_dict()
+ artifacts=strict_json(action.artifacts)
+ request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
+ # Rotation/removal and installed-helper changes cannot hide behind a cached
+ # prepared label. Reauthenticate before each strict CAS, without networking.
+ authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
+ trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
+ verifier=getattr(cfg,'pdp_verifier',None))
+ if authenticated != envelope:
+ _refuse()
+ window=read_window(cfg)
+ lower,upper=validity_bounds(window=window)
+ validated=validate_decision_envelope(envelope,request,
+ accepted_policy_packages={str(cfg.authorization_policy_package)},
+ accepted_policy_versions={str(cfg.authorization_policy_version)},
+ expected_approval_binding_digest=claim['binding']['pdp_digest'],time_window=window)
+ validate_approval_claim(claim,approval_id=action.binding.approval_id,
+ expected_pdp_digest=envelope['binding']['approval_binding_digest'],
+ require_human_control=True,time_window=window)
+ require_execution_deadline(request,time_window=window,
+ bounds=(claim['validity']['expires_at'],envelope['lifetime']['expires_at']))
+ binding=envelope['binding']
+ if (envelope['lifetime'].get('kind')!='ttl'
+ or _stamp(envelope['lifetime'].get('not_before'))>lower):
+ _refuse()
+ if (request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
+ or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
+ or binding.get('context')!=request['context']
+ or request['context'].get('execution_contract')!=descriptor
+ or binding.get('request_digest')!=request_digest(binding)
+ or binding.get('approval_binding_digest')!=approval_binding_digest(binding)
+ or action.binding.request_digest!=validated.request_digest
+ or action.binding.decision_id!=validated.decision_id):
+ _refuse()
+ subject=binding['subject']; resource=binding['resource']
+ if (request['subject']!={'id':'secrets-engine','type':'service'}
+ or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
+ or subject.get('tenant') not in (None,'tenant:platform')
+ or 'principal' in subject.get('attributes',{})
+ or envelope['subject']!=subject or envelope['resource']!=resource
+ or descriptor['execution_actor']!='service:secrets-engine'
+ or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
+ or resource.get('tenant') not in (None,'tenant:platform')
+ or any(resource.get('attributes',{}).get(key)!=value for key,value in request['resource']['attributes'].items())
+ or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
+ _refuse()
+ caller=envelope['provenance'].get('caller')
+ if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
+ or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
+ or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
+ or upper >= _stamp(caller.get('not_after'))):
+ _refuse()
+ native=binding_from_check_request(binding)
+ supplied=claim['binding']
+ # Recompute the served native act too: Python equality alone conflates
+ # boolean/integer enrichment and cannot establish canonical correspondence.
+ if (any(supplied.get(key)!=native[key] for key in _NATIVE_FIELDS)
+ or supplied.get('digest')!=claim_binding_digest(**native)
+ or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
+ _refuse()
+ return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
+ 'request_digest':validated.request_digest,'decision_id':validated.decision_id,
+ 'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
+ 'submitted_request_digest':validated.submitted_request_digest,
+ 'decision_not_before':envelope['lifetime'].get('not_before') or '',
+ 'decision_expires_at':envelope['lifetime']['expires_at'],
+ 'approval_expires_at':claim['validity']['expires_at']}
+
+
+def authorize_task_execution(cfg, entry, *, execution_contract):
+ if type(execution_contract) is not ExecutionContract:
+ _refuse()
+ # No injected transports or policy callbacks: use the actual existing PIP/PDP
+ # clients and mandatory signature validators in the owner process.
+ action=authorize_action(cfg,entry,'execute-ccr',execution_contract=execution_contract)
+ if action is None:
+ _refuse()
+ try:
+ handoff=_validate(cfg,action,execution_contract)
+ return TaskExecutionAuthorization(_ISSUED,action=action,contract=ExecutionContract.parse(execution_contract.as_dict()),
+ coordinates=_coordinates(cfg),handoff=handoff)
+ except (ValueError,KeyError,TypeError,RecursionError) as exc:
+ raise DecisionError('strict task execution handoff malformed') from exc
+
+
+def consume_task_execution(cfg, authorization):
+ if type(authorization) is not TaskExecutionAuthorization:
+ _refuse()
+ with authorization._lock:
+ if authorization._used or _coordinates(cfg)!=authorization._coordinates:
+ _refuse()
+ # Reserve once before all work; failures/unknown outcomes never retry this
+ # prepared object. Platform's durable journal is the cross-process guard.
+ object.__setattr__(authorization,'_used',True)
+ try:
+ handoff=_validate(cfg,authorization._action,authorization._contract)
+ if handoff!=authorization.as_owner_handoff():
+ _refuse()
+ result=consume_approval(base_url=str(cfg.approval_url),
+ token_provider=lambda:approval_token(cfg,scope='approval:consume'),
+ binding=authorization._action.binding,opener=credential_urlopen,strict_response=True)
+ stamp=_stamp(result.consumed_at)
+ window=read_window(cfg);_,upper=validity_bounds(window=window)
+ if (stamp>upper or (handoff['decision_not_before'] and stamp<_stamp(handoff['decision_not_before']))
+ or any(stamp>=_stamp(value) for value in (handoff['decision_expires_at'],handoff['approval_expires_at'],
+ handoff['execution_contract']['operation_deadline']))):
+ _refuse()
+ # Late CAS completion is historical evidence only; refuse native dispatch.
+ _validate(cfg,authorization._action,authorization._contract)
+ return TaskExecutionConsumption(result.approval_id,result.request_digest,result.decision_id,result.consumed_at,result.idempotent)
+ except (ValueError,KeyError,TypeError,RecursionError) as exc:
+ raise DecisionError('strict task execution consumption malformed') from exc
diff --git a/tests/test_task_execution_handoff.py b/tests/test_task_execution_handoff.py
new file mode 100644
index 0000000..2204da2
--- /dev/null
+++ b/tests/test_task_execution_handoff.py
@@ -0,0 +1,231 @@
+"""Real local HTTP/producer signatures, synthetic approval/caller only.
+
+These source fixtures never enable Access's runtime-deny policy or admit a real
+caller, key, owner grant or native backend. Whole-provider integration is separate.
+"""
+import copy
+from datetime import datetime, timedelta, timezone
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+from pathlib import Path
+from threading import Thread
+
+import pytest
+
+from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest
+from secrets_engine.approval_consume import _expected_request
+from secrets_engine.authorization import request_digest, approval_binding_digest
+from secrets_engine.errors import DecisionError
+from secrets_engine.execution_contract import ExecutionContract
+from secrets_engine.task_execution_handoff import (authorize_task_execution,consume_task_execution,
+ TaskExecutionAuthorization)
+from tests.authorization_stub import AuthorizationStub
+from tests.signature_support import signature_tools, admit_tools, sign_envelope
+from tests.test_consume_binding_join import _Cfg,_token,APPROVAL_ID
+from tests.test_execution_context import entry,descriptor
+
+
+def stamp(moment):
+ return moment.isoformat()
+
+
+@pytest.fixture
+def lane(tmp_path,signature_tools):
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
+ admit_tools(cfg,tmp_path,signature_tools)
+ original=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
+ stub=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
+ stub.bind_request(original)
+ state={'calls':[],'claim_change':None,'decision_change':None,'response_change':None,'raw_response':None}
+ claim=stub.claim();native=binding_from_check_request(original)
+ claim['binding'].update(native,digest=claim_binding_digest(**native),human_control=True)
+ state['claim']=claim
+
+ class Handler(BaseHTTPRequestHandler):
+ def log_message(self,*_args):pass
+ def reply(self,payload):
+ data=json.dumps(payload).encode() if isinstance(payload,dict) else payload
+ self.send_response(200);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
+ def do_GET(self):
+ state['calls'].append('claim')
+ value=copy.deepcopy(state['claim'])
+ if state['claim_change']:state['claim_change'](value)
+ self.reply(value)
+ def do_POST(self):
+ submitted=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
+ if self.path=='/v1/check':
+ state['calls'].append('check')
+ envelope=stub.decision(submitted)
+ envelope['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'audience':'flex-auth','not_after':stamp(datetime.now(timezone.utc)+timedelta(minutes=20))}
+ if state['decision_change']:state['decision_change'](envelope)
+ self.reply(sign_envelope(envelope,signature_tools))
+ else:
+ state['calls'].append('consume')
+ payload={'approval_id':APPROVAL_ID,'status':'consumed','request_digest':submitted['request_digest'],
+ 'decision_id':submitted['decision_id'],'consumed_at':stamp(datetime.now(timezone.utc)),'idempotent':False}
+ if state['response_change']:state['response_change'](payload)
+ self.reply(state['raw_response'] if state['raw_response'] is not None else payload)
+ httpd=ThreadingHTTPServer(('127.0.0.1',0),Handler)
+ thread=Thread(target=httpd.serve_forever,kwargs={'poll_interval':0.01},daemon=True);thread.start()
+ cfg.approval_url=cfg.pdp_url=f'http://127.0.0.1:{httpd.server_port}'
+ try:yield cfg,e,contract,state
+ finally:httpd.shutdown();httpd.server_close();thread.join(2)
+
+
+def prepared(lane):
+ cfg,e,contract,state=lane
+ return authorize_task_execution(cfg,e,execution_contract=contract)
+
+
+def test_exact_preparation_then_one_strict_cas(lane):
+ cfg,e,contract,state=lane
+ result=prepared(lane)
+ assert state['calls']==['claim','check']
+ handoff=result.as_owner_handoff()
+ assert set(handoff)=={'execution_contract','approval_id','request_digest','decision_id','native_binding','approval_binding_digest',
+ 'submitted_request_digest','decision_not_before','decision_expires_at','approval_expires_at'}
+ assert handoff['execution_contract']==contract.as_dict()
+ assert handoff['native_binding']==binding_from_check_request(_expected_request(cfg,e,'execute-ccr',execution_contract=contract))
+ changed=result.as_owner_handoff();changed['execution_contract']['ccr_id']='changed'
+ assert result.as_owner_handoff()==handoff
+ with pytest.raises(AttributeError):result._handoff='changed'
+ receipt=consume_task_execution(cfg,result)
+ assert receipt.approval_id==handoff['approval_id'] and receipt.request_digest==handoff['request_digest']
+ assert receipt.decision_id==handoff['decision_id'] and receipt.idempotent is False
+ assert state['calls']==['claim','check','consume']
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+@pytest.mark.parametrize('field',['action','actor','principal','purpose','target','digest'])
+def test_changed_native_claim_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(claim):claim['binding'][field]={'id':'other'} if field=='target' else 'other'
+ state['claim_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['mode','principal','audience','not_after'])
+def test_signed_wrong_caller_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda envelope:envelope['provenance']['caller'].__setitem__(field,'wrong')
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['execution_actor','execution_grant_ccr_id','grant_immutable_sha256','ccr_id','backend_origin'])
+def test_signed_changed_descriptor_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(envelope):envelope['binding']['context']['execution_contract'][field]='wrong'
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['subject','resource','request_digest','submitted_request_digest','approval_binding_digest'])
+def test_signed_changed_request_correspondence_refuses_before_cas(lane,field):
+ cfg,e,contract,state=lane
+ def change(envelope):
+ envelope['binding'][field]={'id':'other'} if field in ('subject','resource') else 'sha256:'+'0'*64
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('field',['approval_id','decision_id','request_digest','status','consumed_at','idempotent'])
+def test_cas_correspondence_never_coerces_or_falls_back(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.__setitem__(field,'wrong')
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+@pytest.mark.parametrize('value',[True,1,0,'false',None])
+def test_nonfresh_or_nonboolean_idempotence_refuses(lane,value):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.__setitem__('idempotent',value)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+
+
+@pytest.mark.parametrize('body',[b'{"status":"consumed","status":"consumed"}',b'{"status":NaN}',b'['*2000+b'0'+b']'*2000])
+def test_duplicate_nonfinite_or_deep_consume_body_refuses(lane,body):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['raw_response']=body
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+
+
+@pytest.mark.parametrize('field',['pdp_url','approval_url','authorization_subject_id','authorization_policy_version','pdp_verifier'])
+def test_changed_owner_coordinates_refuse_before_cas(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane);setattr(cfg,field,'changed')
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+def test_removed_signer_refuses_before_cas(lane):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ trust=json.loads(cfg.pdp_trust_file.read_text());trust['keys']={'other':next(iter(trust['keys'].values()))}
+ cfg.pdp_trust_file.write_text(json.dumps(trust))
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+def test_mapping_boolean_and_public_constructor_are_not_authority(lane):
+ cfg,e,contract,state=lane
+ for value in [True,{},object()]:
+ with pytest.raises(DecisionError):consume_task_execution(cfg,value)
+ with pytest.raises(DecisionError):TaskExecutionAuthorization()
+ assert not state['calls']
+
+
+@pytest.mark.parametrize('field',['approval_id','decision_id','consumed_at','idempotent'])
+def test_absent_cas_fields_cannot_fall_back(lane,field):
+ cfg,e,contract,state=lane;result=prepared(lane)
+ state['response_change']=lambda payload:payload.pop(field)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls'].count('consume')==1
+
+
+def test_expired_preparation_refuses_before_cas(lane,monkeypatch):
+ from types import SimpleNamespace
+ import secrets_engine.task_execution_handoff as module
+ cfg,e,contract,state=lane;result=prepared(lane)
+ later=int((datetime.now(timezone.utc)+timedelta(minutes=2)).timestamp()*1_000_000_000)
+ monkeypatch.setattr(module,'read_window',lambda cfg:SimpleNamespace(lower_ns=later,upper_ns=later))
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('change',['deny','expired','missing-caller','missing-start'])
+def test_signed_decision_missing_authority_refuses_before_cas(lane,change):
+ cfg,e,contract,state=lane
+ def alter(envelope):
+ if change=='deny':envelope['effect']='deny'
+ elif change=='expired':envelope['lifetime']['expires_at']=stamp(datetime.now(timezone.utc)-timedelta(seconds=1))
+ elif change=='missing-start':envelope['lifetime'].pop('not_before')
+ else:envelope['provenance'].pop('caller')
+ state['decision_change']=alter
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+def test_native_binding_equality_does_not_coerce_boolean_enrichment(lane):
+ cfg,e,contract,state=lane
+ evaluated=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
+ evaluated['resource']['attributes']['synthetic_rank']=1
+ native=binding_from_check_request(evaluated)
+ state['claim']['binding'].update(copy.deepcopy(native),digest=claim_binding_digest(**native),
+ pdp_digest=approval_binding_digest(evaluated))
+ # Python dict equality treats True == 1. This malformed provider fact must
+ # refuse even when its reported native digest names the integer counterpart.
+ state['claim']['binding']['target']['attributes']['synthetic_rank']=True
+ def enrich(envelope):
+ envelope['resource']['attributes']['synthetic_rank']=1
+ envelope['binding']['request_digest']=request_digest(envelope['binding'])
+ envelope['binding']['approval_binding_digest']=approval_binding_digest(envelope['binding'])
+ state['decision_change']=enrich
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
diff --git a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
index 01be511..a8a3845 100644
--- a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
+++ b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
@@ -9,7 +9,7 @@ flavor: planning
owner: codex
topic_slug: netkingdom
created: "2026-10-11"
-updated: "2026-10-11"
+updated: "2026-10-16"
depends_on: []
state_hub_workstream_id: "5e38aecc-85ca-5f55-94c6-9fbb08604819"
---
@@ -110,3 +110,17 @@ This is source preparation awaiting independent technical review/publication,
not a formal owner assent, policy deployment or native execution milestone. T01
remains progress; T03 and SECRETS-WP-0008-T06 remain open. Detailed limitations
and retained failed test observations are in the source preparation receipt.
+
+
+2026-10-16 bounded source handoff preparation: `task_execution_handoff.py` uses
+the actual serving approval/mandatory signed Check path and retained detached
+artifacts to check exact descriptor/request/native approval/caller/lifetime joins,
+then performs one strict exact-response CAS only after Platform journal admission.
+It accepts no inbound boolean/mapping/authorizer callback and grants no native
+execution. Platform independently confirms durable consumption with the actual
+Approval provider client before native dispatch. Focused local HTTP/native Go
+producer-signature source checks cover altered joins/caller/grant, missing fields,
+coercion/idempotence, expiry, signer removal and replay refusal. No service binding
+propagation, policy gate flip, identity mapping admission or production trust/key
+change was made. Independent source integration/review and actual whole-provider
+proof remain required. T01/T03 and WP8-T06 retain their open native obligations.

View file

@ -0,0 +1,71 @@
{
"schema": "netkingdom.source-integration.v1",
"owner": "secrets-engine",
"scope": "strict service-task authorization and consumption owner handoff; no native dispatch",
"base_commit": "c80a944105a00220cd3c1a007709e72d830dd08d",
"patch": "implementation-r1.patch",
"patch_sha256": "90e37bf0e53abd361f210b5b4a56e7ab6c507f96aba5a2e4147c61c583ece810",
"source_checkout": "/tmp/netkingdom-secrets-task-handoff",
"files": {
"docs/task-execution-handoff.md": "d60949668c74ef6b317f44c16e22336baf76989b4a063fe10e9831c2f9c127a4",
"src/secrets_engine/approval_consume.py": "bea80a503944b1b9796819cfea5f514c3184a724115130d16c3bc0c675f09a6b",
"src/secrets_engine/task_execution_handoff.py": "671a393aa2b9355e1c81c502a74f9ce8adad75fe7eb03492d11843a94b24d758",
"tests/test_task_execution_handoff.py": "20e7929a67dcfeb5283f5fcad60e4c972c170b82678c32aaa991cd4dcb3033be",
"workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md": "c061dbc1c2c23dc3c79c3de78d1eed6fd57af05b078c8eb42b645813552a129b"
},
"verification": {
"focused": "246 passed in 19.18s",
"full": "736 passed, 16 failed, 1 skipped in 47.37s",
"baseline_comparison": "all16 exact failure IDs match unchanged owner baseline; zero new failure IDs",
"baseline_failure_ids": [
"tests/test_exec_owner.py::test_actual_bound_child_gets_only_selected_secret_and_fixed_environment",
"tests/test_exec_owner.py::test_binding_changed_after_approval_refuses_before_fetch",
"tests/test_exec_owner.py::test_config_changed_during_fetch_does_not_launch",
"tests/test_exec_owner_companions.py::test_companion_fetch_failure_starts_no_child",
"tests/test_exec_owner_companions.py::test_companion_value_reaches_bound_child_and_is_redacted",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[consent]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[env]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[extra]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[field]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[lane]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[missing]",
"tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[stage]",
"tests/test_exec_owner_companions.py::test_each_lane_is_gated_separately_before_backend",
"tests/test_exec_owner_companions.py::test_unresolvable_companion_refuses_before_any_gate",
"tests/test_human_control.py::test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child",
"tests/test_integration_companions.py::test_two_lanes_reach_one_bound_owner"
],
"scanner": {
"blocked_blob_ids": [],
"scanned_blobs": 5,
"scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509",
"scanner_version": "8.30.1",
"scope": "changed-staged-blobs",
"status": "clean"
},
"whitespace": "pass"
},
"interface": {
"module": "secrets_engine.task_execution_handoff",
"prepare": "authorize_task_execution(cfg, entry, *, execution_contract)",
"owner_record": "authorization.as_owner_handoff()",
"consume": "consume_task_execution(cfg, authorization)",
"result_fields": [
"approval_id",
"request_digest",
"decision_id",
"consumed_at",
"idempotent"
]
},
"native_acceptance": false,
"policy_or_mapping_admission": false,
"production_credentials_or_trust_change": false,
"canonical_owner_modified": false,
"limitations": [
"Source-only exact proposed service caller mapping; Access runtime deny gates unchanged",
"Opaque prepared object is process-local; Platform durable journal and independent real Approval GET confirmation remain mandatory",
"Actual Access-serve/Approval-provider whole-stack fixture is separately owned by Platform worker",
"No service_actor_binding request propagation or new live trust/grant materialization"
]
}

View file

@ -0,0 +1,96 @@
diff --git a/.custodian-brief.md b/.custodian-brief.md
index 0cb912a..ba794c8 100644
--- a/.custodian-brief.md
+++ b/.custodian-brief.md
@@ -2,7 +2,7 @@
# Custodian Brief — secrets-engine
**Domain:** infotech
-**Last synced:** 2026-10-11 17:52 UTC
+**Last synced:** 2026-10-16 05:55 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
@@ -11,8 +11,24 @@
Progress: 1/3 done | workplan_id: `5e38aecc-85ca-5f55-94c6-9fbb08604819`
**Open tasks:**
+- ! Define the exact mediated execution contract `b2f0d462`
+ *(wait: Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred.)*
- ! Implement and prove one native metadata action `5f8f7c41`
-- ► Define the exact mediated execution contract `b2f0d462`
+
+### Constrain PDP transport and authenticate decision responses
+Progress: 1/3 done | workplan_id: `7252469d-d7a5-5c18-8911-b511b8c6a736`
+
+**Open tasks:**
+- ! Verify the admitted native consumer and rotation path `85aa4c78`
+ *(wait: Requires reviewed verification-key admission, source enforcement and exact native rollout receipt; no production trust change is proposed yet.)*
+- ► Implement independently anchored responder authentication `877782e5`
+
+### Production-safe provisioning, authorization, and lifecycle hardening
+Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
+
+**Open tasks:**
+- ! Resume native production lane adoption `a0a1dd92`
+ *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
### Activate native Claude credential delivery for Glas
Progress: 2/3 done | workplan_id: `40ccc3b4-d046-5a58-8649-e7935f45c974`
@@ -26,27 +42,20 @@ Progress: 4/6 done | workplan_id: `31f7f8ea-7f73-516c-8877-f03a13f1db82`
**Open tasks:**
- ! Stage live apply and verification `fb103f1e`
- *(wait: OpenRouter key-check native acceptance is recorded in SECRETS-WP-0010-T03. Other lanes still need exact approvals, scoped attended apply and per-lane positive/negative/health/revocation evidence.)*
+ *(wait: Owner work first: publish a reviewed scoped-apply command per lane. Per-lane approvals are no longer needed (D1 standing rule); each lane with a command enters the attended window.)*
- ! Reconcile routing ownership and retire interim proxies `1431edae`
*(wait: Needs owner-agreed routing/proxy retirement for each verified lane and custody disposition of the legacy npm pointer; a single approved OpenRouter key-check does not authorize broader routing cutover.)*
-### Production-safe provisioning, authorization, and lifecycle hardening
-Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
-
-**Open tasks:**
-- ! Resume native production lane adoption `a0a1dd92`
- *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
-
### Evolve the Lifecycle engine to the accepted security layer model
Progress: 5/6 done | workplan_id: `9c9e5164-b2f5-5ea2-a557-5368d65e9fe0`
**Open tasks:**
- ! No standing engine credential `d7bc8bdc`
- *(wait: Positive service-JWT login proof passed 2026-10-10 (SECRETS-WP-0012-T03, docs/evidence/2026-10-10-keycape-openbao-probe.json). Waits on railiance-platform closing RPF-WP-0035-T02 with the CCR-2026-0038 negative checks, then on routing live engine actions through service-jwt instead of bootstrap/env.)*
+ *(wait: Published JWT contract and positive login/revocation are delivered. Native KEY-WP-0039-T02 claim negatives, CCR-2026-0038 effective-policy/capability/expiry/use checks and MASON-WP-0009-T05 remain. Login-self-only grants no protected-operation authority: agree and implement separately reviewed task-scoped backend execution entitlement, then prove functional steady-state service-identity adoption without bootstrap/env fallback.)*
## Inbox Hygiene
-**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start.
+**Stale unread:** 3 message(s) older than 3 day(s) — triage at session start.
**Missing thread_id:** 2 unread message(s) lack supersession chains.
---
diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 70e5831..4d1cfb6 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -22,6 +22,7 @@
| workplan | SECRETS-WP-0011 | finished | — | workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md |
| workplan | SECRETS-WP-0012 | finished | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| workplan | SECRETS-WP-0013 | proposed | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| workplan | SECRETS-WP-0014 | active | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| task | SECRETS-WP-ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md |
| task | SECRETS-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md |
| task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
@@ -83,9 +84,12 @@
| task | SECRETS-WP-0012-T02 | cancel | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T03 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T04 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
-| task | SECRETS-WP-0013-T01 | progress | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0013-T01 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T02 | done | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T03 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0014-T01 | done | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T02 | progress | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T03 | wait | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| intake | SECRETS-IN-0001 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0002 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0003 | closed | — | intakes/intakes.md |

View file

@ -0,0 +1,40 @@
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -134,8 +134,11 @@
_refuse()
native=binding_from_check_request(binding)
supplied=claim['binding']
+ # Recompute the served native act too: Python equality alone conflates
+ # boolean/integer enrichment and cannot establish canonical correspondence.
if (any(supplied.get(key)!=native[key] for key in _NATIVE_FIELDS)
- or supplied.get('digest')!=claim_binding_digest(**native)):
+ or supplied.get('digest')!=claim_binding_digest(**native)
+ or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
--- a/tests/test_task_execution_handoff.py
+++ b/tests/test_task_execution_handoff.py
@@ -210,3 +210,22 @@
state['decision_change']=alter
with pytest.raises(DecisionError):prepared(lane)
assert 'consume' not in state['calls']
+
+
+def test_native_binding_equality_does_not_coerce_boolean_enrichment(lane):
+ cfg,e,contract,state=lane
+ evaluated=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
+ evaluated['resource']['attributes']['synthetic_rank']=1
+ native=binding_from_check_request(evaluated)
+ state['claim']['binding'].update(copy.deepcopy(native),digest=claim_binding_digest(**native),
+ pdp_digest=approval_binding_digest(evaluated))
+ # Python dict equality treats True == 1. This malformed provider fact must
+ # refuse even when its reported native digest names the integer counterpart.
+ state['claim']['binding']['target']['attributes']['synthetic_rank']=True
+ def enrich(envelope):
+ envelope['resource']['attributes']['synthetic_rank']=1
+ envelope['binding']['request_digest']=request_digest(envelope['binding'])
+ envelope['binding']['approval_binding_digest']=approval_binding_digest(envelope['binding'])
+ state['decision_change']=enrich
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']

View file

@ -0,0 +1 @@
{"blocked_blob_ids": [], "scanned_blobs": 5, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}

View file

@ -0,0 +1 @@
{"blocked_blob_ids": [], "scanned_blobs": 5, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}

View file

@ -0,0 +1,6 @@
{
"canonical_base": "c80a944105a00220cd3c1a007709e72d830dd08d",
"canonical_status": "M .custodian-brief.md\n M WORK-RECORDS.md\n",
"dirty_metadata_patch_sha256": "a5ef64f28f7c30e7b71a8f26160e820dfa809407d8cc21336032c61fed70c4c5",
"dirty_metadata_excluded_from_implementation": true
}

View file

@ -0,0 +1,25 @@
# Secrets native CLI admission preparation
The six-file source patch is preparation and documentation only. Review exact
base/files/hash in `integration-manifest-r1.json`; canonical owner is unchanged.
The CLI runtime AST differs only by a corrected explanatory docstring.
The proposal names11 serving authorization call sites across10handlers (including
exec companion delegation), one shared signed Check client, and0/11 current native
effect acceptance. Session-revoke, service-auth identity and route ambient-readiness
paths are explicit exclusions. Generic CLI CAS is not silently relabeled strict
R2 task consumption. There is no execute-ccr CLI command or admitted task placement.
A fresh bundle was built offline from a separate CLEAN canonical ff1a13d checkout,
which includes packaging and strictR2. Its source/artifact hashes and exact manifest
are retained; c80 proof bundles remain intermediate evidence only. No installation
occurred. Actual signer kid/public key/fingerprint/custody inputs stay null; no
synthetic key filled them. The proposed lifecycle trust template's empty key map
was proved to refuse before credential/network access. Scanner is clean6 blobs.
Access/Platform/Approval owner-return artifacts are referenced with hashes. Native
caller/key/pin/package placement, source/provider/audit receipts and actual approved
CCR/catalog/grant/expectation inputs remain explicit. Future B2 out-of-band fresh
Approval facts require owner-reviewed correspondence while preserving original
context; no equality relaxation, digest translation or new schema/helper was added.
Root owns integration/consistency and any separately authorized later admission.

View file

@ -0,0 +1 @@
{"bundle": "/tmp/secrets-cli-ff1-native-proof", "helper_sha256": "a6e4eeb5361c61a2db5526ba5648ef5e48143c1a453e158d2f62991ba7ba3a33", "manifest_sha256": "ee07c0b8755788ad0894c679ee2ca125a1c24df3a895013755d4a965a879a1ec"}

View file

@ -0,0 +1,19 @@
# Secrets consumer admission proposal R1
This packet recommends exact consumer choices under SECRETS-WP-0014-T02/T03. It prepares the existing workstation CLI; it creates no workload, service, launcher or credential provider. Nothing here is installed or admitted. Protected effect acceptance remains **0/11 authorization sites across10 handlers**. Existing WP13/task/native Plan/grant obligations remain separate.
Recommend `worsch`, UID/GID1000, on the existing local workstation. `id worsch` confirms that account. Use the fresh immutable release prefix `/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03` and only its `bin/secrets-engine` CLI entrypoint. Install only after owner selection of the existing offline bundle/manifest and new parent/prefix; the reviewed installer refuses an existing prefix. The fixed helper is `libexec/secrets-engine-pdp-verify`, the offline receipt interpreter is `venv/bin/python`. The current editable checkout, internal venv CLI and rolling aliases are not admitted effect entrypoints. Same-UID code isolation is not established by0700 directories or checksums.
Recommend the exact pre-key config parent `/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1`, evidence parent `/home/worsch/.local/state/secrets-engine/native-lifecycle/lifecycle-20261016-r1`, and per-session caller directory `/run/user/1000/secrets-engine/lifecycle-20261016-r1`. Runtime/config/state directories are owner-controlled0700, files0600. No such proposed path was created by this preparation. `configuration/` contains public metadata copied from exact published runtime4dd004a, the existing total stance, empty-key trust and `environment.proposed.json`. That JSON documents the existing Config.load environment; it is not a new loader. No cwd fallback, ambient secrets, optional clock extra, Approval credential, unsafe-demo or backend provider is selected. Empty BAO/VAULT and Approval endpoints deliberately leave backend/consume unavailable in this stage. No catalog row action is granted by pinning its public metadata.
`consumer-baseline.json` is the exact pre-generation public bundle/config/verifier/policy baseline. Its SHA256 is the proposed consumer_trust_sha256 and bindings.trust_sha256 in a future owner-reviewed Platform packet. It pins the public empty-key trust file, not a future key or online trust. Artifact integrity is not independent package admission: the Secrets owner/founder must independently select these exact bytes and control their delivery/prefix. The producer OCI image is deliberately a separate Platform/Access packet binding. P1 currently records old candidate0a/failed CI400; root's successor image needs a separately frozen packet revision and exact review, not reuse of old approvals.
The installed4dd bundle's real authorize_action rejected this exact empty-key trust before credential/backend access in a disposable mapping with a minimal environment. No custom opener/authorizer or synthetic signer was used. `validation.json` preserves the initial harness assertion correction. Existing root-verified reproducible bundle/install tests remain independent evidence; no new build or dependency download was needed.
After custody, use a new configuration revision at `/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-post-custody-r1` with `trust/lifecycle.json`. Authenticate the Platform public return independently, match exact source/target/receipt lineage, proposed kid, canonical public key and fingerprint, then independently admit the final public trust/config bytes and their new digest. Fingerprint is SHA256 of canonical43ASCII base64url public key, not raw32 and not owner authentication. Null-public/metadata-only/current-absence/unstored-seed recovery cannot admit a key. Do not overwrite the pre-key revision or use a responder key endpoint/private custody read to fill it.
`transport-proposal.json` makes the existing local transport choice concrete: primary/railiance01 owner-verified cluster, exact owner-returned named pod in flex-auth, literal127.0.0.1:18081 to8080, no Service/label-first selection or automatic port substitution. The cluster/Access owner must independently verify pod UID/image/startup signer projection after separately admitted rollout. Stop only the owned port-forward PID when capture ends. Local UID1000 is not the ServiceAccount: a separately authorized existing TokenRequest delivery for secrets-engine/secrets-engine, audienceflex-auth and proposed600second lifetime must place the bounded caller file0600 in the private runtime directory. No caller file was inspected/minted, no token delivery actor selected, no RBAC grant proposed. Record public custody/expiry metadata only. The capture operation and source-record export still need exact Access-owner scope; the prior apply/destroy whynot request pair remains a proposal requiring disposition.
Stage1 uses Access's existing `tools/verify_native_signer_receipt.py` at the exact pinned source/hash with the installed bundle Python/helper. It consumes captured public requests/responses/source records and admitted trust offline; it has no token/network/backend inputs. Do not turn that Python tool into a second effect entrypoint. Approval/client-secret/backend/caller effect grants are unnecessary for this offline proof and remain outside scope. Before any eleven-site effect acceptance, independently select actual handler/catalog scope, reviewed Approval delivery/CAS contract, supported backend and effect/readback/cleanup acceptance; service-jwt protected execution still refuses, and switching to bootstrap/env is no workaround.
Remaining owner decisions are bounded: select this exact baseline/release/config and install custody; supply independently authenticated public signer return and final trust successor; select exact successful producer image/pod/projection/runtime evidence; authorize bounded caller delivery and signer-only request/source-record capture. None is replaced by this source review. Native effect grants/Approval/backend/Plan remain separate. Preserve signed refusal on outage, unknown/removed key, changed helper, wrong pin and unsigned rollback; never blind retry a consume.

View file

@ -0,0 +1,63 @@
id: activity-core-metered-worker-token
kind: kv
org: coulomb
repo: activity-core
stage: prod
description: Activity Core queue token for rein-aharness-metered@railiance01,
delivered only as a companion of the Glas metered owner. Custody and issuance
are activity-core's (ACTIVITY-WP-0039); secrets-engine only reads.
mount: platform
path: workloads/activity-core/ops-run-workers/rein-aharness-metered-railiance01
mount_management: existing
fields:
- token
consumers:
- name: rein-aharness-metered-railiance01
auth: approle
claim: catalog:activity-core-metered-worker-token
purpose: Claim the admitted hfact-metered ops_run as rein-aharness-metered@railiance01
inside the catalog-bound Glas metered owner
workload_delivery: []
delivery_config:
companion_of:
- glas-claude-agent-dev-anthropic
delivery_modes:
- exec-env
delivery_auth:
method: approle
management: engine
policy_name: se-prod-activity-core-metered-worker-token
role_name: se-prod-activity-core-metered-worker-token
metadata_read: false
token_ttl: 5m
token_max_ttl: 15m
secret_id_ttl: 5m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: decision
decision_ref: ACTIVITY-WP-0039
notes: Ordinary lane approval, no human control (operator decision 2026-09-23,
SECRETS-WP-0011). Native apply and exec still need per-lane claim, PDP decision
and consume. This entry is not authorization.
verification:
positive: Exact scoped AppRole reads only token, delivered as ACTIVITY_CORE_WORKER_TOKEN
into the bound Glas metered owner.
negative: The claim-loop worker path, sibling KV, metadata, listing and writes denied;
the Glas lane AppRole cannot read this path.
risk:
classification: standard
notes: Lets the holder claim, heartbeat and close ops_runs as the metered identity
only. No provider spend by itself.
rotation:
owner: activity-core
expectation: Mint a new value at the same path via ACTIVITY-WP-0039 procedure; ESO
resyncs actcore-runtime-secret; next exec reads the new value.
ttl: owner-defined
deactivation:
owner: activity-core
expectation: Remove the identity from ACTIVITY_CORE_WORKERS and the path; revoke
the se-prod AppRole and policy.
audit:
evidence: Lane id, companion primary, actor, exact path, field name, timestamps,
and pass/fail only

View file

@ -0,0 +1,78 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0010.
id: email-connect-transactional
kind: kv
org: coulomb
repo: email-connect
stage: prod
description: >-
IONOS transactional SMTP identity and the shared user-engine ingest bearer for
email-connect. user-engine must never receive the SMTP fields.
mount: platform
path: workloads/email-connect/transactional
mount_management: existing
fields:
- EMAIL_CONNECT_INGEST_TOKEN
- EMAIL_CONNECT_SMTP_USERNAME
- EMAIL_CONNECT_SMTP_PASSWORD
consumers:
- name: email-connect-runtime
auth: token
claim: clustersecretstore:openbao-email-connect
purpose: sync email-connect-runtime through the namespace-limited ESO store
- name: user-engine-ingest-caller
auth: shared-bearer-copy
claim: service:user-engine
purpose: consume only EMAIL_CONNECT_INGEST_TOKEN from its separately managed runtime Secret
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:email-connect-transactional
purpose: approved SMTP/ingest smoke in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + email-connect
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-email-connect-transactional
role_name: se-prod-email-connect-transactional
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0010
notes: >-
CCR-2026-0010 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read each declared field without printing it
negative: unrelated token is denied on platform/data/workloads/email-connect/transactional
risk:
classification: high
notes: SMTP credentials can send as the platform identity and the ingest bearer authorizes transactional requests
rotation:
owner: railiance-platform + email-connect + user-engine
expectation: >-
Rotate overlap-first, wait for ESO refresh, roll email-connect and user-engine
when the bearer changes, verify health, then revoke the old provider password/bearer.
ttl: provider-defined
deactivation:
owner: railiance-platform + email-connect
expectation: >-
Disable the front door, detach ESO/native read access, remove materialized
Secrets, stop email-connect, and revoke the IONOS app password if applicable.
audit:
evidence: CCR id, actor, exact path, field names, ESO refresh timestamp, request ids, and pass/fail only

View file

@ -0,0 +1,44 @@
# Example BUILD-stage lane. Demonstrates that build entries can be looser:
# generated test values are allowed and no production decision is required.
id: example-build-test-token
org: coulomb
repo: platform-ci
stage: build
description: >-
Throwaway generated credential for build-stage integration tests. May be
generated locally; must never be reused in test or prod.
mount: secret
path: build/example/test-token
fields:
- api_token
consumers:
- name: build-runner
auth: approle
claim: "role:build-runner"
purpose: "exercise build-stage integration tests"
delivery_modes:
- exec-env
- read-check
# Build stage permits bootstrap-only / generated values without a prod decision.
approval:
model: bootstrap-only
notes: "Build stage: generated test secret, no production decision required."
verification:
positive: "build-runner token can read the generated value"
negative: "prod consumers cannot read build paths"
rotation:
expectation: "regenerate per run"
ttl: "1h"
deactivation:
expectation: "delete on build teardown"
audit:
evidence: "actor, path, timestamp, result — no secret value"

View file

@ -0,0 +1,77 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0006.
id: forgejo-admin-api-token
kind: kv
org: coulomb
repo: railiance-platform
stage: prod
description: >-
Forgejo site-admin PAT and non-secret companion metadata for attended operator
and automation tooling. This lane is distinct from Forgejo SMTP credentials.
mount: platform
path: workloads/forgejo/forgejo-admin
mount_management: existing
fields:
- API_TOKEN
- API_USER
- API_BASE_URL
- TOKEN_SCOPES
- GENERATED_AT
consumers:
- name: forgejo-platform-operators
auth: oidc
claim: group:net-kingdom-admins
purpose: existing attended reads through forgejo-admin-workload-kv-read
- name: forgejo-automation
auth: approle
claim: catalog:forgejo-admin-api-token
purpose: approved package, repository, webhook, and bootstrap commands in a child process
workload_delivery:
- mode: operator-oidc
owner: railiance-platform
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
# Avoid broad-admin name guards while retaining an exact-path read policy.
policy_name: se-prod-forgejo-operator-api-token
role_name: se-prod-forgejo-operator-api-token
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0006
notes: >-
CCR-2026-0006 approves the existing OIDC/operator lane. A live native
AppRole apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read declared fields and an exec smoke authenticates to Forgejo
negative: unrelated token is denied on platform/data/workloads/forgejo/forgejo-admin
risk:
classification: high
notes: API_TOKEN is a Forgejo site-admin PAT with package, repository, webhook, and operator impact
rotation:
owner: railiance-platform + Forgejo operator
expectation: >-
Mint a replacement PAT as the site-admin account, update OpenBao, verify all
consumers through exec delivery, then delete the old PAT in Forgejo.
ttl: provider-defined
deactivation:
owner: railiance-platform + Forgejo operator
expectation: >-
Disable the front door, detach native/OIDC read access, revoke the PAT in
Forgejo, and preserve package/webhook audit evidence.
audit:
evidence: CCR id, actor, exact path, field names, Forgejo token identifier/scopes if non-secret, timestamps, and pass/fail only

View file

@ -0,0 +1,104 @@
id: glas-claude-agent-dev-anthropic
kind: kv
org: coulomb
repo: sand-boxer
stage: prod
description: Catalog-bound metered owner with Activity Core worker-token companion.
The 2026-09-27 corrected owner pin requires reviewed host installation; the old
200k/32k owner is not admitted by this catalog. Native activation remains SECRETS-WP-0009-T03.
mount: platform
path: workloads/glas-harness/claude-agent-dev
mount_management: existing
fields:
- ANTHROPIC_API_KEY
consumers:
- name: sand-boxer-glas-agent-dev
auth: approle
claim: catalog:glas-claude-agent-dev-anthropic
purpose: Owner-admitted glas-harness agt run through the reviewed local profile;
no caller-facing key fetch
workload_delivery: []
delivery_config:
exec_owner:
status: configured
owner: rein-aharness MessagesOwner (metered-once) with sand-boxer runtime boundary
command:
- /home/tegwick/.helixforge-factory/runtimes/b6e4e8a429393d68831c996a65c0489664205df969ac9882e581983ec2da4969/bin/python3
- -I
- -B
- -m
- rein_aharness.cli
- metered-once
- --owner-config
- /home/tegwick/hfact/owner-metered/owner.json
cwd: /home/tegwick/hfact/owner-metered
environment:
PATH: /usr/bin:/bin
LANG: C.UTF-8
HOME: /home/tegwick
ACTIVITY_CORE_URL: http://127.0.0.1:8010
AGENT_HARNESS_WORKER_ID: rein-aharness-metered@railiance01
AGENT_HARNESS_OPS_LABELS: hfact-metered
AGENT_HARNESS_OPS_LABELS_MODE: all
AGENT_HARNESS_EXECUTION_PROJECT: prj-helixforge-factory
AGENT_HARNESS_REQUIRE_SPEND_ADMISSION: '1'
AGENT_HARNESS_REQUIRE_REQUEST_ADMISSION: '1'
AGENT_HARNESS_SPEND_POLICY: /home/tegwick/hfact/owner-metered/spend-policy.json
AGENT_HARNESS_SPEND_LEDGER: /home/tegwick/hfact/owner-metered/spend.sqlite3
AGENT_HARNESS_REPO_MAP: '{"hfact-glas-proof":"/home/tegwick/hfact/targets/hfact-glas-proof"}'
files:
/home/tegwick/.helixforge-factory/runtimes/b6e4e8a429393d68831c996a65c0489664205df969ac9882e581983ec2da4969/bin/python3:
sha256: e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f
private: false
/home/tegwick/hfact/owner-metered/owner.json:
sha256: e0d3fb84649fdca302eccd9415f3cc2beaee84bf07bd83205cdffbe93e5573bc
private: true
/home/tegwick/hfact/owner-metered/spend-policy.json:
sha256: f31c585916de1ea8bd8e48c72421803dfde1015e6201704b9320f77d2c545d9c
private: true
companions:
- catalog: activity-core-metered-worker-token
field: token
env: ACTIVITY_CORE_WORKER_TOKEN
delivery_modes:
- exec-env
- read-check
delivery_auth:
method: approle
management: engine
policy_name: se-prod-glas-claude-agent-dev-anthropic
role_name: se-prod-glas-claude-agent-dev-anthropic
metadata_read: false
token_ttl: 5m
token_max_ttl: 15m
secret_id_ttl: 5m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
human_control: true
decision_ref: CCR-2026-0016
notes: Custody only has been completed. Native apply and exec require durable exact-action
authorization, engine consume, scoped backend authority and verified delivery
state. This entry is not authorization.
verification:
positive: Exact scoped AppRole reads only ANTHROPIC_API_KEY into the approved child;
owner binding and redaction pass.
negative: Wrong owner profile/project/actor, direct caller fetch, sibling KV, metadata,
listing and writes denied.
risk:
classification: high
notes: API spend; provider expiry 2027-01-31T21:00:00Z is not enforced by Bao token
TTL. Workspace scope and budget unverified.
rotation:
owner: railiance-platform + sand-boxer
expectation: Provider replacement, versioned CAS custody, stop old runs, verify
replacement then revoke predecessor at Anthropic and prove denial.
ttl: provider-defined
deactivation:
owner: railiance-platform + sand-boxer
expectation: Disable lane, stop affected runs, revoke Bao sessions and provider
key. Preserve custody history.
audit:
evidence: CCR id, actor, exact path, field name, provider key identifier if non-secret,
timestamps, and pass/fail only

View file

@ -0,0 +1,78 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0002.
id: issue-core-ingestion-api-key
kind: kv
org: coulomb
repo: issue-core
stage: prod
description: >-
Runtime credentials for issue-core ingestion and its Forgejo backend. The
existing workload path is delivered by ESO; native secrets-engine exec is a
separate exact-path AppRole proposed for attended operator/agent use.
mount: platform
path: workloads/issue-core/issue-core/issue-core-runtime
mount_management: existing
fields:
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
consumers:
- name: issue-core-runtime
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync the issue-core runtime Secret through the namespace-limited ESO store
- name: activity-core-issue-sink
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync the shared ingestion key for activity-core issue emission
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:issue-core-ingestion-api-key
purpose: approved child-process delivery and non-secret verification only
workload_delivery:
- mode: external-secrets
owner: railiance-platform + issue-core + activity-core
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-issue-core-ingestion-api-key
role_name: se-prod-issue-core-ingestion-api-key
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0002
notes: >-
CCR-2026-0002 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read both declared fields without printing them
negative: unrelated token is denied on platform/data/workloads/issue-core/issue-core/issue-core-runtime
risk:
classification: high
notes: path includes the shared ingestion bearer and the issue-core Forgejo backend token
rotation:
owner: railiance-platform + issue-core + activity-core
expectation: >-
Update OpenBao first, wait for both ESO consumers, roll issue-core and the
activity-core workers, then revoke the replaced provider credential.
ttl: provider-defined
deactivation:
owner: railiance-platform + issue-core
expectation: >-
Disable the front door, detach read access, remove materialized workload
Secrets, restart consumers, and preserve non-secret audit evidence.
audit:
evidence: CCR id, actor, exact path, field names, audit request ids, timestamps, and pass/fail only

View file

@ -0,0 +1,74 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0003.
id: openrouter-llm-connect
kind: kv
org: coulomb
repo: llm-connect
stage: prod
description: >-
OpenRouter provider credential used by llm-connect for activity-core. The
catalog does not generalize this key to other OpenRouter consumers.
mount: platform
path: workloads/activity-core/llm-connect/llm-connect-provider-secrets
mount_management: existing
fields:
- OPENROUTER_API_KEY
consumers:
- name: llm-connect-activity-core
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync llm-connect-provider-secrets for the activity-core deployment
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:openrouter-llm-connect
purpose: approved llm-connect smoke in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + llm-connect
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-openrouter-llm-connect
role_name: se-prod-openrouter-llm-connect
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0003
notes: >-
CCR-2026-0003 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
First native-cutover candidate (docs/native-lane-cutover.md); not an
authorization to apply.
verification:
positive: approved delivery AppRole can read OPENROUTER_API_KEY without printing it
negative: unrelated token is denied on platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
risk:
classification: high
notes: provider key can incur spend and remains valid outside OpenBao until revoked at OpenRouter
rotation:
owner: railiance-platform + llm-connect provider operator
expectation: >-
Mint a replacement at OpenRouter, update every live cluster copy, wait for
ESO refresh, restart llm-connect, verify, and revoke the old provider key.
ttl: provider-defined
deactivation:
owner: railiance-platform + llm-connect
expectation: >-
Disable the front door and workload read path, remove materialized Secrets,
stop consumers, and revoke the provider key at OpenRouter.
audit:
evidence: CCR id, actor, exact path, field name, provider key identifier if non-secret, timestamps, and pass/fail only

View file

@ -0,0 +1,77 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0005.
id: reuse-surface-hub-write-token
kind: kv
org: coulomb
repo: reuse-surface
stage: prod
description: >-
Production federation-hub write bearer and Forgejo webhook HMAC. ESO remains
the workload delivery path; native exec is a separate exact-path AppRole.
mount: platform
path: workloads/reuse/reuse-surface/runtime-secrets
mount_management: existing
fields:
- REUSE_SURFACE_TOKEN
- REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET
consumers:
- name: reuse-surface-runtime
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync reuse-surface-env through the namespace-limited ESO store
- name: forgejo-coulomb-org-webhook
auth: shared-hmac
claim: forgejo-org:coulomb
purpose: share the webhook HMAC with the reuse-surface receiver
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:reuse-surface-hub-write-token
purpose: approved hub tooling in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + reuse-surface + railiance-apps
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-reuse-surface-hub-write-token
role_name: se-prod-reuse-surface-hub-write-token
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0005
notes: >-
CCR-2026-0005 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read both declared fields without printing them
negative: unrelated token is denied on platform/data/workloads/reuse/reuse-surface/runtime-secrets
risk:
classification: high
notes: hub writes affect the production federation index and the HMAC has two coordinated consumers
rotation:
owner: railiance-platform + reuse-surface + railiance-apps
expectation: >-
Patch OpenBao, force ESO refresh, roll reuse-surface, and reconfigure the
Forgejo org webhook whenever its HMAC changes.
ttl: rotate-on-compromise
deactivation:
owner: railiance-platform + reuse-surface
expectation: >-
Disable the front door, detach ESO access, and remove the materialized
Secret only after confirming decommission or break-glass posture.
audit:
evidence: CCR id, actor, exact path, field names, ESO refresh timestamp, request ids, and pass/fail only

View file

@ -0,0 +1,102 @@
# warden-sign auth-capability lane.
# NON-SECRET: this file contains only OpenBao policy/AppRole metadata and
# operational pointers. AppRole secret_id values and tokens must never appear in
# Git, State Hub, chat, prompts, workplans, or normal logs.
id: warden-sign
kind: auth-capability
org: netkingdom
repo: ops-warden
stage: prod
description: >-
Scoped OpenBao AppRole used by ops-warden production smoke tests to call the
SSH secrets engine signing API over HTTP. This lane grants update only on the
exact SSH signing roles needed for FLEX-WP-0007 T4; it stores no KV value.
# Existing production OpenBao SSH secrets-engine mount and logical operation.
mount: ssh
path: sign
consumers:
- name: ops-warden-policy-smoke
auth: approle
claim: "agent:agt-state-hub-bridge"
purpose: >-
Run warden sign and the flex-auth policy-gate production smoke without
giving ops-warden standing custody of an OpenBao token.
allowed_actors:
- agt-state-hub-bridge
- agt-codex-interhub-bootstrap
- adm-example
- atm-backup-daily
# The deliverable is a freshly minted AppRole secret_id plus role_id handed to
# the operator out-of-band. There is no KV value and no from-file provisioning.
delivery_modes:
- approle-login
approval:
model: decision
decision_ref: "SECRETS-WP-0004"
notes: >-
Production auth-capability lane. Live apply and handoff require an approved
decision/workplan plus a mode-0600 bootstrap token outside any repo.
verification:
positive: "the warden-sign AppRole token has update on every allowlisted ssh/sign role"
negative: "the warden-sign AppRole token lacks update on ssh/admin/sys/auth/identity probes"
auth_capability:
policy_name: warden-sign
role_name: warden-sign
token_ttl: 15m
token_max_ttl: 15m
token_num_uses: 0
secret_id_ttl: 30m
secret_id_num_uses: 1
oidc:
auth_mount: netkingdom
role_name: warden-sign
role_path: auth/netkingdom/role/warden-sign
login_command: bao login -method=oidc -path=netkingdom role=warden-sign
policies: [warden-sign]
ttl: 15m
user_claim: sub
groups_claim: groups
bound_claims:
groups: [net-kingdom-admins]
oidc_scopes: [openid, profile, email, groups]
allowed_redirect_uris:
- https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
- http://localhost:8250/oidc/callback
- http://127.0.0.1:8250/oidc/callback
allowed_paths:
- path: ssh/sign/agt-role
capabilities: [update]
- path: ssh/sign/adm-role
capabilities: [update]
- path: ssh/sign/atm-role
capabilities: [update]
denied_probe_paths:
- ssh/sign/unlisted-role
- ssh/roles/agt-role
- ssh/config/ca
- sys/policies/acl/warden-sign
- auth/token/create
- identity/entity/id
rotation:
expectation: >-
Mint a fresh single-use secret_id for each attended handoff; revoke the
resulting token by accessor after the FLEX-WP-0007 T4 smoke or when the 15m
TTL expires, whichever comes first.
ttl: 15m
deactivation:
expectation: >-
Delete the warden-sign AppRole and ACL policy, revoke outstanding token
accessors, and record non-secret evidence.
audit:
evidence: >-
policy name, AppRole name, mount, allowed paths, token TTL, role_id file path,
secret_id file path, timestamp, result — no token value or secret_id.

View file

@ -0,0 +1,78 @@
# whynot-design npm publish token — the MVP pilot lane.
# This file is NON-SECRET. It describes where the token lives in OpenBao and how
# it may be consumed. The token VALUE never appears here.
#
# Terminology (Gitea is overloaded — we use the most explicit words):
# org = coulomb the Gitea organisation
# repo = whynot-design the Gitea repository / product (NOT an org, NOT a scope)
# npm package = @whynot/design published to the coulomb Gitea npm registry
# "@whynot" is the npm *scope*; it is neither the org nor the repo name.
id: whynot-design-npm-publish
org: coulomb
repo: whynot-design
stage: prod
description: >-
npm automation token used to publish the @whynot/design package from the
coulomb/whynot-design repo to the coulomb Gitea npm registry. Delivered to
`npm publish` via an exec-time temporary npm config; never printed or exported
into the parent shell.
# OpenBao KV v2 location of the secret material (org/repo-scoped path).
mount: secret
path: coulomb/whynot-design/npm/publish
# Field(s) inside the KV entry. The publish token is stored under this key.
# NOTE: this is the KV field name, NOT the env var the value is injected as.
# The injected name is resolved separately by the publication-scope policy and
# is currently `NPM_AUTH_TOKEN` (see delivery_config.npm.maturity below and
# docs/publication-scope-policy.md). Do not conflate the two.
fields:
- npm_token
# Who may consume this lane and the identity claim that binds them.
consumers:
- name: whynot-design-ci
auth: approle # bound OpenBao auth method
claim: "repo:coulomb/whynot-design"
purpose: "publish @whynot/design to the coulomb Gitea npm registry from CI"
# How the value may leave OpenBao. npm-config = temp .npmrc for the child only.
delivery_modes:
- npm-config
- read-check
# npm-specific delivery target. The registry/scope live here as catalog DATA so
# the engine never hardcodes a registry. Matches coulomb/whynot-design/.npmrc.
delivery_config:
npm:
registry: "https://forgejo.coulomb.social/api/packages/coulomb/npm/"
scope: "@whynot"
package: "@whynot/design"
# Package maturity tag. Under the netkingdom publication-scope policy this
# would map to a publication scope (build->gitea, test->org, prod->repo) once
# netkingdom is production grade. netkingdom is at maturity-build today, so the
# policy is dormant and this lane clamps to repo-scope, injected as
# NPM_AUTH_TOKEN. Set `token_env` to override the resolved name.
maturity: maturity-build
# token_env: NPM_AUTH_WHYNOTDESIGN # explicit per-repo form (optional)
# Privileged actions on this lane require an approved decision/CCR.
approval:
model: decision
decision_ref: "e6381a56-6b04-4fd5-b2de-f3ef59cde888" # Canonical State Hub decision UUID; local fixture is offline fallback
notes: "Production lane: apply requires an approved decision."
# Verification expectations (no value is ever printed).
verification:
positive: "approved consumer token can read the lane field"
negative: "an unrelated token is denied read on the lane path"
rotation:
expectation: "rotate on compromise or every 90 days"
ttl: "90d"
deactivation:
expectation: "revoke approle + delete KV metadata; record evidence"
audit:
evidence: "decision id, actor, path, timestamp, result — no secret value"

View file

@ -0,0 +1,21 @@
{
"SECRETS_ENGINE_CATALOG": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1/catalog",
"SECRETS_ENGINE_POLICIES": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1/policies",
"SECRETS_ENGINE_PEP_STANCE": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1/pep-stance.yaml",
"SECRETS_ENGINE_EVIDENCE": "/home/worsch/.local/state/secrets-engine/native-lifecycle/lifecycle-20261016-r1/evidence",
"SECRETS_ENGINE_HUB_URL": "http://127.0.0.1:8000",
"SECRETS_ENGINE_PDP_URL": "http://127.0.0.1:18081",
"SECRETS_ENGINE_PDP_TOKEN_FILE": "/run/user/1000/secrets-engine/lifecycle-20261016-r1/pdp-caller.token",
"SECRETS_ENGINE_PDP_TRUST_FILE": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1/trust/lifecycle-pre-key.json",
"SECRETS_ENGINE_PDP_VERIFIER": "/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03/libexec/secrets-engine-pdp-verify",
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID": "secrets-engine",
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_TYPE": "service",
"SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE": "secrets-engine.catalog-lane.lifecycle",
"SECRETS_ENGINE_AUTHORIZATION_POLICY_VERSION": "v2",
"SECRETS_ENGINE_AUTHORIZATION_MIN_APPROVALS": "1",
"SECRETS_ENGINE_APPROVAL_URL": "",
"SECRETS_ENGINE_KEYCAPE_TOKEN_URL": "",
"SECRETS_ENGINE_KEYCAPE_ISSUER": "",
"BAO_ADDR": "",
"VAULT_ADDR": ""
}

View file

@ -0,0 +1,81 @@
# secrets-engine — PEP unreachable-engine stance map
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3
# Companion: net-kingdom/SECURITY-COMPANION.md §5
# Voice: INTENT.md / layer.yaml
# Validate: pytest tests/test_layer_conformance.py
#
# §6.4 obligation 3 requires a declared unreachable-engine stance that is
# total, scoped per zone or equivalent, carries no implicit default and no
# per-call discretion, and is published rather than held in code comments.
# Catalog stage is the equivalent scope until security-zone membership arrives
# as a claim on the decision.
#
# Runtime reads this file. tests/test_layer_conformance.py also pins
# secrets_engine.pep_stance.SHIPPED_STANCE to this map. A published map free
# to drift from that pin is worse than none.
schema_version: "0.1"
framework: netkingdom-security-layer-model
standard_version: "0.7"
repository: secrets-engine
pep_shape: true
declared_by: INTENT.md
decision_engine: access-engine # flex-auth until the governed rename
scope: catalog-stage # equivalent scope; not security-zones yet
protected_action: >-
OpenBao metadata apply, KV provision, verification, exec delivery,
auth-capability handoff, native revoke, and lifecycle
suspend/deactivate/destroy
# Total by construction over every catalog stage plus unknown.
# No implicit default — an unlisted value is a config error, not a fallback.
# "fail_closed" means no protected side effect without a durable
# access-engine / ActionAuthorization record.
# "fail_open" here means the engine-unreachable residue: proceed only under
# the existing lane-approval check (State Hub or local fixture), which is
# itself a tracked gap relative to access-engine, not a manufactured allow.
stance:
build: fail_open
test: fail_open
prod: fail_closed
unknown: fail_closed
# Explicit, three-factor exception. Not a stance row and not a default.
# All three must hold or the production gate remains closed.
demo_exception:
id: unsafe-local-demo
requires:
- "SECRETS_ENGINE_UNSAFE_DEMO=1"
- "hub URL empty"
- "OpenBao target is loopback"
applies_to: prod
note: >-
Throwaway local demonstration only. Not a production residue and not
available when any one factor is missing.
on_apply:
recorded_fields:
- stance_stage
- stance_failure_mode
- stance_demo_exception
- stance_decision_id # present only where a decision was resolved
- approval_status # rejected when the stance refuses
- error_type # DecisionError when fail_closed fires
written_to:
- "local JSONL (.evidence/evidence-YYYY-MM-DD.jsonl)"
- "evidence_dir/outbox/ for load-bearing classes"
- "best-effort State Hub progress note (attributive only)"
never_recorded: "any secret material, token, password, or raw value"
# §6.4 obligation 2 — the verdict is never cached. A retry of the same
# canonical request digest inside the decision lifetime is the decision
# being used as issued; a different resource is not.
verdict_caching: none
# §6.4 obligation 4 / §9.6 — attributive today. No control branches on the
# presence of a local evidence record. Completeness is not claimed.
reconstructability:
bound: "§9.6 attributive — completeness is not claimed"
declared_at: layer.yaml#evidence_bound

View file

@ -0,0 +1,46 @@
# Publication-scope policy — owned by the netkingdom security-infrastructure domain,
# enforced by secrets-engine.
#
# Binds npm package *publication scope* to package *maturity*, and GATES the
# graduated (relaxed) scoping behind netkingdom itself reaching production grade.
#
# Design intent (proves policy-bound secret handling):
# - A repo's maturity translates into the maturity tag of the package it ships
# (maturity-build / maturity-test / maturity-prod).
# - Each maturity tag is restricted by the publication scope its token carries.
# - Low-stakes build artifacts may publish with a BROAD shared token; high-stakes
# production publishes require a TIGHT, repo-scoped token. Scope narrows as
# stakes rise.
# - The graduated table is DORMANT until netkingdom is production grade. While
# dormant, every lane clamps to the safest scope (repo) — fail-safe, never
# fail-open.
domain: netkingdom
policy: publication-scope
version: "0.1.0"
# netkingdom's own maturity. `production_grade` is derived: it is true only when
# this equals "maturity-prod". Today netkingdom is at maturity-build.
netkingdom_maturity: maturity-build
# When ACTIVE (production_grade == true): package maturity tag -> required scope.
maturity_scope:
maturity-build: gitea # gitea-wide publication rights
maturity-test: org # org-wide publication rights
maturity-prod: repo # repo-scoped publication rights
# Safe scope applied while DORMANT, and the floor that cannot be exceeded before
# netkingdom is production grade.
dormant_scope: repo
# Token env-var naming convention by effective scope. {ORG}/{SCOPE}/{REPO} are
# upper-cased, de-hyphenated forms of the lane's org / npm-scope / repo.
# org coulomb -> COULOMB
# scope @whynot -> WHYNOT (npm scope = "project"; defined, NOT used by policy)
# repo whynot-design -> WHYNOTDESIGN
# The default repo-scope name is the bare NPM_AUTH_TOKEN; the explicit per-repo
# form (NPM_AUTH_{REPO}) is available when a lane overrides it.
token_env:
gitea: "NPM_AUTH_GITEA_TOKEN"
org: "NPM_AUTH_{ORG}_TOKEN"
scope: "NPM_AUTH_{SCOPE}_TOKEN"
repo: "NPM_AUTH_TOKEN"

View file

@ -0,0 +1,36 @@
# secrets-engine-build — build/dev stage role policy.
#
# Scope: manage approved BUILD-stage metadata and values under the build/ prefix
# only. May generate throwaway test values. Cannot touch test/ or prod paths,
# cannot administer sys/, auth/, or identity/, cannot act as root.
# KV v2 data + metadata under the build prefix.
path "secret/data/build/*" {
capabilities = ["create", "read", "update", "patch", "delete"]
}
path "secret/metadata/build/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Manage only the build consumer policies this role owns.
path "sys/policies/acl/se-build-*" {
capabilities = ["create", "read", "update", "delete"]
}
# Manage build approle roles only.
path "auth/approle/role/se-build-*" {
capabilities = ["create", "read", "update", "delete"]
}
path "auth/approle/role/se-build-*/role-id" {
capabilities = ["read"]
}
path "auth/approle/role/se-build-*/secret-id" {
capabilities = ["create", "update"]
}
# DENY everything outside the build lane (explicit, for reviewer clarity).
path "secret/data/test/*" { capabilities = ["deny"] }
path "secret/data/prod/*" { capabilities = ["deny"] }
path "sys/*" { capabilities = ["deny"] }
path "auth/token/*" { capabilities = ["deny"] }
path "identity/*" { capabilities = ["deny"] }

View file

@ -0,0 +1,63 @@
# secrets-engine-prod — production stage role policy.
#
# Scope: apply approved PRODUCTION metadata (ACL policies + approle roles) and
# write approved lane values for provisioning. Deliberately NARROW:
# - no root / sudo
# - no sys/* administration beyond its own se-prod-* ACL policies
# - no identity/* administration
# - no auth/token administration
# - cannot reach into build/ or test/ prefixes
#
# Production raw-value READ stays gated: this role can write a value during an
# approved provisioning step, but broad read of arbitrary prod paths is denied.
# Consumers read their own lane through the narrow se-prod-<lane> consumer policy.
# Apply approved production lane values (provisioning) and metadata.
# Owner-scoped prod lanes (e.g. whynot-design/...) — NOT under build/ or test/.
path "secret/data/+/*" {
capabilities = ["create", "update", "patch"]
}
path "secret/metadata/+/*" {
capabilities = ["create", "read", "update", "list"]
}
# Manage only the prod consumer policies this role owns.
path "sys/policies/acl/se-prod-*" {
capabilities = ["create", "read", "update", "delete"]
}
# Exact exception for SECRETS-WP-0004 auth-capability lane.
path "sys/policies/acl/warden-sign" {
capabilities = ["create", "read", "update", "delete"]
}
# Manage prod approle roles only.
path "auth/approle/role/se-prod-*" {
capabilities = ["create", "read", "update", "delete"]
}
path "auth/approle/role/se-prod-*/role-id" {
capabilities = ["read"]
}
path "auth/approle/role/se-prod-*/secret-id" {
capabilities = ["create", "update"]
}
# Exact exception for SECRETS-WP-0004 auth-capability lane.
path "auth/approle/role/warden-sign" {
capabilities = ["create", "read", "update", "delete"]
}
path "auth/approle/role/warden-sign/role-id" {
capabilities = ["read"]
}
path "auth/approle/role/warden-sign/secret-id" {
capabilities = ["create", "update"]
}
# Hard denials — production appliers must not become broad admins.
path "secret/data/build/*" { capabilities = ["deny"] }
path "secret/data/test/*" { capabilities = ["deny"] }
path "sys/policies/acl/secrets-engine-*" { capabilities = ["deny"] } # can't edit stage roles
path "auth/token/*" { capabilities = ["deny"] }
path "identity/*" { capabilities = ["deny"] }
path "sys/auth/*" { capabilities = ["deny"] }
path "sys/mounts/*" { capabilities = ["deny"] }

View file

@ -0,0 +1,33 @@
# secrets-engine-test — test/staging stage role policy.
#
# Scope: manage approved TEST-stage metadata and values under the test/ prefix,
# and run positive/negative verification. Cannot touch build/ or prod paths,
# cannot administer sys/, auth/, or identity/, cannot act as root.
path "secret/data/test/*" {
capabilities = ["create", "read", "update", "patch", "delete"]
}
path "secret/metadata/test/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/policies/acl/se-test-*" {
capabilities = ["create", "read", "update", "delete"]
}
path "auth/approle/role/se-test-*" {
capabilities = ["create", "read", "update", "delete"]
}
path "auth/approle/role/se-test-*/role-id" {
capabilities = ["read"]
}
path "auth/approle/role/se-test-*/secret-id" {
capabilities = ["create", "update"]
}
# DENY everything outside the test lane.
path "secret/data/build/*" { capabilities = ["deny"] }
path "secret/data/prod/*" { capabilities = ["deny"] }
path "sys/*" { capabilities = ["deny"] }
path "auth/token/*" { capabilities = ["deny"] }
path "identity/*" { capabilities = ["deny"] }

View file

@ -0,0 +1,9 @@
{
"schema": "secrets-engine.pdp-trust.v1",
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"keys": {}
}

View file

@ -0,0 +1,95 @@
{
"schema": "secrets-engine.consumer-baseline-proposal.v1",
"proposal_id": "SECRETS14-CONSUMER-ADMISSION-20261016-R1",
"state": "owner-selectable source preparation; not installed or admitted",
"execution_identity": {
"host": "existing local workstation",
"user": "worsch",
"uid": 1000,
"gid": 1000,
"observation": "id worsch; no credential/process environment read",
"same_uid_code_isolation_established": false
},
"release": {
"runtime_source_revision": "4dd004a2250104ba56293b2d7b0d96f4e076ea99",
"documentation_source_revision": "3718e60d7385db091d3423352f44e922863087de",
"bundle_manifest_sha256": "sha256:105dcebe6f03e9799f91ea75175e20500df2aa7f08b5e32d39c1c367593ca47e",
"wheel_sha256": "sha256:198dca5c025fe22d60b3dead8244d112a554bf837c28f6cd2877aafa05924ae7",
"helper_sha256": "sha256:ed922b62533e4d33ae2d5f11175111f6f28e684a19e63ac704d43b63f0e0e12e",
"prefix": "/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03",
"entrypoint": "/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03/bin/secrets-engine",
"helper": "/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03/libexec/secrets-engine-pdp-verify",
"python": "/home/worsch/.local/share/secrets-engine/releases/4dd004a22501-105dcebe6f03/venv/bin/python",
"install_contract": "new prefix only; existing reviewed offline installer with independently admitted manifest; never overwrite or use ambient editable/venv CLI"
},
"configuration": {
"parent": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-pre-key-r1",
"files_sha256": {
"catalog/activity-core-metered-worker-token.yaml": "sha256:67d6235bbf6e28b7d1392ff26fd13ce4933843d11407a8fe2b1c793b3748a1f5",
"catalog/email-connect-transactional.yaml": "sha256:7869e96a7fca4c508f8a2a11a9b614db8c8a4100c2402597bde48a06aef77f97",
"catalog/example-build-test-token.yaml": "sha256:6e2396af8c711c10a5af83edb3925227c251e82d16a5d5a7b7a87948e2229f54",
"catalog/forgejo-admin-api-token.yaml": "sha256:193e231c5dbc175e82614fe35092ab649cf4152878f2679332b37888f176a991",
"catalog/glas-claude-agent-dev-anthropic.yaml": "sha256:61f40b124d937516b18b77b3ae3253079f024f4522191e22a81fae3d859abb63",
"catalog/issue-core-ingestion-api-key.yaml": "sha256:401b8c7a57d3e9a54e98711387577ecc6fc0674f2e32759a5c675e1d4adf176f",
"catalog/openrouter-llm-connect.yaml": "sha256:3d46e4896aa3b2a9838f72d4f2e3b1afc9deb2c7b1be0c62ae1900eb9cb1713b",
"catalog/reuse-surface-hub-write-token.yaml": "sha256:6f2ab0e31ba71bffb5d48b8ac787dee52b73f865af1c9f155d5046067a841111",
"catalog/warden-sign.yaml": "sha256:54a9ec01bf87fa3cc6ab75fa6c818c4977f2f70800a16890a836a509ad5c73c9",
"catalog/whynot-design-npm-publish.yaml": "sha256:18ada66f109a5532462b70b4d213bb73c7ab2e0ef9a4a2a149b7f9fcd77b034f",
"environment.proposed.json": "sha256:78a19a1a33f1faf61353ce674048826c89c45e84f15467992fca347679c4908d",
"pep-stance.yaml": "sha256:969ffd12506e2954c273e19ed6505692c1bacbe97a262a85862793254263e737",
"policies/netkingdom-publication-scope.yaml": "sha256:018000cb29edf7707f3885bec2f54285adef1e5a71c749b6b7ab6044203de89b",
"policies/secrets-engine-build.hcl": "sha256:ec5ce1987fbf2818f1ecd612d280cc419990dce12f85d7f5f38b621f4f9dbf69",
"policies/secrets-engine-prod.hcl": "sha256:1d12ca47e4f3ab234370ddfadad2aca8ae0943d95833798be5af0ea09860c6ce",
"policies/secrets-engine-test.hcl": "sha256:53c1d4146116784ba0024cc2c9f318e6443471ee97b7b1d2056ea840eb4ff6f6",
"trust/lifecycle-pre-key.json": "sha256:5f01b418cccef03326c424a1de649a4bde42811aa1e2041a6d0417c8d65a3f6a"
},
"format": "environment.proposed.json describes existing Config.load environment only; not a new runtime loader",
"owner_directory_mode": "0700",
"metadata_file_mode": "0600",
"evidence_root": "/home/worsch/.local/state/secrets-engine/native-lifecycle/lifecycle-20261016-r1",
"runtime_credential_directory": "/run/user/1000/secrets-engine/lifecycle-20261016-r1",
"runtime_directory_mode": "0700",
"credential_file_mode": "0600",
"credential_owner_uid": 1000
},
"trust": {
"pre_key_file": "trust/lifecycle-pre-key.json",
"pre_key_file_sha256": "sha256:5f01b418cccef03326c424a1de649a4bde42811aa1e2041a6d0417c8d65a3f6a",
"keys": {},
"expected_behavior": "empty-key trust must refuse before caller credential access; no pre-generation online trust",
"post_custody_successor_config_parent": "/home/worsch/.config/secrets-engine/native-lifecycle/lifecycle-20261016-post-custody-r1",
"post_custody_successor_trust_relative_path": "trust/lifecycle.json",
"post_custody_successor_digest": null,
"independent_key_admission_required": true
},
"policy": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4"
},
"unavailable_for_this_scope": {
"approval_delivery": "not configured; stage1 offline receipt proof only",
"backend_entitlement": "not configured; no backend invocation",
"clock_extra": "not configured; no optional extra admitted",
"native_task_runtime": "not selected; separate exact Plan/grant/source required"
},
"environment_to_be_absent": [
"BAO_TOKEN",
"VAULT_TOKEN",
"SECRETS_ENGINE_UNSAFE_DEMO",
"SECRETS_ENGINE_APPROVAL_TOKEN_FILE",
"SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE",
"SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE",
"SECRETS_ENGINE_OPENBAO_JWT_LOGIN",
"SECRETS_ENGINE_CLOCK_TRUST_FILE"
],
"scope": "retained lifecycle signer authentication only; catalog snapshot pins data, grants no catalog action",
"producer_image": "separate native packet input; intentionally not a consumer baseline hash input",
"execution_authorized": false,
"native_trust_admitted": false,
"protected_effect_sites_accepted": 0,
"protected_effect_sites": 11,
"protected_effect_handlers": 10
}

View file

@ -0,0 +1 @@
PASS: actual installed authorize_action refused empty-key trust before credential/backend access