Register informed-decision.memo.posed for the platform tenant.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Accept the retained pose envelope and refuse packet, brief, and secret
content. Heartbeat and reconciliation follow the existing load-bearing
class pattern. This does not connect the sink or claim runtime readiness.

Assistant: grok
Assistant-Session: 01a10329-31cb-78d3-b7a3-787a69c89d54
This commit is contained in:
tegwick 2026-10-03 21:18:13 +02:00
parent fdca5758e1
commit 70e4044128
16 changed files with 534 additions and 10 deletions

View file

@ -45,6 +45,7 @@ from audit_core.redaction import (
SecretFieldRejection,
apply_policy,
finding_from_path,
scan,
)
from audit_core.senders import WILDCARD, SenderIdentity, SenderRegistry, development_registry
from audit_core.sqlite_backend import SQLiteAuditBackend
@ -552,11 +553,71 @@ class IngestionApplication:
return [body]
# Local registration of informed-decision.memo.posed (intake dependency T07).
# The retained envelope is the one informed-decision already queues: the eight
# wire fields, with data limited to the six ids and digests below. Packet,
# brief, and secret-shaped keys are refused before any other error so a later
# invalid_event cannot write that body into the dead-letter store.
POSED_EVENT_TYPE = "informed-decision.memo.posed"
POSED_SOURCE = "informed-decision"
POSED_TENANT = "tenant:platform"
POSED_DATA_KEYS = frozenset({
"memo_id",
"approval_id",
"approval_binding_digest",
"submitter",
"memo_class",
"submission_digest",
})
CONTENT_REFUSED = "content_refused"
_POSED_CONTENT_KEYS = frozenset({"packet", "brief"})
def _has_posed_content_key(value: Any) -> bool:
if isinstance(value, dict):
for key, item in value.items():
if str(key).lower() in _POSED_CONTENT_KEYS or _has_posed_content_key(item):
return True
elif isinstance(value, list):
return any(_has_posed_content_key(item) for item in value)
return False
def _refuse_posed_content(payload: dict[str, Any]) -> None:
"""Refuse packet, brief, and secret-shaped keys on this class only.
Other event types keep the sender's secret_policy. This class does not:
the dependency says refuse, and redacting would still accept the event.
"""
findings = scan(payload)
if findings:
raise SecretFieldRejection(findings)
if _has_posed_content_key(payload):
raise ValueError(CONTENT_REFUSED)
def _require_posed_envelope(payload: dict[str, Any]) -> None:
"""Accept only the locally retained field set, for this source and tenant."""
if str(payload.get("source") or "") != POSED_SOURCE:
raise ValueError("source_not_allowed")
if str(payload.get("tenant") or "") != POSED_TENANT:
raise ValueError("tenant_not_allowed")
data = payload.get("data")
if (
not isinstance(data, dict)
or set(data) != POSED_DATA_KEYS
or any(type(data[key]) is not str or not data[key].strip() for key in POSED_DATA_KEYS)
):
raise ValueError("invalid_event")
def normalize(
payload: dict[str, Any],
idempotency_key: str | None,
identity: SenderIdentity | None = None,
) -> AuditEvent:
if isinstance(payload, dict) and payload.get("type") == POSED_EVENT_TYPE:
_refuse_posed_content(payload)
required = (
"id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data",
)
@ -575,6 +636,8 @@ def normalize(
raise ValueError("tenant_not_allowed")
elif source != "user-engine":
raise ValueError("source_not_allowed")
if payload.get("type") == POSED_EVENT_TYPE:
_require_posed_envelope(payload)
observed_at = _normalize_timestamp(payload["occurred_at"])
policy = identity.secret_policy if identity is not None else POLICY_REDACT
# Raises SecretFieldRejection under the reject policy; that exception

View file

@ -166,10 +166,10 @@ MIGRATIONS: list[tuple[str, str]] = [
),
]
# Rejection reasons whose payload must never be persisted — storing the body of
# an event rejected *for carrying secret-shaped material* would write that
# material into the audit store.
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field"})
# Rejection reasons whose payload must never be persisted. Storing the body of
# an event rejected for secret-shaped material, or for packet/brief content,
# would write that material into the audit store.
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field", "content_refused"})
class PostgresAuditBackend:

View file

@ -74,9 +74,9 @@ CREATE TABLE IF NOT EXISTS secret_findings (
"""
# Rejection reasons whose payload must never be persisted. Storing the body of
# an event rejected *for containing secret-shaped material* would write that
# material into the audit store — the precise outcome the rejection prevents.
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field"})
# an event rejected for secret-shaped material, or for packet/brief content,
# would write that material into the audit store.
WITHHOLD_PAYLOAD_REASONS = frozenset({"secret_shaped_field", "content_refused"})
class SQLiteAuditBackend:

View file

@ -45,6 +45,9 @@
],
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"
"evidence_kind": "load-bearing",
"heartbeat_classes": {
"informed-decision.memo.posed": 86400
}
}
]

View file

@ -67,6 +67,9 @@ data:
],
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"
"evidence_kind": "load-bearing",
"heartbeat_classes": {
"informed-decision.memo.posed": 86400
}
}
]

View file

@ -79,7 +79,7 @@ as the sender's claim rather than the archive's.
| --- | --- |
| `202` | Accepted and stored |
| `200` | Duplicate `id` — already stored, reconciled rather than re-linked. Not an error; do not retry |
| `400` | Rejected and **dead-lettered**. Body carries the reason: `invalid_event`, `idempotency_key_mismatch`, `source_not_allowed`, `tenant_not_allowed`, `secret_shaped_field` |
| `400` | Rejected and **dead-lettered**. Body carries the reason: `invalid_event`, `idempotency_key_mismatch`, `source_not_allowed`, `tenant_not_allowed`, `secret_shaped_field`, `content_refused` |
| `401` | Unknown or expired token |
| `403` | The credential lacks `may_write` |

View file

@ -213,3 +213,47 @@ receiver source/tenant/read refusal, independent readback and audit bearer
revocation. No native producer event or operational heartbeat is claimed by
the custody receipt. Attestation/offsite operation remains a separate evidence
quality bound in AUDIT-WP-0009-T12.
## Event class `informed-decision.memo.posed` — 2026-10-03
`AUDIT-WP-0011`. Intake dependency T07. Same sender, same source, same
`tenant:platform` scope. Not a new credential, not a new custody class, and
not a change to `evidence_kind`, `secret_policy`, or `completeness_trade`.
The accepted body is the envelope informed-decision already retains for the
posed job. `data` is exactly these fields, each a non-empty string:
| Field | What it is |
| --- | --- |
| `memo_id` | memo id |
| `approval_id` | approval id |
| `approval_binding_digest` | carried binding digest |
| `submitter` | authenticated forwarding submitter |
| `memo_class` | memo class |
| `submission_digest` | submission digest |
`source` must be `informed-decision` and `tenant` must be `tenant:platform`.
The bare tenant string `platform` is not registered. No other `data` key is
part of this registration.
Packet and brief keys are `content_refused`. Secret-shaped keys (the existing
name scan: password, secret, token, credential, private_key) are
`secret_shaped_field` for this class even though the sender's policy remains
`redact` for every other class. Both refusals withhold the payload from the
dead-letter store. This receiver still does not verify the digest, read the
memo, or decide whether the approval is valid.
Idempotency is the existing receipt: `Idempotency-Key` must equal `id`; `202`
`accepted` with `reference` `audit:<id>`; an exact replay is `200`
`duplicate` with the same reference.
The class is load-bearing and low-volume, so it uses the existing
heartbeat-or-reconciliation requirement rather than a rate. `heartbeat_classes`
on this registration is `informed-decision.memo.posed` at 86400 seconds — the
interval this sender already publishes for its other low-volume classes and
the interval the approval-engine registration tests use. Reconciliation is
`GET /v1/reconciliation` for source `informed-decision` and tenant
`tenant:platform`, counts only. Neither control is claimed to be operating,
and this registration does not make the receiver runtime-ready for the posed
job. Connecting `AuditCoreSink` stays informed-decision's step, after this
contract.

View file

@ -0,0 +1,17 @@
{
"id": "posed-fixture-1",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "tenant:platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
}

View file

@ -0,0 +1,17 @@
{
"id": "posed-fixture-1",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "tenant:platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
}

View file

@ -0,0 +1,18 @@
{
"id": "posed-refusal-brief",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "tenant:platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"brief": "fixture-brief"
}
}

View file

@ -0,0 +1,18 @@
{
"id": "posed-refusal-packet",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "tenant:platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"packet": "fixture-packet"
}
}

View file

@ -0,0 +1,18 @@
{
"id": "posed-refusal-secret",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "tenant:platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"password": "fixture-not-a-credential"
}
}

View file

@ -0,0 +1,17 @@
{
"id": "posed-refusal-tenant",
"type": "informed-decision.memo.posed",
"source": "informed-decision",
"subject": "memo-fixture-1",
"tenant": "platform",
"correlation_id": "approval-fixture-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {
"memo_id": "memo-fixture-1",
"approval_id": "approval-fixture-1",
"approval_binding_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"submitter": "state-hub",
"memo_class": "fixture-class",
"submission_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
}

View file

@ -0,0 +1,261 @@
"""Local registration of informed-decision.memo.posed.
Same sender pattern as the other registered sources: scope binds the
credential, Idempotency-Key receipts are 202 then 200, and a low-volume
load-bearing class is a heartbeat class counted by /v1/reconciliation.
"""
import json
from pathlib import Path
import pytest
from audit_core.ingestion import (
CONTENT_REFUSED,
POSED_EVENT_TYPE,
IngestionApplication,
)
from audit_core.senders import SenderRegistry
from audit_core.sqlite_backend import SQLiteAuditBackend
from audit_core.stream_findings import HEARTBEAT_ACTION, NEVER_HEARTBEAT
from test_ingestion import invoke
from test_senders import SCOPE_FILE
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "informed-decision-memo-posed"
WINDOW = (
"/v1/reconciliation?source=informed-decision&tenant=tenant:platform"
"&since=2026-01-01T00:00:00%2B00:00&until=2027-01-01T00:00:00%2B00:00"
)
def _load(name: str) -> dict:
return json.loads((FIXTURES / name).read_text())
def _registry() -> SenderRegistry:
return SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": json.dumps([
{
"name": "informed-decision",
"tokens": ["fixture-only"],
"sources": ["*"],
"tenants": ["*"],
"may_read": True,
"secret_policy": "redact",
"evidence_kind": "load-bearing",
},
{
"name": "user-engine",
"tokens": ["user-fixture"],
"sources": ["*"],
"tenants": ["*"],
"may_read": False,
"secret_policy": "redact",
},
{
"name": "operator",
"tokens": ["operator-fixture"],
"sources": ["informed-decision"],
"tenants": ["*"],
"may_write": False,
"may_read": True,
},
]),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(SCOPE_FILE),
})
@pytest.fixture
def app(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "posed.db"))
return IngestionApplication(backend, _registry()), backend
def _post(app, payload, *, token="fixture-only", key=None):
return invoke(app, payload, token=token, key=payload["id"] if key is None else key)
def test_scope_registration_keeps_sender_custody_and_adds_the_class():
identity = _registry().authenticate("Bearer fixture-only")
assert identity.name == "informed-decision"
assert identity.is_load_bearing
assert identity.completeness_trade is None
assert identity.secret_policy == "redact"
assert identity.may_write and not identity.may_read
assert identity.permits_source("informed-decision")
assert not identity.permits_source("user-engine")
assert identity.permits_tenant("tenant:platform")
assert not identity.permits_tenant("platform")
assert dict(identity.heartbeat_classes) == {POSED_EVENT_TYPE: 86400}
def test_positive_fixture_is_accepted_and_duplicate_fixture_reconciles(app):
application, backend = app
positive = _load("positive.json")
duplicate = _load("duplicate.json")
assert duplicate == positive
status, body = _post(application, positive)
assert status.startswith("202")
assert body == {"status": "accepted", "reference": "audit:posed-fixture-1"}
status, body = _post(application, duplicate)
assert status.startswith("200")
assert body == {"status": "duplicate", "reference": "audit:posed-fixture-1"}
assert backend.db.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 1
status, stored = invoke(
application, None, path="/v1/events/posed-fixture-1", method="GET",
body=b"", token="operator-fixture",
)
assert status.startswith("200")
assert stored["action"] == POSED_EVENT_TYPE
assert stored["source"] == "informed-decision"
assert stored["tenant"] == "tenant:platform"
assert stored["details"]["data"] == positive["data"]
assert "redaction" not in stored["details"]
status, denied = invoke(
application, None, path="/v1/events/posed-fixture-1", method="GET",
body=b"", token="fixture-only",
)
assert status.startswith("403")
assert denied["error"] == "read_forbidden"
def test_idempotency_key_must_equal_the_event_id(app):
application, _ = app
status, body = _post(application, _load("positive.json"), key="other-key")
assert status.startswith("400")
assert body["error"] == "idempotency_key_mismatch"
def _get(app, path, *, token="fixture-only"):
import io
base, _, query = path.partition("?")
environ = {
"PATH_INFO": base,
"QUERY_STRING": query,
"REQUEST_METHOD": "GET",
"CONTENT_LENGTH": "0",
"wsgi.input": io.BytesIO(b""),
"HTTP_AUTHORIZATION": f"Bearer {token}",
}
result = {}
out = b"".join(app(environ, lambda status, headers: result.update(status=status)))
return result["status"], (json.loads(out) if out else {})
def test_reconciliation_counts_the_class_and_returns_no_payload(app):
application, _ = app
assert _post(application, _load("positive.json"))[0].startswith("202")
status, body = _get(application, WINDOW)
assert status.startswith("200")
assert body["source"] == "informed-decision"
assert body["tenant"] == "tenant:platform"
assert body["counts"] == [{"class": POSED_EVENT_TYPE, "count": 1}]
assert "memo-fixture-1" not in json.dumps(body)
assert "completeness" in body["means"]
def test_a_missing_heartbeat_for_the_class_is_a_finding_until_one_arrives(app):
application, _ = app
status, body = invoke(
application, None, path="/v1/stream-findings", method="GET",
body=b"", token="operator-fixture",
)
assert status.startswith("200")
posed = [row for row in body["stream_findings"] if row["class"] == POSED_EVENT_TYPE]
assert [row["kind"] for row in posed] == [NEVER_HEARTBEAT]
assert posed[0]["source"] == "informed-decision"
assert posed[0]["expected_within_seconds"] == 86400
assert "not proof of suppression" in posed[0]["means"].lower()
heartbeat = {
"id": "hb-posed-1",
"type": HEARTBEAT_ACTION,
"source": "informed-decision",
"subject": "informed-decision",
"tenant": "tenant:platform",
"correlation_id": "hb-posed-1",
"occurred_at": "2026-10-03T12:00:00+00:00",
"data": {"class": POSED_EVENT_TYPE, "assertion": "nothing-to-report"},
}
assert _post(application, heartbeat)[0].startswith("202")
_, cleared = invoke(
application, None, path="/v1/stream-findings", method="GET",
body=b"", token="operator-fixture",
)
assert cleared["stream_findings"] == []
def test_another_sender_cannot_write_the_class(app):
application, backend = app
status, body = _post(application, _load("positive.json"), token="user-fixture")
assert status.startswith("400")
assert body["error"] == "source_not_allowed"
assert backend.db.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 0
@pytest.mark.parametrize("name,error,marker", [
("refusal-packet.json", CONTENT_REFUSED, "fixture-packet"),
("refusal-brief.json", CONTENT_REFUSED, "fixture-brief"),
("refusal-secret.json", "secret_shaped_field", "fixture-not-a-credential"),
])
def test_refusal_fixtures_are_not_retained(app, name, error, marker):
application, backend = app
payload = _load(name)
status, body = _post(application, payload)
assert status.startswith("400")
assert body["error"] == error
assert backend.db.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 0
_, letters = invoke(
application, None, path="/v1/dead-letters", method="GET",
body=b"", token="operator-fixture",
)
entry = letters["dead_letters"][0]
assert entry["reason"] == error
assert entry["payload_withheld"] is True
assert entry["payload"] is None
assert marker not in json.dumps(letters)
def test_bare_platform_tenant_is_not_the_registered_tenant(app):
application, _ = app
status, body = _post(application, _load("refusal-tenant.json"))
assert status.startswith("400")
assert body["error"] == "tenant_not_allowed"
def test_other_classes_still_follow_the_sender_redaction_policy(app):
"""The posed refusal is not a new rule for the sender's other classes."""
application, _ = app
payload = _load("positive.json")
payload = {
**payload,
"id": "presentation-1",
"type": "informed-decision.presentation",
"data": {"packet": "still-stored", "auth_token": "fixture-token"},
}
assert _post(application, payload)[0].startswith("202")
_, stored = invoke(
application, None, path="/v1/events/presentation-1", method="GET",
body=b"", token="operator-fixture",
)
assert stored["details"]["data"]["packet"] == "still-stored"
assert stored["details"]["data"]["auth_token"] == "[redacted]"
def test_both_backends_withhold_posed_content_refusals():
from audit_core.sqlite_backend import WITHHOLD_PAYLOAD_REASONS as sqlite_reasons
assert "content_refused" in sqlite_reasons
assert "secret_shaped_field" in sqlite_reasons
try:
from audit_core.postgres_backend import WITHHOLD_PAYLOAD_REASONS as postgres_reasons
except ImportError:
pytest.skip("postgres backend extra is not installed")
assert postgres_reasons == sqlite_reasons

View file

@ -353,7 +353,12 @@ def test_informed_decision_is_declared_load_bearing():
assert entry["may_read"] is False
# §9.6 permits no completeness trade for a load-bearing source.
assert "completeness_trade" not in entry
assert "emission_cadence" not in entry
assert "tokens" not in entry and "token" not in entry
# Same interval this sender already publishes for its low-volume classes,
# and the same interval the approval-engine registration tests use. Not a
# new custody class and not a rate declaration.
assert entry["heartbeat_classes"] == {"informed-decision.memo.posed": 86400}
def test_informed_decision_scope_entry_admits_nothing_without_a_token():

View file

@ -0,0 +1,40 @@
---
id: AUDIT-WP-0011
type: workplan
title: "Register informed-decision.memo.posed"
domain: infotech
repo: audit-core
status: finished
flavor: implementation
owner: grok
topic_slug: railiance
created: "2026-10-03"
updated: "2026-10-03"
---
# Register informed-decision.memo.posed
Local registration only, for intake dependency T07 (hub message
`23b670ac-809e-49bf-9233-e0b62cf0a039`). No listener, no deployment, and no
claim that informed-decision may connect `AuditCoreSink`.
## Register the posed event class
```task
id: AUDIT-WP-0011-T01
status: done
priority: high
```
Register `informed-decision.memo.posed` on the existing `informed-decision`
sender for `tenant:platform`. Accept the locally retained envelope (memo id,
approval id, carried binding digest, authenticated forwarding submitter, memo
class, submission digest). Refuse packet content, brief content, and secrets
for this class. Confirm sender custody, Idempotency-Key receipts, and the
existing per-class heartbeat and reconciliation requirement in tests, with
positive, duplicate, and refusal fixtures.
Done locally. Evidence kind, secret policy, completeness, and custody class
are unchanged. The heartbeat interval is the sender's existing low-volume
interval, 86400 seconds, not a new detection rule. Runtime readiness is not
claimed. `INFD-WP-0005` is not updated by this file.