secrets-engine/tests/test_live_decision_enrichment.py

182 lines
6.9 KiB
Python
Raw Normal View History

feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
"""The digest join against a REAL request, not one rebuilt from the binding.
tests/test_decision_replay.py passes every digest assertion because
``_request_from()`` reconstructs the request out of ``envelope["binding"]`` --
which is the *enriched* form the evaluator hashed. That is a self-consistent
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
fake agreeing with itself, and it hid a real defect through three rounds of
digest work: the validator required byte-equality between the binding and our
unenriched request, which no real decision can satisfy.
The rule was already published in flex-auth's canonical-request-digest.md
("Normalization"): the evaluator copies the request tenant onto subject and
resource, a registry hit copies type/tenant/selected attributes onto the refs,
and a consumer is told to compare structured binding fields to the proposed
action rather than re-hash its own request. This module builds the request the
way the engine actually builds it, compares it to a decision the engine actually
received, and proves the corrected check accepts it.
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
"""
from __future__ import annotations
import json
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
from datetime import datetime, timedelta, timezone
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
from pathlib import Path
import pytest
from secrets_engine.authorization import (
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
binding_tuple,
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
build_action_request,
canonical_check_request,
request_digest,
validate_decision_envelope,
)
from secrets_engine.catalog import load_catalog
from secrets_engine.errors import DecisionError
LIVE = Path(__file__).parent / "fixtures" / "flex-auth-live" / "decision_rotate_glas_live.json"
CATALOG = Path(__file__).resolve().parents[1] / "catalog"
PACKAGE = "secrets-engine.catalog-lane.lifecycle"
VERSION = "v2"
def _live():
return json.loads(LIVE.read_text())
def _our_request():
entry = load_catalog(CATALOG)["glas-claude-agent-dev-anthropic"]
return build_action_request(
entry,
"rotate",
subject_id="secrets-engine",
subject_type="service",
purpose="live-adoption-proof",
fields=["api_key"],
request_id="check:secrets-engine-adoption-proof",
)
def test_the_live_decision_is_a_real_v2_allow():
"""Provenance of the artifact these assertions rest on."""
env = _live()
assert env["effect"] == "allow"
assert env["reason"] == "catalog_lane_policy_matched"
provenance = env["provenance"]
assert provenance["policy_package"] == PACKAGE
assert provenance["policy_version"] == VERSION
assert env["binding"]["tenant"] == "tenant:platform"
def test_the_evaluator_enriches_subject_and_resource_before_hashing():
"""Names exactly which fields appeared that we never sent.
If flex-auth publishes an enrichment rule that differs from this, this test
fails and tells us the shape moved -- which is the point. It asserts the
observed gap, not a rule we invented.
"""
sent = _our_request()
bound = _live()["binding"]
assert "attributes" not in sent["subject"]
assert set(bound["subject"]["attributes"]) == {
"description", "display_name", "groups", "organization_relation", "roles",
}
assert "tenant" not in sent["subject"]
assert bound["subject"]["tenant"] == sent["tenant"]
assert "tenant" not in sent["resource"]
assert bound["resource"]["tenant"] == sent["tenant"]
# Everything we DID send survived unchanged. The enrichment is additive, so
# the decision is about the action we proposed -- which is why staying
# fail-closed here costs correctness nothing today.
assert bound["action"] == sent["action"]
assert bound["tenant"] == sent["tenant"]
assert bound["context"] == sent["context"]
assert bound["subject"]["id"] == sent["subject"]["id"]
assert bound["subject"]["type"] == sent["subject"]["type"]
for key in ("id", "type", "system", "attributes"):
assert bound["resource"][key] == sent["resource"][key]
def test_our_digest_cannot_match_a_real_binding():
"""The join is unsatisfiable against a real request, not merely mismatched.
We hash what we sent; the evaluator hashed what it enriched. No amount of
care on our side closes that, because the registry material is not ours.
"""
sent = _our_request()
bound = _live()["binding"]
assert bound["request_digest"] != request_digest(sent)
assert canonical_check_request(bound) != canonical_check_request(sent)
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
def test_the_live_allow_now_validates_under_the_documented_rule():
"""The real decision validates -- this is the fix, proved against the artifact.
canonical-request-digest.md "Normalization" is the published rule, and it
says a consumer must compare structured binding fields to the proposed
action rather than re-hash its own request. Under that rule this envelope,
which our previous byte-equality check rejected outright, is accepted.
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
Only the lifetime is refreshed: the decision was issued on 2026-09-07 with a
bounded lifetime, and pinning a clock-dependent field is what the fixture
provenance forbids.
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
"""
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
env = _live()
now = datetime.now(timezone.utc)
env["lifetime"] = {
"kind": "bounded",
"not_before": (now - timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
"expires_at": (now + timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
result = validate_decision_envelope(
env,
_our_request(),
accepted_policy_packages={PACKAGE},
accepted_policy_versions={VERSION},
)
assert result.action == "rotate"
assert result.subject_id == "secrets-engine"
assert result.decision_id == "decision:0f9c98f14545c42d"
def test_the_unrefreshed_live_decision_is_refused_on_lifetime():
"""It is a real expired allow, so it must be refused -- and for that reason.
Reaching the lifetime check at all is the evidence that every binding check
before it now passes against a real decision.
"""
with pytest.raises(DecisionError, match="lifetime has expired"):
feat: adopt the owner-documented PDP access path, and prove it live Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness (GLAS-WP-0015), plus the first real decision this engine has obtained from the deployed pin -- which found a defect the fixtures could not. ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names and any non-loopback host. This is no longer a unilateral call: the owner path is documented as loopback kubectl port-forward over the authenticated Kubernetes API, which is what authenticates the responder transitively (FLEX-DEC-2026-010). A Service name from a workstation does not fail, it resolves through the DNS search suffix to an unrelated public host, and since decision records carry no signature, a responder knowing the published package and version can return an allow that passes every check we make. Fail-closed protects against a PDP that is absent, not one that lies. The guard runs before the token is read, so a misdirected request cannot leak it; a test pins that ordering. LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded after), forwarded to the named pod, and sent a real CheckRequest for glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched, served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as outstanding has landed and the pin no longer serves the tenant-blind v1. Our tenant fix is confirmed against the real service: binding.tenant is tenant:platform. THE DEFECT IT FOUND. The evaluator enriches from its registry before hashing -- subject gains attributes and tenant, resource gains tenant -- so binding.request_digest is over material we never sent and cannot reproduce. validate_decision_envelope rejects every real allow. Every replay test passes because _request_from() rebuilds the request out of the binding, i.e. the enriched form: a self-consistent fake agreeing with itself, which hid this through three rounds of digest work. Third time a real artifact has beaten a fake in this integration. NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe direction. Which fields may be enriched is flex-auth's contract to publish; inferring it means accepting a binding that differs from our proposal in a way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them. Tenant question closed by operator decision 5ed3fb35: tenant:platform exactly, and service_auth.TENANT stays tenant:coulomb because the two identity layers are to remain distinct. Declining to author that mapping was right -- the answer was neither reading offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00
validate_decision_envelope(
_live(),
_our_request(),
accepted_policy_packages={PACKAGE},
accepted_policy_versions={VERSION},
)
fix: compare structured binding fields, not a digest we cannot reproduce The live proof in 03c0569 showed validate_decision_envelope rejecting every real allow. The evaluator normalizes before hashing -- the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs -- so binding.request_digest covers material we never sent. Byte-equality against our unenriched request was unsatisfiable, not merely mismatched. THE RULE WAS ALREADY PUBLISHED. flex-auth's canonical-request-digest.md section "Normalization" states the enrichment and tells consumers what to do instead: compare structured binding fields to the proposed action, treat request_digest as the evaluator's statement of what it hashed, and recompute independently over the tuple the binding carries. I raised this with them as an unpublished gap and asked them to pick between three shapes; it was in their contract already and the answer was the first of the three. Nothing was blocked on them, and this follows the published rule rather than one I inferred. - _require_binding_corresponds: everything we proposed must survive unchanged -- tenant, action, context, subject.id/type, resource.id/type/system, and every attribute we sent. - Enrichment may add only type, tenant, attributes. Any other added field is refused, and an enriched tenant must be the request tenant, so a cross-tenant binding cannot arrive wearing our request's clothes. - request_digest is still verified, now against binding_tuple(binding) for self-consistency rather than against material we never sent. - The envelope's top-level subject/resource get the same rule; they are enriched too. Proved against the artifact: the real decision:0f9c98f14545c42d now validates, and the unrefreshed envelope is refused on lifetime -- reaching the lifetime check at all is the evidence the binding checks pass on a real decision. Negatives cover a restated resource.attributes.stage, a foreign subject.tenant, and an unexpected enrichment field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij Assistant: claude-code Assistant-Model: opus Assistant-Process: 715726@bnt-lap001 Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 09:04:57 +02:00
def test_the_digest_is_verified_against_the_binding_it_carries():
"""Independent recomputation, per the contract's own instruction.
"To recompute independently, hash the same normalized tuple the binding
carries." So the digest is still checked -- it is simply checked for
self-consistency rather than against material we never sent.
"""
binding = _live()["binding"]
assert request_digest(binding_tuple(binding)) == binding["request_digest"]
def test_a_tampered_binding_still_fails_the_digest():
"""Self-consistency is a real check, not a formality."""
env = _live()
env["binding"]["resource"]["attributes"]["stage"] = "build"
with pytest.raises(DecisionError, match="does not match"):
validate_decision_envelope(
env,
_our_request(),
accepted_policy_packages={PACKAGE},
accepted_policy_versions={VERSION},
)