DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-098-engine-api-boundary-closure-v0_1.md

Change Request — Engine API boundary closure — v0.1

CR number. CR-2026-098 (confirmed next-free against the record 2026-06-07; CR-2026-097 was the prior highest). Version. 0.1 Date. 2026-06-07 Engine anchor. loomworks-engine, branch main, HEAD 5472335 (547233503bc4d1229a64811920fa173a77d1d0a6), working tree clean. Re-anchored 2026-06-07; engine unmoved since the inspections. All line references below are current as of this HEAD. Source. loomworks-record/investigations/loomworks-engine-api-boundary-inspection-findings-v0_1 (two read-only inspection passes). Seed alignment. Checked against loomworks-candidate-seed-v0_12 (current canonical). No conflict — these are boundary-integrity fixes. The seed's UUID-identity, OVA-authorization, and human-Operator-authority commitments are reinforced, not altered. The signup-throttle finding is deliberately NOT in this CR (it is the first piece of the two-tier signup build, sequenced after the seed and carrying an open mechanism decision). Consumer. Claude Code executing against loomworks-engine.


Purpose

The API boundary inspections established that the managed API is not yet the sole path to authoritative Engine state. This CR closes the four findings that are pure boundary-integrity fixes — no seed dependency, no design choices — so that the engagement-scoped surface a freely-constructed Companion would call is complete and regulated.

Four fixes:

The A1–A2 ordering matters: A2's unauthenticated route is the membership-planting step in the compose-chain that makes B1 exploitable. Removing A2 de-fangs B1 even before B1 is tightened. Both are fixed here regardless.


Scope discipline

This CR removes two routes and tightens two authorization sites. It does not refactor surrounding code, does not change the credit data model, does not alter the migration data model (only removes the now-unused HTTP entry points), and does not touch the signup routes. If execution surfaces any ambiguity — a caller not anticipated here, a test that depends on removed behavior in a way the repoint does not cover, a shared helper that other live code needs — HALT and report rather than working around it.


A1 — Remove migration mint route and migration passkey ceremony

Rationale. The mint route (POST /admin/mint-migration-passkey-token) is an unauthenticated route that mints a first-credential bridge token; its sole caller is scripts/migrate_to_person_layer.py. The ceremony routes it feeds (/auth/migration/passkey/begin, /auth/migration/passkey/complete) exist only to redeem that token. The Phase 14 migration was a one-time event; the Operator confirms no future migrations are planned and the system is not deployed. The inspection confirmed a normal new account never touches these routes (signup issues its own credential inline, and a net-new account sets first_login_at=now at creation, making it structurally ineligible for the mint route's first_login_at IS NULL gate). These routes are therefore spent tooling carrying an unauthenticated account-takeover exposure.

Files and sites (current @ 5472335).

Changes.

  1. Remove the three routes from migration.py. If removing them empties the router module of all routes, remove the module and its router registration in the app factory; if other live routes remain in the module, leave the module and remove only these three handlers.
  2. Remove the router's mount/registration for any removed routes in the app factory (where migration.py's router is include_router-ed).
  3. scripts/migrate_to_person_layer.py is migration tooling for a completed, non-recurring migration. Since the route it calls is being removed, the script's mint call (:86) will no longer resolve. Per Operator confirmation that no future migrations are planned: remove the script, OR if the script has other still-useful structure, remove only its call to the deleted route and mark the migration path retired with a comment. Recommend: remove the script entirely — it is dead tooling for a finished one-time migration. HALT and confirm with the Operator (via the relayed channel) before deleting the script if there is any doubt about whether it carries reusable structure.
  4. Remove the underlying token-mint primitive ONLY if it has no other caller. The inspection located issue_post_migration_token (src/loomworks/persons/migration.py:123) and the in-process PostMigrationTokenStore. Check for other callers first; if the mint route and ceremony were the only consumers, remove the primitive and the store. If anything else references them, leave them and report what does.

Tests. Remove or update any tests that exercise the migration mint/ceremony routes. Report which test files were touched. If a removed test was the only coverage of a behavior that still exists elsewhere, flag it.

Verification.


A2 — Remove initial-contributors route; repoint test callers to the inner helper

Rationale. POST /engagements/{engagement_id}/initial-contributors is a Phase 3 legacy route (predates the person-layer model by ten days) that mints commit-authority contributors with no authentication, gated only by candidate-state plus knowledge of an engagement UUID. It has no production caller — not the frontend, Companion, CLI, or any script. Its only callers are the test harness. It was superseded by the signup → person → membership creation model. The clean close is to remove the network route and have the tests call the inner helper directly, so the unauthenticated route stops existing while test seeding is preserved.

Files and sites (current @ 5472335).

Changes.

  1. Remove the register_initial_contributors route handler (:128–160) from contributors.py. Keep the inner helper materialize_initial_contributors (:65) — it is the seam the tests will call directly.
  2. If removing the route empties the module of routes, remove the module and its registration; otherwise remove only this handler and leave the rest.
  3. Remove the route's registration in the app factory.
  4. Repoint each test caller from an HTTP POST .../initial-contributors to a direct in-process call of materialize_initial_contributors (or register_contributor, whichever the helper exposes for the per-contributor write), passing the same InitialContributorSpec entries the HTTP body carried. The seeding behavior the tests rely on must be preserved exactly — same contributors, same commit_authority, same engagement.
  5. Update the engagements.py:25 docstring to drop the reference to the removed route.

Tests. The four named test files must pass with their seeding repointed to the helper. Report the diff in how each seeds contributors. If any test was asserting something about the route itself (its auth posture, its candidate-state gate) rather than using it as setup, flag it — that test's intent changes when the route is removed.

Verification.


A3 — Add a privilege check to the /admin/* credit routes

Rationale. Five /admin/* credit routes authenticate the caller (get_current_person) but apply no privilege check, so any logged-in person can grant credits, provision, or set spend authorization. Credit is authoritative state (the first infrastructure engagement). The route docstrings already defer "role-aware admin auth" to Phase 48; this CR supplies the missing check.

Files and sites (current @ 5472335). src/loomworks/api/routers/admin_grants.py, each currently gated only by person: Person = Depends(get_current_person):

Open question for the Operator — DO NOT GUESS. This CR knows the gap (no privilege check) but not the intended privilege model. Before implementing, the Operator must confirm what authority these routes require. Candidate answers, for the Operator to choose:

HALT here for the Operator decision. The mechanical change once decided is a dependency swap: replace Depends(get_current_person) with Depends(<the chosen authorization dependency>) on each route, matching how operator-authority is enforced elsewhere in the engine (the inspection noted operator-role authorization is genuinely enforced in-body on other routes — reuse that existing mechanism rather than inventing one). Do not implement A3 until the Operator picks (a), (b), or (c).

Tests. Add coverage: an authenticated non-privileged person is rejected (403) on each write route; a privileged actor succeeds. Match the chosen model.

Verification.


B1 — Re-apply finalize authorization gates

Rationale. _route_finalize_project (the Companion's converse path to finalizing a candidate engagement) calls verify_project_membership then commit_engagement directly, without the two HTTP-layer gates the direct /instantiate route applies: get_committing_contributor (human actor with commit_authority=True) and assert_engagement_creator (caller is the candidate's creator). commit_engagement itself enforces agent-rejection, fail-closed step-up presence proof, and seed convergence on both paths — so this is NOT an agent commit and NOT an unauthenticated commit. The specific drop is the creator-identity and commit-authority checks: a present, authenticated, step-up-satisfied member who is neither creator nor commit-authorized can finalize via the Companion, where /instantiate would reject them.

Files and sites (current @ 5472335).

Changes. Between the membership check (:1048) and the commit_engagement call (:1092) in _route_finalize_project, re-apply the two checks the direct /instantiate route enforces, so the converse finalize path is authorization-equivalent to the direct path:

  1. Commit-authority check — equivalent to get_committing_contributor: the actor must be a human contributor with commit_authority=True. Note the converse path synthesizes an ActorRef(kind="companion", ...); finalize_project specifically uses kind="person" (per the inspection). Apply the check against the resolved person actor. Reuse the existing dependency/helper logic rather than reimplementing the rule — confirm whether get_committing_contributor can be applied in-process here or whether its in-body equivalent must be called directly (the inspection notes other converse seams re-run their gates in-process via helpers in converse.py; follow that established pattern).
  2. Creator-identity check — equivalent to assert_engagement_creator: the caller must be the candidate's created_by_person_id. The converse seam helpers already re-run assert_engagement_creator in-process at converse.py:219/297/390 for other intents; apply the same helper here.

The goal is exact parity: a caller who would be rejected at /instantiate must be rejected at converse-finalize for the same reason, with the same error semantics where practical.

Tests. Add coverage proving parity:

Verification.


Execution sequence and halts

  1. Confirm the CR number against the record (replace the placeholder).
  2. Re-confirm main @ 5472335, clean tree, before starting. If the engine has moved since this CR was drafted, re-anchor the line references before editing.
  3. A3 has a mandatory HALT — do not implement until the Operator picks the privilege model (a/b/c). A1, A2, and B1 can proceed without further Operator input.
  4. Execute A2 and A1 (route removals + repoints), then B1 (finalize parity), then A3 (after the Operator decision).
  5. Halt before any push. Report what changed, which tests were touched, and the resulting test-suite status, for Operator review before the work lands.
  6. Commit and push only on explicit Operator go-ahead.

Done means


DUNIN7 — Done In Seven LLC — Miami, Florida Change Request — Engine API boundary closure — v0.1 — 2026-06-07