docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
2f18be7880
commit
014bbd2893
50 changed files with 6134 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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`
|
||||
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"blocked_blob_ids": [], "scanned_blobs": 21, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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']
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"blocked_blob_ids": [], "scanned_blobs": 5, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"blocked_blob_ids": [], "scanned_blobs": 5, "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", "scanner_version": "8.30.1", "scope": "changed-staged-blobs", "status": "clean"}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"bundle": "/tmp/secrets-cli-ff1-native-proof", "helper_sha256": "a6e4eeb5361c61a2db5526ba5648ef5e48143c1a453e158d2f62991ba7ba3a33", "manifest_sha256": "ee07c0b8755788ad0894c679ee2ca125a1c24df3a895013755d4a965a879a1ec"}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
PASS: actual installed authorize_action refused empty-key trust before credential/backend access
|
||||
Loading…
Add table
Add a link
Reference in a new issue