Extend the tenant-engine write-API policy package with tenant.update, tenant.retire, and tenant.reactivate, the three actions TEN-WP-0005 introduced. Until now every lifecycle mutation resolved to deny unknown_action, blocking TEN-WP-0005-T05 production rollout. - policy_package.md: three actions in valid_actions and frontmatter; CARING capabilities extended with EditAny/Archive/Restore - protected_system_manifest.yaml, subject_manifest.yaml, and the rebuilt registry_snapshot.json carry all seven actions - docs/tenant-engine-action-vocabulary.md documents the HTTP surfaces - 8 new fixture pairs and 5 new Rego tests, including action-drift guards (tenant.retired, tenant_update -> unknown_action) T02 decision, recorded in policy_package.md: tenant.retire does NOT get stricter authorization yet. With one service subject and no assurance claim in the CheckRequest there is nothing stricter to check, and a condition the sole caller always satisfies would falsely read as separate control. Retirement is reversible and non-destructive. Revisit on KEY-WP-0005 assurance claims or a second operator subject. Verified: 11/11 Rego tests, 16/16 fixtures, go test ./... green, gofmt and go vet clean. End-to-end against a live serve with a real tenant-engine through the unmodified FlexAuthWriteAuthorizer: update 6176c39c2f4d7b15, retire 8a801b8ee8455080, reactivate c64cf3713cecd970 all allow; unregistered actor denied unknown_subject (59d3e99c6416be89, 403 write_denied). Handoff revision for TEN-WP-0005-T05: package tenant-engine.write-api.mutate v1 (ready, caring-0.4.0-rc2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 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-0010 | workplan | Authorize tenant-engine lifecycle actions | infotech | flex-auth | finished | codex | netkingdom | P1 | 100 |
|
|
2026-08-10 | 2026-08-10 | c159fe8b-d35b-4a74-8a15-c1263f7f6392 |
FLEX-WP-0010 - Authorize tenant-engine lifecycle actions
Extend the existing tenant-engine policy package (FLEX-WP-0008-T02) with the
three tenant lifecycle actions TEN-WP-0005 introduced. Until they exist here,
every lifecycle mutation resolves to deny with unknown_action — correct
fail-closed behaviour, not a defect, but it means tenant-engine's new update
and retirement endpoints cannot perform a single write in production.
Requested by: tenant-engine (TEN-WP-0005, T01–T04 complete and merged).
Blocks: TEN-WP-0005-T05 (production rollout), and through it
USER-WP-0021 (user-engine platform operator UI/API).
Requirements from tenant-engine
Three new actions, all on the existing tenant resource type, all against
system: "tenant-engine":
| Action | Resource type | HTTP surface |
|---|---|---|
tenant.update |
tenant |
PATCH /tenants/{tenant_id} |
tenant.retire |
tenant |
POST /tenants/{tenant_id}/retire |
tenant.reactivate |
tenant |
POST /tenants/{tenant_id}/reactivate |
These strings are already live in
tenant-engine/src/tenant_engine/authz.py's _RESOURCE_TYPES mapping —
they are the exact values FlexAuthWriteAuthorizer sends. Match them
verbatim; do not let the two repos invent different strings for the same
action (the coordination rule FLEX-WP-0008-T01 already established).
The CheckRequest shape is unchanged from FLEX-WP-0008: same
FlexAuthCheckClient, same subject_type: "service", resource_id =
tenant_id. No new request fields, no schema change.
Why three actions rather than one tenant.write: tenant-engine
deliberately separated them so policy can permit an operator to rename a
tenant without thereby permitting them to retire it. Consumer contract:
tenant-engine/docs/tenant-lifecycle-api.md.
Honest caveat on what this buys today: under FLEX-WP-0008-T02's current
model there is exactly one known operator (the tenant-engine service
identity) and no assurance claim, so all three actions will resolve
identically once added. The separation is a seam for later, not
differentiated authorization now. Adding the actions is what unblocks
production; differentiating them is T02 below, and may well close as "not
yet, and here is why".
T01 - Extend the policy package with the lifecycle actions
id: FLEX-WP-0010-T01
status: done
priority: high
state_hub_task_id: "e92e45b9-724a-4fbd-8302-75558cf4b6c1"
Add tenant.update, tenant.retire, and tenant.reactivate to
examples/tenant-engine/policy_package.md's valid_actions set and to the
frontmatter actions: list. Extend the caring.capabilities list if the
existing vocabulary (Create/Grant/Revoke/Bind/Audit) does not
already cover an update/retire/reactivate shape — a judgement call for this
repo, not pre-specified here.
Update examples/tenant-engine/subject_manifest.yaml's subject metadata
description, which currently enumerates the original four actions.
Update docs/tenant-engine-action-vocabulary.md with the three actions, their
resource type, and the HTTP surface each gates. Keep the existing note that
this policy governs who may call tenant-engine's admin API, not a tenant's
own PLTF/IAM/VEN/CUS capability roles — the lifecycle actions do not
change that boundary and should not be read as doing so.
Rebuild examples/tenant-engine/registry_snapshot.json.
Done when valid_actions carries all seven actions and the vocabulary doc
and subject manifest no longer describe only four.
Done 2026-08-10: all seven actions are in valid_actions, the frontmatter
actions: list, and the protected-system manifest. CARING capabilities were
extended with EditAny (update), Archive (retire), and Restore
(reactivate) — the existing Create/Grant/Revoke/Bind set had no
update or lifecycle shape, and Archive/Restore name the reversible pair
exactly as tenant-engine implements it. registry_snapshot.json was rebuilt
from the two manifests, the subject metadata now enumerates all seven, and
docs/tenant-engine-action-vocabulary.md carries the three HTTP surfaces
plus an explicit note that tenant.retire asks whether this caller may
retire a tenant, not what a retired tenant may do.
T02 - Decide whether retirement warrants stricter authorization
id: FLEX-WP-0010-T02
status: done
priority: medium
state_hub_task_id: "5aa9e104-a4b3-40cd-b5ad-4ff56421ce69"
tenant.retire is the highest-consequence action in the set: it suspends a
tenant's ability to receive new capability grants and plan changes across the
whole platform. Decide whether it should require anything tenant.update does
not — a distinct subject, group, or (once available) assurance level.
Record the decision with its reasoning, in the style of FLEX-WP-0008-T02's
closure note. "No, not until KEY-WP-0005 gives callers a real
assurance-bearing identity to check" is a perfectly good outcome — the point
is that the decision is explicit and recorded rather than defaulted into by
treating all seven actions as interchangeable.
Note that retirement is reversible by design (tenant.reactivate), and
that tenant-engine hard-deletes nothing — so the blast radius of a wrongly
allowed retirement is recoverable, which is legitimate input to this decision.
Done when the decision and its reasoning are recorded in
policy_package.md, and implemented if the answer is "yes, stricter".
Done 2026-08-10: no — tenant.retire is authorized by the same rule as the
other six. Recorded with full reasoning under "Decision: tenant.retire
does not require stricter authorization yet" in
examples/tenant-engine/policy_package.md. The load-bearing reason is that
there is nothing stricter to check: one service subject, no second operator,
no assurance field in the CheckRequest. A condition the sole caller
always satisfies would read to a later reviewer as though retirement were
separately controlled when it is not — a false assurance in a package whose
job is inspectability. Retirement being reversible and non-destructive makes
that acceptable. Revisit when KEY-WP-0005 supplies an assurance-bearing
identity, or when a second tenant-engine operator subject is registered.
T03 - Fixtures and verification
id: FLEX-WP-0010-T03
status: done
priority: high
state_hub_task_id: "59856c87-a1b4-4fce-b554-9b13181bb8fd"
Add allow/deny fixture pairs to
examples/tenant-engine/policy_fixtures.yaml for each of the three actions,
mirroring the existing eight pairs' shape. Cover at minimum: authorized
service subject → allow; unknown subject → deny unknown_subject; and a
misspelled lifecycle action → deny unknown_action (the case that guards
against the two repos drifting apart on action strings).
Verify the way FLEX-WP-0008-T02 did, against the real binary rather than in
isolation: go build ./cmd/flex-auth, then test-policy (all Rego tests and
fixtures), load-registry, and check against standalone request files.
Then close the loop end-to-end, which is what TEN-WP-0005-T05 actually needs
evidence of: run a live flex-auth serve with this registry and point a real
tenant-engine at it (TENANT_ENGINE_FLEX_AUTH_URL), then exercise
PATCH /tenants/{id}, POST .../retire, and POST .../reactivate through
the unmodified FlexAuthWriteAuthorizer. tenant-engine's lifecycle endpoints
require Idempotency-Key and If-Match headers — read the record first and
echo its ETag back; tenant-engine/docs/tenant-lifecycle-api.md has the
call shapes.
Done when all three lifecycle mutations return a real allow from the real
Rego engine over real HTTP, go test ./... is green across the repo, and
gofmt/go vet are clean.
Done 2026-08-10: eight fixture pairs added (allow for each of the three
actions, unknown_subject deny for each, and two action-drift denies —
tenant.retired and tenant_update, both unknown_action). Five embedded
Rego tests added. test-policy reports 11/11 tests and 16/16 fixtures
passing; load-registry loads clean; check returns the expected decisions
for the two new standalone request files.
End-to-end against a live flex-auth serve (127.0.0.1:9098) with a real
tenant-engine on TENANT_ENGINE_FLEX_AUTH_URL, driven through the
unmodified FlexAuthWriteAuthorizer with Idempotency-Key and an If-Match
echoed from a prior GET:
| Call | tenant-engine | flex-auth decision |
|---|---|---|
POST /tenants |
201 | decision:174dc9ecb03ed9e5 allow |
PATCH /tenants/t-e2e-1 |
200 active |
decision:6176c39c2f4d7b15 allow |
POST .../retire |
200 retired |
decision:8a801b8ee8455080 allow |
POST .../reactivate |
200 active |
decision:c64cf3713cecd970 allow |
POST .../retire as actor: ops |
403 write_denied |
decision:59d3e99c6416be89 deny unknown_subject |
go test ./... green across all packages; gofmt -l empty; go vet ./...
clean. Evidence table mirrored into examples/tenant-engine/README.md.
T04 - Closure and handoff
id: FLEX-WP-0010-T04
status: done
priority: low
state_hub_task_id: "04141e54-a250-424e-8dfd-4148875e67da"
Confirm T01–T03. Run statehub fix-consistency. Notify tenant-engine that
TEN-WP-0005-T05 is unblocked, naming the policy revision — T05's own
done-criteria require the handoff to name the authorization policy revision
alongside the immutable image and API version.
Done 2026-08-10: T01–T03 confirmed. Policy revision for the handoff is
package tenant-engine.write-api.mutate version v1, status ready,
profile caring-0.4.0-rc2, now carrying all seven actions — the version
string is unchanged because the package is additive and v1 remains the
activated revision; the identifying fact for TEN-WP-0005-T05 is that v1 as
of this commit contains tenant.update, tenant.retire, and
tenant.reactivate. TEN-WP-0005-T05 is unblocked.