Skip to content

feat(status): llm_24h reads the hourly rollup plus the edge hour (W06-5) - #75

Merged
GottZ merged 1 commit into
rootfrom
feat/oai-w06-5
Nov 3, 2026
Merged

GottZ merged 1 commit into
rootfrom
feat/oai-w06-5

Conversation

@GottZ

@GottZ GottZ commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Wave W06-5 of the OpenAI-compatible API plan (RC-A, telemetry foundation; design/06 §4.7). No migration, no /v1 surface.

What this changes

  • /api/status llm_24h (server view, queryLLM24h) and the per-tenant llm_24h generation (buildTenantRollupInto) read the hourly rollup context_llm_usage_hourly from the first full UTC hour after now − 24 h plus the raw context_llm_log rows of that first, partial hour, through two new readers in internal/llmusage (Groups, TenantGroups), instead of scanning 24 h of raw rows per tick. The tenant reader joins both parts through context_api_keys → context_tenant_scopes exactly as the raw query did; a consumer's row (api_key_id NULL) counts for no tenant.
  • llm_24h_complete comes from the rollup's unattributed_ok (successful rows without a backend) plus the same count over the edge rows: a failed row without a backend keeps complete true, as before.
  • Output is byte-identical to the raw aggregate (golden below); the one deliberate difference is a deterministic tie order.
  • Docs: docs/operations.md (rollup readers now quota + status; rollback coupling), docs/architecture.md.

Behaviour changes (intended)

  • Rows with the same calls are now ordered by backend, then pipeline (the raw query ordered by calls alone and left ties to the plan; live has 16/16 ties).
  • Backend and pipeline names longer than 100 characters show cut to 100 — the rollup's dimension width since migration 159 (the longest live name has 24 characters). Pinned by a test.
  • The window ends at the daemon's clock, not the database's now() (same host in production; tests pin the anchor on the boundaries).
  • On an error in the middle of the result the server view returns an empty list and complete=false instead of the rows read so far plus false.

Consumers of the changed contracts

Changed symbol Callers / readers Covered by
queryLLM24h, buildTenantRollupInto (same signatures, new source) status tick (assemble), SnapshotForTenant, SSE /api/events TestStatusLLM24hGolden (byte-equal to the base), TestStatusLLM24hParity (300 random seeds vs the raw SQL), TestStatusLLM24hPlan, TestStatusPerTenantView, TestStatusGoldenKeys, TestStatusEventCarriesEveryDualPathField, 58 handler top-level tests
llmusage.Groups, TenantGroups (new) the two readers above internal/llmusage suite (9/9), the handler suites
promptguard call-site census internal/handler:g.Pipeline passthrough (a copied string, no prompt) promptguard_callsite_test.go

Overlap search

Open PRs #55/#57/#63/#67 on 2026-11-03: no shared files. Based on #74 (14b99639); root unchanged since.

Cost

Per status tick: the edge is at most one hour of raw rows bounded on both sides (23 rows in the plan test vs 1 079 for the raw 24 h scan), the rest through idx_llm_usage_bucket; pinned under the custom and the forced-generic plan. Tenant view: the same two parts joined to the key and scope tables.

Verification

Gates, each red on 14b99639 first: (a) existing suites green before and after (handler 52 → 58, llmusage 9/9); (b) golden: a 16 027-byte JSON of both views generated on the base tree from the base SQL (only now() → $1 and the tie-break added; the base functions match it up to tie order), new code byte-equal (same sha256, empty diff); red on base by compile error, red against overlays: no edge hour 253/300 seeds, truncated mean 262/300, no tie-break 298/300; (c) complete probe: red when the rollup read omits unattributed_ok and when the edge does not count it; an error row without a backend keeps complete true; (d) EXPLAIN custom + generic: 23 raw rows (exactly the edge) vs 1 079, rollup via idx_llm_usage_bucket; the base SQL fails the same check; (e) consumer rows absent from the tenant view — pinned, red against an overlay that credits them to a tenant.

Suites on the head: go build ./..., go vet -tags=integration ./..., golangci-lint run with and without --build-tags=integration (0), go test -short -race ./..., deadcode.sh + deadcode.sh testonly, integration -p 1 for internal/llmusage and -run 'Status|Events|LLMLog|Quota' for internal/handler + internal/backends. Independent review by a fresh agent during the build: 1 minor (the 100-character cut — documented and pinned), 4 nits (3 fixed, the error-path change documented above).

Not verified

  • Clock skew between daemon and database shifts the window by that skew (same host in production).
  • The 100-character cut is only observable with names ctx does not produce today.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Updates
    • Server-wide and tenant-level 24-hour LLM usage summaries now use hourly usage data alongside records from the current partial hour.
    • Summaries indicate when usage data is incomplete because successful calls could not be attributed.
    • Backend and pipeline names are limited to 100 characters in summaries; names that match after truncation are grouped together.
  • Documentation
    • Updated architecture and operations guidance for usage summaries, attribution, retries, and the effects of removing or disabling rollup data collection.

The status page's llm_24h rows - the server view (queryLLM24h) and the
per-tenant generation (buildTenantRollupInto) - no longer scan 24 hours of
context_llm_log on every tick. They read through two new readers of
internal/llmusage, Groups and TenantGroups: the hourly rollup
context_llm_usage_hourly from the first full UTC hour after now - 24 h,
plus the raw rows of the partial hour before it (llmusage.Bounds), each
side aggregated by backend and pipeline (the tenant view by scope as
well) and added in NUMERIC. The edge applies the rollup trigger's
normalisation, so edge and rollup groups meet in one row.
llm_24h_complete is false exactly when unattributed_ok > 0. The tenant
view keeps the join through context_api_keys and context_tenant_scopes,
so a consumer row (api_key_id NULL) counts for no tenant. The error
behaviour is the same, except that a mid-stream server error now returns
no rows instead of the rows read so far.

What changes on the wire (hence feat, docs updated):
- Rows with as many calls are now ordered by backend, then pipeline. The
  raw query ordered by calls alone and left ties to the plan; the base
  code returned the golden seed's ties in another order than the
  tie-broken reference.
- A backend or pipeline name longer than 100 characters shows cut to
  100, the rollup's dimension width. The longest live name has 24.
- The window anchor is the Go clock instead of the database's now().
  Both run on the same host.
For names of up to 100 characters the figures are byte-equal.

Gates (red first, then green):
- (a) Existing suites before and after:
  - -run 'Status|Events|LLMLog' ./internal/handler/: 52 -> 58 top-level
    passes, 0 fail.
  - ./internal/llmusage/: 9 passes, 0 fail.
- (b) Golden: the raw-row SQL of 14b9963 (now() -> $1, plus the
  tie-break) over a mixed seed at 3 anchors wrote
  testdata/llm24h_golden.json (16027 bytes) on the base tree. The base
  functions match it apart from tie order. The rollup readers are
  byte-equal to it (sha256 6ed75ed3, empty diff).
  - Parity property: 300 random seeds, 0 differ.
  - Red against the base: compile error (llm24hNow, llm24hSpan,
    llmusage.Groups undefined).
  - Red against overlays: no edge (golden + 253/300 seeds), truncating
    avg_ms (262/300), no tie-break (298/300).
- (c) complete probe: red against a rollup read without unattributed_ok
  (oc1, rollup-hour case) and an edge without it (oc2, edge case). A
  failed row without backend_name keeps complete true.
- (d) EXPLAIN, custom and generic plan: the raw edge is bounded on both
  sides and touches 23 rows (edge 23, 24 h 1079); the rollup is read via
  idx_llm_usage_bucket. The base SQL fails the same check (1079 rows,
  lower bound only).
- (e) Tenant view without consumer rows: pinned. Red against a
  consumer-union overlay.

Suites: build, vet -tags=integration, go test -short -race ./... (all
ok), golangci-lint in both forms (0 issues), deadcode.sh and deadcode.sh
testonly (ok).

Also:
- The promptguard census lists llm24hRowOf's Pipeline: g.Pipeline as a
  passthrough.
- llmusage edgeRows uses an exact set (Copilot nit on #74).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 3, 2026 13:07
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

Server and tenant status views now read 24-hour LLM usage through shared readers that combine hourly rollups with raw rows from the edge hour. Integration tests compare these readers with raw-row references and inspect their PostgreSQL execution plans.

Changes: Hourly LLM status rollups

Layer / File(s) Summary
Shared reader contract and queries
go/internal/llmusage/groups.go, go/internal/llmusage/window.go, go/internal/llmusage/window_integration_test.go, docs/architecture.md, docs/operations.md
The shared readers aggregate server-wide or tenant-scoped usage from hourly rollups and raw edge-hour rows. They normalize backend and pipeline names to 100 characters and order results deterministically. The documentation also describes identity constraints, retry behavior, and rollup operations.
Server and tenant status integration
go/internal/handler/status.go, go/internal/handler/status_tenant.go, go/internal/handler/status_llm24h_ref_integration_test.go, go/internal/handler/status_llm24h_golden_integration_test.go, go/internal/handler/testdata/llm24h_golden.json, go/internal/promptguard/promptguard_callsite_test.go, docs/operations.md
Both status paths use the shared readers. Server completeness is false when a group has unattributed successful calls. Integration fixtures check output against raw-row references, including tenant attribution, edge-hour cases, and truncated names. Operations documentation describes status behavior when the rollup table or trigger is removed.
Parity and query-plan validation
go/internal/handler/status_llm24h_parity_integration_test.go, go/internal/handler/status_llm24h_plan_integration_test.go
Integration tests compare randomized reader results with raw-row references and check custom and generic PostgreSQL plans for bounded raw scans and rollup index use.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant ServerStatus
  participant TenantStatus
  participant llmusage
  participant PostgreSQL
  ServerStatus->>llmusage: Groups(ctx, now, 24h)
  llmusage->>PostgreSQL: Read raw edge rows and hourly rollups
  PostgreSQL-->>llmusage: Return grouped usage
  llmusage-->>ServerStatus: Return groups
  TenantStatus->>llmusage: TenantGroups(ctx, now, 24h)
  llmusage->>PostgreSQL: Read tenant-scoped edge rows and rollups
  PostgreSQL-->>llmusage: Return scoped groups
  llmusage-->>TenantStatus: Return groups
Loading

Possibly related PRs

  • GottZ/ctx#74: Adds the shared rolling-window package that these status readers extend.
  • GottZ/ctx#73: Adds the hourly rollup table and trigger queried by these readers.
  • GottZ/ctx#72: Adds the consumer identity constraint used by tenant attribution and the status fixtures.

Suggested reviewers: w4-nerf

Merge Risk: Minimal · up to 76779

No actionable issue is established for the server or tenant 24-hour status views; the change is mergeable after normal checks.

Security architecture risk: Low · up to 76779

The reviewed changes preserve administrator and tenant visibility controls. No introduced security vulnerability was established. The main operational consideration is keeping the status readers and rollup database objects compatible during rollback.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The affected output is usage telemetry on the existing administrator-or-tenant-administrator status endpoint. Proven server administrators receive the global view; tenant administrators receive a projection keyed by their authenticated HomeScope.

Trust Boundaries and Controls

  • observed — The new aggregation functions do not replace route authorization or caller-specific projection. Tenant ownership continues to be resolved through the same current key-to-tenant relationship used by the base raw query; historical reattribution through mapping changes is therefore a pre-existing behavior, not an introduced boundary bypass.

Resilience and Maintainability Implications

  • observed — Status inherits the rollup's insert-only accounting: manual raw-log deletion does not remove the corresponding rolled-up counts. Documentation specifies stopping the daemon and rebuilding the rollup transactionally after such deletion. This affects interpretation and recovery of telemetry, not tenant authorization.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: server and tenant llm_24h status views now read from the hourly rollup plus the edge hour.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 23 functions across 10 files. (3 skipped: 3 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 98e82b44-0562-4b32-b6e0-b38d54ce0507
Commits

Reviewing files that changed from the base of the PR and between 14b9963 and 7677938.

Files selected for processing: 13
  • docs/architecture.md
  • docs/operations.md
  • go/internal/handler/status.go
  • go/internal/handler/status_llm24h_golden_integration_test.go
  • go/internal/handler/status_llm24h_parity_integration_test.go
  • go/internal/handler/status_llm24h_plan_integration_test.go
  • go/internal/handler/status_llm24h_ref_integration_test.go
  • go/internal/handler/status_tenant.go
  • go/internal/handler/testdata/llm24h_golden.json
  • go/internal/llmusage/groups.go
  • go/internal/llmusage/window.go
  • go/internal/llmusage/window_integration_test.go
  • go/internal/promptguard/promptguard_callsite_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟢 Approval recommended

The change replaces the 24h status aggregation with rollup-plus-edge-hour readers and includes extensive parity/golden/plan integration tests and documentation updates, with no unresolved issues noted beyond a minor performance nit.

No open issues 🎉

What changed in this PR

This pull request updates the /api/status llm_24h aggregation (server-wide and per-tenant) to compute results from the hourly rollup table (context_llm_usage_hourly) plus the raw “edge hour” of context_llm_log, preserving output parity with the legacy 24-hour raw scan while significantly reducing per-tick query cost and making tie ordering deterministic.

Changes:

  • Add new rollup-backed readers in internal/llmusage (Groups, TenantGroups) to aggregate (backend, pipeline) over a 24h window using rollup + bounded raw edge-hour rows.
  • Switch status collection paths (queryLLM24h and per-tenant rollup generation) to use the new readers, including anchored time handling for tests.
  • Add/refresh integration tests (golden/parity/plan/reference) and update docs (operations, architecture) to reflect the new read path and rollback coupling.
File Summary
go/​internal/​promptguard/​promptguard_callsite_test.go Update promptguard callsite census coverage for the status pipeline passthrough.
go/​internal/​llmusage/​window.go Package-level documentation updates for the window/reader functionality.
go/​internal/​llmusage/​window_integration_test.go Adjust integration test helpers for edge-hour row membership/counting.
go/​internal/​llmusage/​groups.go Implement rollup + edge-hour readers for server and tenant grouped usage (note: one nit raised about avoiding per-row allocation in scan destination construction in readGroups).
go/​internal/​handler/​testdata/​llm24h_golden.json Add/refresh golden fixture for llm_24h outputs.
go/​internal/​handler/​status.go Switch server llm_24h to llmusage.Groups and add mapping/anchoring support.
go/​internal/​handler/​status_tenant.go Switch per-tenant llm_24h generation to llmusage.TenantGroups.
go/​internal/​handler/​status_llm24h_ref_integration_test.go Add raw SQL reference helpers for parity/golden comparisons.
go/​internal/​handler/​status_llm24h_plan_integration_test.go Add plan assertions to enforce bounded raw-edge reads and rollup index access paths.
go/​internal/​handler/​status_llm24h_parity_integration_test.go Add randomized parity test comparing new readers vs reference queries.
go/​internal/​handler/​status_llm24h_golden_integration_test.go Add golden/completeness/consumer-exclusion/long-name-cut integration tests.
docs/​operations.md Document operational implications of status reading rollup + edge hour (including rollback coupling).
docs/​architecture.md Update architecture documentation to reflect the new rollup-backed status reader behavior.

Review effort: Balanced

@GottZ
GottZ merged commit b4d430c into root Nov 3, 2026
23 checks passed
@GottZ
GottZ deleted the feat/oai-w06-5 branch November 3, 2026 13:21
GottZ added a commit that referenced this pull request Nov 3, 2026
…r and channel (Migration 160) (#76)

Wave W06-6 of the OpenAI-compatible API plan (RC-A, telemetry
foundation; design/06 §3.3 M-06-3, masterplan K16). Migration **160**,
no new table, no `/v1` surface. The read surface `GET /api/audit`
follows in W06-7.

## What this changes

- `go/migrations/160_audit_keys_quota.sql`: audit rows in
`context_settings_audit` for `context_api_keys` (`create`, `update`,
`revoke`, `reactivate`, `delete`) and `context_tenant_quota` (`create`,
`update`, `delete`). State is projected through **whitelist** functions
`audit_state_api_key` / `audit_state_tenant_quota` (never `to_jsonb(NEW)
- …`): `key_hash`, `last_used_at`, `created_at`, `updated_at`, `id`
never land in `old_value`/`new_value`. The key UPDATE trigger fires only
on the ten audited columns and only when one of them changed, so the
per-request `last_used_at` write of authentication never audits
(B-06-1). `audit_actor_meta()` reads the transaction-local stamps
(`ctx.api_key_id`, `ctx.request_id`, `ctx.audit_via`) and resolves the
actor's label, principal, tenant and server-admin flag into `metadata` —
the inputs of the W06-7 redaction. Backfill: one `snapshot` row per
existing key and quota row with `metadata.tenant_id` (12 rows on the
live install), idempotent. Index `idx_settings_audit_entity
(entity_type, created_at DESC)`.
- `internal/store/audit_via.go`: the `via` vocabulary as Go constants
(`api`, `bootstrap`, `provision`, `offboarding`, `migration`; `sql` = no
stamp), `AuditStamp`, `stampAudit` (one `set_config(…, true)` statement
inside the write transaction). Every API-driven writer of the two tables
now stamps actor + request id + via: `MintKeyWithQuota`, `MintOwnerKey`
(tenant create, project provision), `DeleteApiKey`, `UpdateApiKey`,
`UpsertTenantQuota`, the bootstrap admin key (now in a transaction,
`via='bootstrap'`, no actor), tenant offboarding (`via='offboarding'`).
A row written by psql reads `via='sql'` with no actor — the signal of a
direct intervention.
- Classification pin: `audit_classification_test.go` compares the live
column list of each audited table against audited ∪ explicitly excluded
(as data); a new unclassified column turns it red (B-06-2), per table
and per column.
- Docs: `docs/security.md` "Audit trail" (what is audited, what never
is, `via` values, actor fields), `docs/operations.md` "Migration 160".

- Hardening after review (`258e0953`): `stampAudit` refuses a non-empty
stamp on a handle that is not a `pgx.Tx` (`store: audit stamp outside a
transaction`) before anything is written — on a pool the `set_config(…,
true)` would be its own transaction and the write would silently read
`via='sql'` without an actor (red-proof: `MintOwnerKey(pool, APIStamp)`
created the key with a `via='sql'` row on `7d4adf3d`; now an error and 0
rows). Every production caller passes a transaction.
- The private plan submodule pointer is bumped in a separate commit
(plan corpus of this build).

## Behaviour changes (intended)

- Key and quota writes now leave one audit row each; the audit table
grows with the lifecycle of keys and quotas (rare), never with requests.
- The bootstrap admin key is inserted inside a transaction (was a single
statement).
- `UpsertTenantQuota` is stamped as well (otherwise the quota row would
carry no `request_id`).

## Deviations from design/06 §3.3, with reasons

- `via` falls back to `api` only when an actor was stamped, else `sql`
(the design's `COALESCE` would read a psql quota update as `api`).
- Quota trigger: the `updated_by` fallback actor applies on INSERT or
when `updated_by` changed; an UPDATE that changes no audited state
writes no row (no empty rows from `ON DELETE SET NULL`).
- Quota `entity_key` is the `scope` (the row's natural key).
- The index is created before the triggers (shorter lock window,
reviewer finding).
- Two existing tenant-create tests used `"non-admin-key-id"` as an actor
id; the stamp now casts it to `uuid`, so the fixtures use a UUID
(production actor ids are always UUIDs).

## Consumers of the changed contracts

| Changed symbol / object | Callers / readers | Covered by |
|---|---|---|
| `MintKeyWithQuota`, `MintOwnerKey`, `DeleteApiKey`, `UpdateApiKey`,
`UpsertTenantQuota` (new `AuditStamp` parameter) |
`handler/context_manage.go`, `tenant_manage.go`, `quota_manage.go`,
`project_provision.go`, `store/provision.go`, `store/tenant.go`,
`bootstrap_admin_key.go` | `api_keys_audit_integration_test.go` (store,
8 gates; handler, 4 gates), `TestProvisionAuditVia`, bootstrap/prune
suites |
| `context_settings_audit` rows for `api_key` / `tenant_quota` | W06-7
reader (next wave), `/api/settings/{key}` audit (unchanged: filters
`entity_type='setting'`) | settings suites unchanged |
| schema-contract manifest (+ functions, triggers, index) |
`/api/contract`, `TestCheck_FreshDB_NoDrift` | red without the 160
entries |

## Overlap search

Open PRs #55/#57/#63/#67 on 2026-11-03: no shared files. Based on #75
(`b4d430cb`); root unchanged since.

## Cost

Boot: catalog work plus the backfill (12 rows live); `CREATE TRIGGER`
takes SHARE ROW EXCLUSIVE on `context_api_keys` briefly (lock_timeout 3
s). Per key/quota write: one `audit_actor_meta` lookup (one indexed
`SELECT` on `context_api_keys`) and one audit INSERT. Per
authentication: nothing — the `last_used_at` UPDATE is outside the
trigger's column list (gate: 6 authentications + 2 no-op updates ⇒ 0
rows).

## Verification

Gates, each red on `b4d430cb` first: store gates 8 FAIL → 8 PASS,
handler gates 4 FAIL → 4 PASS, `via` pin compile error → PASS. Negative
1 (trigger without the column list): 8 audit rows for 6 authentications
+ 2 no-op updates → 0. Negative 2 (blacklist `to_jsonb(k) -
'key_hash'`): `last_used_at`, `created_at`, `updated_at`, `id` leak into
old/new → only the ten whitelisted columns. Negative 3 (probe column
`secret_probe`): classification pin red → green. Negative 4
(`MintOwnerKey` without stamp): `via='sql'`, no actor → `via='api'` with
actor and `request_id`; with all stamps disabled every writer gate was
red, bootstrap, provision and offboarding included. Backfill: N keys ⇒ N
`snapshot` rows with `tenant_id`, second run 0 rows and byte-identical
catalog.

Suites on the head: `go build ./...`, `go vet -tags=integration ./...`,
`golangci-lint run` with and without `--build-tags=integration` (0), `go
test -short -race ./...` (66 ok), `deadcode.sh` + `deadcode.sh
testonly`, integration `-p 1`: `internal/store` (330),
`internal/handler` (138 in the
manage/tenant/key/audit/settings/provision selection), `internal/auth`
(14), `cmd/ctxd` (51), `internal/schemacontract` (44). Independent
reviews: one during the build (0 blocking, 2 should-fix applied — docs
downgrade sentence, per-column pin — 6 notes) and one briefed from the
main context after it (findings folded in below where they changed the
diff).

## Not verified

- The settings trigger writes shadow rows with `via='api'` during
offboarding and `PruneTenant` leaves quota rows in place — both
pre-existing, not touched here.
- `metadata.tenant_id` has no index yet; the W06-7 tenant filter runs on
`idx_settings_audit_entity` plus a JSON predicate (fine at today's row
counts, to be measured at 1 M+ rows).

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Added audit trails for API key and tenant quota creation, updates, and
deletion, with relevant actor and request details.
* Provisioning and offboarding actions are also attributed in audit
records. Unchanged updates and authentication-only activity are
excluded.
* Existing keys and quotas without audit history receive an initial
snapshot.
* **Documentation**
* Added guidance on audit coverage, recorded details, attribution, and
inspecting audit records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants