Enforce companion-only credential delivery and refresh activation handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 7s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e324-abce-7e51-bb2b-496f097afdb0
This commit is contained in:
tegwick 2026-09-27 15:58:53 +02:00
parent 303cbf652b
commit 5c6f2b319d
8 changed files with 150 additions and 24 deletions

View file

@ -1,6 +1,7 @@
# Draft exec_owner for glas-claude-agent-dev-anthropic (SECRETS-WP-0009-T03). # Draft exec_owner for glas-claude-agent-dev-anthropic (SECRETS-WP-0009-T03).
# Not in the catalog: it replaces the pending binding only after # Not in the catalog. Activity Core reports custody and identity live as of
# activity-core-metered-worker-token is seeded and ACTIVITY-WP-0039 is live. # 2026-09-24 (ACTIVITY-WP-0039). Admission still requires current owner/pin
# validation and exact per-lane approvals for attended native activation.
exec_owner: exec_owner:
status: configured status: configured
owner: rein-aharness MessagesOwner (metered-once) with sand-boxer runtime boundary owner: rein-aharness MessagesOwner (metered-once) with sand-boxer runtime boundary

View file

@ -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 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 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. 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 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. 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 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 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. 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 Proof: `tests/test_exec_owner_companions.py`, plus
`tests/test_integration_companions.py` on a throwaway OpenBao (two lanes, one `tests/test_integration_companions.py` on a throwaway OpenBao (two lanes, one

View file

@ -37,23 +37,27 @@ approval, OpenBao access, provider authentication or production readiness.
## Activation requirements ## Activation requirements
The current engine's production stance refuses before opening the backend: As of 2026-09-27, the shared approval/consume/PDP chain has live evidence from
`production action 'exec' requires a durable access-engine decision record; SECRETS-WP-0010-T03. The Glas catalog still has a pending owner binding and
live production remains disabled`. This refusal was exercised with the proposed refuses exec before approval consumption or backend access. The earlier lack
catalog and service-jwt selection. No real value was requested. of a served decision path is no longer the current activation blocker.
Activation depends on SECRETS-WP-0007-T04 (exact production actions) and The metered owner configuration and binding were prepared on 2026-09-23.
SECRETS-WP-0008-T02/T06 (decision consumption and service authority). Require Activity Core reports ACTIVITY-WP-0039 complete on 2026-09-24: custody and the
canonical validated DecisionEnvelope for each protected action, successful consume, separate `rein-aharness-metered@railiance01` identity are live. See the exact
and exact scoped backend authority. This draft cannot authorize itself; an handoff in SECRETS-WP-0011. Its worker token is companion-only; direct exec of
operator browser token or unsafe-demo flag is not a runtime substitute. `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 SECRETS-WP-0009-T03 still owns current recipient/pin admission and the attended
exact policy/AppRole with scoped authority, verify positive read and denied activation. Review the draft binding, revalidate installed files and private
metadata/sibling/write access without exposing values, and record delivery-ready state, configure the approved owner, and obtain exact per-action/per-lane
state. Bind approved exec authorization and named engine service authentication approvals. Apply the scoped policy/AppRole, verify positive read and denied
to the sand-boxer owner route. Prove actual provider authentication and a bounded metadata/sibling/write access with an unrelated negative identity, then prove
Glas task, then activate routing and only the validated profile. 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 Rotation: store replacement with CAS, stop old runs, verify replacement, revoke
predecessor at Anthropic and prove denial. Bao session expiration does not revoke predecessor at Anthropic and prove denial. Bao session expiration does not revoke

View file

@ -221,14 +221,28 @@ def exec_with_secret(
f"(allowed {sorted(declared)})" 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: if session_evidence is None:
value = _fetch_value(client, entry, field) value = _fetch_value(client, entry, field)
else: else:
value = _fetch_value( value = _fetch_value(
client, entry, field, session_evidence=session_evidence 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 # 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. # before a child exists, and the values already read go out of scope.
extra: dict[str, str] = {} extra: dict[str, str] = {}

View file

@ -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: 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) binding = owner_binding(entry)
if binding is None: if binding is None:
return None return None

View file

@ -179,3 +179,47 @@ def test_unresolvable_companion_refuses_before_any_gate(bound, monkeypatch, tmp_
command = data["delivery_config"]["exec_owner"]["command"] command = data["delivery_config"]["exec_owner"]["command"]
with pytest.raises(DeliveryError, match="unavailable"): with pytest.raises(DeliveryError, match="unavailable"):
cli.cmd_exec(_cfg(tmp_path), SimpleNamespace(field=None, catalog=entry.id, command=command, mode="exec-env")) 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,
)

View file

@ -8,7 +8,7 @@ status: blocked
flavor: implementation flavor: implementation
owner: codex owner: codex
created: "2026-09-05" created: "2026-09-05"
updated: "2026-09-21" updated: "2026-09-27"
state_hub_workstream_id: "40ccc3b4-d046-5a58-8649-e7935f45c974" 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 id: SECRETS-WP-0009-T03
status: wait status: wait
priority: high 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" 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 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). 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.

View file

@ -9,7 +9,7 @@ flavor: implementation
owner: claude-code owner: claude-code
topic_slug: netkingdom topic_slug: netkingdom
created: "2026-09-23" created: "2026-09-23"
updated: "2026-09-23" updated: "2026-09-27"
related_workplans: related_workplans:
- SECRETS-WP-0009 - SECRETS-WP-0009
- HFACT-WP-0001 - HFACT-WP-0001
@ -93,7 +93,7 @@ a throwaway OpenBao dev server.
id: SECRETS-WP-0011-T04 id: SECRETS-WP-0011-T04
status: wait status: wait
priority: high 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" 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) 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 relayed 2026-09-24 on activity-core thread `7b51d9c3`. Store policy was done 2026-09-23T18:06Z
(CCR-2026-0029/0030). (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.