Phase 5 Implementation — Next-Session Prompt
Use a fresh session. Model: Claude Sonnet (latest) / Cursor Auto (⚡ — implementation against an already-ratified contract). Escalate to a thinking model ONLY if a security-tier test reveals a genuine ambiguity in the Phase 5 contract — stop and ask rather than guessing.
You are implementing Phase 5 — the Companion App shell on Muse branch feat/companion-app.
This is the ⚡ implementation phase against an already-ratified design. The bind gate
docs/COMPANION-APP-PHASE-5-BIND-GATE.md (D5.1–D5.8) was owner-approved 2026-06-06. Do not
re-litigate the design — implement it exactly. If you find a real gap in the contract, stop and ask;
do not silently deviate (Aaron's Rule #1/#2).
Read first (in full)
docs/COMPANION-APP-PHASE-5-BIND-GATE.md— the ratified contract you are implementing (D5.1–D5.8, threat model P-a…P-k, §11 test obligations, §0 scope boundary).- The four pure cores you are wiring (do not modify their security invariants):
lib/companion-loopback-guard.mjs(Phase 2 —verifyLoopbackRequest)lib/companion-oauth-pkce.mjs+lib/companion-token-custody.mjs(Phase 3)lib/companion-runtime-manager.mjs(Phase 4 — lifecycle/admission/integrity/resources)lib/model-runtime-lane.mjs(Phase 1 —selectLane,companionAvailableseam)
hub/gateway/native-oauth-provider.mjs(the server you authenticate against —/api/v1/auth/native, emitsiss, loopback variable-port, scope ceiling). ✅ already implemented.lib/daemon-llm.mjs+lib/llm-complete.mjs(the OpenAI-compatible call path the runtime speaks — reuse withbase_url = http://127.0.0.1:<port>or the UDS).test/helpers/companion-keychain-fake.mjs(the in-memory keychain fake already on the branch — use it in tests; the real adapters are what you build).
What to build (run-from-source; NOT packaging — that's Phase 7, §0)
Real I/O adapters + the orchestration/shell that wires the pure cores. Suggested module layout (adjust names if a better fit, but keep one concern per file):
- Keychain adapter (D5.3) —
lib/companion-keychain-adapter.mjs(+ per-OS backends):{ get, set, delete }on the fourKEYCHAIN_ACCOUNTSonly; reject unknown accounts; macOS Keychain…WhenUnlockedThisDeviceOnly(no iCloud sync), Windows per-user DPAPI, Linux libsecret; no plaintext fallback (fail-closed when locked/unavailable). Pick the OS backend at runtime; shell out tosecurity/secret-toolor use a vetted native binding — document the choice. - Inference listener + Phase 2 guard front-door (D5.1) —
lib/companion-inference-listener.mjs:listen(0,'127.0.0.1')(or[::1]), buildallowedHostsfrom the bound port, callverifyLoopbackRequestbefore any model work, never emit permissive CORS, noSO_REUSEPORT, hard-abort if loopback bind fails. - OAuth redirect listener + flow driver (D5.2) —
lib/companion-oauth-flow.mjs: separate one-shot ephemeral loopback listener, open the system browser withbuildAuthorizationUrl, validate the callback withvalidateAuthorizationResponse({..., expectedIssuer}), POST the token request over TLS (full cert verification), store via custody. - Spawn adapter (D5.4) —
lib/companion-spawn-adapter.mjs:spawn/kill/healthCheck; absolute binary path,shell:false, argv array, scrubbed child env (noSESSION_SECRET/*_API_KEY/JWT/ tokens/keychain refs), process-group, loopback/UDS-0600bind flag; detect-and-reclaim a stale runtime on start. - Download adapter (D5.5) —
lib/companion-download-adapter.mjs: dumb HTTPS-only streaming byte pump (download(url, onChunk)), full TLS verification, no integrity decision in the adapter. The orchestrator ownscreateIntegrityAccumulator+finalize(), atomic temp→verified move, and the first-party out-of-band manifest fetch (trust anchor — NOT from the model host). - Resource probe (D5.6) —
lib/companion-resource-probe.mjs: runtime-PID-scoped RAM/CPU; VRAM aggregate-only, never enumerate other GPU processes; no privilege escalation; ≤500ms cache. - Orchestrator / shell (D5.7, D5.8) —
lib/companion-shell.mjs: wires everything; computescompanionAvailable(true only when integrity-verified ∧ lifecycleready∧ recent health round-trip, recency-bounded; false on any doubt); enforces object-capability segregation — the runtime group (spawn/download/health/stat) is constructed with no reference to the authority group (keychain/JWT/canister).
Hard constraints
- Muse-canonical, stay on
feat/companion-app. No git/gh for this tree. - Fail-closed at every bind/custody/spawn/download/probe point.
- No secret in any log, error, adapter interface, or redirect (loopback token, JWT, refresh token,
code,
code_verifier,SESSION_SECRET, digest, model path). - Do not change the pure cores' security invariants or the server-side OAuth provider.
- Do not do packaging/signing/notarization/auto-update (Phase 7), new storage paths (Phase 6), or new server routes (none needed).
Tests (Aaron's Rule #0 — all 7 tiers before any merge to main)
Per PHASE-5-BIND-GATE.md §11. Centerpiece security tier MUST include: loopback-only bind (reject
0.0.0.0/::/routable); DNS-rebinding + cross-origin still 403 at the front-door; runtime back-end
carries no authority + no CORS; keychain surface minimal + device-local; child-env contains no
secret; architecture/import test: runtime group imports no authority module (build-blocking);
manifest trust-anchor out-of-band from the model host; resource probe never enumerates other GPU
processes; companionAvailable fail-closed; no secret in any output.
Suggested order
- Keychain adapter (+ 7 tiers) — everything else depends on custody.
- Inference listener + guard front-door (+ 7 tiers).
- Spawn + download + resource adapters (+ 7 tiers each).
- OAuth flow driver (+ 7 tiers; stub the browser-open in tests).
- Orchestrator/shell (+ integration/e2e: sign in → manifest → download → verify → spawn → health →
companionAvailable=true→ enrich locally; plus the architecture/env-scrub security tests). - Run the full suite once, fix all failures, re-run only the failed files, then full suite as the
final gate. Commit on
feat/companion-appvia Muse.