tenant-engine/workplans/ADHOC-2026-07-24.md
tegwick 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

71 lines
3.2 KiB
Markdown

---
id: TEN-WP-ADHOC-2026-07-24
type: workplan
title: "Resolve tenants by identifier, not only internal tenant_id"
domain: infotech
repo: tenant-engine
status: finished
owner: codex
topic_slug: netkingdom
created: "2026-07-24"
updated: "2026-07-24"
---
# Resolve tenants by identifier, not only internal tenant_id
Discovered via a real cross-service check while implementing `key-cape`'s
`KEY-WP-0005-T02` (`tenant_roles` cache-read integration): `key-cape`'s Go
adapter called `GET /tenants/tenant:coulomb/roles` and got a genuine `404
tenant_not_found` for a tenant that actually existed. External callers
(`key-cape`, `flex-auth`) only ever have a tenant's **profile identifier**
(the IAM Profile `tenant` claim value, e.g. `tenant:coulomb`) — never
tenant-engine's internal `tenant_id`, which is caller-chosen at creation
(`POST /tenants`) and otherwise opaque. Every existing test happened to
pass an identical string for both fields, so this gap was invisible to
unit/API tests until a genuine second, independent caller (`key-cape`,
written against the documented contract, not against tenant-engine's
internal test conventions) exercised it for real.
## Task: Fix
```task
id: TEN-WP-ADHOC-2026-07-24-T01
status: done
priority: high
```
`InMemoryTenantStore` gained a `_by_identifier` index (identifier →
internal `tenant_id`) and a private `_resolve()` helper every method now
calls first, so `get_tenant`, `grant_role`, `revoke_role`, `active_roles`,
and `assign_plan` all transparently accept either the internal id or the
identifier. `create_tenant` also now rejects a duplicate identifier under a
different internal id (previously unenforced — an oversight the same fix
surfaced).
Done when: a real cross-process check (real `flex-auth`, real
`tenant-engine`, real `key-cape` adapter code, over actual HTTP) resolves a
tenant by identifier correctly, and all existing tests still pass
unmodified in intent.
**Done 2026-07-24:** Fixed in `store.py`. Added
`test_get_tenant_resolves_by_identifier_not_only_internal_id`,
`test_active_roles_resolves_by_identifier`,
`test_revoke_role_resolves_by_identifier`, and
`test_create_tenant_rejects_duplicate_identifier_with_different_internal_id`
in `tests/test_store.py`; added
`test_cache_read_roles_resolves_by_identifier_not_only_internal_id` in
`tests/test_api_reads.py` (the exact HTTP-level scenario, with the colon
characters a real `tenant:friendly:binky`-shaped identifier puts in the URL
path). `pytest``65 passed` (all 60 pre-existing tests unaffected).
Re-ran the exact real end-to-end check that found the bug, on fresh ports
to avoid colliding with another session's own `flex-auth serve` process on
this shared workstation (a collision that led to accidentally killing that
other process — a low-harm, trivially-restartable local dev server, but
noted here as a mistake to avoid repeating: check `pgrep` output carefully
and prefer distinctive ports before killing anything matching a broad
pattern): created a tenant with a genuinely different internal id and
identifier, granted it a role through the real `flex-auth`-gated write
path, then called `GET /tenants/tenant:coulomb/roles` from `key-cape`'s
actual `internal/adapters/tenantengine.Client` (not a stand-in) — correctly
returned `roles=[IAM] ok=true`.