Parent: #452
Depends on: #456 (AccountStore.sync_governance typed surface)
JS source
.changeset/create-tenant-store.md (6.7) — the headline 6.7 helper.
Problem solved
Resolves the class of bug "adopters routing by wire-supplied operator without cross-checking the auth principal could write across tenants." This is solved by baking the per-entry tenant gate INTO the framework — cross-tenant entries never reach adopter code.
Proposed API
from adcp.decisioning import create_tenant_store
store = create_tenant_store(
resolve_by_ref=lambda ref: ..., # AccountStore.resolve
resolve_from_auth=lambda ctx: tenant_id_or_none, # tenant from auth principal
tenant_id=lambda account: account.tenant_id, # extract tenant from Account
tenant_to_account=lambda tenant_id: account_or_none,
upsert_row=lambda row, ctx: ..., # optional adapter hook
sync_governance_row=lambda row, ctx: ..., # optional adapter hook
)
Fail-closed semantics
- If
resolve_from_auth(ctx) returns None, every entry fails PERMISSION_DENIED regardless of operator.
accounts.upsert and accounts.sync_governance on the returned store are immutable (Python equivalent of JS's writable: false) so adopters can't override and bypass the gate.
- Adopters with genuine custom needs use composition or write a plain
AccountStore.
Acceptance criteria
create_tenant_store exported from adcp.decisioning.
- Fail-closed test:
resolve_from_auth returns None → every entry rejected with PERMISSION_DENIED.
- Cross-tenant test: principal's tenant != entry's tenant → rejected before reaching
upsert_row/sync_governance_row.
- Immutability test: attempting to override
accounts.upsert on the returned store raises.
Parent: #452
Depends on: #456 (AccountStore.sync_governance typed surface)
JS source
.changeset/create-tenant-store.md(6.7) — the headline 6.7 helper.Problem solved
Resolves the class of bug "adopters routing by wire-supplied operator without cross-checking the auth principal could write across tenants." This is solved by baking the per-entry tenant gate INTO the framework — cross-tenant entries never reach adopter code.
Proposed API
Fail-closed semantics
resolve_from_auth(ctx)returnsNone, every entry failsPERMISSION_DENIEDregardless of operator.accounts.upsertandaccounts.sync_governanceon the returned store are immutable (Python equivalent of JS'swritable: false) so adopters can't override and bypass the gate.AccountStore.Acceptance criteria
create_tenant_storeexported fromadcp.decisioning.resolve_from_authreturnsNone→ every entry rejected withPERMISSION_DENIED.upsert_row/sync_governance_row.accounts.upserton the returned store raises.