Task — engineering-spec@1

"TEN plan-change HTTP host — POST/GET plan + history TX trigger + plan_tier TEXT→enum"

doneTASK-TEN-203
module ten · class product · priority p0 · created 2026-07-26 · shipped null
depends on TASK-TEN-002 · blocks TASK-TEN-003

TASK-TEN-203: TEN plan-change HTTP host

Summary

Wire the shipped cyberos-ten pure decision substrate into the auth admin axum host: POST / GET /v1/admin/tenants/{id}/plan, persist plan changes with a same-TX history row enforced by a Postgres trigger, and cut over auth tenants.plan_tier from TEXT (+sandbox CHECK) to the closed plan_tier enum.

Problem

TASK-TEN-002 landed library + additive migrations only (decide_plan_change, PlanShow, enum CREATE TYPE). There is no HTTP route, no history TX trigger, and auth still stores plan_tier as TEXT with a CHECK that includes sandbox. Operators cannot change plans via API; billing (TASK-TEN-003) has no host surface to call.

Proposed Solution

  1. Add services/auth/src/plan_admin.rs with:
  1. Role gate: tenant-admin for own tenant, or founder role for any; founder-tenant short-circuit already in decide_plan_change.
  2. Auth migration 0034_plan_tier_enum_and_history.sql: create enums if missing; relocate any sandbox rows to starter; drop TEXT CHECK; cast column to plan_tier; ensure is_founder_tenant / history table; install require_plan_history BEFORE UPDATE trigger (P0301).
  3. Auth models: validate create-tenant plan_tier against the closed 3-value set (reject sandbox at API).
  4. Depend on cyberos-ten from cyberos-auth.

Error mapping: Forbidden/FounderImmutable → 403; SameTier → 409 no_change; DowngradeViolation → 409 downgrade_violation.

Alternatives Considered

Success Metrics

Scope

In scope

Out of scope / Non-Goals

Dependencies

AI Authorship Disclosure

Generated then reviewed against as-built services/ten + services/auth deep map (2026-07-26).

Acceptance Criteria

  1. GET plan — JWT tenant-admin for tenant T receives 200 with tier, caps matching caps_for(tier), and is_founder_tenant.
  2. POST upgrade — Starter→Team with effective: immediate returns 200; tenants.plan_tier is team; one tenant_plan_history row with positive proration_amount_cents when mid-period.
  3. Downgrade violation — usage above target seats without acknowledge_data_loss → 409 downgrade_violation.
  4. Founder lock — non-founder actor against is_founder_tenant=true → 403 founder_tenant_plan_immutable.
  5. Same tier — POST with current tier → 409 no_change.
  6. History triggerUPDATE tenants SET plan_tier=… without prior history INSERT in same TX → SQLSTATE matching P0301 / raise exception.
  7. Enum cutoverplan_tier column type is enum; create-tenant rejecting sandbox with 400; cardinality of enum = 3.
  8. Auth depcyberos-auth depends on cyberos-ten; handlers call decide_plan_change (not a reimplemented decision tree).

Verification

cd services
cargo test -p cyberos-auth --test plan_change_http_test -- --test-threads=1
cargo test -p cyberos-ten -- --test-threads=1
bash .cyberos/cuo/gates/run-gates.sh

Failure Modes

FailureDetectionOutcomeRecovery
Missing history INSERTP0301 triggerTX abortHandler always INSERT then UPDATE
Sandbox row at migrateMigrate relocate to starterEnum cast succeedsLog count relocated
Stale TEXT in RustCompile / sqlxBuild failUse PlanTier / String as_str
Concurrent plan POSTsUnique race on historyLast writer wins per TXRate-limit later
JWT wrong tenantRole gate403Client retry
DB downsqlx error500Retry
Invalid target_tier JSONSerde / parse400Client fix
Downgrade ack falsedecide_plan_change409Ack or reduce usage
Founder immutabledecide_plan_change403Founder path later
Migration order vs TEN 0004IF NOT EXISTS / DO blocksIdempotentRe-run migrate

End of TASK-TEN-203.

Audit

§1 — Verdict summary

Focused residual host task: wires TEN-002 library into auth admin routes + enum cutover. 8 ACs, 10 failure-mode rows, explicit Out of scope for TEN-002 residuals not in host-a.

§2 — Findings (all resolved)

ISS-001 — Host choice ambiguous

Could have been a new ten binary. Resolved: Proposed Solution #1 + Alternatives; AC #8 auth dep.

ISS-002 — Sandbox cutover underspecified

TEXT CHECK includes sandbox. Resolved: migrate relocates sandbox→starter; AC #7.

ISS-003 — Missing P0301

TEN-002 deferred trigger. Resolved: Scope + AC #6 + failure table.

ISS-004 — Scope creep into rate-limit/dry-run

Full TEN-002 §1 #14+. Resolved: Out of scope list.

ISS-005 — Error mapping not closed

HTTP codes for PlanChangeError. Resolved: Proposed Solution error mapping + AC #3–#5.

ISS-006 — Decision reimplementation risk

Handlers might fork logic. Resolved: AC #8 must call decide_plan_change.

§3 — Resolution

All 6 mechanical concerns addressed. Score = 10/10.


End of TASK-TEN-203 audit.