Commit Graph
19 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 5.5 8b30e73661 chore(plan): T-15 — a feedback message to tower-control may have no sender
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:10:01 +02:00
jpmschweitzerandClaude Opus 5.5 7c0ff2bfe0 chore(plan): T-9 and T-13 done in 1.14.0; T-14, and a note on T-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:58:36 +02:00
jpmschweitzerandClaude Opus 5.5 0bba3baf32 chore(plan): T-13 — archive and trash a message in your own mailbox
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:35:21 +02:00
jpmschweitzerandClaude Opus 5.5 c21c03c289 chore(plan): T-11 done — live in 1.13.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:16:05 +02:00
jpmschweitzerandClaude Opus 5.5 fca545aa49 chore(plan): T-12 — the test database URL leaks through pytest -v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:08:36 +02:00
jpmschweitzerandClaude Opus 5.5 008996e030 chore(plan): T-11 — a default address on each consumer grant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:38:18 +02:00
jpmschweitzerandClaude Opus 5.5 d1641f1480 chore(plan): T-10 — Scribe messages number per tenant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:12:23 +02:00
jpmschweitzerandClaude Opus 5.5 1c4cfc4f1b chore(plan): T-9 — the mailbox answers by message number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:08:02 +02:00
jpmschweitzerandClaude Opus 5.5 87481a016a chore(plan): T-1 and T-8 done — live in 1.12.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:48:04 +02:00
jpmschweitzerandClaude Sonnet 5 99ee994023 feat(tenants): add the tenant registry, the one enforcement point Book and Scribe now share (T-8)
A tenant has been an unchecked string since T-4/T-7: the Office's
X-Office-Tenant header or a user JWT's tatlock_user claim, with Book and
Scribe each running their own _require_tenant that checked only presence.
A typo'd claim opened a fresh, empty namespace rather than being refused.

src/domains/tenants/ (controller/models/schemas/service) adds tenants and
tenant_consumers (migration 007, down_revision="006"): tenants.id is the
slug a tatlock tenant carries, kind (household|person|test) and status
(active|suspended) CHECK-constrained; tenant_consumers is the grant pairing
a consumer to a tenant. Migration 007 seeds the two tenants in use today —
jpmschweitzer (household, "Jeroen") and llm_tester (test, "Playground") —
each granted to office-claude, the only Office consumer configured in
Authentik. Every existing owner_tenant column (book_records,
book_tenant_counters, scribe_addresses, scribe_messages,
scribe_mailbox_entries — five, grepped and confirmed) now carries a
foreign key to tenants.id, ON DELETE RESTRICT, at both the migration and
the ORM model.

TenantsService.require_tenant is the one enforcement point: no tenant is
400 tenant_required (unchanged); an unknown tenant is 403 tenant_unknown;
a suspended one is 403 tenant_suspended; an Office caller whose consumer
holds no grant in that tenant is 403 consumer_not_granted. One query — a
LEFT JOIN against the grant — so presence, status and the grant are all
read in a single round trip. Book's and Scribe's own _require_tenant now
delegate to it, translating TenantError to their own exception type so
every call site and each controller's single except clause keep working
unchanged.

Read-only this round: GET /tenants/me answers the caller's own tenant and
its granted consumers (the cheap "am I allowed here" probe); GET /tenants
answers every tenant for a tatlock/scheduler caller, or a list of one for
a tenant-bearing caller — another tenant's existence is never revealed.
No write routes; provisioning a tenant is workspace T-195.

Tests: tests/test_tenants.py and tests/test_tenants_migration.py (new);
a TestTenantRegistryEnforcement class added to both tests/test_book.py and
tests/test_scribe.py proving all four cases (unknown, suspended,
ungranted-consumer, and the control) on a live Book and Scribe route, not
only on /tenants itself. tests/conftest.py gains a tenant_registry schema
fixture and a suspended_tenant fixture (inserted and deleted per test; no
FK references it); book_schema and scribe_schema each independently grant
a second, test-environment-only consumer (office-gemini) in both seeded
tenants, since test_book.py and test_scribe.py have used it as a stand-in
second consumer since before this grant check existed — the migration
itself still seeds only office-claude, matching the dossier.

Mutation-checked: dropping the grant check fails the three
consumer_not_granted tests (200 instead of 403); dropping the status
check fails the three tenant_suspended tests; making Book bypass the
helper fails its unknown-tenant test (200 instead of 403). All three
restored and the full suite re-verified green (508 + 35 = 543 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 17:30:55 +02:00
jpmschweitzerandClaude Opus 5.5 e52c727d99 chore(plan): T-7 — the Book schema and tenant boundary round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:07:03 +02:00
jpmschweitzerandClaude Opus 5.5 c5baaeea99 chore(plan): T-5 done; T-6 filed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:24:11 +02:00
jpmschweitzerandClaude Opus 5.5 739b33b198 chore(plan): T-5 — the Scribe messaging round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:57:49 +02:00
jpmschweitzerandClaude Opus 5.5 e395220c1f chore(plan): T-4 done; T-3 gains the dev-server target
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:46:48 +02:00
jpmschweitzerandClaude Opus 5.5 1da057bea6 chore(plan): round office-2 ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:29:43 +02:00
jpmschweitzerandClaude Opus 5.5 3171389583 chore(plan): T-2 — the Book carries an owner, the tatlock tenant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:37:01 +02:00
jpmschweitzerandClaude Opus 5.5 e31990c217 chore(plan): T-1 reviewed; T-2 and T-3 filed
T-2 finishes the Book before its first deploy: the remaining verbs,
short outward ids (amending migration 005), and work/personal as tags.
T-3 names the alembic target instead of inheriting production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:20:53 +02:00
jpmschweitzerandClaude Opus 5.5 0df3fdddcb chore(plan): T-1 — caller auth, locked housekeeping routes, the Book
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:43:41 +02:00
jpmschweitzerandClaude 29bfa3259d docs: replace AGENTS.md with a repo-specific CLAUDE.md
One agent doc per repo, and it is CLAUDE.md. Two docs describing one repo
drift, and the one nobody read is always the one holding the rule that
mattered. Written fresh rather than reformatted, so the structure follows
what someone working here actually needs.

Two rules from the old file are gone deliberately. The mandate to branch
for every change was retired in favour of one linear-history policy, and
the release snippet used `git add -A`, which sweeps in whatever else is
dirty.

The architecture section is the part worth reading. An earlier draft
called src/auth, src/controllers, src/clients, src/dns and src/models
dead code, derived from grepping main.py's imports. That was wrong:
main.py:55 calls initialize_oidc(), which imports and configures
src.auth.oidc from inside the function body, so src/auth is configured
with live Authentik issuers on every boot. It also missed four genuinely
unreferenced packages. The section now states the method used -- import
the app in the container and read sys.modules -- and its blind spot, that
a cold snapshot cannot see a module imported on a request path.

Also records that the README's "runs as non-root user (uid 1000)" is
false: the Dockerfile has no USER directive. Flagged rather than fixed,
since changing the runtime user is not a docs change.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-09 03:12:02 +02:00