Register informed-decision.memo.posed for the platform tenant.
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:
parent
fdca5758e1
commit
70e4044128
16 changed files with 534 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
17
tests/fixtures/informed-decision-memo-posed/duplicate.json
vendored
Normal file
17
tests/fixtures/informed-decision-memo-posed/duplicate.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
17
tests/fixtures/informed-decision-memo-posed/positive.json
vendored
Normal file
17
tests/fixtures/informed-decision-memo-posed/positive.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
18
tests/fixtures/informed-decision-memo-posed/refusal-brief.json
vendored
Normal file
18
tests/fixtures/informed-decision-memo-posed/refusal-brief.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
18
tests/fixtures/informed-decision-memo-posed/refusal-packet.json
vendored
Normal file
18
tests/fixtures/informed-decision-memo-posed/refusal-packet.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
18
tests/fixtures/informed-decision-memo-posed/refusal-secret.json
vendored
Normal file
18
tests/fixtures/informed-decision-memo-posed/refusal-secret.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
17
tests/fixtures/informed-decision-memo-posed/refusal-tenant.json
vendored
Normal file
17
tests/fixtures/informed-decision-memo-posed/refusal-tenant.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
261
tests/test_informed_decision_memo_posed.py
Normal file
261
tests/test_informed_decision_memo_posed.py
Normal 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
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
40
workplans/AUDIT-WP-0011-informed-decision-memo-posed.md
Normal file
40
workplans/AUDIT-WP-0011-informed-decision-memo-posed.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue