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.
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:
initial-contributors route (Phase 3 legacy, no production caller); repoint its test-only callers to the inner helper./admin/* credit routes (authenticated today, but any logged-in person can grant/provision credits)._route_finalize_project so finalize-via-Companion matches the direct /instantiate route.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.
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.
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).
src/loomworks/api/routers/migration.py:POST /admin/mint-migration-passkey-token — decorator :161, path :162, handler to ~:212.POST /auth/migration/passkey/begin — :65–66.POST /auth/migration/passkey/complete — :108–109.scripts/migrate_to_person_layer.py — calls the mint route at :86 (the :106 reference is a docstring).Changes.
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.migration.py's router is include_router-ed).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.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.
/admin/mint-migration-passkey-token or /auth/migration/passkey/* is mounted (grep the app's route table at startup, or assert via the OpenAPI schema).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).
src/loomworks/api/routers/contributors.py:POST /engagements/{engagement_id}/initial-contributors — decorator/path :128–129, handler register_initial_contributors :133, runs to :160.materialize_initial_contributors — :65.tests/conftest.py, tests/test_api_contributors_active.py, tests/test_api_commit_rejects_agent.py, tests/test_phase13a_totp_endpoints.py.src/loomworks/api/routers/engagements.py:25 — docstring reference only (not a caller; update the docstring to remove the mention of the deleted route).Changes.
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.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.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.
/engagements/{id}/initial-contributors is mounted./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):
POST /admin/grants — :97–98, dep :104GET /admin/grants — :257–258, dep :262POST /admin/provisioning — :377–378, dep :384PUT /admin/engagements/{engagement_id}/spend-authorization — :437–438, dep :444GET /admin/engagements/{engagement_id}/spend-authorization — :463–464, dep :469Open 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:
GET /admin/grants, GET .../spend-authorization) under a lighter check than the write routes.
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.
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).
src/loomworks/orchestration/router.py:_route_finalize_project — :1023, runs to ~:1138.verify_project_membership at :1048 and commit_engagement at :1092.get_committing_contributor nor assert_engagement_creator appears anywhere in router.py (grep count 0).src/loomworks/engagement/creation.py:commit_engagement def — :709 (internal gates: agent-reject, _assert_commit_step_up, convergence).
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:
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).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:
/instantiate).commit_authority is rejected at converse-finalize.commit_engagement internal gates (agent-reject, step-up) remain intact and are not duplicated or bypassed by the new checks.Verification.
/instantiate reject the same callers for the same reasons.commit_engagement are unchanged.main @ 5472335, clean tree, before starting. If the engine has moved since this CR was drafted, re-anchor the line references before editing.initial-contributors route no longer exist on the API surface./admin/* credit routes enforce the Operator-chosen privilege model./instantiate.DUNIN7 — Done In Seven LLC — Miami, Florida Change Request — Engine API boundary closure — v0.1 — 2026-06-07