INFD-WP-0010: present_existing intake profile (S1) and owner answer to decision services alignment

A registered submitter with a present_existing profile may pose a memo that
pins an approval it created itself in approval-engine. Intake verifies the
object read-only (requested, no entries, human_control, actor == profile
actor, digest == memo, binding == intent, required_count 1, unexpired, one
memo per approval) and never creates, enters or consumes. The recorded
requester reaches review and the access-engine context. Existing submitters
are unchanged.

Answer doc accepts S1 (profile, not route; packet carries the admission),
S4 in principle (waits on gate-house classes and policy) and S3 (exists
behind INFD_HUB_WRITEBACK). Workplan INFD-WP-0010 proposed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
tegwick 2026-10-31 23:07:43 +01:00
parent d5cb527d4c
commit 0e66a33389
8 changed files with 731 additions and 24 deletions

View file

@ -20,7 +20,10 @@ class RequesterError(Exception):
class RequesterTokenProvider:
def __init__(self, issuer, secret_file, *, transport=None, clock=time.time):
def __init__(self, issuer, secret_file, *, transport=None, clock=time.time, scope="approval:create"):
if scope not in ("approval:create", "approval:read"):
raise ValueError("requester scope is create or read only")
self.scope = scope
self.issuer = https_origin(issuer)
self.secret_file = Path(secret_file)
if not self.secret_file.is_absolute():
@ -44,7 +47,7 @@ class RequesterTokenProvider:
headers={"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic " + credentials},
body=urlencode({"grant_type": "client_credentials",
"scope": "approval:create"}).encode())
"scope": self.scope}).encode())
if (status != 200 or not isinstance(result.get("token_type"), str)
or result["token_type"].lower() != "bearer"
or "refresh_token" in result):
@ -71,7 +74,7 @@ class RequesterTokenProvider:
or claims.get("tenant") != "tenant:platform"
or claims.get("principal_type") != "service"
or claims.get("roles") != [CLIENT_ID]
or claims.get("scope") != "approval:create"
or claims.get("scope") != self.scope
or type(claims["iat"]) is not int or type(claims["exp"]) is not int
or claims["iat"] > now or claims["exp"] <= now
or not 0 < claims["exp"] - claims["iat"] <= 900):
@ -88,9 +91,13 @@ def compose_intake(store, token_review_call, *, issuer, secret_file,
This factory grants no submitter and starts no socket. The separate private
listener and deployment remain reviewed owner configuration.
"""
from .intake_approval import IntakeApprovalClient
from .intake_approval import IntakeApprovalClient, IntakeApprovalReader
from .intake_service import IntakeService, TokenReview
provider = RequesterTokenProvider(issuer, secret_file, transport=transport)
creator = IntakeApprovalClient(approval_origin, provider, allow_internal_http=True)
# present_existing only; needs approval:read on the requester client (KeyCape registration).
# Until granted, that exchange is refused and only present_existing submitters fail closed.
read_provider = RequesterTokenProvider(issuer, secret_file, transport=transport, scope="approval:read")
reader = IntakeApprovalReader(approval_origin, read_provider, allow_internal_http=True)
return IntakeService(store, TokenReview(token_review_call), creator,
submitters_path=submitters_path, groups_path=groups_path)
submitters_path=submitters_path, groups_path=groups_path, reader=reader)