# 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.0–0.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`. - `createModelRegistryForAgentDir` → `createModelRuntimeForAgentDir(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`. - `saveApiKey` → `runtime.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. - `logoutProvider` → `await runtime.logout(providerId)`. - `refreshAuthState` → `await 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` and `runSessionDaemonStartup` `await`s 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 2–4 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`) 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`) 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` instead of `authStorage: Pick`; 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`: `select` → `waitForSelect` (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_url` → `auth:{url,instructions?}`; `device_code` → reuse `auth` field (`url: verificationUri`, instructions `"Enter code: "`) 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).