Commit graph

16 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
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
749461b97b Implement PostgreSQL production store path
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
Add the PostgreSQL backend, migration and stopped-write transfer tools, lease-aware deployment manifests, tenancy declarations, and shared conformance coverage. Persist grouping mutations in durable stores and separate process liveness from database readiness.
2026-08-19 14:43:08 +02:00
2063470ac8 Send a projected flex-auth caller token on every check
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m7s
TENANT_ENGINE_FLEX_AUTH_TOKEN_FILE is read per request so hourly
projection rotation needs no restart. Missing or unreadable file fails
closed as a local deny and never calls flex-auth. Needed before
flex-auth-tenant-engine can enforce (FLEX-WP-0015-T02).
2026-08-19 14:31:39 +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
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
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
1e44cace1f Deploy durable tenant authority runtime 2026-08-09 01:35:00 +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