Commit graph

44 commits

Author SHA1 Message Date
f91c5323c3 Correct the audit-core envelope to the published wire contract
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
2026-09-10 18:46:16 +02:00
59c59a1560 Record TEN-WP-0012 hub ids and refresh the work-record index
fix-consistency C-06 wrote state_hub_workstream_id and the two
state_hub_task_ids back into the workplan file and regenerated
WORK-RECORDS.md. Committing the writeback so the file and hub agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHwvAEQfmzLHtrFGhXtVjq

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 823014@bnt-lap001
Assistant-Session: 2a0786b1-efea-4c38-959b-6e86a493f259
2026-09-07 13:46:58 +02:00
7721ec6a31 Track the two external dispositions as TEN-WP-0012
TEN-WP-0011 closed correctly: T04 and T06 built everything this repo
owns. What neither could close is the other side — a sender credential
audit-core issues (AUDIT-IN-0002) and a canon edit only net-kingdom may
make (NET-IN-0002). Both intakes were filed outbound, so nothing in this
checkout held them.

Session protocol Step 3 scans workplans/ for open work. With every
workplan finished it found none, and each session re-derived the same two
externally-owned gaps from SCOPE.md prose and the audit-core source
before concluding there was nothing to do. TEN-WP-0012 holds that
conclusion so it is read rather than rediscovered, as a blocked workplan
with two wait tasks — the convention's status for work owed by another
party.

Records for each what would close it, including the decline branch, and
states the two things not to do: no editing net-kingdom canon from here,
and no requesting the audit-core token over the message bus.

No code change; 287 tests pass and ruff is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHwvAEQfmzLHtrFGhXtVjq

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 823014@bnt-lap001
Assistant-Session: 2a0786b1-efea-4c38-959b-6e86a493f259
2026-09-07 13:44:47 +02:00
6644ad8402 Onboard tenant-engine to the staged-promotion contract
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 50s
TEN-WP-0008. railiance/app.toml declares criticality=high, empty secrets,
isolated canary, and the live PostgreSQL digest as previous_stable.
Manifests render through kustomize (deploy/ and deploy/canary/). Stage 1
passed. Stage 2/3 Helm-only CLI gap requested as RAIL-BS-IN-0001 rather
than a dummy chart.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 14:51:27 +02:00
672cf4da6e Implement TEN-WP-0011 security layer conformance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client
scan). Writes persist a decision record or the published fail-closed
stance, live-lookup freshness is published, events_for is tenant-scoped,
and mutation evidence drains to audit-core from a local outbox without
blocking the mutation.

Sender registration is requested as AUDIT-IN-0002. Boundary-contract
amendment is requested as NET-IN-0002.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 13:02:51 +02:00
repo-manager
c722970aab chore(registrar): assign State Hub identifiers
Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 12:02:53 +02:00
76677bffc5 Declare Engine/PIP and open TEN-WP-0011 for v0.7 conformance
The security layer model is accepted at v0.7. INTENT.md now declares
Engine / PIP in this repository's own voice (TEN-DEC-2026-001), SCOPE.md
is brought current with shipped APIs, and the intent/scope/implementation
review is history/2026-08-29-security-layer-intent-scope-review.md.

TEN-IN-0003 is absorbed by the declaration. Approvals stay with
approval-engine. TEN-IN-0001 and TEN-IN-0002 promote into TEN-WP-0011.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 11:57:37 +02:00
f82017090b fix(workplans): qualify ad-hoc identifiers with the repository prefix
`ADHOC-YYYY-MM-DD` is unique per date but not per repository, so any two repos
opening an ad-hoc on the same day collide. The 2026-08-26 fleet projection
reset refused 9 records for exactly this reason.

Canon (work-record-types_v0.1, CUST-WP-0066) settled the form as
`{PREFIX}-WP-ADHOC-YYYY-MM-DD`, filename unchanged, and grandfathered existing
ids on the condition they are never *silently* re-derived. This is the explicit
migration that clause allows for.

The hub id is derived from the record id, so a changed id is a different
record: stale state_hub_*_id fields are dropped and fix-consistency re-derives.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-28 00:28:24 +02:00
7a1725160f docs: finish PostgreSQL production cutover 2026-08-21 01:20:54 +02:00
custodian-sync
0a65a4a25c chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-08-18:
  - workplan status: ready → active
2026-08-18 07:29:03 +02:00
b22d4a7294 TEN-WP-0010-T01: mark proposed-and-recorded as done, ratification outstanding
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:53:52 +02:00
bdf9f5f643 Finish TEN-WP-0010: mutable grouping, contract corrected, handoffs sent
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:53:10 +02:00
b998ca2332 Finish TEN-WP-0010-T03/T04: audited grouping mutation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:09:24 +02:00
1b2526910b TEN-WP-0010-T02 done, T01 proposed: no consumer parses the grouping segment
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:05:59 +02:00
20979040b2 Plan mutable grouping (TEN-WP-0010)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:41:05 +02:00
5a9e78f657 TEN-WP-0009-T01: record rapp-postgres acceptance, add postgres-client label
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 10:59:32 +02:00
05d12fe39f TEN-WP-0009-T01: target platform-pg via rapp-postgres consumer declaration
Corrects the credential assumption: shared-cluster access is a broker lease,
not a CNPG-minted secretKeyRef.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 10:44:59 +02:00
cf7ca1a692 TEN-WP-0009: make database placement portable rather than settled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:59:30 +02:00
781359e7d4 Plan PostgreSQL production store (TEN-WP-0009)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:25:35 +02:00
c05cc704b2 Plan staged-promotion onboarding (TEN-WP-0008)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:17:39 +02:00
76196aa39f Finish TEN-WP-0007: guardrail surface live in production
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:34:44 +02:00
custodian-sync
1020758e83 chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-08-16:
  - workplan status: ready → active
2026-08-16 10:02:49 +02:00
611431c2b7 TEN-WP-0007: pin guardrail image, record CI digest and flex-auth nine-action pin
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:02:34 +02:00
9f1e58a797 Plan guardrail production rollout (TEN-WP-0007)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:52:05 +02:00
955fe339fd Finish TEN-WP-0006: guardrail and quota policy
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 43s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:19:48 +02:00
d1d9c9a735 TEN-WP-0006-T05: cross-tenant denial and write-path outage conformance
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:18:52 +02:00
b6d016869f Finish TEN-WP-0006-T04: expose guardrail read and write APIs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:18:01 +02:00
f631224ab5 Finish TEN-WP-0006-T03: persist guardrails in both stores
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:14:31 +02:00
33ceb882ee Finish TEN-WP-0006-T02: implement guardrail domain model
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:10:38 +02:00
0d1435c2d2 Finish TEN-WP-0006-T01: specify guardrail model and boundary
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:07:42 +02:00
b8af02d74d Plan guardrail and quota policy (TEN-WP-0006)
Closes the guardrail/quota concern reserved since repo creation. Notably
ADR-0013 specifies trial tenants default to a zero spend budget; no such
default exists today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 01:50:08 +02:00
9ed391aec7 Finish TEN-WP-0005-T05: ship lifecycle image and verify production
Pin tenant-engine to the CI-built digest from 7e68cc8, record rollback
digests, and close T05 after live create/update/retire/replay/reactivate
evidence against a disposable production tenant. Hands the 0.1.0 contract
to USER-WP-0021.
2026-08-14 01:42:41 +02:00
7e68cc835e Start TEN-WP-0005-T05: recover deploy manifests and add CI image build
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 20s
Production still serves the TEN-WP-0004 image, which has no lifecycle
routes. Recover the live railiance01 objects into deploy/ so rollback
does not depend on a cluster annotation, and add the fleet CI image
workflow so the lifecycle image is built from a forge revision.
2026-08-14 01:37:44 +02:00
d6fd73bd42 Implement tenant update and reversible retirement API (TEN-WP-0005 T01-T04)
Add an explicit tenant lifecycle (active/retired), allow-listed mutable
metadata, record versioning, and lifecycle timestamps to the tenant authority.

- domain: TenantLifecycle, with_metadata/retire/reactivate, immutability and
  transition invariants. Identifier stays immutable -- it is the IAM Profile
  `tenant` claim key-cape mints into tokens.
- store: mutate_tenant() commits idempotency replay, version CAS, mutation,
  and audit event together; durable receipts survive restart. Retired tenants
  refuse new grants and plan changes but keep their history.
- sqlite: forward-only idempotent migration; existing rows default to active
  at version 1. Reads now take the write lock -- the concurrent-writer test
  caught unguarded reads on the shared connection observing mid-transaction
  state as a spurious tenant_not_found.
- api: GET/PATCH /tenants/{id}, POST retire|reactivate. Idempotency-Key and
  If-Match required, distinct flex-auth actions per operation, stable error
  schema, redacted 503s.
- docs/tenant-lifecycle-api.md: consumer contract for user-engine.

Implemented against SQLite, not PostgreSQL as the workplan assumed --
TEN-WP-0004 shipped SQLite on a PVC as the production store.

124 tests pass (was 66); no breaking change to existing endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:00:43 +02:00
7dcccafc03 Plan tenant update and retirement APIs 2026-08-10 18:54:28 +02:00
a48d3711c9 Sync tenant runtime workplan state 2026-08-09 01:49:11 +02:00
1e44cace1f Deploy durable tenant authority runtime 2026-08-09 01:35:00 +02:00
20bda2f0e5 chore(consistency): register ADHOC-2026-07-24 in State Hub [auto]
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:16:21 +02:00
31e237cfa6 Fix: resolve tenants by identifier, not only internal tenant_id
Found via a real cross-service check while implementing key-cape's
KEY-WP-0005-T02: key-cape's Go adapter called GET /tenants/tenant:coulomb/roles
and got a genuine 404 for a tenant that existed. External callers
(key-cape, flex-auth) only ever have a tenant's profile identifier, never
tenant-engine's internal tenant_id (caller-chosen at creation, otherwise
opaque). Every existing test happened to use identical strings for both
fields, so this was invisible until a real, independent second caller
exercised the documented contract.

InMemoryTenantStore gained a _by_identifier index and a _resolve() helper
every method calls first; create_tenant now also rejects a duplicate
identifier under a different internal id (an oversight the same fix
surfaced). 5 new tests, including the exact HTTP-level scenario with colon
characters in the URL path. 65 total, all 60 pre-existing tests unaffected.

Re-verified end-to-end for real: fresh flex-auth + tenant-engine + key-cape's
actual adapter code, over real HTTP -- roles=[IAM] ok=true resolving by
identifier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:15:26 +02:00
5d57c7d488 TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().

authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.

config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).

60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.

Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
adb74d2443 TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
  write endpoint calls it before touching the store; denial maps to
  403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
  GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
  that fails closed (503) on StoreUnavailableError -- deliberately made
  identical rather than giving cache-read weaker guarantees than the task
  strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
  all four gated by the WriteAuthorizer seam, domain/store errors mapped to
  400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.

43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.

TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
0770ce82d9 TEN-WP-0002 T01-T03: service skeleton, domain model, storage layer
Python 3.12 + FastAPI, pyproject.toml + Makefile mirroring
qonto-assistant's exactly. src/tenant_engine/{domain,store,app,main}.py:

- domain.py: Tenant, CapabilityRole (PLTF/IAM/VEN/CUS), RoleGrant,
  PlanAssignment, create_role_grant() enforcing ADR-0014's invariants.
  Refinement made while implementing: platform_default grants are valid
  for trial-grouped tenants OR the reserved tenant:platform/tenant:coulomb
  tenants (their baseline roles were never purchased either) -- the task
  spec only named the trial case.
- store.py: TenantStore Protocol + InMemoryTenantStore, every mutation
  emits a DomainEvent per the boundary contract's Audit Correlation
  Contract.
- app.py/main.py: FastAPI factory + /health endpoint, verified live on
  127.0.0.1:8090.

29 tests passing, including non-exclusive role coexistence (CUS+VEN
simultaneously) and append-only revoke semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:01:23 +02:00
7eb21c05b8 chore(consistency): register TEN-WP-0001/0002 in State Hub [auto]
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:56:43 +02:00
9370348d54 Bootstrap repo: State Hub registration, agent docs, TEN-WP-0001/0002
statehub register + repo-seed template scaffold (CLAUDE.md, .claude/rules/,
registry/). INTENT.md and SCOPE.md rewritten from the generated stub to
match net-kingdom's ratified tenant-engine-boundary-contract_v0.1.md
(Purpose, Responsibility Boundary, Non-Goals). topic_slug corrected from
the auto-assigned custodian default to netkingdom, matching key-cape and
user-engine.

TEN-WP-0001 (bootstrap) complete: files reviewed/refined, stack decided
(Python 3.12 + FastAPI, matching qonto-assistant's convention), first real
workplan seeded.

TEN-WP-0002 drafted: service skeleton, domain model (tenant/grouping/
capability-role/plan-grant), storage layer, and the three boundary-contract
API surfaces (cache-read for key-cape, live-lookup for flex-auth with an
explicit fail-closed requirement, write API behind a WriteAuthorizer seam
since real flex-auth integration is a declared non-goal for this pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:56:07 +02:00