diff --git a/docs/approval-consumption.md b/docs/approval-consumption.md index 5ea5597..687c6f0 100644 --- a/docs/approval-consumption.md +++ b/docs/approval-consumption.md @@ -188,3 +188,14 @@ pins — and that pin has not been created, so the PDP call is not wired yet. - It does not render or cache an authorization decision. - It does not infer consumption from a decision record or from local evidence. + + +### Native CLI authority and target correction — 2026-09-14 + +The CLI now submits the catalog's explicit policy and AppRole targets, matching +its frozen review inputs. After a valid native claim/PDP join, it retains the +actual PDP decision id for evidence and still requires successful CAS consume. +The catalog CCR reference remains provenance; the native path does not demand a +second hub decision or local fixture with that CCR identifier. Unserved legacy +build/test paths retain their existing review gate. Claim, PDP, human-control, +stance, freshness and consume refusals still prevent backend access. diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py index f86f419..47407ba 100644 --- a/src/secrets_engine/approval_consume.py +++ b/src/secrets_engine/approval_consume.py @@ -65,6 +65,11 @@ class AuthorizedAction: decision_id: str expires_at: str + @property + def id(self) -> str: + """The actual PDP decision id for shared CLI evidence/plan callers.""" + return self.decision_id + def as_evidence(self) -> dict[str, object]: return { "authorization_decision_id": self.decision_id, diff --git a/src/secrets_engine/cli.py b/src/secrets_engine/cli.py index 1b3ff26..6ed4fc5 100644 --- a/src/secrets_engine/cli.py +++ b/src/secrets_engine/cli.py @@ -126,7 +126,9 @@ def _require_lane_approval( # unreachable-engine residue inapplicable. Absent configuration returns # None and production stays closed exactly as before. authorization = authorize_action( - cfg, entry, action or "unknown", None, fields=fields + cfg, entry, action or "unknown", None, fields=fields, + policy_targets=(entry.policy_name,) if entry.has_delivery_auth else (), + auth_targets=(entry.role_name,) if entry.has_delivery_auth else (), ) stance = apply_unreachable_engine_stance( cfg, entry, action or "unknown", authorized=authorization is not None @@ -135,8 +137,11 @@ def _require_lane_approval( evidence.mark_stance(stance) if authorization is not None: evidence.detail.update(authorization.as_evidence()) - decision = None - if entry.approval_required(): + # A served claim/PDP decision is the native authority. The CCR reference + # remains provenance; the hub/fixture path is only the legacy fallback. + # Neither path can discharge the required native CAS below. + decision = authorization + if authorization is None and entry.approval_required(): decision = resolve_decision( hub_url=cfg.hub_url, repo_root=repo_root(), diff --git a/tests/test_approval_auth.py b/tests/test_approval_auth.py index 077bb46..7d9ce7a 100644 --- a/tests/test_approval_auth.py +++ b/tests/test_approval_auth.py @@ -220,7 +220,7 @@ def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure monkeypatch.setattr(approval_auth, "credential_urlopen", exchange) cfg.approval_url = cfg.pdp_url = stub.url from secrets_engine.approval_consume import _expected_request - stub.bind_request(_expected_request(cfg, entry, "apply")) + stub.bind_request(_expected_request(cfg, entry, "apply", policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,))) if failure == "conflict": stub.consume_status = 409 try: diff --git a/tests/test_approval_consume.py b/tests/test_approval_consume.py index 446b1b8..519fbc9 100644 --- a/tests/test_approval_consume.py +++ b/tests/test_approval_consume.py @@ -359,3 +359,25 @@ def test_confirmed_consume_allows_openbao_resolve(tmp_path, monkeypatch): assert records[-1]["detail"]["approval_consumed"] is True assert records[-1]["detail"]["approval_id"] == "appr_test-1" assert TOKEN not in json.dumps(records) + + +def test_native_human_control_uses_real_join_not_a_hub_fixture(tmp_path, monkeypatch): + entry = validate_entry(dict(VALID, stage="prod", approval={"model": "ccr", + "human_control": True, "decision_ref": "CCR-native-provenance"})) + authorized = _authorized() + monkeypatch.setattr(cli, "authorize_action", lambda *_a, **_k: authorized) + monkeypatch.setattr(cli, "apply_unreachable_engine_stance", _allow_prod_stance) + monkeypatch.setattr(cli, "resolve_decision", lambda **_k: pytest.fail("native authority must not resolve a legacy fixture")) + calls = [] + def consume(cfg, actual, **kwargs): + assert kwargs["binding"] is authorized.binding + calls.append("consume") + monkeypatch.setattr(cli, "require_production_consume", consume) + result = cli._require_lane_approval(_config(tmp_path), entry, "apply") + assert result is authorized and result.id == authorized.decision_id + assert calls == ["consume"] + def refused(*_a, **_k): + raise DecisionError("consume refused") + monkeypatch.setattr(cli, "require_production_consume", refused) + with pytest.raises(DecisionError, match="consume refused"): + cli._require_lane_approval(_config(tmp_path), entry, "apply") diff --git a/tests/test_integration_authorization.py b/tests/test_integration_authorization.py index 33c7f44..8c9ce84 100644 --- a/tests/test_integration_authorization.py +++ b/tests/test_integration_authorization.py @@ -127,7 +127,7 @@ def _prime(stub, cfg, entry, action="apply"): """Tell the stub which exact request the engine will propose.""" from secrets_engine.approval_consume import _expected_request - stub.bind_request(_expected_request(cfg, entry, action)) + stub.bind_request(_expected_request(cfg, entry, action, policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,))) def test_full_authorization_chain_reaches_openbao(bao_dev, stub, lane, tmp_path): @@ -136,7 +136,7 @@ def test_full_authorization_chain_reaches_openbao(bao_dev, stub, lane, tmp_path) cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) - authorization = authorize_action(cfg, entry, "apply", None) + authorization = authorize_action(cfg, entry, "apply", None, policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,)) assert authorization is not None assert authorization.decision_id == "decision:stub-apply" assert stub.calls == ["claim", "check"], "PIP then PDP, in that order" diff --git a/tests/test_openrouter_binding.py b/tests/test_openrouter_binding.py index 9ec69d6..25bab13 100644 --- a/tests/test_openrouter_binding.py +++ b/tests/test_openrouter_binding.py @@ -59,3 +59,26 @@ def test_openrouter_plan_exposes_all_limits_and_preserves_existing_custody(): "token_ttl": "15m", "token_max_ttl": "30m", "token_num_uses": 8, "secret_id_ttl": "15m", "secret_id_num_uses": 1, } + + +@pytest.mark.parametrize("action", ["apply", "verify", "exec"]) +def test_cli_submits_the_exact_reviewed_openrouter_targets(action, tmp_path, monkeypatch): + import json + from secrets_engine import cli + from secrets_engine.authorization import digest_material + from secrets_engine.config import Config + root = Path(__file__).resolve().parents[1] + entry = load_entry(root / "docs/proposals/openrouter-key-check.yaml") + expected = json.loads((root / f"docs/evidence/2026-09-14-openrouter-final-{action}-request.json").read_text()) + class Observed(Exception): + pass + def observe(cfg, actual_entry, actual_action, decision, **kwargs): + actual = build_action_request(actual_entry, actual_action, + subject_id="secrets-engine", subject_type="service", + purpose=actual_entry.approval["purpose"], **kwargs) + assert digest_material(actual) == digest_material(expected) + raise Observed + monkeypatch.setattr(cli, "authorize_action", observe) + with pytest.raises(Observed): + cli._require_lane_approval(Config.load(), entry, action, + fields=() if action == "apply" else tuple(entry.fields)) diff --git a/workplans/SECRETS-WP-0010-openrouter-native-access.md b/workplans/SECRETS-WP-0010-openrouter-native-access.md index 483f821..066b1e6 100644 --- a/workplans/SECRETS-WP-0010-openrouter-native-access.md +++ b/workplans/SECRETS-WP-0010-openrouter-native-access.md @@ -4,7 +4,7 @@ type: workplan title: "Native OpenRouter access for intelligence-radar" domain: infotech repo: secrets-engine -status: blocked +status: active owner: codex topic_slug: netkingdom created: "2026-09-14" @@ -52,7 +52,7 @@ and bounded plan are review artifacts, not runtime grants. ```task id: SECRETS-WP-0010-T03 -status: wait +status: progress priority: high state_hub_task_id: "2aa6d2d3-bebc-5ae2-a04b-1bb2e9605405" ``` @@ -180,3 +180,19 @@ Repeatable contained helper: key-cape/tools/register-informed-decision.py (default preflight; --apply mutates only a missing exact registration). Human callback/MFA/token proof and T03 approval entries remain pending. The previous ready check established service health, not browser login acceptance. + + +### Human approval and execution preparation — 2026-09-14 + +The three native Approval Engine records are approved by the actual signed-in +human uid=platform-root,ou=people,dc=netkingdom,dc=local, with one human entry each +and no consumption at inspection. The exact action/memo IDs remain unchanged. +Execution preflight found and corrected two CLI gaps: missing explicit policy/ +role targets, and an unnecessary hub/fixture lookup after an already validated +native claim/PDP join. Real claim validation and mandatory CAS remain in place; +unserved legacy review paths are unchanged. 414 regression tests passed, including +frozen approved-request comparisons and consume-refusal/backend isolation. +The attended owner procedure is in platform scripts/t03-native-execution.py and +t03-attended-delivery.py. It uses the exact scoped client reader, contained +platform administration, named/pinned native pods, private runtime storage, and +native CLI handlers. No approval has yet been consumed by this preparation.