DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path investigations/loomworks-engine-standalone-api-walk-v0_1.md

Loomworks — Engine standalone API walk (external-client test)

Version. 0.1 Date. 2026-06-03 Provenance. Live exercise of the running engine over its real HTTP API, Operator: Marvin Percival. A test of the engine's standalone behaviour against its described contract, and a real-world check of the parity audit's findings. Status. Investigation / live test — report only. No engine code changed, no refactor, no commits. Test mutations were cleaned up (see §5). Method. Drove the engine on http://localhost:8000 (confirmed GET /healthz{"version":"0.1.0","database_reachable":true,"administrative_engagement_present":true}) with curl as a foreign client would — real routes, no _check=None, no _internal_delegation, no direct handler/core calls, no session-mint shortcut. The lifecycle was driven anonymously over HTTP (the form path). The only in-process step was the post-test cleanup script, which read the engine's own DB config internally (no secret echoed). Cross-links. investigations/loomworks-engine-api-parity-audit-v0_1.md (the eight holes this checks); standing-notes/loomworks-standing-note-engine-enforced-authentication-v0_1.md (P5); planning/loomworks-engine-boundary-enforcement-plan-v0_1.md.


1. What worked end-to-end through the clean API (engine standing alone)

| Step | Call | Result | Verdict | |---|---|---|---| | Health | GET /healthz | 200version 0.1.0, database_reachable: true, administrative_engagement_present: true | ✓ stands alone | | Draft | POST /engagements (rich 6-field seed, anonymous) | 201{engagement_id, seed_id, seed_version:1, state:"candidate"} | ✓ the form-path draft genuinely stands alone — no auth needed | | Induct | POST /engagements/{id}/seed/induct (anonymous) | 200cycle_number:1, two findings correctly scoped to my seed (R-A9 success-conditions, R-A10 contributors/agents), made a real Anthropic LLM call | ✓ induction runs standalone over HTTP; per-seed convergence logic is correct |

So a caller going through the real API can drive the pre-commit shaping half of the lifecycle — draft → induct (→ amend → re-induct, same anonymous route family) — with no insider path. Convergence is reachable in principle by amend+re-induct cycles; I ran one cycle (the contract behaviour — induct produces per-seed findings, non-converged — is demonstrated).

2. Where it failed or required an insider path (parity holes, live)

F1 — Door-level auth is WebAuthn-gated; a headless client cannot get a session at all. /auth/signup/{begin,passkey,totp-verify} and /auth/login/{begin,complete} all require a WebAuthn passkey attestation/assertion (verified from the route surface). A headless/foreign client has no authenticator, so the only way to obtain a session is the in-process issue_session shortcut. → Parity hole #1 (acting-as auth), live, at the front door. A foreign wrapper can't even log in over pure HTTP.

F2 — Read-back of the seed is auth-gated while draft is anon-open. GET /engagements/{id}/seed401 "Authentication required: provide a session cookie or an Authorization: Bearer header." A client that just drafted anonymously cannot read its own seed back. Write-open / read-closed asymmetry. → maps to the auth-context / missing-read-route theme.

F3 — The findings read route leaks other engagements' findings (multi-tenant isolation breach), and is anonymously readable. GET /engagements/{id}/seed/findings200 anonymously, but it returned a pile of findings from other engagements ("Fenwick", "illustrated stories / YouTube / merchandise / website", "Marvin") — none mine, and my seed had not even been inducted yet at that point. This is list_current_findings querying all open findings in the administrative engagement without a seed_id filter at the HTTP layer (the route's own docstring concedes "multi-candidate scoping is a deferred problem"). → Parity hole: read scoping / multi-tenant isolation, live. Anonymous readability compounds it (info-leak across tenants). Nuance: induction's own convergence read (_load_induction_state) IS per-seed (F-step Induct returned only my seed's findings) — the bug is the read route, not induction.

> ✅ FIXED — engine main 9172b94. list_current_findings is now scoped to the engagement's own candidate seed_id, requires get_current_person (401 anonymously, matching seed-read gating), and assert_engagement_creator enforces creator-match (403 for another tenant). Tests: tests/test_f3_findings_scoping.py (caller A cannot read caller B's findings; anonymous → 401).

F4 — Commit is blocked: a headless client cannot start the WebAuthn ceremony. POST /engagements/{id}/instantiate/challenge401 "Authentication required (no session cookie)."; POST /engagements/{id}/instantiate401. The weighty action cannot proceed without a session and the WebAuthn commit attestation. → Parity hole: the WebAuthn-commit gate — and this is P5 working as designed. The engine refuses to commit without the user-bound credential; the in-process commit_engagement bypass the brain uses is the only way commit happens without WebAuthn today, and over the clean API no caller (insider or not) gets that. A browser client with a passkey would pass here.

F5 (new — not in the original eight) — an anonymously-drafted candidate cannot be deleted via the API. DELETE /engagements/{id}/candidate refuses NULL-creator candidates ("Only the creating person may discard"); DELETE /engagements/{id} (hard delete) requires operator designation. An anonymous draft has neither, so it is an orphan-by-construction — only direct DB deletion clears it (which is how I cleaned up, §5). → a lifecycle/cleanup gap in the anon-open form path (and a live instance of the recurring orphan-engagement issue).

> ✅ FIXED — engine main 9172b94. discard_candidate_route now lets an authenticated caller discard an unowned (NULL-creator) candidate; owned candidates keep strict creator-match (403 for non-creators) and the operator-gated hard-delete is unchanged. Tests: tests/test_f5_orphan_candidate_cleanup.py (orphan discardable via API; owned still creator-gated).

Holes from the audit NOT exercised here (out of a single anonymous walk's reach, not disproven): composite/transaction endpoints (#5), composite billing (#6), runner provisioning (#7), governed versioning (#8) — these bear on the brain-as-client refactor, not on a single external lifecycle walk. Per the co-resident resolution, #5–#7 are largely set aside anyway.

3. Honest verdict — how close to "works as described, standalone"

For its intended client — an authenticated browser with a WebAuthn passkey — the engine genuinely stands alone. Login → draft → induct → amend → WebAuthn-commit → render are all real HTTP routes; the only things that blocked me were the WebAuthn ceremonies, which a browser satisfies. The engine is a real standalone API for the client it was built for; the boundary design rests on an engine that already works through its API for that client.

For a headless / foreign wrapper with no WebAuthn, the split is clean and matches P5:

So roughly the pre-commit shaping half of the core flow is completable by an external headless client unaided; commitment is not — by design. That is exactly P5 in practice: cheap actions ride the open/session path; the weighty action (commit) requires a user-bound credential the engine verifies and refuses without — confirmed live (/instantiate → 401, no bypass available over HTTP). The one thing that is not "by design" is F3, the cross-tenant findings leak — a genuine isolation bug the walk surfaced, independent of the auth story.

Bottom line: the engine stands on its own through its API for an authenticated client; the parity holes that block a foreign/headless caller are real and were observed live (F1 front-door auth, F2 read asymmetry, F4 commit gate) — and they are overwhelmingly the auth holes the boundary project already names, with P5 demonstrably enforced at the commit boundary. The boundary design therefore rests on a sound standalone engine, not a broken one — with two concrete defects to track: F3 (findings cross-tenant leak / anon-readable) and F5 (anon-drafted orphan candidates undeletable via API).

4. Evidence (actual responses)

5. Cleanup (mutations removed)

The walk created one candidate engagement (c5933bd5…), one seed (34b8a648…), and — from the induct cycle — 2 findings + 1 induction-cycle record in the administrative engagement's event log. Because an anonymous candidate is undeletable via the API (F5), cleanup used a throwaway script against the engine's own configured DB session (no engine source changed, not committed). Verified: candidate row 1 → 0; admin-log events for my seed 4 → 0; my finding IDs no longer appear in the findings read. Footprint fully removed. (The other engagements' findings visible in F3 are pre-existing residue, not mine — and are themselves evidence of the same accumulation/leak.)


Investigation / live test only. No engine code changed; no refactor; nothing committed. Test mutations cleaned up and verified. Filed in loomworks-record/investigations/. Pairs with investigations/loomworks-engine-api-parity-audit-v0_1.md and standing-notes/loomworks-standing-note-engine-enforced-authentication-v0_1.md.