From 5c6f2b319d1eb43a316bdce38b292f39a9ac2269 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 27 Sep 2026 15:58:53 +0200 Subject: [PATCH] Enforce companion-only credential delivery and refresh activation handoff Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e324-abce-7e51-bb2b-496f097afdb0 --- docs/drafts/glas-exec-owner-configured.yaml | 5 ++- docs/exec-owner-binding.md | 7 ++- docs/glas-claude-delivery.md | 34 +++++++------- src/secrets_engine/exec_delivery.py | 18 +++++++- src/secrets_engine/exec_owner.py | 2 + tests/test_exec_owner_companions.py | 44 +++++++++++++++++++ ...ETS-WP-0009-glas-claude-native-delivery.md | 20 ++++++++- ...-WP-0011-multi-lane-exec-owner-delivery.md | 44 ++++++++++++++++++- 8 files changed, 150 insertions(+), 24 deletions(-) diff --git a/docs/drafts/glas-exec-owner-configured.yaml b/docs/drafts/glas-exec-owner-configured.yaml index 2de3b41..5ea4e55 100644 --- a/docs/drafts/glas-exec-owner-configured.yaml +++ b/docs/drafts/glas-exec-owner-configured.yaml @@ -1,6 +1,7 @@ # Draft exec_owner for glas-claude-agent-dev-anthropic (SECRETS-WP-0009-T03). -# Not in the catalog: it replaces the pending binding only after -# activity-core-metered-worker-token is seeded and ACTIVITY-WP-0039 is live. +# Not in the catalog. Activity Core reports custody and identity live as of +# 2026-09-24 (ACTIVITY-WP-0039). Admission still requires current owner/pin +# validation and exact per-lane approvals for attended native activation. exec_owner: status: configured owner: rein-aharness MessagesOwner (metered-once) with sand-boxer runtime boundary diff --git a/docs/exec-owner-binding.md b/docs/exec-owner-binding.md index 60c6c7f..0ddc922 100644 --- a/docs/exec-owner-binding.md +++ b/docs/exec-owner-binding.md @@ -109,7 +109,9 @@ The companion lane must consent in its own catalog entry with 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. +Pending owners cannot list companions. A lane declaring `companion_of` cannot +be selected as the primary `exec` lane: it is delivered only through a listed +primary with a configured owner, even if standalone lane approval exists. 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. @@ -120,6 +122,9 @@ 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. +The delivery helper independently checks that the supplied companions exactly +match the pinned lane/field/environment list and still satisfy consent and stage +constraints before reading the primary or any companion. Proof: `tests/test_exec_owner_companions.py`, plus `tests/test_integration_companions.py` on a throwaway OpenBao (two lanes, one diff --git a/docs/glas-claude-delivery.md b/docs/glas-claude-delivery.md index a11ee67..0b20059 100644 --- a/docs/glas-claude-delivery.md +++ b/docs/glas-claude-delivery.md @@ -37,23 +37,27 @@ approval, OpenBao access, provider authentication or production readiness. ## Activation requirements -The current engine's production stance refuses before opening the backend: -`production action 'exec' requires a durable access-engine decision record; -live production remains disabled`. This refusal was exercised with the proposed -catalog and service-jwt selection. No real value was requested. +As of 2026-09-27, the shared approval/consume/PDP chain has live evidence from +SECRETS-WP-0010-T03. The Glas catalog still has a pending owner binding and +refuses exec before approval consumption or backend access. The earlier lack +of a served decision path is no longer the current activation blocker. -Activation depends on SECRETS-WP-0007-T04 (exact production actions) and -SECRETS-WP-0008-T02/T06 (decision consumption and service authority). Require -canonical validated DecisionEnvelope for each protected action, successful consume, -and exact scoped backend authority. This draft cannot authorize itself; an -operator browser token or unsafe-demo flag is not a runtime substitute. +The metered owner configuration and binding were prepared on 2026-09-23. +Activity Core reports ACTIVITY-WP-0039 complete on 2026-09-24: custody and the +separate `rein-aharness-metered@railiance01` identity are live. See the exact +handoff in SECRETS-WP-0011. Its worker token is companion-only; direct exec of +`activity-core-metered-worker-token` is refused. The intended recipient is the +metered MessagesOwner described in [exec owner binding](exec-owner-binding.md), +not the historical sandbox helper above. -Once those services exist: obtain the reviewed apply authorization, apply this -exact policy/AppRole with scoped authority, verify positive read and denied -metadata/sibling/write access without exposing values, and record delivery-ready -state. Bind approved exec authorization and named engine service authentication -to the sand-boxer owner route. Prove actual provider authentication and a bounded -Glas task, then activate routing and only the validated profile. +SECRETS-WP-0009-T03 still owns current recipient/pin admission and the attended +activation. Review the draft binding, revalidate installed files and private +state, configure the approved owner, and obtain exact per-action/per-lane +approvals. Apply the scoped policy/AppRole, verify positive read and denied +metadata/sibling/write access with an unrelated negative identity, then prove +bounded owner delivery and session revocation. Both lanes must independently +pass approval, PDP, consume and delivery readiness. The handoff and draft are +not runtime authorization. No production activation was performed in this review. Rotation: store replacement with CAS, stop old runs, verify replacement, revoke predecessor at Anthropic and prove denial. Bao session expiration does not revoke diff --git a/src/secrets_engine/exec_delivery.py b/src/secrets_engine/exec_delivery.py index f86c2b1..50cc485 100644 --- a/src/secrets_engine/exec_delivery.py +++ b/src/secrets_engine/exec_delivery.py @@ -221,14 +221,28 @@ def exec_with_secret( f"(allowed {sorted(declared)})" ) + # The caller's resolved lanes must exactly match the pinned recipient. + # Refuse missing/substituted companions before reading even the primary. + from secrets_engine.exec_owner import companion_specs, resolve_companions + + supplied = [ + {"catalog": lane.id, "field": lane_field, "env": env_name} + for lane, lane_field, env_name in companions + ] + if supplied != companion_specs(entry): + raise DeliveryError("companion delivery differs from the catalog-bound exec owner") + if companions: + if binding_digest is None or mode != "exec-env": + raise DeliveryError("companion delivery requires a configured exec owner and exec-env") + lanes = {lane.id: lane for lane, _, _ in companions} + resolve_companions(entry, lanes.__getitem__) + if session_evidence is None: value = _fetch_value(client, entry, field) else: value = _fetch_value( client, entry, field, session_evidence=session_evidence ) - if companions and (binding_digest is None or mode != "exec-env"): - raise DeliveryError("companion delivery requires a configured exec owner and exec-env") # Every lane is read through its own AppRole session; any failure raises # before a child exists, and the values already read go out of scope. extra: dict[str, str] = {} diff --git a/src/secrets_engine/exec_owner.py b/src/secrets_engine/exec_owner.py index 19e8139..8fee16f 100644 --- a/src/secrets_engine/exec_owner.py +++ b/src/secrets_engine/exec_owner.py @@ -152,6 +152,8 @@ def _check_path(path: Path, *, directory: bool = False, private: bool = False) - def validate_delivery_target(entry, field: str, command: list[str], mode: str) -> str | None: + if entry.delivery_config.get("companion_of"): + raise DeliveryError("companion-only lane requires delivery through its bound primary owner") binding = owner_binding(entry) if binding is None: return None diff --git a/tests/test_exec_owner_companions.py b/tests/test_exec_owner_companions.py index cd2d94d..3ac7f41 100644 --- a/tests/test_exec_owner_companions.py +++ b/tests/test_exec_owner_companions.py @@ -179,3 +179,47 @@ def test_unresolvable_companion_refuses_before_any_gate(bound, monkeypatch, tmp_ command = data["delivery_config"]["exec_owner"]["command"] with pytest.raises(DeliveryError, match="unavailable"): cli.cmd_exec(_cfg(tmp_path), SimpleNamespace(field=None, catalog=entry.id, command=command, mode="exec-env")) + + +def test_companion_only_lane_refuses_standalone_exec_before_gate(monkeypatch, tmp_path): + lane = validate_entry(_companion()) + monkeypatch.setattr(cli, "get_entry", lambda *a: lane) + monkeypatch.setattr(cli, "_require_lane_approval", lambda *a, **k: pytest.fail("no gate")) + monkeypatch.setattr(cli, "_open_backend", lambda *a, **k: pytest.fail("no backend")) + with pytest.raises(DeliveryError, match="companion-only"): + cli.cmd_exec(_cfg(tmp_path), SimpleNamespace( + field=None, catalog=lane.id, command=["/bin/sh"], mode="exec-env", + )) + + +def test_companion_only_lane_refuses_direct_delivery_before_read(monkeypatch): + lane = validate_entry(_companion()) + monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read")) + with pytest.raises(DeliveryError, match="companion-only"): + exec_delivery.exec_with_secret(object(), lane, "worker_token", ["/bin/sh"]) + + +@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"]) +def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change): + data, _, _ = bound + entry = validate_entry(_with_companion(data)) + lane_data = _companion() + if change == "consent": + lane_data["delivery_config"] = {} + elif change == "stage": + lane_data.update(stage="build", path="build/team/worker") + elif change == "lane": + lane_data["id"] = "another-worker" + lane = validate_entry(lane_data) + companions = [(lane, "worker_token", "WORKER_TOKEN")] + if change == "missing": companions = [] + elif change == "extra": companions *= 2 + elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")] + elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")] + monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read")) + monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child")) + with pytest.raises(DeliveryError, match="companion"): + exec_delivery.exec_with_secret( + object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"], + companions=companions, + ) diff --git a/workplans/SECRETS-WP-0009-glas-claude-native-delivery.md b/workplans/SECRETS-WP-0009-glas-claude-native-delivery.md index 38bfa04..e447558 100644 --- a/workplans/SECRETS-WP-0009-glas-claude-native-delivery.md +++ b/workplans/SECRETS-WP-0009-glas-claude-native-delivery.md @@ -8,7 +8,7 @@ status: blocked flavor: implementation owner: codex created: "2026-09-05" -updated: "2026-09-21" +updated: "2026-09-27" state_hub_workstream_id: "40ccc3b4-d046-5a58-8649-e7935f45c974" --- @@ -50,7 +50,7 @@ synthetic exec-env transport proof passed; no real secret was read. id: SECRETS-WP-0009-T03 status: wait priority: high -blocking_reason: "Shared native chain proved by SECRETS-WP-0010-T03 (2026-09-16): Approval Engine, CCR-2026-0019 reader, requester client, Informed Decision human review, current PDP. Lane-specific residue: operator inputs (placement, spend envelope/model bounds, unrelated negative identity), private SpendPolicy/ledger and owner config (HFACT-WP-0001-T01/T04), configured exec_owner, three human approvals, attended apply/verify/exec/revoke." +blocking_reason: "Shared native chain proved by SECRETS-WP-0010-T03. Metered owner provisioned and binding drafted (2026-09-23); Activity Core reports identity live (2026-09-24). Remaining: current recipient/pin admission, unrelated negative identity, configured exec_owner, exact per-action/per-lane approvals and attended apply/verify/exec/revoke." state_hub_task_id: "f8069c8a-ad6b-5d0b-9a36-c2326699437d" ``` @@ -487,3 +487,19 @@ that it answers before the attended session. Founder decision 2026-09-23, relayed to activity-core (thread `a5449d3f`): the claim-loop token is minted fresh with a coordinated claim-loop restart, not moved (ACTIVITY-WP-0039-T03). + + +### 2026-09-27 Activity Core dependency resolved + +Activity Core's 2026-09-24 handoff (`a2eae5f8`, `bfef619d`; source `54ab1e4`) +reports the metered identity live, HTTP 200 for its own no-match claim and HTTP +403 for a claim as the loop identity. SECRETS-WP-0011 records that return and +corrects its former wait reason. The drafted worker identity and label match. + +This resolves the seed/rollout prerequisite in the 2026-09-23 note. It does not +activate the native lane. T03 still needs current recipient admission and file +pin validation, exact approvals for each protected action/lane, and attended +native apply, positive/negative verification, exec and session revocation. +The production catalog remains pending. The companion-only delivery guards +added under SECRETS-WP-0011-T05 must be included in the host checkout used for +activation. No credential read, queue claim or paid run occurred in this review. diff --git a/workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md b/workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md index 38b0695..b756f37 100644 --- a/workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md +++ b/workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md @@ -9,7 +9,7 @@ flavor: implementation owner: claude-code topic_slug: netkingdom created: "2026-09-23" -updated: "2026-09-23" +updated: "2026-09-27" related_workplans: - SECRETS-WP-0009 - HFACT-WP-0001 @@ -93,7 +93,7 @@ a throwaway OpenBao dev server. id: SECRETS-WP-0011-T04 status: wait priority: high -blocking_reason: "Lane cataloged; token seeded (ACTIVITY-WP-0039-T03 done 2026-09-23). Waits on T04 cutover: railiance-platform store policy and founder go-ahead. Do not apply or deliver until activity-core reports the metered identity authenticates." +blocking_reason: "Activity Core reports ACTIVITY-WP-0039 finished and metered identity authentication proved (2026-09-24). Remaining: admit the configured Glas owner, obtain exact per-lane approvals, and perform attended native apply/verify/delivery under SECRETS-WP-0009-T03." state_hub_task_id: "cf465065-de7a-5d9c-bc80-fee16ffef70d" ``` @@ -131,3 +131,43 @@ approval). Not applied. Suite: 467 passed. Founder go-ahead for ACTIVITY-WP-0039-T04 (ExternalSecret, multi-identity rollout, claim-loop restart) relayed 2026-09-24 on activity-core thread `7b51d9c3`. Store policy was done 2026-09-23T18:06Z (CCR-2026-0029/0030). + + +## Enforce companion-only delivery at both entry points + +```task +id: SECRETS-WP-0011-T05 +status: done +priority: high +``` + +Review on 2026-09-27 found that `companion_of` consent was checked when resolving +companions, but a caller could select that lane directly as the primary `exec` +lane with an arbitrary child. The delivery helper also accepted a supplied +companion list without comparing it with the pinned owner declaration. + +Refuse standalone companion delivery before approval/backend/read. Require the +helper's supplied lane/field/environment list to match the owner declaration, +and recheck companion consent and stage before any value is read. Preserve +ordinary lanes and the configured multi-lane child. Regression tests cover both +entry points, omissions, additions, substituted lane/field/environment, revoked +consent and stage mismatch. + +Validation: 476 tests passed, including disposable OpenBao integration tests; +layer conformance and `git diff --check` passed. No production credential was +requested or delivered. + +## Activity Core handoff reviewed — 2026-09-27 + +Owner messages `a2eae5f8-60cf-499b-8f52-dd04ac407924` and +`bfef619d-53e0-4b4c-8b92-ef092450e4a1` report ACTIVITY-WP-0039 finished at +activity-core `54ab1e4`. The founder-run check on 2026-09-24 returned HTTP 200 +for the metered identity on a no-match label, and HTTP 403 when that token +claimed the loop identity. The existing loop continued to claim with its own +fresh token. This is the owner's reported evidence, not a new live check here. + +The former token seeding/store-policy/identity-rollout blocker is resolved. +T04 remains wait for native lane activation with SECRETS-WP-0009-T03: current +owner admission/pins, exact per-lane approvals and attended apply/verify/exec. +The draft binds `rein-aharness-metered@railiance01` and `hfact-metered`, matching +the handoff. No production policy, role, catalog binding or credential changed.