Package protected review runtime and prepare deployment admission

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-11 01:21:37 +02:00
parent bb5b607bbd
commit bda9381f07
20 changed files with 3534 additions and 7 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
**
!Containerfile
!requirements.lock
!dist/
!dist/informed_decision-*.whl

2
.gitignore vendored
View file

@ -3,3 +3,5 @@ __pycache__/
.pytest_cache/ .pytest_cache/
*.egg-info/ *.egg-info/
.venv/ .venv/
dist/
build/

24
Containerfile Normal file
View file

@ -0,0 +1,24 @@
# Base matches the approved Approval Engine Alpine family; scan each candidate.
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
RUN python -m venv /opt/venv
COPY requirements.lock /build/requirements.lock
RUN pip install --no-cache-dir --require-hashes --only-binary=:all: -r /build/requirements.lock
COPY dist/informed_decision-*.whl /build/
RUN pip install --no-cache-dir --no-deps --no-index /build/informed_decision-*.whl
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
RUN apk add --no-cache 'libuuid>=2.42.3-r1' \
&& addgroup -S -g 10001 informed \
&& adduser -S -u 10001 -G informed -H informed
COPY --from=build /opt/venv /opt/venv
# No package installer or build tooling belongs in the running service.
RUN rm -rf /usr/local/bin/pip* /usr/local/lib/python3.12/site-packages/pip* \
/usr/local/lib/python3.12/site-packages/setuptools* /usr/local/lib/python3.12/site-packages/pkg_resources \
/opt/venv/bin/pip* /opt/venv/lib/python3.12/site-packages/pip* \
/opt/venv/lib/python3.12/site-packages/setuptools* /opt/venv/lib/python3.12/site-packages/pkg_resources
WORKDIR /app
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["informed-decision-container"]

View file

@ -1,4 +1,12 @@
.PHONY: test check sync .PHONY: test check sync package image-build
IMAGE ?= informed-decision:local
package:
uv build --wheel
image-build: package
docker build -f Containerfile -t $(IMAGE) .
test: test:
python3 -m pytest -q python3 -m pytest -q

View file

@ -10,8 +10,9 @@
durable evidence store and scheduled Audit Core delivery are implemented. durable evidence store and scheduled Audit Core delivery are implemented.
The approval surface is not deployed.** The approval surface is not deployed.**
What exists and is tested (344 tests and 12 Chromium checks, including actual What exists and is tested (371 automated tests and 11 container checks; the
Flex Auth, Approval Engine and Audit Core with synthetic identity/custody): previous browser milestone passed 12 Chromium checks). Component tests use
actual Flex Auth, Approval Engine and Audit Core with synthetic identity/custody:
- layer and stance declarations — `layer.yaml`, `pep-stance.yaml`, - layer and stance declarations — `layer.yaml`, `pep-stance.yaml`,
`informed_decision/stance.py`, with published-equals-shipped asserted; `informed_decision/stance.py`, with published-equals-shipped asserted;
@ -39,10 +40,15 @@ Flex Auth, Approval Engine and Audit Core with synthetic identity/custody):
accept/return/discuss/decline and visible original/unresolved entry state; accept/return/discuss/decline and visible original/unresolved entry state;
- `runtime.py` — explicit owner configuration, rotating credential-file readers, - `runtime.py` — explicit owner configuration, rotating credential-file readers,
30-second audit draining, heartbeat/reconciliation and delivery readiness. 30-second audit draining, heartbeat/reconciliation and delivery readiness.
- `Containerfile`, `container.py`, `requirements.lock` and deployment renderer —
a locally built/scanned image, private projected-config handoff, one serving
writer, backup/inspection commands and eight review-only Kubernetes objects.
Network-isolated container restart/restore preserves unresolved submissions.
Remaining: native policy package/caller/assignment admission, registered human Remaining: native policy package/caller/assignment admission, registered human
login and deployed binding, independent production audit custody, packaging, login and deployed binding, independent production audit custody, image
backup/restore and operator recovery admission. The legacy `evidence.Outbox` remains an publication/cutover, platform backup/restore and operator recovery admission.
The legacy `evidence.Outbox` remains an
in-memory test double; the new `Store` supplies durable atomicity. Browser in-memory test double; the new `Store` supplies durable atomicity. Browser
sessions are ephemeral, with no approval state. sessions are ephemeral, with no approval state.
Without owner runtime configuration `/readyz` returns 503. With it, readiness Without owner runtime configuration `/readyz` returns 503. With it, readiness
@ -51,6 +57,8 @@ The origin's last observed deployment was an nginx placeholder on 2026-09-10.
See [browser-authentication.md](docs/browser-authentication.md) and See [browser-authentication.md](docs/browser-authentication.md) and
[durable-review-evidence.md](docs/durable-review-evidence.md) and [durable-review-evidence.md](docs/durable-review-evidence.md) and
[protected-browser-review.md](docs/protected-browser-review.md). [protected-browser-review.md](docs/protected-browser-review.md).
The [deployment packet](deploy/README.md) records native wiring observations,
the missing Approval Engine namespace and remaining owner admission.
`INFD-WP-0001-T08` remains open for the live end-to-end proof, which is gated on `INFD-WP-0001-T08` remains open for the live end-to-end proof, which is gated on
`APPROVAL-WP-0002-T01` and a deployed `approval-engine`. `APPROVAL-WP-0002-T01` and a deployed `approval-engine`.

184
deploy/README.md Normal file
View file

@ -0,0 +1,184 @@
# Review service deployment candidate
`INFD-WP-0001-T08` owns this packet. It prepares the existing
`decisions.coulomb.social` origin for the review service. **No image publication,
production configuration, credential provisioning or cluster apply is performed
by the build, renderer or tests.** The origin is still the Railiance Apps nginx
placeholder. Native policy, custody, registration and service admission remain
required before cutover.
## Artifact and local proof
```sh
make image-build IMAGE=informed-decision:local
python tools/smoke_container.py --image informed-decision:local --receipt /tmp/infd-container-proof.json
```
`Containerfile` uses the digest-pinned Alpine base already used by Approval
Engine, installs the hashed `requirements.lock`, then installs the built wheel
without dependency resolution. The image contains no pip or build installer.
Its user/group is 10001; runtime deployments must use a read-only root filesystem
and drop capabilities. Rebuild the wheel before building the image. Refresh
dependency pins deliberately with `uv pip compile pyproject.toml --python-version
3.12 --generate-hashes --output-file requirements.lock`, then repeat checks and
the image scan. A successful local build is not a published registry digest.
The dated evidence pins the exact wheel/image/runtime-file hashes and scanner.
The local image passed 11 container checks and a HIGH/CRITICAL Trivy scan with
zero findings. The scanner also emitted an Alpine lifecycle-list warning; the
receipt retains it. This scan is evidence about the selected severities and
database at that time, not a general security attestation.
The smoke harness creates and removes its own labeled containers/volumes. It
uses `--network none`, publishes no port and mounts only synthetic configuration
and credentials. It tests the installed entrypoint, private file ownership,
anonymous refusal, unhealthy audit readiness, exclusion of a second writer,
consistent backup, restart, and restore onto another volume. A synthetic
unresolved submission and its undelivered evidence survive unchanged. No native
issuer, PDP, Approval Engine or Audit Core is contacted. Actual component/browser
contracts have separate receipts from the previous integration milestone.
The lifecycle check caught the initial PID-1 process requiring Docker's forced
kill (exit 137). The container now handles SIGTERM through Waitress shutdown and
the audit pump's stop path; the final image exits cleanly within the tested
15-second window. Kubernetes allows 45 seconds. Forced termination still cannot
justify retrying an unresolved approval entry.
## Private storage and projected configuration
The deployment uses a single replica with **Recreate**, a ReadWriteOnce PVC and
an OS lock held for the life of the serving process. Do not add replicas, a
rolling surge or another writer: sessions are process-local and this store is a
local-filesystem design. The lock rejects accidental concurrent service starts;
it is not an admission of network-filesystem locking semantics.
Kubernetes projected ConfigMaps are root-owned and symlinked. The container
entrypoint reads at most 16 KiB from `/configuration/runtime.json`, requires
`evidence_db=/data/private/review.sqlite`, and copies the configuration into
owned ephemeral storage at `/run/informed-decision/private/runtime.json` (0600).
It creates `/data/private` as the process user with mode 0700 and refuses unsafe
existing ownership/modes. It does not silently chmod or take over existing data.
The evidence database remains 0600. Credential callbacks read rotating projected
files directly; bearer tokens are never copied into SQLite or the config snapshot.
The pod uses fsGroup 10001 with `OnRootMismatch`. Its storage driver must make
the volume root writable by that group while preserving the private child's
ownership/modes on later mounts. A restored volume whose root triggers a
recursive permission rewrite needs owner repair before admission, not relaxed
store checks. The local-path class exists on Railiance; its backup/retention and
restore admission are still the platform owner's work.
The normal workstation entrypoint continues listening on 127.0.0.1. The container
entrypoint explicitly selects 0.0.0.0; the public issuer/callback remain fixed,
and Host/forwarded headers cannot alter them.
## Render the review packet
Copy `admission.example.json` outside Git and replace its intentionally invalid
placeholders with the published image manifest digest, admitted PDP endpoint and
package/version/digest, exact PDP pod label, observed/admitted issuer IPs and
storage class. The renderer accepts no inline token or broad egress CIDR.
```sh
python tools/render_deployment.py --input /operator/review-inputs.json --output /tmp/infd-candidate.json
kubectl --kubeconfig /operator/railiance-kubeconfig apply --dry-run=server -f /tmp/infd-candidate.json
```
Rendering and server dry-run do not admit the values supplied. The renderer
produces eight review objects: service account, PVC, immutable configuration,
Recreate Deployment, Service, the review pod's NetworkPolicy, and two exact
counterparty ingress proposals. It creates no Secret, RBAC grant, Namespace,
Ingress or policy assignment. The service retains the existing name/port and
selects the review component. The Deployment keeps the existing immutable
selector so the same origin can be replaced under a reviewed cutover.
The incoming Traefik peer is namespace AND pod label. Egress permits DNS,
exact Approval/Audit/PDP peers, exact public KeyCape addresses on 443, and the
observed Traefik websecure peer on 8443 for enforcement after Service DNAT.
Railiance metadata on 2026-09-11 confirmed the Traefik label/8443 port and
`kc.coulomb.social -> 92.205.62.239`. These observations do not prove the CNI
path; test the actual issuer token/JWKS reachability after admission. The shared
HTTPS router is an L3/L4 boundary, not a hostname authorization policy; the
client itself pins the issuer and refuses redirects.
Approval Engine's existing caller policy permits Secrets Engine or namespaces
carrying its client label. That does not admit this review pod. The packet adds
an **owner-reviewed proposal** ANDing namespace, app label and component rather
than labelling the whole namespace as an approved caller. Flex Auth receives a
similarly exact peer proposal. Audit Core already owns its prepared Informed
Decision ingress under `AUDIT-WP-0009-T11`; do not create a competing copy.
Seven of eight objects passed server dry-run in their actual namespaces. The
Approval Engine peer failed because namespace `approval-engine` is absent.
The identical policy shape passed with only its metadata namespace/name mapped
to an existing namespace for schema validation. That does not close the missing
owner deployment. Schema checks used fixture policy pins and an unpublished image
reference; their rendered output is not an admitted production configuration.
## Serving health and acceptance readiness
Startup, Kubernetes readiness and liveness use `/healthz`. An audit dependency
outage must not restart the writer or remove the service's refusal/recovery
pages from the origin. `/readyz` separately reports recent audit delivery health,
and the application refuses accept while it is unhealthy. Monitor `/readyz`
and pending/blocked outbox counts explicitly; Kubernetes `Ready=True` alone is
not deployment acceptance or native policy proof. A healthy process can still
refuse every action when policy/custody has not been admitted.
## Native admission and cutover
Use the existing owners and records:
1. Flex Auth/Informed Decision admit the exact request package, assignments and
caller `informed-decision=system:serviceaccount:informed-decision:review`.
The projected token's audience is `flex-auth`; no browser token substitutes.
The service account spelling is a candidate until this return is recorded.
2. Platform/Audit Core complete `AUDIT-WP-0009-T11`: the candidate expects
`informed-decision-audit` with key `token`, projected read-only. Its sender is
exact `informed-decision`, tenant `tenant:platform`, write-only, load-bearing,
redact. Run `warden route find/show` before any credential request. Rendering
the Secret reference provisions nothing and confirms no custody name.
3. Approval Engine admits its namespace/service and the exact review caller;
KeyCape rolls out the already published browser registration. Railiance Apps
admits image publication/cutover against its existing origin. Complete
private-state backup/restore, monitoring and rollback ownership.
4. Capture the current Deployment and Service configuration, close new review
traffic for cutover, and retain any existing source-held evidence. Only then
apply the admitted packet. Its Recreate rollout intentionally incurs downtime.
5. Check both process health and application readiness, policy caller/subject
allow/refusal cases, genuine human login and a declared human-control approval.
Retrieve the independent audit receipt and the original presentation. Recheck
the callback's TLS/redirect/cookie behavior and source custody after restart.
The image publication, workload/policy/custody admission and rollout remain
distinct. No signed decision attribution, native approval or factory admission
is inferred from this packet. `FLEX-WP-0024` and the commitment-only custody
limitations remain as recorded in the consumer contract.
## Backup, restore and rollback
The installed `informed-decision-admin` supports local custody operations only:
```sh
informed-decision-admin inspect --db /data/private/review.sqlite
informed-decision-admin backup --db /data/private/review.sqlite --output /backup/private/review.sqlite
```
The backup parent must already be owned/private 0700; the destination must not
exist. Backup uses SQLite's consistent backup API while the service may be live.
Do not copy a running SQLite file while ignoring WAL. Inspection prints counts
and schema version, not memo content or credentials. These are privileged
operator custody commands, not browser endpoints. An independent backup copy,
retention, access controls and an actual platform restoration drill remain owed.
For restore, stop the serving writer, restore to a separately prepared private
volume, verify the immutable content and unresolved submission state, then
mount that volume as `/data`. A restart requires a new human login. Never reset
an unresolved intent or POST again to manufacture a presentation correlation.
Do not roll schema v2 back to an older reader against the same live database.
For a failed initial cutover, stop the new writer, retain the PVC and restore
Railiance Apps' **Deployment and Service** from the prior origin manifest.
Restoring only the Deployment is insufficient: the candidate Service selector
also names the review component. Returning to the placeholder is a service
rollback, not deletion or reconciliation of any approval/evidence already held.

View file

@ -0,0 +1,12 @@
{
"image": "REQUIRED: forgejo.coulomb.social/coulomb/informed-decision@sha256:<published manifest digest>",
"policy": {
"origin": "REQUIRED: http://<admitted service>.flex-auth.svc.cluster.local:8080",
"package": "REQUIRED: owner-admitted package",
"version": "REQUIRED: owner-admitted version",
"package_digest": "REQUIRED: sha256:<native package digest>",
"pod_name": "REQUIRED: exact app.kubernetes.io/name label of admitted PDP"
},
"keycape_egress_ips": ["REQUIRED: observed and admitted public issuer IPv4/IPv6 address"],
"storage_class": "REQUIRED: admitted local-filesystem ReadWriteOnce storage class"
}

View file

@ -0,0 +1,143 @@
{
"schema": "informed-decision.container-candidate.v1",
"observed_at": "2026-09-10T23:15:35.252989+00:00",
"base_commit": "83849b75d44cab3d01d7ea56f6c839aef8e20046",
"contract_sources": {
"approval-engine": "a0a602976eef818f36dde35f76f7f2e589bd051b",
"audit-core": "5c0ad522fb36092aa7ec2e8d72f63a5a91853b5b",
"flex-auth": "88b354377c8e26b162f1234e673072f1c06dcd89",
"railiance-apps": "eff457ef52a29afc7b9beccf38480a9955c5233e"
},
"source_task": "INFD-WP-0001-T08",
"status": "progress",
"image": {
"local_tag": "informed-decision:hfact-20260911",
"local_image_id": "sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4",
"wheel_sha256": "00930d4fe7415c93f36ee82735abcbc69ca7cad694e2963b8f0ad8abdac6cb34",
"runtime_files": {
"container.py": "a8e7516ef6a7f110d8ed95493ae3b9492d913cc7a5650b88a519334ba544f9dd",
"presentation.py": "54375ccd8358fac41de220bccadbee09f8b7c8ebbeeafaafd381a7456331da50",
"disposition.py": "5ddd19c2444f8e6c9bb090d04045cf1227a34554e000f07b3f677325bf8089f8",
"records.py": "82f696151895c51b168144a3078d05764da3499b96917c465629ce0836fa8e33",
"audit.py": "06da36a8f3451b4abc7a46da2df66d5939524c170658b879499b2a47a1b10cd3",
"oidc.py": "35b37bae67968c2179b22af8151afe2981d5468debf4488771eaf3121b75e221",
"__main__.py": "3ba1467fa2d47fcf0bd032e8694904450775adf763a6e6f9e4888cc3ce3ed92b",
"stance.py": "62b96b76eba4ec277344172f10f6233e979dda4d079f972d9a55c4e5aed96c3c",
"policy.py": "7bf814a8332ff054bf5f1f651d8816a408138b1f197b17da0abb7f87411e8812",
"review.py": "c2d2aa1cf48d10d85b3606acf2830ab22701c1274221e5e2760d29901adfd786",
"http_transport.py": "fc9c1fd05cc89120d70bfcdb4c60fe244b81c8054d21505df8cfa9c5a229f7bc",
"approval_client.py": "56c201dda358cb4bef11a15da141bb68562cdde9e37b4a880bf1e9257ae82cb6",
"runtime.py": "6bc6a74657f526743cca6c6cdc0a86fff43d8282e0b867dc9288344636abe5eb",
"provenance.py": "de02c6bf162865ba9619d74cfd68dd0a5d7f81e6c9e427f67f848cb2c2214f83",
"memo.py": "697058863a4e6de2c143a2433eaaf5e15e5e7375dc5459af6911b146567e8b5d",
"canonicalize.py": "517bbc6b81cc839636b97863ebdefd38aa375d10f4915feb9bf21867f992f765",
"evidence.py": "5f84997f66409c7b6cec6ab57e73ad5bba2c1b2d669604adcbf36e72a824a88d",
"__init__.py": "0ffebe048ba583e7b623f60e8f449950dc5fd49afce2398381fa143eee834e19",
"approval_http.py": "203f462ff78f1409f2f924d4723197fdd502087c7f6da319f46ffffce2a9d478",
"web.py": "97f7e040237fa3b755e39d13e628181dc2a3f6a31b4331ca7284bef05f8d6038",
"store.py": "7a58b36d409cb4161b65985545e13573a5196779b4c6e95ca03ce45c6fada808",
"ui.py": "69fb7fc1794c8842f42e7f0f5a261c4cf64176053ef0a532db46ee6566621219"
},
"installed_versions": {
"informed-decision": "0.2.0",
"PyJWT": "2.13.0",
"cryptography": "50.0.1",
"cffi": "2.1.1",
"pycparser": "3.0",
"waitress": "3.0.2"
},
"requirements_lock_sha256": "539072cb7def057c170c065363d365fe7fb7cf540bef04b2a4c9fab8ee05a7db",
"all_22_runtime_files_match_source": true,
"published": false
},
"verification": {
"command": "INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine INFD_AUDIT_CORE_SOURCE=/home/worsch/audit-core INFD_FLEX_AUTH_BINARY=<compiled-flex-auth> make check",
"tests_passed": 371,
"tests_failed": 0,
"tests_skipped": 0,
"new_tests": 27,
"schema_json_valid": true,
"container_checks_passed": 11,
"container_receipt": "2026-09-11-container-smoke.json",
"container_network": "none",
"fixture_cleanup_complete": true,
"prior_browser_checks": {
"owner_commit": "83849b75d44cab3d01d7ea56f6c839aef8e20046",
"passed": 12,
"rerun_in_this_increment": false
}
},
"scan": {
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
"created_at": "2026-09-10T23:13:55.674183643Z",
"high": 0,
"critical": 0,
"exit_code": 0,
"report": "2026-09-11-container-scan.json",
"warning": "Alpine 3.24 is not on the scanner lifecycle/EOL list; no lifecycle acceptance is inferred."
},
"cluster_schema_check": {
"apply_mode": "dry-run=server",
"candidate_objects": 8,
"exact_namespace_objects_passed": 7,
"refusal": "approval-engine namespace absent; peer ingress cannot be created there yet",
"representative_namespace_schema_check_passed": true,
"representative_change": "metadata namespace/name only for the missing peer rule",
"fixture_policy_pins": true,
"unpublished_image_reference": true,
"cluster_changes_applied": 0,
"schema_only_image_reference": "unpublished pre-shutdown-fix local image 3d2b62f; image bytes are not evaluated by API schema validation"
},
"native_metadata_observations": {
"traefik_namespace": "kube-system",
"traefik_app_label": "traefik",
"websecure_container_port": 8443,
"keycape_public_addresses": [
"92.205.62.239"
],
"storage_class": "local-path",
"storage_provisioner": "rancher.io/local-path"
},
"source_sha256": {
".dockerignore": "fa81d3c056779e8fd5fea58022e42cf9704b16e4d24d75896a2ef2f413f426e8",
".gitignore": "bde5cbd5d9797b283d92cd9c69b12981a2539f4b7fb6d191ab63dd015c4a2cc3",
"Containerfile": "a598563e9b2b4a80fd3c3ac580cf1e63ec44466a893d0068566036487aed20d2",
"Makefile": "15ffb7f2621dacd6145609119994193cd0c7962f01b4e869d4a415200f55e9fa",
"SCOPE.md": "d7e59d6906326846e4bc9ca8300215596fd316b7af9abce55dad67b229b1aa8d",
"deploy/README.md": "5705c1c317834b71087692b2209bbe33b447c5f4ffcbc7b2434cc0b1f87a165b",
"deploy/admission.example.json": "09de47289135c415927d524edf6c20f256a6f10fb467ceebccf08f221e64dfdb",
"docs/evidence/2026-09-11-container-scan.json": "e926951ef4f085542116b8bb9c68c9717688957ad2913b0c94fd141c11f4dd03",
"docs/evidence/2026-09-11-container-smoke.json": "6a71afa39cfd58b1502e300d8a9176905006b0d6bcd4f4989e200dcf371eeb38",
"docs/protected-browser-review.md": "6c9b1323e0eb28553033a9d7c3e5e0f8c2c02350f13ea5e5da1395499baf81c9",
"informed_decision/container.py": "a8e7516ef6a7f110d8ed95493ae3b9492d913cc7a5650b88a519334ba544f9dd",
"informed_decision/web.py": "97f7e040237fa3b755e39d13e628181dc2a3f6a31b4331ca7284bef05f8d6038",
"pyproject.toml": "5ce28585861413c0835699c5e14ac0ae80185fdf342dc1c765f1d9a0d1b6e211",
"requirements.lock": "539072cb7def057c170c065363d365fe7fb7cf540bef04b2a4c9fab8ee05a7db",
"tests/test_container_runtime.py": "475bf316bd9a1a014ccdbdb3267f40f42678d89849dc2e29d8345c27d1fc68e3",
"tests/test_deployment_candidate.py": "296bc1090db5e33e025ccb53d780c0523ab6368278d8cf1778f7df91af13fe50",
"tools/render_deployment.py": "51ea03afeb6c8986dfeb05bd76f4aa7fa8a22e8c6c1e522cc92dcaa6e348e377",
"tools/smoke_container.py": "1385986cbef1d673e7ea3ed57c8d50b5ca34520a298eaf70e1a9af988b210644",
"workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md": "4cc4a953a8e17adacc9d8fc19f1fd2cab441e8e8b47df07d533c2bf2732f25b4"
},
"remaining": [
"native policy package/caller/assignment and exact peer admission",
"AUDIT-WP-0009-T11 custody and protected receiver registration",
"image publication and approved Railiance Apps cutover",
"Approval Engine namespace/service admission and registered human binding",
"native CNI/readiness/audit receipt proof and platform backup/restore operating admission",
"existing T08 broader product acceptance"
],
"native_secret_reads": 0,
"cluster_apply": false,
"factory_attempts": 0,
"paid_model_calls": 0,
"lifecycle_finding": {
"initial_local_image": "sha256:3d2b62f6ce8914b04c4d944d30996331b3c80028a3763da53055cb4b8d71a7c3",
"initial_exit_code": 137,
"corrected_exit_code": 0,
"graceful_stop_window_seconds": 15,
"correction": "explicit PID-1 SIGTERM handler through Waitress and audit-pump shutdown"
},
"integrated_parent_commit": "bb5b607bbd3ae518f3dc101dc6402753564b84ed",
"concurrent_owner_change_preserved": "T07 premise correction; no runtime code changed"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
{
"status": "passed",
"image_id": "sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4",
"repo_digests": [
"informed-decision@sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4"
],
"checks_passed": 11,
"checks": [
"installed container entrypoint serves with projected configuration",
"non-root read-only runtime has no external network or published port",
"private config/database modes enforced; package installer absent",
"configured review refuses anonymous access and reports audit-unready status",
"a second writer cannot acquire the active evidence volume",
"synthetic uncertain intent and undelivered evidence remain explicit",
"consistent backup created while serving; overwrite refused",
"SIGTERM closes the serving writer without forced kill",
"restart preserves immutable content and unresolved submission without retry",
"separate restored volume preserves exact content and unresolved state",
"incomplete owner configuration exits before serving"
],
"fixture_only": true,
"external_network": "none",
"content_snapshot_sha256": "08669bc5e86f292e516045483d9889b05881d0278add97c92a905a58d077d69a",
"native_identity_policy_audit_proven": false,
"published": false,
"deployed": false,
"factory_attempts": 0,
"paid_model_calls": 0,
"cleanup_complete": true
}

View file

@ -108,7 +108,9 @@ needed, so projected rotation does not require storing a token in configuration
or SQLite. Runtime loading provisions nothing. Fixed internal `.svc` origins or SQLite. Runtime loading provisions nothing. Fixed internal `.svc` origins
are supported explicitly; public cleartext origins and redirects are refused. are supported explicitly; public cleartext origins and redirects are refused.
The server remains one Waitress process, loopback port 8080, four threads. The server remains one Waitress process, loopback port 8080, four threads.
Multi-replica sessions/storage and deployment packaging require separate work. Multi-replica sessions/storage require separate work. Single-writer container
packaging and the review-only deployment packet are now supplied in
[deploy/README.md](../deploy/README.md); publication and native rollout remain open.
The audit thread ticks every 30 seconds, generates the declared per-class The audit thread ticks every 30 seconds, generates the declared per-class
heartbeats, drains at most ten records per tick and writes a private heartbeats, drains at most ten records per tick and writes a private

View file

@ -0,0 +1,114 @@
"""Container boundary: private runtime config, local evidence and one writer.
Kubernetes projected configuration is root-owned and symlinked. Copy its bounded
non-secret JSON into an owned ephemeral file; do not weaken Runtime's private
file checks or copy projected bearer credentials into persistent storage.
"""
import argparse
from contextlib import contextmanager
import fcntl
import json
import os
from pathlib import Path
import sqlite3
import signal
import stat
import tempfile
from .store import Store
def private_directory(path):
path = Path(path)
if not path.is_absolute():
raise ValueError("absolute private directory required")
path.mkdir(mode=0o700, exist_ok=True)
info = path.lstat()
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid()
or stat.S_IMODE(info.st_mode) != 0o700):
raise ValueError("private directory has unsafe ownership or mode")
return path
def prepare_configuration(source, *, data_root=Path('/data'), run_root=Path('/run/informed-decision')):
with Path(source).open('rb') as handle:
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
raise ValueError('configuration must be a regular projected file')
raw = handle.read(16385)
if len(raw) > 16384:
raise ValueError('configuration too large')
data = json.loads(raw)
evidence = Path(data_root) / 'private'
if not isinstance(data, dict) or data.get('evidence_db') != str(evidence / 'review.sqlite'):
raise ValueError('container evidence must use its private persistent volume')
private_directory(evidence)
destination = private_directory(Path(run_root) / 'private') / 'runtime.json'
fd, temporary = tempfile.mkstemp(prefix='.runtime-', dir=destination.parent)
try:
with os.fdopen(fd, 'wb') as handle:
handle.write(raw); handle.flush(); os.fsync(handle.fileno())
os.replace(temporary, destination)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return destination, evidence
@contextmanager
def single_writer(evidence):
directory = private_directory(evidence)
fd = os.open(directory / 'serve.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
try:
info = os.fstat(fd)
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
or info.st_nlink != 1 or stat.S_IMODE(info.st_mode) != 0o600):
raise ValueError('unsafe service lock')
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise ValueError('another review service holds this evidence volume') from None
yield
finally:
os.close(fd)
def main():
from .web import main as serve
def terminate(signum, frame):
# PID 1 does not get ordinary default signal behavior. Let Waitress
# drain its dispatcher and web.main stop the audit pump on SIGTERM.
raise SystemExit(0)
signal.signal(signal.SIGTERM, terminate)
try:
config, evidence = prepare_configuration(os.environ.get('INFD_CONTAINER_CONFIG', '/configuration/runtime.json'))
with single_writer(evidence):
os.environ['INFD_REVIEW_CONFIG'] = str(config)
os.environ['INFD_LISTEN_HOST'] = '0.0.0.0'
serve()
except (OSError, ValueError, sqlite3.Error):
# Configuration can be supplied by an operator. Do not echo its values
# or an upstream response on startup failure.
raise SystemExit('Container runtime could not start; inspect configuration and private volume admission.') from None
def admin():
parser = argparse.ArgumentParser(description='Local custody operations; no network or approval mutation.')
commands = parser.add_subparsers(dest='command', required=True)
backup = commands.add_parser('backup', help='SQLite-consistent backup to a new private file')
backup.add_argument('--db', type=Path, required=True)
backup.add_argument('--output', type=Path, required=True)
inspect = commands.add_parser('inspect', help='Report delivery/submission counts, never private content')
inspect.add_argument('--db', type=Path, required=True)
args = parser.parse_args()
if not args.db.is_file():
parser.error('existing evidence database required')
store = Store(args.db)
if args.command == 'backup':
store.backup(args.output)
print(json.dumps({'status': 'backed_up', 'consistent_snapshot': True}))
else:
with store._connection() as db:
print(json.dumps({'schema_version': db.execute('PRAGMA user_version').fetchone()[0],
'outbox': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM outbox GROUP BY state')},
'submissions': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM submissions GROUP BY state')}}))

View file

@ -213,6 +213,9 @@ class App:
def main(): def main():
from waitress import serve from waitress import serve
host = os.environ.get("INFD_LISTEN_HOST", "127.0.0.1")
if host not in ("127.0.0.1", "0.0.0.0"):
raise ValueError("unsupported listener address")
# Waitress does not log request targets; a proxy must also omit callback # Waitress does not log request targets; a proxy must also omit callback
# query strings and cookies. No debug traceback middleware belongs here. # query strings and cookies. No debug traceback middleware belongs here.
login = KeyCapeLogin(os.environ["INFD_KEYCAPE_ISSUER"]) login = KeyCapeLogin(os.environ["INFD_KEYCAPE_ISSUER"])
@ -224,7 +227,7 @@ def main():
app = App(login, runtime.controller if runtime else None, app = App(login, runtime.controller if runtime else None,
readiness=runtime.pump.ready if runtime else lambda: False) readiness=runtime.pump.ready if runtime else lambda: False)
try: try:
serve(app, host="127.0.0.1", port=8080, threads=4) serve(app, host=host, port=8080, threads=4)
finally: finally:
if runtime: if runtime:
runtime.pump.stop() runtime.pump.stop()

View file

@ -7,6 +7,8 @@ dependencies = ["PyJWT[crypto]>=2.10,<3", "waitress>=3,<4"]
[project.scripts] [project.scripts]
informed-decision-web = "informed_decision.web:main" informed-decision-web = "informed_decision.web:main"
informed-decision-container = "informed_decision.container:main"
informed-decision-admin = "informed_decision.container:admin"
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest>=8", "PyYAML>=6,<7"] dev = ["pytest>=8", "PyYAML>=6,<7"]

164
requirements.lock Normal file
View file

@ -0,0 +1,164 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml --python-version 3.12 --generate-hashes --output-file requirements.lock
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
--hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \
--hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \
--hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \
--hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \
--hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \
--hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \
--hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \
--hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \
--hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \
--hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \
--hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \
--hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \
--hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \
--hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \
--hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \
--hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \
--hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \
--hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \
--hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \
--hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \
--hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \
--hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \
--hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \
--hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \
--hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \
--hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \
--hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \
--hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \
--hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \
--hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \
--hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \
--hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
--hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \
--hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \
--hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \
--hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \
--hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \
--hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \
--hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \
--hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \
--hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \
--hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \
--hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \
--hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \
--hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \
--hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \
--hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \
--hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \
--hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \
--hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \
--hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \
--hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \
--hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \
--hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \
--hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \
--hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \
--hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \
--hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \
--hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \
--hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \
--hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \
--hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \
--hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \
--hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \
--hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \
--hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \
--hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \
--hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \
--hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \
--hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \
--hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \
--hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \
--hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \
--hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \
--hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \
--hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \
--hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \
--hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \
--hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \
--hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \
--hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \
--hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \
--hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
--hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \
--hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \
--hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \
--hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \
--hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \
--hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \
--hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \
--hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \
--hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \
--hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \
--hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \
--hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \
--hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
# via cryptography
cryptography==50.0.1 \
--hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \
--hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \
--hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \
--hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \
--hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \
--hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \
--hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \
--hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \
--hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \
--hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \
--hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \
--hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \
--hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \
--hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \
--hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \
--hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \
--hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \
--hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \
--hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \
--hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \
--hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \
--hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \
--hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \
--hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \
--hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \
--hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \
--hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \
--hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \
--hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \
--hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \
--hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \
--hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \
--hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \
--hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \
--hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \
--hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \
--hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \
--hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \
--hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \
--hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \
--hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \
--hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \
--hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
# via pyjwt
pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via informed-decision (pyproject.toml)
waitress==3.0.2 \
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
# via informed-decision (pyproject.toml)

View file

@ -0,0 +1,103 @@
import json
import os
from pathlib import Path
import stat
import pytest
from informed_decision.container import prepare_configuration, private_directory, single_writer
from informed_decision.runtime import Runtime
from informed_decision.store import Store
@pytest.fixture
def projected(tmp_path):
data=tmp_path/'data';data.mkdir()
run=tmp_path/'run';run.mkdir()
config={'schema':'informed-decision.review-runtime.v1','evidence_db':str(data/'private/review.sqlite'),
'approval_origin':'http://approval-engine.approval-engine.svc.cluster.local:8080',
'policy':{'origin':'http://flex-auth-review.flex-auth.svc.cluster.local:8080',
'package':'fixture','version':'v1','package_digest':'sha256:'+'a'*64,
'caller_token_file':str(tmp_path/'not-provisioned-caller')},
'audit':{'origin':'http://audit-core.audit-core.svc.cluster.local:8080',
'sender_token_file':str(tmp_path/'not-provisioned-sender')}}
payload=tmp_path/'projected';payload.write_text(json.dumps(config));payload.chmod(0o444)
link=tmp_path/'runtime.json';link.symlink_to(payload)
return link,data,run,config
def test_projected_config_becomes_private_without_reading_or_copying_credentials(projected):
source,data,run,expected=projected
result,evidence=prepare_configuration(source,data_root=data,run_root=run)
assert not result.is_symlink() and stat.S_IMODE(result.stat().st_mode)==0o600
assert result.stat().st_uid==os.getuid() and json.loads(result.read_text())==expected
assert stat.S_IMODE(evidence.stat().st_mode)==0o700
runtime=Runtime.from_file(result)
assert not runtime.pump.ready()
assert not Path(expected['policy']['caller_token_file']).exists()
assert not Path(expected['audit']['sender_token_file']).exists()
assert sorted(p.name for p in result.parent.iterdir())==['runtime.json']
@pytest.mark.parametrize('fault',['oversize','outside_db','unsafe_directory','symlink_directory'])
def test_unsafe_container_storage_and_config_refused(projected,tmp_path,fault):
source,data,run,config=projected
source.resolve().chmod(0o644)
if fault=='oversize':source.write_text('x'*16385)
if fault=='outside_db':
config['evidence_db']=str(tmp_path/'outside.sqlite');source.write_text(json.dumps(config))
if fault=='unsafe_directory':(data/'private').mkdir(mode=0o755)
if fault=='symlink_directory':(data/'private').symlink_to(run)
with pytest.raises((ValueError,FileExistsError)):
prepare_configuration(source,data_root=data,run_root=run)
assert not (data/'private/review.sqlite').exists()
def test_second_service_cannot_open_the_same_evidence_volume(tmp_path):
private=private_directory(tmp_path/'private')
with single_writer(private):
with pytest.raises(ValueError,match='another review service'):
with single_writer(private):pass
with single_writer(private):pass # Process/holder release permits restart.
@pytest.mark.parametrize('fault',['symlink','hardlink','mode'])
def test_unsafe_service_lock_refused(tmp_path,fault):
private=private_directory(tmp_path/'private');lock=private/'serve.lock'
if fault=='symlink':lock.symlink_to(tmp_path/'outside')
else:
lock.touch(mode=0o600)
if fault=='hardlink':os.link(lock,private/'other')
else:lock.chmod(0o644)
with pytest.raises((ValueError,OSError)):
with single_writer(private):pass
def test_reconfiguration_preserves_persisted_evidence(projected):
source,data,run,config=projected
result,_=prepare_configuration(source,data_root=data,run_root=run)
first=Runtime.from_file(result);digest=first.controller.store.put_document(b'private fixture content')
source.resolve().chmod(0o644)
config['policy']['version']='v2';source.write_text(json.dumps(config))
result,_=prepare_configuration(source,data_root=data,run_root=run)
second=Runtime.from_file(result)
assert second.controller.policy.version=='v2'
with second.controller.store._connection() as db:
assert db.execute('SELECT content FROM documents WHERE digest=?',(digest,)).fetchone()[0]==b'private fixture content'
@pytest.mark.parametrize('host',[None,'0.0.0.0','localhost'])
def test_listener_exposure_requires_explicit_supported_setting(monkeypatch,host):
import waitress
from informed_decision import web
monkeypatch.delenv('INFD_REVIEW_CONFIG',raising=False)
monkeypatch.setenv('INFD_KEYCAPE_ISSUER','https://keycape.test')
monkeypatch.delenv('INFD_LISTEN_HOST',raising=False)
if host:monkeypatch.setenv('INFD_LISTEN_HOST',host)
observed=[]
monkeypatch.setattr(waitress,'serve',lambda app,**kwargs:observed.append(kwargs))
if host=='localhost':
with pytest.raises(ValueError):web.main()
assert not observed
else:
web.main();assert observed[0]['host']==(host or '127.0.0.1')

View file

@ -0,0 +1,77 @@
import copy
import importlib.util
from pathlib import Path
import pytest
spec=importlib.util.spec_from_file_location('render_deployment',Path(__file__).parents[1]/'tools/render_deployment.py')
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
def inputs():
return {'image':'forgejo.coulomb.social/coulomb/informed-decision@sha256:'+'a'*64,
'policy':{'origin':'http://flex-auth-review.flex-auth.svc.cluster.local:8080','package':'review.example',
'version':'v1','package_digest':'sha256:'+'b'*64,'pod_name':'flex-auth-review'},
'keycape_egress_ips':['92.205.62.239'],'storage_class':'local-path'}
def test_candidate_does_not_create_custody_and_uses_one_private_writer():
result=module.render(inputs());objects=result['items']
assert not any(o['kind'] in {'Secret','Role','RoleBinding','ClusterRoleBinding','Ingress','Namespace'} for o in objects)
deploy=next(o for o in objects if o['kind']=='Deployment')['spec']
assert deploy['replicas']==1 and deploy['strategy']=={'type':'Recreate'}
pod=deploy['template']['spec'];container=pod['containers'][0]
assert pod['automountServiceAccountToken'] is False and pod['serviceAccountName']=='review'
assert pod['securityContext']['runAsUser']==pod['securityContext']['fsGroup']==10001
assert container['securityContext']['readOnlyRootFilesystem'] is True
assert container['securityContext']['capabilities']['drop']==['ALL']
caller=next(v for v in pod['volumes'] if v['name']=='caller')
assert caller['projected']['sources']==[{'serviceAccountToken':{'path':'token','audience':'flex-auth','expirationSeconds':3600}}]
# Healthy serving remains available for refusal/recovery; application itself
# gates accept on audit /readyz, without a dependency-driven restart cycle.
assert container['readinessProbe']['httpGet']['path']=='/healthz'
assert container['livenessProbe']['httpGet']['path']=='/healthz'
def test_policies_and_service_selectors_do_not_include_placeholder_or_whole_namespaces():
objects=module.render(inputs())['items']
service=next(o for o in objects if o['kind']=='Service')
assert service['spec']['selector']['app.kubernetes.io/component']=='review'
policy=next(o for o in objects if o['kind']=='NetworkPolicy')['spec']
for rule in policy['ingress']+policy['egress']:
for peer in rule.get('from',rule.get('to',[])):
if 'namespaceSelector' in peer:assert 'podSelector' in peer
assert policy['egress'][-1]['to']==[{'ipBlock':{'cidr':'92.205.62.239/32'}}]
assert policy['egress'][-1]['ports']==[{'protocol':'TCP','port':443}]
assert policy['egress'][-2]['to'][0]['podSelector']['matchLabels']=={'app.kubernetes.io/name':'traefik'}
assert policy['egress'][-2]['ports']==[{'protocol':'TCP','port':8443}]
peers=[o for o in objects if o['kind']=='NetworkPolicy' and o['metadata']['namespace']!='informed-decision']
assert {o['metadata']['namespace'] for o in peers}=={'approval-engine','flex-auth'}
for obj in peers:
caller=obj['spec']['ingress'][0]['from'][0]
assert caller['namespaceSelector']['matchLabels']=={'kubernetes.io/metadata.name':'informed-decision'}
assert caller['podSelector']['matchLabels']['app.kubernetes.io/component']=='review'
@pytest.mark.parametrize('fault',['tag','other_image','missing_pin','external_pdp','wildcard_pod','unknown_field',
'broad_egress','loopback','empty_ips','empty_storage','empty_package'])
def test_incomplete_or_broad_deployment_inputs_refuse(fault):
data=inputs()
if fault=='tag':data['image']='forgejo.coulomb.social/coulomb/informed-decision:latest'
if fault=='other_image':data['image']=data['image'].replace('informed-decision','other')
if fault=='missing_pin':del data['policy']['package_digest']
if fault=='external_pdp':data['policy']['origin']='https://elsewhere.test'
if fault=='wildcard_pod':data['policy']['pod_name']='*'
if fault=='unknown_field':data['token']='not-permitted-inline'
if fault=='broad_egress':data['keycape_egress_ips']=['0.0.0.0/0']
if fault=='loopback':data['keycape_egress_ips']=['127.0.0.1']
if fault=='empty_ips':data['keycape_egress_ips']=[]
if fault=='empty_storage':data['storage_class']=''
if fault=='empty_package':data['policy']['package']=''
with pytest.raises(ValueError):module.render(data)
def test_policy_revision_changes_immutable_configuration_name():
first=inputs();second=copy.deepcopy(first);second['policy']['version']='v2'
names=lambda data:[o['metadata']['name'] for o in module.render(data)['items'] if o['kind']=='ConfigMap']
assert names(first)!=names(second)

131
tools/render_deployment.py Normal file
View file

@ -0,0 +1,131 @@
"""Render a review-only Kubernetes candidate. Never apply or provision custody."""
import argparse
import ipaddress
import json
from pathlib import Path
import re
import hashlib
NAME = 'informed-decision'
SHA = re.compile(r'sha256:[0-9a-f]{64}')
LABEL = {'app.kubernetes.io/name': NAME}
def render(inputs):
if not isinstance(inputs,dict) or set(inputs)!={'image','policy','keycape_egress_ips','storage_class'}:
raise ValueError('exact image, policy, KeyCape egress IPs and storage class inputs required')
if not re.fullmatch(r'forgejo\.coulomb\.social/coulomb/informed-decision@sha256:[0-9a-f]{64}',inputs['image']):
raise ValueError('immutable Informed Decision registry digest required')
policy=inputs['policy']
if not isinstance(policy,dict) or set(policy)!={'origin','package','version','package_digest','pod_name'}:
raise ValueError('exact policy endpoint, package pins and pod selector required')
if not re.fullmatch(r'http://[a-z0-9-]+\.flex-auth\.svc\.cluster\.local:8080',policy['origin']):
raise ValueError('exact in-cluster Flex Auth endpoint required')
if not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',policy['pod_name']):
raise ValueError('exact Flex Auth pod label required')
if not SHA.fullmatch(policy['package_digest']) or any(
not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,127}',policy[key]) for key in ['package','version']):
raise ValueError('exact policy package/version/digest required')
if not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',inputs['storage_class']):
raise ValueError('explicit storage class required')
ips=inputs['keycape_egress_ips']
if not isinstance(ips,list) or not 1<=len(ips)<=4:
raise ValueError('one to four exact admitted KeyCape egress addresses required')
cidrs=[]
for ip in ips:
address=ipaddress.ip_address(ip)
if not address.is_global:
raise ValueError('KeyCape public origin requires exact global addresses')
cidrs.append(str(address)+'/'+str(address.max_prefixlen))
config={'schema':'informed-decision.review-runtime.v1','evidence_db':'/data/private/review.sqlite',
'approval_origin':'http://approval-engine.approval-engine.svc.cluster.local:8080',
'policy':{key:policy[key] for key in ['origin','package','version','package_digest']},
'audit':{'origin':'http://audit-core.audit-core.svc.cluster.local:8080',
'sender_token_file':'/var/run/secrets/informed-decision/audit/token'}}
config['policy']['caller_token_file']='/var/run/secrets/informed-decision/policy/token'
raw=json.dumps(config,sort_keys=True,separators=(',',':'))
config_name=NAME+'-runtime-'+hashlib.sha256(raw.encode()).hexdigest()[:12]
def obj(api,kind,name,**values):
return {'apiVersion':api,'kind':kind,'metadata':{'name':name,'namespace':NAME,
'annotations':{'informed-decision.coulomb.social/admission':'review-only; INFD-WP-0001-T08'}},**values}
container_security={'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,'capabilities':{'drop':['ALL']}}
mount=lambda name,path:{'name':name,'mountPath':path}
readonly=lambda name,path:{**mount(name,path),'readOnly':True}
probe=lambda path:{'httpGet':{'path':path,'port':'http'},'periodSeconds':10,'timeoutSeconds':2}
pod={'automountServiceAccountToken':False,'serviceAccountName':'review','terminationGracePeriodSeconds':45,
'securityContext':{'runAsNonRoot':True,'runAsUser':10001,'runAsGroup':10001,'fsGroup':10001,
'fsGroupChangePolicy':'OnRootMismatch','seccompProfile':{'type':'RuntimeDefault'}},
'containers':[{'name':NAME,'image':inputs['image'],'securityContext':container_security,
'env':[{'name':'INFD_KEYCAPE_ISSUER','value':'https://kc.coulomb.social'},
{'name':'INFD_CONTAINER_CONFIG','value':'/configuration/runtime.json'}],
'ports':[{'name':'http','containerPort':8080}],
'startupProbe':{**probe('/healthz'),'failureThreshold':12},
# Keep read/recovery pages reachable during an audit outage. The
# application gates acceptance on /readyz internally; monitoring
# must scrape it separately. An outage must not restart the writer.
'readinessProbe':probe('/healthz'),'livenessProbe':probe('/healthz'),
'resources':{'requests':{'cpu':'50m','memory':'64Mi'},'limits':{'cpu':'500m','memory':'256Mi'}},
'volumeMounts':[mount('data','/data'),mount('runtime','/run/informed-decision'),mount('tmp','/tmp'),
readonly('configuration','/configuration'),readonly('caller','/var/run/secrets/informed-decision/policy'),
readonly('audit','/var/run/secrets/informed-decision/audit')]}],
'volumes':[{'name':'data','persistentVolumeClaim':{'claimName':NAME+'-data'}},
{'name':'runtime','emptyDir':{'medium':'Memory','sizeLimit':'16Mi'}},
{'name':'tmp','emptyDir':{'medium':'Memory','sizeLimit':'16Mi'}},
{'name':'configuration','configMap':{'name':config_name,'defaultMode':0o444}},
{'name':'caller','projected':{'defaultMode':0o440,'sources':[{'serviceAccountToken':{
'path':'token','audience':'flex-auth','expirationSeconds':3600}}]}},
{'name':'audit','secret':{'secretName':NAME+'-audit','defaultMode':0o440,
'items':[{'key':'token','path':'token'}]}}]}
peer=lambda namespace,name:{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':namespace}},
'podSelector':{'matchLabels':{'app.kubernetes.io/name':name}}}
http=[{'protocol':'TCP','port':8080}]
objects=[
obj('v1','ServiceAccount','review',automountServiceAccountToken=False),
obj('v1','PersistentVolumeClaim',NAME+'-data',spec={'accessModes':['ReadWriteOnce'],
'storageClassName':inputs['storage_class'],'resources':{'requests':{'storage':'1Gi'}}}),
obj('v1','ConfigMap',config_name,immutable=True,data={'runtime.json':raw}),
obj('apps/v1','Deployment',NAME,spec={'replicas':1,'strategy':{'type':'Recreate'},'progressDeadlineSeconds':600,
'selector':{'matchLabels':LABEL},'template':{'metadata':{'labels':{**LABEL,'app.kubernetes.io/component':'review'}},'spec':pod}}),
obj('v1','Service',NAME,spec={'type':'ClusterIP','selector':{**LABEL,'app.kubernetes.io/component':'review'},
'ports':[{'name':'http','port':80,'targetPort':'http','protocol':'TCP'}]}),
obj('networking.k8s.io/v1','NetworkPolicy',NAME+'-review',spec={'podSelector':{'matchLabels':{**LABEL,
'app.kubernetes.io/component':'review'}},'policyTypes':['Ingress','Egress'],
'ingress':[{'from':[peer('kube-system','traefik')],'ports':http}],
'egress':[{'to':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'kube-system'}},
'podSelector':{'matchLabels':{'k8s-app':'kube-dns'}}}],
'ports':[{'protocol':'UDP','port':53},{'protocol':'TCP','port':53}]},
{'to':[peer('approval-engine','approval-engine'),peer('audit-core','audit-core'),
peer('flex-auth',policy['pod_name'])],'ports':http},
# Traefik's observed websecure target is 8443. Include
# its exact peer for CNI enforcement after Service DNAT.
{'to':[peer('kube-system','traefik')],'ports':[{'protocol':'TCP','port':8443}]},
{'to':[{'ipBlock':{'cidr':cidr}} for cidr in cidrs],'ports':[{'protocol':'TCP','port':443}]}]})]
# Counterparty proposals travel in the review packet. Neither the existing
# Approval Engine namespace gate nor other Flex callers admits this pod.
# Audit Core already owns its prepared exact informed-decision ingress.
for namespace,name,task in [('approval-engine','approval-engine','APPROVAL-WP-0002-T01'),
('flex-auth',policy['pod_name'],'INFD-WP-0001-T08 policy-owner return')]:
item=obj('networking.k8s.io/v1','NetworkPolicy',NAME+'-review-ingress',spec={
'podSelector':{'matchLabels':{'app.kubernetes.io/name':name}},'policyTypes':['Ingress'],
'ingress':[{'from':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':NAME}},
'podSelector':{'matchLabels':{**LABEL,'app.kubernetes.io/component':'review'}}}],'ports':http}]})
item['metadata']['namespace']=namespace
item['metadata']['annotations']['informed-decision.coulomb.social/owner-review']=task
objects.append(item)
return {'apiVersion':'v1','kind':'List','items':objects}
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--input',type=Path,required=True)
parser.add_argument('--output',type=Path)
args=parser.parse_args()
try: result=render(json.loads(args.input.read_text()))
except (ValueError,TypeError,KeyError,OSError) as exc:parser.error(str(exc))
raw=json.dumps(result,indent=2)+'\n'
if args.output:args.output.write_text(raw)
else:print(raw,end='')
if __name__=='__main__':main()

150
tools/smoke_container.py Normal file
View file

@ -0,0 +1,150 @@
"""Exercise a local image, private volumes and restore without external network.
Only synthetic records/tokens are created. No native issuer, policy, audit or
Approval Engine is contacted. Every container and volume is created by this
process, labeled, and removed in finally; the image is retained for inspection.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
import uuid
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--image',required=True)
parser.add_argument('--receipt',type=Path,required=True)
args=parser.parse_args()
prefix='infd-proof-'+uuid.uuid4().hex[:12]
volumes=[];containers=[];checks=[]
def docker(*words,check=True):
return subprocess.run(['docker',*words],capture_output=True,text=True,check=check,timeout=60)
def command(name,code):
return docker('exec',name,'python','-c',code).stdout.strip()
def admin(name,*args):
return json.loads(docker('exec',name,'informed-decision-admin',*args).stdout)
def stop(name):
docker('stop','--time','15',name)
state=json.loads(docker('inspect',name).stdout)[0]['State']
assert state['ExitCode']==0, 'service did not stop gracefully: '+str(state['ExitCode'])
def wait_health(name):
for _ in range(40):
result=docker('exec',name,'python','-c',
"import urllib.request; assert urllib.request.urlopen('http://127.0.0.1:8080/healthz',timeout=1).status==200",check=False)
if result.returncode==0:return
state=json.loads(docker('inspect',name).stdout)[0]['State']
if not state['Running']:raise AssertionError('container exited before health; '+docker('logs',name).stdout)
time.sleep(.2)
raise AssertionError('container did not become healthy')
def volume(suffix):
name=prefix+'-'+suffix
docker('volume','create','--label','informed-decision.fixture='+prefix,name);volumes.append(name)
docker('run','--rm','--network','none','--read-only','--user','0:0','--cap-drop','ALL','--cap-add','CHOWN',
'--mount','type=volume,src='+name+',dst=/data','--entrypoint','python',args.image,'-c',
"import os; os.chown('/data',0,10001); os.chmod('/data',0o2770)")
return name
def start(suffix,data,fixture,backup=None):
name=prefix+'-'+suffix;containers.append(name)
words=['run','-d','--name',name,'--label','informed-decision.fixture='+prefix,
'--network','none','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges',
'--tmpfs','/run/informed-decision:rw,nosuid,nodev,noexec,uid=10001,gid=10001,mode=0700',
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,mode=1777',
'--mount','type=volume,src='+data+',dst=/data',
'--mount','type=bind,src='+str(fixture)+',dst=/configuration,readonly',
'--env','INFD_KEYCAPE_ISSUER=https://kc.coulomb.social']
if backup:words+=['--mount','type=volume,src='+backup+',dst=/backup']
docker(*words,args.image);return name
snapshot="""import hashlib,json
from informed_decision.store import Store
s=Store('/data/private/review.sqlite')
with s._connection() as db:
rows={t:[list(r) for r in db.execute('SELECT * FROM '+t+' ORDER BY rowid')] for t in ['memos','presentations','dispositions','submissions','evidence','documents']}
rows['documents']=[[r[0],r[1],hashlib.sha256(r[2]).hexdigest()] for r in rows['documents']]
print(hashlib.sha256(json.dumps(rows,sort_keys=True).encode()).hexdigest())
"""
seed="""from informed_decision.store import Store
from informed_decision.memo import Memo,BindingSlice,Principal,Scope,BindingLevel,StepKind,PacketItem
from informed_decision.provenance import Claim,Route
from informed_decision.disposition import Actor,ActorKind,Verb
s=Store('/data/private/review.sqlite');digest=s.put_document(b'Synthetic retained content; no real approval.')
m=Memo(id='container-fixture',version=1,question='Synthetic custody exercise?',requested_act='deliver',binding_level=BindingLevel.ORGANIZATIONAL,brief='Fixture only',binding=BindingSlice(Principal('fixture-human','person','Fixture'),Scope('resource','fixture','Fixture')),step_kind=StepKind.APPROVE,packet=(PacketItem('doc','Fixture',digest),),approval_id='fixture',approval_binding_digest='sha256:'+'1'*64)
s.save_memo(m)
p=s.present(m.id,principal_sub='fixture-human',tenant=Claim('tenant:platform',Route.REGISTRATION),principal_type=Claim('human',Route.AUTHENTICATION))
d=s.record_disposition(p.id,Actor('fixture-human',ActorKind.PERSON),Verb.ACCEPT,operation_id='container-fixture-op')
attempt=s.begin_submission(d.id);s.finish_submission(d.id,attempt)
assert s.submission(d.id)['state']=='unresolved'
"""
try:
with tempfile.TemporaryDirectory(prefix=prefix+'-') as temp:
fixture=Path(temp);fixture.chmod(0o755)
(fixture/'audit-token').write_text('synthetic-audit-token');(fixture/'caller-token').write_text('synthetic-caller-token')
config={'schema':'informed-decision.review-runtime.v1','evidence_db':'/data/private/review.sqlite',
'approval_origin':'http://127.0.0.1:18082','policy':{'origin':'http://127.0.0.1:18083',
'package':'container.fixture','version':'v1','package_digest':'sha256:'+'a'*64,
'caller_token_file':'/configuration/caller-token'},
'audit':{'origin':'http://127.0.0.1:18084','sender_token_file':'/configuration/audit-token'}}
(fixture/'runtime.json').write_text(json.dumps(config))
for file in fixture.iterdir():file.chmod(0o444)
data=volume('data');backup=volume('backup')
first=start('first',data,fixture,backup);wait_health(first)
checks.append('installed container entrypoint serves with projected configuration')
info=json.loads(docker('inspect',first).stdout)[0]
assert info['Config']['User']=='10001:10001' and info['HostConfig']['ReadonlyRootfs']
assert info['HostConfig']['NetworkMode']=='none' and not info['NetworkSettings']['Ports'].get('8080/tcp')
checks.append('non-root read-only runtime has no external network or published port')
command(first,"import os,stat,importlib.util; assert os.getuid()==10001; assert importlib.util.find_spec('pip') is None; assert stat.S_IMODE(os.stat('/data/private').st_mode)==0o700; assert stat.S_IMODE(os.stat('/data/private/review.sqlite').st_mode)==0o600; assert stat.S_IMODE(os.stat('/run/informed-decision/private/runtime.json').st_mode)==0o600")
checks.append('private config/database modes enforced; package installer absent')
command(first,"import urllib.request,urllib.error\nfor path,status in [('/readyz',503),('/review?memo_id=missing',401)]:\n try: urllib.request.urlopen('http://127.0.0.1:8080'+path)\n except urllib.error.HTTPError as e: assert e.code==status\n else: raise AssertionError(path)")
checks.append('configured review refuses anonymous access and reports audit-unready status')
command(first,"from informed_decision.container import single_writer\ntry:\n with single_writer('/data/private'): pass\nexcept ValueError: pass\nelse: raise AssertionError('second writer admitted')")
checks.append('a second writer cannot acquire the active evidence volume')
command(first,seed)
state=admin(first,'inspect','--db','/data/private/review.sqlite')
assert state['submissions']=={'unresolved':1} and state['outbox'].get('pending',0)>0
original=command(first,snapshot)
checks.append('synthetic uncertain intent and undelivered evidence remain explicit')
command(first,"from informed_decision.container import private_directory; private_directory('/backup/private')")
result=admin(first,'backup','--db','/data/private/review.sqlite','--output','/backup/private/review.sqlite')
assert result['consistent_snapshot'] is True
repeat=docker('exec',first,'informed-decision-admin','backup','--db','/data/private/review.sqlite','--output','/backup/private/review.sqlite',check=False)
assert repeat.returncode!=0
checks.append('consistent backup created while serving; overwrite refused')
stop(first)
checks.append('SIGTERM closes the serving writer without forced kill')
docker('start',first);wait_health(first)
assert command(first,snapshot)==original
assert admin(first,'inspect','--db','/data/private/review.sqlite')['submissions']=={'unresolved':1}
checks.append('restart preserves immutable content and unresolved submission without retry')
stop(first)
restored=start('restored',backup,fixture);wait_health(restored)
assert command(restored,snapshot)==original
assert admin(restored,'inspect','--db','/data/private/review.sqlite')['submissions']=={'unresolved':1}
checks.append('separate restored volume preserves exact content and unresolved state')
stop(restored)
(fixture/'runtime.json').chmod(0o644);(fixture/'runtime.json').write_text('{}');(fixture/'runtime.json').chmod(0o444)
invalid=start('invalid',data,fixture)
code=int(docker('wait',invalid).stdout)
assert code!=0
checks.append('incomplete owner configuration exits before serving')
image=json.loads(docker('image','inspect',args.image).stdout)[0]
receipt={'status':'passed','image_id':image['Id'],'repo_digests':image['RepoDigests'],
'checks_passed':len(checks),'checks':checks,'fixture_only':True,'external_network':'none',
'content_snapshot_sha256':original,'native_identity_policy_audit_proven':False,
'published':False,'deployed':False,'factory_attempts':0,'paid_model_calls':0}
finally:
for name in reversed(containers):docker('rm','-f',name,check=False)
for name in reversed(volumes):docker('volume','rm',name,check=False)
receipt['cleanup_complete']=all(docker('inspect',name,check=False).returncode!=0 for name in containers)
receipt['cleanup_complete'] &= all(docker('volume','inspect',name,check=False).returncode!=0 for name in volumes)
assert receipt['cleanup_complete']
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps({'status':'passed','checks_passed':len(checks),'cleanup_complete':True,'image_id':receipt['image_id']}))
if __name__=='__main__':main()

View file

@ -623,6 +623,50 @@ not be silently claimed by the bounded factory profile. The exact operational
setup and remaining limits are in `docs/protected-browser-review.md`. setup and remaining limits are in `docs/protected-browser-review.md`.
T08 remains `progress`; no residual has been hidden by finishing the workplan. T08 remains `progress`; no residual has been hidden by finishing the workplan.
2026-09-11 — **container and deployment candidate prepared and exercised.**
`Containerfile` and hashed dependency lock build the installed wheel on the
digest-pinned Alpine base. The runtime has no package installer, runs as 10001
on a read-only root filesystem, and copies bounded projected configuration into
owned ephemeral 0600 storage. It keeps private persistent evidence at 0700/0600
and takes a process-lifetime lock before opening the serving runtime. This
preserves the store's checks despite Kubernetes projected-file ownership and
prevents a second service from sharing its evidence volume.
The installed custody CLI creates SQLite-consistent backups and reports only
schema/delivery/submission counts. Eleven real-container checks passed with no
external network or published port: restart and restore onto a second volume
preserved exact content, undelivered evidence and a synthetic unresolved intent.
All created containers/volumes were removed. The full suite passes 371 tests
(27 added). Runtime files in the image match all 22 source modules; dependencies
match the hashed lock. Trivy returned zero HIGH/CRITICAL findings; its Alpine
lifecycle-list warning is retained in the receipt.
The final lifecycle check exposed a real PID-1 shutdown defect: the first image
needed a forced kill (exit 137). An explicit SIGTERM handler now lets Waitress
drain and the audit pump stop; the final image exits cleanly within 15 seconds.
Recreate rollout and the 45-second pod termination window retain one writer.
The renderer prepares a Recreate Deployment, scoped projected caller token,
immutable runtime config, PVC/Service and exact network rules. Kubernetes serving
health remains separate from audit acceptance readiness, preserving refusal/
recovery access during an audit outage. The source now includes exact ingress
proposals for Approval Engine and the admitted PDP, because the existing caller
rules do not automatically admit this pod. No namespace-wide caller label is
added and no Secret, RBAC, policy assignment or Ingress is created.
Seven objects passed server dry-run in their exact namespaces. The eighth,
Approval Engine ingress, found its namespace absent. Its unchanged policy shape
passed in a representative existing namespace; that is schema validation only.
Observed Traefik label/websecure port, KeyCape public IP and local-path storage
were used to make the candidate concrete, not to claim CNI or custody proof.
Evidence: `docs/evidence/2026-09-11-container-candidate.json`; operating packet:
`deploy/README.md`. The image is local and unpublished. T08 stays `progress` for
the admitted native policy/caller/assignments, AUDIT-WP-0009-T11 custody, image
publication, owner service/namespace and exact peer admission, registration and
real human/deployed binding, platform backup/restore and product acceptance.
No cluster apply, native secret read, factory attempt or paid call occurred.
## Known risks ## Known risks
- **T02 is a hard gate.** Writing the blueprint before the layer ruling risks - **T02 is a hard gate.** Writing the blueprint before the layer ruling risks