This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/relays/issue-62-authstorage/log.md
T

21 KiB
Raw Blame History

Relay log — issue-62-authstorage

Append-only. One concise entry per leg. Do not read end-to-end for orientation; use status.md. Targeted lookups only.


Leg 0 — Planning (assessment + relay packet)

Did:

  • Read issue #62 and confirmed the crash: static import { AuthStorage } fails at ESM load with Pi 0.80.8+.
  • Investigated all AuthStorage/ModelRegistry usage in src/ (3 production files + 5 test/support files under src/server/sessions/; other pi-coding-agent imports unaffected).
  • Downloaded and diffed real npm tarballs (pi-coding-agent 0.80.7/0.80.8/0.80.10 and pi-ai 0.80.7/0.80.8) into /srv/dev/pi-inspect (scratch, outside repo) to establish the exact new export surface: AuthStorage and its backends removed from exports; new ModelRuntime (async) + readStoredCredential; changed ModelRegistry (constructed from a runtime, refresh() now async, no authStorage); pi-ai CredentialStore/InMemoryCredentialStore/ AuthInteraction model. Confirmed 0.80.8 and 0.80.10 .d.ts are identical for the affected files (stable target).
  • Wrote ASSESSMENT-issue-62.md (root).

Decisions:

  • Clean migration to ModelRuntime, no dual-version compat shim. Rationale: sync→async, credential-store contract change, OAuth callback contract change, and session-services option change span both surfaces with no small clean adapter; 0.80.00.80.7 is already broken/superseded (latest = 0.80.10).
  • Dep range fix: peerDeps >=0.80.0 <1>=0.80.8 <0.81 for all three @earendil-works/* packages; upper bound <0.81 because this line ships breaking changes within 0.80.x.
  • Relay packet placed under relays/issue-62-authstorage/ (committed; not in package.json files, so not published; .pi-web/ is gitignored so not used).

Artifacts changed: ASSESSMENT-issue-62.md; relays/issue-62-authstorage/{charter,status,log}.md.

Status update: last completed leg 0, next leg 1 = charter slice 0 (Bootstrap: install Pi 0.80.8+, correct dep ranges).

Blockers: none. Noted /tmp disk-quota issue (install in worktree) and the pending sessiond restart for later daemon-path slices.

Handoff: Planning only — NOT auto-spawning the first implementation leg. Assessment + relay plan are laid out ready to be kicked off by the user.


Leg 1 — Slice 0 Bootstrap (deps + range correction)

Did:

  • Corrected package.json: three @earendil-works/* devDependencies ^0.80.6^0.80.8; peerDependencies >=0.80.0 <1>=0.80.8 <0.81.
  • npm install in the worktree. The default /tmp-based node-gyp build of node-pty failed with "Disk quota exceeded" (/tmp is a 5.8G tmpfs at ~81%). Re-ran with TMPDIR="$PWD/.tmp-build" npm install, which succeeded (618 packages, 0 vulnerabilities). Removed .tmp-build after.
  • Confirmed installed versions: all three packages resolve to 0.80.10 (lockfile updated).
  • Verified the new export surface via a node ESM check: ModelRuntime ✓, readStoredCredential ✓, ModelRegistry ✓, AuthStorage absent ✓, pi-ai InMemoryCredentialStore ✓.
  • Ran npx tsc --noEmit: 24 errors, all in src/server/sessions/ at the expected migration sites (removed AuthStorage, ModelRegistry.create/inMemory, authStorage on session-services options, PiAgentSession.modelRegistry). This matches assessment §2/§3 — the new surface resolves; only Pi Web's old usage is broken.

Decisions:

  • Committed with git commit --no-verify: the pre-commit hook runs a whole-project typecheck which cannot pass until the migration lands. Charter explicitly permits non-verify-green legs. Documented this + the TMPDIR workaround in status.md so future legs don't rediscover them.
  • Did not touch any src/ migration code — kept strictly to slice 0 scope.

Artifacts changed: package.json, package-lock.json (commit 0fa9d0e); relays/issue-62-authstorage/{status,log}.md. node_modules/ installed (gitignored, not committed).

Status update: last completed leg 1, next leg 2 = charter slice 1 (authService.ts core migration → propagate async construction to sessiond.ts).

Blockers: none. Sessiond restart still pending (becomes relevant from leg 2 onward once sessiond.ts changes land).

Handoff: spawning leg 2 (slice 1).


Leg 2 — slice 1: authService.ts core migration (commit e37148c)

What I did: migrated src/server/sessions/authService.ts off the removed AuthStorage / ModelRegistry.create surface onto the async ModelRuntime API, and propagated the async construction into the session daemon.

  • authService.ts:
    • Imports ModelRuntime from @earendil-works/pi-coding-agent and AuthInteraction (type) from @earendil-works/pi-ai. Dropped AuthStorage / ModelRegistry.
    • createModelRegistryForAgentDircreateModelRuntimeForAgentDir(agentDir) returning `ModelRuntime.create({ authPath: /auth.json, modelsPath: /models.json })`.
    • Construction is now async: private constructor + static AuthService.create({ agentDir? | runtime? | authFlows? }). runtime dep replaces the old modelRegistry dep; no-agentDir fallback is ModelRuntime.create({}).
    • Public field readonly runtime: ModelRuntime replaces modelRegistry.
    • saveApiKeyruntime.login(providerId, "api_key", interaction) where interaction is a non-interactive AuthInteraction (prompt: async () => key, notify: () => {}). Verified against pi-ai envApiKeyAuth().login, which calls interaction.prompt({ type: "secret" }) and persists the returned { type:"api_key", key } through credentials.modify inside Models.login. This is the credential-persistence path the assessment (§5.1) called for.
    • logoutProviderawait runtime.logout(providerId).
    • refreshAuthStateawait runtime.refresh() (no more authStorage.reload() — the file store is re-read by the runtime). Now async.
    • authProviders and requireOAuthLoginProvider became async, awaiting runtime.refresh() and the now-async getLogin/LogoutProviderOptions.
    • startOAuthLogin passes runtime: this.runtime into OAuthLoginFlowService.start (slice 3 will consume it via runtime.login).
  • sessiond.ts: createRuntime() is now async; new AuthService(...)await AuthService.create({ agentDir }); PiSessionService now receives modelRuntime: auth.runtime instead of modelRegistry: auth.modelRegistry.
  • sessiond/sessionDaemonStartup.ts: createRuntime may now return Runtime | Promise<Runtime> and runSessionDaemonStartup awaits it. The existing sync test doubles still satisfy the widened type.

Decisions:

  • saveApiKey via runtime.login("api_key", …) rather than reaching for a raw CredentialStore.modify: the pi-ai CredentialStore is not exposed off ModelRuntime publicly, and the provider's own api-key login is the intended persistence entry point (it writes through credentials.modify). Feeding the key back through a non-interactive AuthInteraction.prompt keeps us on the supported public surface. This matches assessment §5.1's "credential persistence via the pi-ai CredentialStore.modify path" without depending on unexported internals.
  • Kept AuthService construction async via a static factory (private ctor) rather than an init() method — cleaner async boundary, single valid construction path (code-quality-architecture skill).
  • Did NOT touch authProviderOptions.ts, oauthLoginFlowService.ts, piSessionService.ts, or any test/support files — strictly slice 1 scope. The async call sites I introduced (await getLoginProviderOptions(...), runtime: in authFlows.start, modelRuntime: in PiSessionService deps) deliberately point at the interfaces slices 24 will expose.

Typecheck state: npx tsc --noEmit = 31 errors (was 24 at slice 0). The increase is expected and honest: the migrated authService now calls runtime-based interfaces that slices 2/3/4 have not migrated yet. All remaining authService.ts / sessiond.ts errors are cross-slice (authProviderOptions shape → slice 2; OAuthLoginFlowService.start runtime param → slice 3; PiSessionServiceDependencies.modelRuntime → slice 4). Test/support files (slice 5) still import the removed AuthStorage.

Artifacts changed: src/server/sessions/authService.ts, src/server/sessiond.ts, src/server/sessiond/sessionDaemonStartup.ts (commit e37148c); relays/issue-62-authstorage/{status,log}.md.

Status update: last completed leg 2, next leg 3 = charter slice 2 (authProviderOptions.ts migration).

Blockers: none. Sessiond restart now ACTIVE-pending — slice 1 changed sessiond.ts and the daemon auth construction path; the human must manually restart the sessiond service once the migration lands (noted in status.md). Also: human confirmed /tmp is usable again, so the TMPDIR install workaround is no longer required (status.md updated).

Handoff: spawning leg 3 (slice 2).

Leg 3 — slice 2: authProviderOptions.ts migration (commit d09d7cc)

Did: Migrated src/server/sessions/authProviderOptions.ts off the removed authStorage-centric surface onto the new ModelRuntime API.

  • Replaced the AuthProviderModelRegistry structural interface (which required authStorage.getOAuthProviders()/list()/get(), getAll(), getProviderDisplayName()) with a runtime-shaped AuthProviderRuntime interface exposing getProviders() ({ id, name, auth: { apiKey?, oauth? } }), listCredentials() (Promise<{ providerId, type }[]>), and getProviderAuthStatus(id). Kept it structural (not Pick<ModelRuntime,...>) so the test can supply a lightweight double; verified the real ModelRuntime satisfies it (call sites in authService.ts typecheck clean).
  • Made getLoginProviderOptions / getLogoutProviderOptions async to match the await call sites already present in authService.ts (leg 2).
  • Login options: OAuth options from providers with auth.oauth; api-key options from providers with auth.apiKey filtered through the unchanged OAUTH_ONLY_PROVIDERS / isApiKeyLoginProvider logic. Display names now come from Provider.name (replacing getProviderDisplayName). Logout options derived from listCredentials(), mapping provider id -> name via getProviders().
  • Rewrote the authProviderOptions.test.ts double to the runtime shape (a getProviders array with per-provider auth, a listCredentials promise, getProviderAuthStatus); made the two option-building tests async. All 3 tests pass.

Decisions: AuthProviderInfo.auth typed as { apiKey?: unknown; oauth?: unknown } — presence is all this module needs, and unknown keeps the double trivial while remaining assignable-from the real ProviderAuth. Structural interface (not Pick<ModelRuntime>) chosen for testability per code-quality-architecture (injectable seam, no SDK construction in unit test).

Verify state: npx tsc --noEmit 31 -> 28 errors. No authProviderOptions errors; getLogin/LogoutProviderOptions call sites in authService.ts clean. Remaining 28 are cross-slice: authService.ts line-83 OAuthLoginFlowService. start still expects authStorage (slice 3); sessiond.ts(1)+piSessionService. ts(6) slice 4; test/support files slice 5.

Artifacts: src/server/sessions/authProviderOptions.ts, src/server/sessions/authProviderOptions.test.ts; status.md updated; committed d09d7cc with --no-verify (migration not yet verify-green, permitted).

Handoff: spawning leg 4 (slice 3, oauthLoginFlowService.ts). Sessiond restart from leg 2 still pending — carried forward, not cleared.

Leg 4 — slice 3: oauthLoginFlowService.ts migration (commit 1c3d6db)

What: Reimplemented OAuthLoginFlowService against the pi-ai AuthInteraction contract and rewrote its test.

  • start() now takes runtime: Pick<ModelRuntime, "login"> instead of authStorage: Pick<AuthStorage, "login">; login driven via runtime.login(providerId, "oauth", interaction). Resolves the authService.ts line-83 tsc error (authService.ts now at 0 errors).
  • Built a single AuthInteraction adapter ({ signal, prompt, notify }) replacing the six OAuthLoginCallbacks (onAuth/onDeviceCode/onPrompt/ onManualCodeInput/onSelect/onProgress).
  • Mapping decisions (verified carefully — riskiest slice):
    • prompt(AuthPrompt) dispatches on type: selectwaitForSelect (options {id,label,description?} → CommandOption {value:id,label}, resolves chosen id); manual_code → web-UI prompt kind manual; text/secret → web-UI prompt kind prompt. Old code special-cased onManualCodeInput with a hardcoded message; now the provider supplies the manual_code message, which is more correct.
    • notify(AuthEvent): auth_urlauth:{url,instructions?}; device_code → reuse auth field (url: verificationUri, instructions "Enter code: <userCode>") exactly as the old onDeviceCode did; info+progress → append message to progress (old code only had onProgress; info folds in naturally).
    • Old OAuthPrompt.allowEmpty/placeholder handling: the new AuthPrompt has no allowEmpty, so interactive prompts are always required (allowEmpty:false); select keeps allowEmpty:true. Placeholder still forwarded when present.
  • New behavior: per-prompt AuthPrompt.signal now aborts just that pending request (rejects "Prompt cancelled", clears the interaction from state) without ending the overall flow — the documented manual_code-vs-callback race. Added bindPromptSignal + a dedicated test for it.
  • Test: rewrote oauthLoginFlowService.test.ts with a fakeRuntime login double (returns a stub oauth credential). Replaced the old device-code-via-onDeviceCode coverage with an explicit notify device_code test and a per-prompt-signal-abort test. 9 tests pass; both files lint clean.

tsc: 28 → 26 errors. authService.ts = 0. Remaining: slice 4 (sessiond.ts 1, piSessionService.ts 6) and slice 5 test/support files (authService.test.ts 10, .testSupport.ts 3, .promptQueue.test.ts 2, .warnings.test.ts 4).

Status: updated (current position, leg tracking → last leg 4 / next leg 5, next task = slice 4). Committed with --no-verify (migration not yet verify-green, per charter).

Blockers: none. Sessiond-restart-pending note still ACTIVE (unchanged; this slice did not touch the daemon path, but slice 1 did). Handing off to leg 5 (slice 4).


Leg 5 — slice 4: piSessionService.ts migration (commit 4ccd4f8)

What: Migrated src/server/sessions/piSessionService.ts to the new ModelRuntime API.

  • createDefaultRuntimeFactory now takes a ModelRuntime and passes modelRuntime to createAgentSessionServices({ cwd, agentDir, modelRuntime }) (dropped the authStorage + modelRegistry args).
  • PiAgentSession.modelRegistry: ModelRegistryInstancemodelRuntime: ModelRuntime. Removed the ModelRegistryInstance type alias and the AuthStorage/ModelRegistry SDK imports; added type ModelRuntime + readStoredCredential imports and join from node:path. authService.js import reduced to just AuthChange (dropped createModelRegistryForAgentDir).
  • anthropicSubscriptionWarning(session, authPath?): reads via readStoredCredential("anthropic", authPath); param narrowed to Pick<PiAgentSession, "model" | "settingsManager">. warningsForSession passes join(this.agentDir, "auth.json").
  • Model reads rederived onto the runtime: availableModels/setModelawait modelRuntime.refresh() + getAvailableSnapshot() + getModel(...); syncCurrentModelAuthWarninggetModel(...) + hasConfiguredAuth(providerId).
  • applyAuthChange no longer refreshes a registry (shared runtime is refreshed by AuthService before emit; all sessions share it), keeping the subscribe callback synchronous.

Decision: made modelRuntime a required PiSessionServiceDependencies field rather than keeping an optional modelRegistry?-style fallback. The old fallback built a registry synchronously in the constructor; ModelRuntime can only be created by the async ModelRuntime.create, which cannot run in a constructor. sessiond.ts already injects modelRuntime: auth.runtime (slice 1), so production wiring is unaffected. Consequence: the slice-5 test surface is wider than the four files the assessment listed — every new PiSessionService(...) in tests now needs modelRuntime, and fakeRuntime/the TestSession type in testSupport.ts must expose modelRuntime. Documented in status.md "Next task".

Result: npx tsc --noEmitsessiond.ts and piSessionService.ts at 0 errors; all production code migrated (tsc output filtered to non-test/support files is empty). Remaining errors are slice-5 test/support only: authService.test (10), testSupport (4), warnings (5), promptQueue (17), lifecycle (19), archiveCleanup (9), spawnSession (3), spawnSubsession (18), sessionRoutes (1). piSessionService.ts lints clean. Committed --no-verify (migration not yet verify-green, per charter).

Blockers: none. Sessiond-restart-pending note still ACTIVE — this slice added piSessionService.ts (a session-daemon path) to the pending-restart surface; do not clear the note. Handing off to leg 6 (slice 5: tests + testSupport).

Leg 6 — slice 5 (tests + testSupport migration) — commit d0cc55c

What: Migrated all test doubles + piSessionService.testSupport.ts off the removed AuthStorage.inMemory / ModelRegistry.create|inMemory surface to the pi-ai InMemoryCredentialStore + async ModelRuntime.create({ credentials }). npm run verify is now fully green (typecheck + lint + knip + 1390 tests, 2 skipped) — the migration goal (charter criteria 1, 2, 5) is met; only the changeset (criterion 4) remains for slice 6.

Files changed (all src/server/sessions/):

  • piSessionService.testSupport.ts: new seams createTestModelRuntime, shared testModelRuntime (top-level await), seedCredential; fakeRuntime and testModel moved onto modelRuntime; dropped AuthStorage/ModelRegistry.
  • archiveCleanup/lifecycle/promptQueue/spawnSession/spawnSubsession/ sessionRoutes tests: injected modelRuntime: testModelRuntime into every new PiSessionService(...) (now required) + imported the shared runtime.
  • promptQueue.test.ts auth-loss test: live InMemoryCredentialStore + createTestModelRuntime(credentials), driving changes through delete/seedCredential + refresh() + applyAuthChange(...).
  • warnings.test.ts: anthropicSubscriptionWarning seam is now a temp auth.json read via readStoredCredential(id, authPath); type narrowed.
  • authService.test.ts: reworked to async AuthService.create + InMemoryCredentialStore; awaits async ops; OAuth-complete uses vi.waitFor.

Decisions:

  • Used a single shared testModelRuntime (top-level await in testSupport, an allowed ESM pattern here) for the no-auth catalog case so the many PiSessionService constructions and fakeRuntime sessions inject it synchronously — avoided making fakeRuntime itself async (which would have rippled through ~90 call sites). Auth-dependent tests build a dedicated per-test runtime via createTestModelRuntime(credentials).
  • anthropicSubscriptionWarning seam: chose the on-disk temp auth.json + readStoredCredential path (matches production exactly) rather than adding a new injectable credential-read seam. Clean; no intervention needed.
  • Made getLoginProviderOptions synchronous (it did no async work) to satisfy require-await; de-awaited its 2 call sites in authService.ts and the test. Also fixed pre-existing lint debt from earlier slices surfaced now that lint ran green for the first time: authRoutes.ts return-await, authService.ts api-key interaction (() => Promise.resolve(key) / notify: () => undefined).
  • Verified SDK behavior empirically with throwaway probe scripts (removed) before writing doubles: providers/models catalog, api-key login persistence, getModel, hasConfiguredAuth across store mutations + refresh, and readStoredCredential against a temp auth.json.

Verification: npm run verify green. Pre-commit hook (whole-project typecheck + knip + eslint + related vitest) passed — committed normally (no --no-verify needed since the tree is verify-green).

Blockers: none. Sessiond-restart-pending note still ACTIVE — unchanged this leg but slices 1 + 4 touched session-daemon paths; only the human clears it after restarting the sessiond service. Handing off to leg 7 (slice 6: changeset + final verify + cleanup; the finish line — no PR).