CR number. CR-2026-098 (confirmed next-free against the record 2026-06-07; CR-2026-097 was the prior highest).
Version. 0.2
Date. 2026-06-07 (v0.1) · v0.2 amendment 2026-06-07.
Engine anchor. loomworks-engine, branch main, HEAD 5472335 (547233503bc4d1229a64811920fa173a77d1d0a6), working tree clean. Re-anchored 2026-06-07 (Step 0); engine unmoved since the inspections (0 commits since the anchor). 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), plus the Step 0 re-anchor inspection (2026-06-07).
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.
v0.2 changes (from v0.1) — recorded, not smoothed.
POST /me/engagements/create-from-conversation forward-ref (llm_client: Optional["LLMClient"], LLMClient imported only under TYPE_CHECKING) that 500s the entire OpenAPI document. New fifth work item. It is a bug fix, not a boundary finding, hence the BUG1 label (not an A/B finding). Surfaced during the API-documentation inspection that follows this CR./admin/* routes. The v0.1 (a)/(b)/(c) candidates are preserved below with the choice marked, so the reasoning trajectory survives. The exact dependency symbol is to be located (not invented) in Step 1.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. v0.2 also folds in one bug fix (BUG1) that blocks the API-documentation effort sequenced after this CR.
Four boundary fixes plus one folded-in bug fix:
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). Privilege model decided in v0.2 (see A3)._route_finalize_project so finalize-via-Companion matches the direct /instantiate route.create-from-conversation forward-ref that 500s OpenAPI schema generation (folded in; blocks the documentation effort and the OpenAPI-based route-absence verification A1/A2 rely on).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 (plus A1's now-orphaned token-mint wiring), tightens two authorization sites, and fixes one schema-blocking forward-ref (BUG1). 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 their dead wiring), 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, or a token-mint reference outside the migration-only sites Step 0 found — 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. Step 0 further confirmed the token-mint primitive behind them is wired only in migration-only sites with no other consumer, so that wiring is dead once the routes go.
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).src/loomworks/persons/migration.py — issue_post_migration_token def :123, whose only call site is the mint route at migration.py:204; plus the PostMigrationTokenStore class.src/loomworks/api/deps.py ~:395–398 — get_post_migration_token_store provider (constructs PostMigrationTokenStore() at :398).src/loomworks/api/app.py ~:223–229 — app.state.post_migration_token_store = PostMigrationTokenStore() (at :229).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) — its sole call site is the mint route at migration.py:204; remove the function.get_post_migration_token_store provider in src/loomworks/api/deps.py (~:395–398) — remove.app.state.post_migration_token_store wiring in src/loomworks/api/app.py (~:223–229) — remove.PostMigrationTokenStore class is then unreferenced — remove it too.If Step 1 finds any reference outside these migration-only sites (contrary to Step 0's finding), HALT and report rather than removing.
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 — note: the OpenAPI-schema assertion requires BUG1 fixed first).issue_post_migration_token, PostMigrationTokenStore, get_post_migration_token_store, or app.state.post_migration_token_store remain (grep clean; app imports cleanly).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 :469admin_grants.py ~:16 and ~:110.Decision (v0.2 — resolves the v0.1 HALT). The privilege model is decided: these routes require the Engine's existing Operator/founder authority — the same authority the Engine already enforces elsewhere for founder/Operator-only operations. NOT a new admin role; NOT a hardcoded UUID. Implementation constraints:
/admin/* credit routes — Depends(<existing founder/Operator authority dependency>) replacing Depends(get_current_person) on each — rather than inlining the check per route. Centralizing it in one dependency means a later move to a platform-admin designation or a read/write split is a one-function change at a single site, not a five-route edit.admin_grants.py ~:16 and ~:110) to reflect this decision rather than deferring "role-aware admin auth."Trajectory (preserved from v0.1). The v0.1 candidates for the Operator to choose among were: (a) a platform/operator-admin designation a person must hold (the natural reading of the deferred "role-aware admin auth"); (b) restriction to the Loomworks Operator / founder authority already used elsewhere; (c) a finer split — read routes under a lighter check than the write routes. Chosen: (b), implemented as a single shared dependency precisely so that (a) or (c) remains a cheap later change. There is no remaining Operator HALT for A3 — only the conditional Step 1 HALT above (if no reusable founder/Operator dependency exists).
Tests. Add coverage: an authenticated non-privileged person is rejected (403) on each route; the privileged actor (the Operator/founder, per the chosen dependency) succeeds. Match the implemented 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.create-from-conversation forward-ref that breaks OpenAPI schema generation
(Folded in at v0.2. This is a bug fix, not a boundary finding — hence the BUG1 label rather than an A/B index.)
Rationale. POST /me/engagements/create-from-conversation carries a handler parameter llm_client: Optional["LLMClient"] = None — a string forward-reference. LLMClient is imported only under if TYPE_CHECKING:, so the reference is unresolvable at runtime. FastAPI treats the parameter as a request input and tries to build a Pydantic schema for it; resolution fails with PydanticUserError (TypeAdapter[... Optional[ForwardRef('LLMClient')] ...] is not fully defined). FastAPI's schema build is all-or-nothing, so this single route 500s the entire OpenAPI document — GET /openapi.json returns HTTP 500, and /docs + /redoc render an error because their client-side schema fetch fails. This blocks the API-documentation effort sequenced after this CR (a schema-driven console has nothing to read), and it also blocks the OpenAPI-based route-absence verification A1/A2 rely on.
Files and sites (current @ 5472335).
src/loomworks/api/routers/me_create_engagement.py:POST /me/engagements/create-from-conversation — path ~:528, handler create_engagement_from_conversation ~:532.llm_client: Optional["LLMClient"] = None — ~:539.LLMClient imported only under if TYPE_CHECKING: — ~:41–42.
Changes.
Make llm_client not enter the request schema, so FastAPI no longer tries to schematize an unresolvable forward-ref. Exact mechanism is Step 1 design — this item records WHAT and WHERE, not the final line edit. Two viable shapes:
LLMClient importable/resolvable when the schema is built and/or annotate so FastAPI excludes it from the request model), OR
Whichever shape is chosen, llm_client must remain functional as the internal seam input it is today (the converse path threads it in); only its presence in the request schema changes. If Step 1 finds that removing it from the request signature changes the route's behavior for any live caller, HALT and report.
Tests.
GET /openapi.json returns 200, or constructing the app and calling app.openapi() raises no error.POST /me/engagements/create-from-conversation still functions (existing behavior unchanged for its direct callers and the converse seam that threads llm_client).Verification.
GET /openapi.json returns 200; the full OpenAPI schema generates without PydanticUserError.create-from-conversation still works end to end.CR-2026-098 (the v0.1 placeholder is resolved; no action).main @ 5472335, clean tree, before starting. Step 0 (2026-06-07) re-anchored and confirmed the engine is unmoved (0 commits since the anchor); all line references hold. If the engine moves before Step 1, re-anchor the line references before editing.initial-contributors route no longer exist on the API surface, and the migration token-mint wiring (issue_post_migration_token, PostMigrationTokenStore, the deps.py provider, the app.py app.state wiring) is removed with no dangling references./admin/* credit routes enforce the existing Operator/founder authority via a single shared authorization dependency, and the Phase-48-deferral docstrings are updated to reflect the enforced model./instantiate.GET /openapi.json returns 200 and the full OpenAPI schema generates; create-from-conversation still functions.DUNIN7 — Done In Seven LLC — Miami, Florida Change Request — Engine API boundary closure — v0.2 — 2026-06-07