All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 43s
audit-core registered our sender (AUDIT-IN-0002) and, reviewing the emitter, found that no event we sent could ever have been accepted. envelope_for sent five of the eight required fields under its own names -- event_id, action, resource, observed_at, details -- and omitted correlation_id entirely. normalize() rejects that whole, 400. Our drain treated 400 as terminal, so every event would have been marked handled here while audit-core held only an unchained dead letter: lost on both sides, silently, with the integration looking healthy. - envelope_for emits exactly the eight required fields and none of the six audit-core derives. The acting principal moves into `data`, where it reads as our claim rather than the archive's finding. - Thread correlation_id through create / revoke / plan, which had no such field. Optional on those three bodies for compatibility; when a caller supplies none this engine mints req-<uuid> for the operation it performed and returns it. The store mints op-<uuid> as a floor for direct callers, written into the local payload so both records agree. - Send Idempotency-Key equal to the body id. - A 400 no longer dead-letters. The row stays pending with the reason recorded on it: a 400 is an integration defect to fix, not a delivery outcome to record. - wire_envelope upgrades outbox rows written in the old shape at send time, and refuses to send one whose correlation cannot be recovered from its payload rather than inventing one. Verified by running all nine event types, through the API, through audit-core's actual normalize() with a matching SenderIdentity -- all accepted. A live non-production 202 still needs the token, so TEN-WP-0012-T01 stays `wait`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFmHM6fugwfqoobUCp9GiQ Assistant: claude-code Assistant-Model: opus Assistant-Process: 2106375@bnt-lap001 Assistant-Session: aa26c34d-71e8-4478-a962-c79c74694dc8
152 lines
6.7 KiB
Markdown
152 lines
6.7 KiB
Markdown
---
|
|
id: TEN-WP-0012
|
|
type: workplan
|
|
title: "Track the two external dispositions tenant-engine is waiting on"
|
|
domain: infotech
|
|
repo: tenant-engine
|
|
status: blocked
|
|
owner: claude
|
|
topic_slug: netkingdom
|
|
created: "2026-09-07"
|
|
updated: "2026-09-10"
|
|
depends_on:
|
|
- TEN-WP-0011
|
|
unblocks: []
|
|
origin: residual
|
|
origin_ref: TEN-WP-0011
|
|
state_hub_workstream_id: "cb7387d0-431d-56a2-bb7b-86a024aeeefb"
|
|
---
|
|
|
|
# TEN-WP-0012 — Track the two external dispositions tenant-engine is waiting on
|
|
|
|
`TEN-WP-0011` closed correctly. Its T04 and T06 are `done` in the sense that
|
|
matters here: this repository built everything it owns. What neither task
|
|
could close is the other side — a credential audit-core issues, and a canon
|
|
edit only `net-kingdom` may make.
|
|
|
|
Those two waits have no durable local record. The intakes were filed
|
|
outbound, on other repositories' intake files, so nothing in this checkout
|
|
holds them. `SCOPE.md` mentions both in prose, but prose is not scanned.
|
|
|
|
The cost is small and repeating: session protocol Step 3 scans `workplans/`
|
|
for open work, finds none, and each session re-derives the same two
|
|
externally-owned gaps from `SCOPE.md` and the audit-core source before
|
|
concluding — correctly — that there is nothing to do. This workplan holds
|
|
that conclusion so it is read rather than rediscovered.
|
|
|
|
## Why this is a workplan and not an intake
|
|
|
|
This repository's intake files record work *it* may act on. Both records
|
|
here are the opposite: work filed elsewhere, awaiting someone else's
|
|
disposition. Only `workplans/` is read at session start, and `wait` is the
|
|
convention's status for work that is blocked on another party.
|
|
|
|
## Non-goals
|
|
|
|
- Not editing `net-kingdom` canon from this repository. `TEN-WP-0011-T06`
|
|
settled that: file an intake, never fork the contract.
|
|
- Not requesting a credential over the message bus. Credential routing is
|
|
explicit that `POST /messages/` asking for a token is an anti-pattern;
|
|
sender registration is audit-core's to issue.
|
|
- Not re-opening `TEN-WP-0011`. Our side of both items shipped.
|
|
- No code change was expected from this workplan. That held until
|
|
2026-09-10, when audit-core's disposition on T01 arrived carrying a
|
|
blocking defect in our emitter. The fix was small, wholly inside this
|
|
repo, and belonged to the wait it unblocks, so it was made here rather
|
|
than spun into a workplan of its own. A larger one still would.
|
|
|
|
## T01 — audit-core sender registration (AUDIT-IN-0002)
|
|
|
|
```task
|
|
id: TEN-WP-0012-T01
|
|
status: wait
|
|
priority: medium
|
|
state_hub_task_id: "7898c1aa-b5de-54cf-87b7-67b727b5d0b2"
|
|
```
|
|
|
|
The emission path shipped in `TEN-WP-0011-T04`: the local outbox is inside
|
|
the mutation transaction, the drain POSTs `/v1/events` and never fails a
|
|
mutation, and the trade is declared in `docs/evidence-emission.md`. What is
|
|
missing is sender registration — a credential audit-core issues and holds
|
|
custody of. `src/tenant_engine/audit_core.py` documents that this module
|
|
holds no audit-core credential beyond that sender token.
|
|
|
|
Waiting on: `AUDIT-IN-0002`, filed on audit-core.
|
|
|
|
**2026-09-10 — registered, and a blocking defect found on our side.**
|
|
audit-core registered the sender (attributive, `source` pinned to
|
|
`tenant-engine`, `may_write`, `secret_policy: redact`, `tenants: ["*"]`
|
|
justified per sender) and recorded our declared completeness trade on the
|
|
sender identity itself. The token and the operator apply are still theirs
|
|
to issue, so this task stays `wait`.
|
|
|
|
Their review then found that **no event we emitted could ever have been
|
|
accepted**. `envelope_for` sent five of the eight required fields under
|
|
its own names (`event_id`, `action`, `resource`, `observed_at`,
|
|
`details`) and omitted `correlation_id` entirely; `normalize()` rejects
|
|
that whole, `400`. Our drain treated `400` as terminal, so every event
|
|
would have been marked handled here while audit-core held only an
|
|
unchained dead letter — lost on both sides, silently, with the
|
|
integration looking healthy. The root cause was on their side: the wire
|
|
contract was published nowhere we could read, and
|
|
`docs/audit-backend-contract.md` describes the *stored* record, from
|
|
which the names we chose were a reasonable inference. It is now published
|
|
as audit-core `docs/event-envelope.md`.
|
|
|
|
Fixed here the same day, ahead of any token:
|
|
|
|
- `envelope_for` emits exactly the eight required fields and sends none
|
|
of the six audit-core derives.
|
|
- `correlation_id` is threaded through create / revoke / plan, which had
|
|
no such field. It is optional on those three request bodies for
|
|
compatibility; when a caller supplies none this engine mints
|
|
`req-<uuid>` for the operation it actually performed and returns it.
|
|
Never synthesized at the archive, which is what audit-core refuses and
|
|
is right to refuse.
|
|
- `Idempotency-Key` is sent and equals the body `id`.
|
|
- `400` no longer dead-letters. The row stays pending with the rejection
|
|
reason recorded on it, per their guidance that a 400 is an integration
|
|
defect to fix, not a delivery outcome to record.
|
|
- `wire_envelope` upgrades outbox rows written in the old shape at send
|
|
time, and refuses to send one whose correlation cannot be recovered
|
|
from its payload.
|
|
|
|
Verified by running all nine event types, through the API, through
|
|
audit-core's actual `normalize()` with a matching `SenderIdentity` — all
|
|
accepted. That is the check their message asked for, short of a live
|
|
non-production `202`, which still needs the token.
|
|
|
|
Done when: audit-core registers the sender and production mutations land
|
|
externally, **or** audit-core declines and `SCOPE.md` row
|
|
"Independent audit-core emission" is restated to say so.
|
|
|
|
Do not: request the token by message, or synthesize a local stand-in that
|
|
would make the outbox look drained when it is not.
|
|
|
|
## T02 — boundary-contract amendment (NET-IN-0002)
|
|
|
|
```task
|
|
id: TEN-WP-0012-T02
|
|
status: wait
|
|
priority: low
|
|
state_hub_task_id: "f7d27d7c-f599-5d0e-b60b-9c3ad2b27b87"
|
|
```
|
|
|
|
`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` still
|
|
calls guardrail/quota policy "reserved, not implemented" and this repository
|
|
"not a policy enforcement point". Guardrails shipped in `TEN-WP-0006`/`0007`,
|
|
and the write path is PEP-shaped without this repository being a PDP, so
|
|
both sentences now mislead a reader who trusts the contract over the code.
|
|
|
|
Waiting on: `NET-IN-0002`, filed on `net-kingdom` by `TEN-WP-0011-T06`.
|
|
|
|
Done when: the contract is amended at its next cut, **or** `net-kingdom`
|
|
declines and the standing divergence is recorded here rather than left as
|
|
a live contradiction between contract and code.
|
|
|
|
## Closing this workplan
|
|
|
|
Close when both tasks reach a disposition, whichever way each falls. If
|
|
either stays open long enough to stop being worth re-reading, `cancel` the
|
|
task and say in `SCOPE.md` that the gap is permanent — an accepted,
|
|
documented divergence beats an indefinite `wait`.
|