These workplans exist only in the retired local hub. Their random pre-ADR-007 identifiers are refused by C-06 as stale references, so they cannot be registered. Deriving from the canonical record id takes no identity from anything: central does not hold them and the old ids die with the cache. Records central already holds were deliberately left untouched. Refs CUST-WP-0068-T06 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
11 KiB
| id | type | title | domain | repo | status | owner | topic_slug | planning_priority | planning_order | depends_on_workplans | related_workplans | created | updated | state_hub_workstream_id | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FLEX-WP-0008 | workplan | tenant-engine Consumer Integration | infotech | flex-auth | finished | codex | flex-auth | P1 | 80 |
|
|
2026-07-23 | 2026-07-23 | 1358db95-967c-5a03-8b8c-4816dc106594 |
FLEX-WP-0008: tenant-engine Consumer Integration
Purpose
Make tenant-engine a registered protected-system consumer of flex-auth,
in both directions its boundary contract requires
(net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md):
- tenant-engine's writes need authorization.
tenant-engine'sFlexAuthWriteAuthorizer(TEN-WP-0003) callsPOST /v1/checkfor every mutation (tenant.create,tenant.role.grant,tenant.role.revoke,tenant.plan.assign). Until a policy package exists here, every check resolves todeny/not_applicable— correct fail-closed behavior, but it meanstenant-enginecannot actually perform any write yet. This workplan is what makes realallowdecisions reachable. - flex-auth's own decisions need tenant capability roles as input, for
any protected system whose policy depends on a tenant's
PLTF/IAM/VEN/CUSroles (ADR-0014) ataal2-class decision time. flex-auth must calltenant-engine's live-lookup endpoint (GET /tenants/{id}/roles/live) to enrich decision context, the same role acontextadapter plays for any other external fact source.
Closer in shape to FLEX-WP-0006/FLEX-WP-0007 (ops-warden's small,
security-lane SSH-certificate integration) than FLEX-WP-0003 (Markitect's
document-heavy, CARING-benchmark integration) — tenant-engine's resource
and action vocabulary is small and doesn't need CARING descriptor mapping.
Task: Define tenant-engine resource and action vocabulary
id: FLEX-WP-0008-T01
status: done
priority: high
state_hub_task_id: "5606408f-f94c-5d79-ba41-ed23fadac480"
Resource types: tenant, role-grant, plan-assignment. Actions:
tenant.create, tenant.role.grant, tenant.role.revoke,
tenant.plan.assign — matching TEN-WP-0003-T02's action-name mapping
exactly (coordinate values, don't let the two repos invent different
strings for the same action). Model on examples/ops-warden's fixture
shape (schemas/check_request.schema.json's resource{id,type,system},
system: "tenant-engine"), not Markitect's.
Done when: resource/action vocabulary documented under docs/ following
the markitect-resource-namespace.md / markitect-action-vocabulary.md
naming pattern (tenant-engine-resource-namespace.md,
tenant-engine-action-vocabulary.md).
Done 2026-07-23: Both docs written. One deliberate deviation from the
ops-warden template, documented in tenant-engine-resource-namespace.md:
no resource_manifest.yaml — ops-warden's SSH certificates are a fixed,
pre-registrable inventory; tenant-engine's tenants are created
dynamically, so the policy evaluates input directly rather than looking
up a registered resource by id. Action vocabulary cross-checked against
tenant-engine/src/tenant_engine/authz.py's actual
_RESOURCE_TYPES mapping — the four action strings and their resource
types match exactly, not just by convention.
Task: Author and register the tenant-engine policy package
id: FLEX-WP-0008-T02
status: done
priority: high
state_hub_task_id: "bbe1a8f4-dcd9-58bb-8d82-386ee0c11a51"
Policy: writes require an aal2+ assurance actor holding an appropriate
capability role for the action (e.g. tenant.plan.assign might require
PLTF or a platform-operator subject type; exact rules are a design
decision for this task, not pre-specified here — coordinate with ADR-0014's
role semantics, not invented fresh). Register in registry/ per the
existing pattern; add fixtures (allow/deny request/response pairs)
mirroring examples/ops-warden/check_request_allow_adm.json.
Done when: POST /v1/check against a request matching TEN-WP-0003's
FlexAuthCheckClient shape returns allow for an authorized actor/action
and deny for an unauthorized one, exercised by main_test.go-style
integration tests.
Done 2026-07-23, with the design decision made explicit: chose
operator/service-identity authorization, not aal2+assurance +
tenant-capability-role checking, for this specific policy. Reasoning,
recorded here rather than left implicit: tenant_engine's CheckRequest
today carries no assurance claim at all (there's no authenticated caller
identity yet — TEN-WP-0003's closure note already flagged actor as a
plain request-body field, not a real auth context), and a tenant's
capability roles (PLTF/IAM/VEN/CUS) are a property of the tenant
being written to, not of who is allowed to call tenant-engine's admin API
— checking the wrong one would be a category error, the same one flagged
in docs/tenant-engine-action-vocabulary.md. So: examples/tenant-engine/policy_package.md
authorizes exactly one registered service subject (tenant-engine itself,
subject_manifest.yaml) for the four known actions against
system: "tenant-engine". Revisit once KEY-WP-0005 gives callers a real
assurance-bearing identity to check.
Registered: protected_system_manifest.yaml, subject_manifest.yaml,
policy_package.md (Rego rules + embedded tests), policy_fixtures.yaml
(8 allow/deny pairs), assembled registry_snapshot.json, two standalone
check_request_*.json examples, README.md.
Verified for real, not just written: built the actual flex-auth
binary (go build ./cmd/flex-auth) and ran it against every artifact:
test-policy — all 6 Rego tests and all 8 fixtures pass; load-registry
loads the snapshot cleanly; check against both standalone request files
returns the expected allow/deny. Then went one step further than the
task's own done-criteria: ran a live flex-auth serve with this exact
registry+policy and pointed tenant-engine's real, unmodified
FlexAuthCheckClient/FlexAuthWriteAuthorizer at it (TENANT_ENGINE_FLEX_AUTH_URL)
— POST /tenants with actor="ops" → 403 unknown_subject; with
actor="tenant-engine" → 201. Full round trip, both processes, real
HTTP, real Rego evaluation. go test ./... still green across the whole
flex-auth repo — nothing broken.
Task: tenant-engine live-lookup context adapter
id: FLEX-WP-0008-T03
status: done
priority: medium
state_hub_task_id: "e27d24b3-0aef-5bd4-b7e3-81532cd7aa9c"
A context-enrichment adapter (mirrors internal/adapters/{relationship,rule,topaz}'s
shape) that calls tenant-engine's GET /tenants/{id}/roles/live and
attaches the result to decision context for policies that need it — used by
other protected systems' policies that key off tenant capability role,
not only by tenant-engine's own policy from T02. Must fail closed: adapter
error or tenant-engine unavailability must not silently produce an empty
role list indistinguishable from "no roles granted" (same rule
tenant-engine's own read endpoints already enforce — don't weaken it on
the consuming side).
Done when: a policy package referencing tenant capability role context correctly denies when the adapter call fails, not just when it succeeds with an empty result.
Done 2026-07-23, one architectural finding recorded first: confirmed by
reading internal/decision/engine.go that Check() has no context-adapter
hook at all, and confirmed via cmd/flex-auth/main.go that none of the
existing topaz/relationship/rule adapters are wired into the shipped
binary either — they're standalone Go packages for downstream composition,
not auto-invoked plugins. This adapter follows the same shape: Rego
evaluation is stateless and can't make an HTTP call mid-evaluation, so
internal/adapters/tenantengine is a request-preparation helper a
protected system's own request-building code calls before submitting to
POST /v1/check — not an engine-internal hook.
HTTPClient.LiveRoles(ctx, tenantID) (LiveRolesResult, error) calls
GET {base}/tenants/{id}/roles/live. LiveRolesResult.Available is the
load-bearing field: false on any failure (transport error, non-200,
malformed body) — never inferred as zero roles.
AttachToContext(context, result) writes tenant_roles and
tenant_roles_available into a CheckRequest.Context map; the
Available: false / tenant_roles_available: false pairing is what a
consuming policy must check before trusting tenant_roles at all —
documented in the package doc comment as the required Rego precondition.
9 Go tests (http_client_test.go, context_test.go): success, non-200,
malformed body, connection failure, context-timeout, empty base URL
rejected, context attachment (including nil-context and
non-clobbering-existing-fields cases). gofmt/go vet/go build ./...
clean; go test ./... still green across the whole repo.
Verified as a real three-service chain, not a mock: ran a live
flex-auth serve (T02's registry+policy) and a live tenant-engine
pointed at it (TENANT_ENGINE_FLEX_AUTH_URL), created a tenant and granted
it a CUS role through the real, flex-auth-gated write path, then called
this new Go adapter (via a throwaway cmd/ harness, removed after use, not
committed) against the running tenant-engine: roles=[CUS] available=true for the known tenant, roles=[] available=false err="status 404" for an unknown one — the exact fail-closed distinction
this task exists to guarantee, proven end-to-end across Go → Python → Go
(via HTTP), not asserted in isolation.
Task: Closure review
id: FLEX-WP-0008-T04
status: done
priority: low
state_hub_task_id: "0f319a2f-e4bb-5ba8-92de-b693ae9a2aa3"
Confirm T01–T03 done; run flex-auth's existing test suite plus the new
fixtures. Note in closure: TEN-WP-0003's FlexAuthWriteAuthorizer should
be re-verified against a real allow decision once this workplan's policy
package exists (it was only tested against deny/not_applicable
responses when built, since this workplan didn't exist yet). Run statehub fix-consistency.
Closed 2026-07-23. T01–T03 done. go test ./... green across the whole
repo (including the new internal/adapters/tenantengine package);
gofmt/go vet clean.
The re-verification this task asked for happened twice, for real: in T02's
closure (a live flex-auth serve + tenant-engine's unmodified
FlexAuthWriteAuthorizer, allow → 201) and again in T03's closure (the
same live pair, plus a write through it, plus the new Go adapter reading
the result back) — FlexAuthWriteAuthorizer was never re-tested against a
mock standing in for allow; every allow assertion in this workplan came
from the real Rego engine.
Full picture after this workplan: tenant-engine's write path is real
end-to-end (tenant-engine → real flex-auth → real allow/deny), and
any other protected system can now pull live tenant capability-role
context via internal/adapters/tenantengine, fail-closed. What's still
open, unchanged from TEN-WP-0003's own closure: KEY-WP-0005 (key-cape
doesn't emit IAM Profile core claims yet — the cache-read/tenant_roles
direction depends on it, not this workplan). This workplan's own T02 policy
(operator/service identity, not aal2+role) should be revisited once
KEY-WP-0005 gives real caller assurance to check.