2026-06-28 12:28:45 +02:00
|
|
|
"""Runtime configuration resolved from environment and repo layout.
|
|
|
|
|
|
2026-09-02 01:24:08 +02:00
|
|
|
Nothing here is a secret. Backend auth (named providers in engine_auth) is
|
2026-06-28 12:28:45 +02:00
|
|
|
resolved lazily inside the backend adapter, never cached on disk by this module.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def repo_root() -> Path:
|
|
|
|
|
"""Repo root = nearest ancestor containing pyproject.toml (fallback: cwd)."""
|
|
|
|
|
here = Path(__file__).resolve()
|
|
|
|
|
for parent in (here, *here.parents):
|
|
|
|
|
if (parent / "pyproject.toml").exists():
|
|
|
|
|
return parent
|
|
|
|
|
return Path.cwd()
|
|
|
|
|
|
|
|
|
|
|
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00
|
|
|
def _positive_int(raw: str, default: int = 1) -> int:
|
|
|
|
|
"""Parse a positive approval threshold. Anything malformed keeps the default."""
|
|
|
|
|
try:
|
|
|
|
|
value = int(raw)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return default
|
|
|
|
|
return value if value >= 1 else default
|
|
|
|
|
|
|
|
|
|
|
2026-06-28 12:28:45 +02:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Config:
|
|
|
|
|
catalog_dir: Path
|
|
|
|
|
policy_dir: Path
|
|
|
|
|
evidence_dir: Path
|
|
|
|
|
hub_url: str
|
|
|
|
|
bao_addr: str
|
|
|
|
|
topic_id: str
|
2026-09-02 01:06:50 +02:00
|
|
|
approval_url: str = ""
|
|
|
|
|
approval_token_file: Path | None = None
|
2026-09-02 01:24:08 +02:00
|
|
|
keycape_token_url: str = ""
|
|
|
|
|
keycape_issuer: str = ""
|
|
|
|
|
keycape_client_secret_file: Path | None = None
|
|
|
|
|
openbao_jwt_login_file: Path | None = None
|
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00
|
|
|
# PIP/PDP join (SECRETS-WP-0007-T04 / SECRETS-WP-0008-T02). All absent by
|
|
|
|
|
# default: an unset value fails production closed exactly as before.
|
|
|
|
|
authorization_subject_id: str = ""
|
|
|
|
|
authorization_subject_type: str = ""
|
|
|
|
|
authorization_policy_package: str = ""
|
|
|
|
|
authorization_policy_version: str = ""
|
|
|
|
|
authorization_min_approvals: int = 1
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
pdp_url: str = ""
|
|
|
|
|
pdp_token_file: Path | None = None
|
2026-06-28 12:28:45 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def load(cls) -> "Config":
|
|
|
|
|
root = repo_root()
|
2026-09-02 01:06:50 +02:00
|
|
|
token_file = os.environ.get("SECRETS_ENGINE_APPROVAL_TOKEN_FILE", "")
|
2026-09-02 01:24:08 +02:00
|
|
|
keycape_secret = os.environ.get("SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE", "")
|
|
|
|
|
jwt_login = os.environ.get("SECRETS_ENGINE_OPENBAO_JWT_LOGIN", "")
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
pdp_token = os.environ.get("SECRETS_ENGINE_PDP_TOKEN_FILE", "")
|
2026-06-28 12:28:45 +02:00
|
|
|
return cls(
|
|
|
|
|
catalog_dir=Path(os.environ.get("SECRETS_ENGINE_CATALOG", root / "catalog")),
|
|
|
|
|
policy_dir=Path(os.environ.get("SECRETS_ENGINE_POLICIES", root / "policies")),
|
|
|
|
|
evidence_dir=Path(os.environ.get("SECRETS_ENGINE_EVIDENCE", root / ".evidence")),
|
|
|
|
|
hub_url=os.environ.get("SECRETS_ENGINE_HUB_URL", "http://127.0.0.1:8000"),
|
|
|
|
|
bao_addr=os.environ.get("BAO_ADDR", os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")),
|
|
|
|
|
topic_id=os.environ.get(
|
|
|
|
|
"SECRETS_ENGINE_TOPIC_ID", "cee7bedf-2b48-46ef-8601-006474f2ad7a"
|
|
|
|
|
),
|
2026-09-02 01:06:50 +02:00
|
|
|
approval_url=os.environ.get("SECRETS_ENGINE_APPROVAL_URL", ""),
|
|
|
|
|
approval_token_file=Path(token_file) if token_file else None,
|
2026-09-02 01:24:08 +02:00
|
|
|
keycape_token_url=os.environ.get("SECRETS_ENGINE_KEYCAPE_TOKEN_URL", ""),
|
|
|
|
|
keycape_issuer=os.environ.get("SECRETS_ENGINE_KEYCAPE_ISSUER", ""),
|
|
|
|
|
keycape_client_secret_file=Path(keycape_secret) if keycape_secret else None,
|
|
|
|
|
openbao_jwt_login_file=Path(jwt_login) if jwt_login else None,
|
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00
|
|
|
authorization_subject_id=os.environ.get(
|
|
|
|
|
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID", ""
|
|
|
|
|
),
|
|
|
|
|
authorization_subject_type=os.environ.get(
|
|
|
|
|
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_TYPE", ""
|
|
|
|
|
),
|
|
|
|
|
authorization_policy_package=os.environ.get(
|
|
|
|
|
"SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE", ""
|
|
|
|
|
),
|
|
|
|
|
authorization_policy_version=os.environ.get(
|
|
|
|
|
"SECRETS_ENGINE_AUTHORIZATION_POLICY_VERSION", ""
|
|
|
|
|
),
|
|
|
|
|
authorization_min_approvals=_positive_int(
|
|
|
|
|
os.environ.get("SECRETS_ENGINE_AUTHORIZATION_MIN_APPROVALS", "")
|
|
|
|
|
),
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
pdp_url=os.environ.get("SECRETS_ENGINE_PDP_URL", ""),
|
|
|
|
|
pdp_token_file=Path(pdp_token) if pdp_token else None,
|
2026-06-28 12:28:45 +02:00
|
|
|
)
|