secrets-engine/docs/exec-owner-binding.md
tegwick bc58184d71
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Add companion lanes to catalog-bound exec owners (SECRETS-WP-0011 T01-T03)
A configured exec owner may receive fields from other consenting kv lanes.
Each lane is gated, consumed and read through its own AppRole; any refusal
starts no child. Companions are part of the owner digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 226514@bnt-lap001
Assistant-Session: 26ba103d-05fe-45a1-9cd7-9475bf239df6
2026-09-23 17:27:09 +02:00

6.7 KiB

Bind native exec delivery to its reviewed recipient

SECRETS-WP-0009-T03. Implemented 2026-09-10. This constrains delivery; it is not an approval or proof of native credential access.

Previously exec consumed approval for a catalog/action/field, then accepted an arbitrary child command and copied the invoking engine's full environment. The factory recipient is now a pinned metered owner outside the sandbox. A different command or inherited engine credential would violate that boundary.

Catalog delivery_config.exec_owner is optional for existing lanes. The Claude factory lane explicitly requires it and currently declares status: pending, owner and reason. Pending means no exec: refusal precedes approval consumption, backend opening and secret retrieval. Routing stays unready even if custody exists.

A reviewed binding uses exactly these keys:

Key Required content
status configured; this describes configuration, not authorization
owner Named trusted recipient
command Exact argv; absolute executable, no caller-selected additional arguments
cwd Exact private owner directory
environment Complete non-secret environment; no inheritance from the engine
files Absolute path → {sha256: <64 lowercase hex>, private: <boolean>}

The executable and every absolute file argument require pins. Include the exact owner JSON and all immutable files it references, such as the spend-policy and accepted profile/descriptor configuration. Mutable ledgers are private state, not immutable file pins. The runtime owner retains whole-artifact and operational profile verification; hashing an interpreter alone does not verify its packages.

File checks refuse symlinks, nonregular inputs, untrusted owners, group/world writable paths and ancestors, changed bytes and exposed private inputs. Root-owned sticky temporary ancestors are allowed for contained proof. The working directory is private. The engine and configured owner UID remain trusted: this does not protect against that same trusted UID replacing files between checks and launch. Runtime and native service admission must preserve this trust boundary.

Only exec-env is allowed with this binding. The child receives the fixed environment plus the selected declared field; loader overrides and engine/provider auth variables cannot be supplied as fixed environment configuration. No values belong in the catalog. Queue/service credentials, if needed by the owner, require their separate admitted custody path. Child stdin is closed; stdout/stderr retain the existing value redactor. An arbitrary encoding by a malicious recipient is not made safe by redaction; the recipient itself must be trusted and admitted.

SHA-256 of the complete canonical binding is carried as context.exec_owner_sha256 on the exec CheckRequest. The existing producer digest and approval join bind it without introducing another decision type. Changing command, file pins, environment, cwd or owner changes the request and requires new exact-action approval. Local checks run before approval/backend, again before retrieval, and after retrieval before launch; the admitted digest must still match. Configured bindings still pass existing approval, consume, lane-state and scoped backend checks. Other actions and lanes without this opt-in retain their contracts.

Factory activation input

Use the installed artifact and paths in prj-helixforge-factory/evidence/2026-09-10-runtime-placement.json. The intended argv:

<protected-runtime>/bin/python3 -I -B -m rein_aharness.cli metered-once --owner-config <private-owner-config>

Do not use the rolling CLI alias or a persistent claim loop. Complete and pin the private owner configuration, profile and spend inputs under HFACT-WP-0001-T01/T04; bind the required worker environment explicitly. Then review the MessagesOwner holder under this task and the existing native action/reader gates. The provider key stays in the metered owner; the workload receives the opaque local route token. The historical sandbox-helper child-key route is not this holder admission.

Verification

The full suite passed 361 tests, including a real child with a synthetic key, fixed environment, closed stdin, redaction and substituted/config-changed refusal. The disposable integration exercise passed 18 checks using the pinned KeyCape image, actual Approval Engine and compiled Flex Auth, including five new owner checks: substituted argv refusal before consume/backend, changed-environment replay refusal, consume-before-backend, isolated child environment and preserved recipient digest. OpenBao and recipient are fixtures in this exercise. The historical combined operator identity is a synthetic test issuer fixture, not a request to restore CCR-2026-0020 or evidence of real human approval.

uv run --extra dev --with 'PyJWT[crypto]>=2.7,<3' python tools/exercise_approval_identity.py \
  --keycape-source ../key-cape --approval-engine-source ../approval-engine \
  --flex-auth-source ../flex-auth --exec-owner --receipt /tmp/<new-receipt>.json

Receipt: docs/evidence/2026-09-10-exec-owner-approval-exercise.json. Native custody, exact operator group/file delivery, real human/audit/service path, accepted factory configuration and paid execution remain open.

Companion lanes (SECRETS-WP-0011)

A configured owner may list extra lanes to deliver alongside the primary field:

exec_owner:
  status: configured
  # command, cwd, environment, files as above
  companions:
    - {catalog: <kv-lane-id>, field: <declared-field>, env: <ENV_NAME>}

The companion lane must consent in its own catalog entry with delivery_config.companion_of: [<primary-lane-id>]. It must be a kv lane in the same stage, declare the field and exec-env, and bind no exec owner of its own. Env names must be unique, must not match the fixed environment or the primary field's name, and must not use loader or engine credential prefixes. Pending owners cannot list companions.

Companions are part of the owner binding, so changing a companion's lane, field or env name changes the owner digest and invalidates earlier decisions.

At exec time, companions are resolved before any approval is consumed. Each lane then gets its own privileged evidence record, stance, approval and consume for action exec. No lane's decision covers another lane. After every gate passes, each value is read through its own lane's AppRole session. A failure on any lane starts no child. The binding is checked again after the reads, and all values are injected together and redacted from the output.

Proof: tests/test_exec_owner_companions.py, plus tests/test_integration_companions.py on a throwaway OpenBao (two lanes, one owner, and the primary AppRole denied the companion path).