diff --git a/.changeset/data-driven-auth-provider-options.md b/.changeset/data-driven-auth-provider-options.md deleted file mode 100644 index ac49865..0000000 --- a/.changeset/data-driven-auth-provider-options.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@jmfederico/pi-web": patch ---- - -Offer API-key and OAuth login options for every provider the agent backend supports each method for, instead of a curated hardcoded list. Providers that support both methods (such as Anthropic and GitHub Copilot) now surface both an API-key and an OAuth login option, driven purely by what the backend reports. diff --git a/.changeset/fix-pi-0-80-8-modelruntime-auth.md b/.changeset/fix-pi-0-80-8-modelruntime-auth.md index 751ff1d..0e25b42 100644 --- a/.changeset/fix-pi-0-80-8-modelruntime-auth.md +++ b/.changeset/fix-pi-0-80-8-modelruntime-auth.md @@ -2,4 +2,4 @@ "@jmfederico/pi-web": patch --- -Fix the session daemon crashing on startup with Pi (`@earendil-works/pi-coding-agent`) 0.80.8 and newer. Pi removed the `AuthStorage` API in 0.80.8, which caused Pi Web to fail at module load. Authentication, OAuth login, API-key save/logout, provider listing, and the Anthropic subscription warning now run on Pi's new `ModelRuntime` credential APIs. Pi Web now requires Pi `>=0.80.8`. +Restore session-daemon startup and authentication on supported Pi `>=0.80.8 <0.81` releases by migrating model and credential handling to `ModelRuntime`. Login options now follow each provider's interactive API-key and OAuth capabilities, OAuth prompts retain their input, selection, and device-code semantics, and unsupported multi-step API-key setup fails safely instead of storing malformed credentials. PI WEB now requires Node.js `>=22.19.0`. diff --git a/ASSESSMENT-issue-62.md b/ASSESSMENT-issue-62.md deleted file mode 100644 index d2958c1..0000000 --- a/ASSESSMENT-issue-62.md +++ /dev/null @@ -1,391 +0,0 @@ -# Assessment — Issue #62: `AuthStorage` export removed in Pi 0.80.8 - -## 1. Summary - -Pi Web's session daemon crashes at ESM module initialization after -`@earendil-works/pi-coding-agent` is resolved at **0.80.8 or later**: - -``` -SyntaxError: The requested module '@earendil-works/pi-coding-agent' -does not provide an export named 'AuthStorage' -``` - -The crash is a hard, load-time failure (a static `import { AuthStorage } ...` -that no longer resolves), so Pi Web is completely unusable with any Pi in the -0.80.8+ line. The permissive peer/dev range `>=0.80.0 <1` lets npm resolve the -incompatible release. - -**Root cause:** Pi 0.80.8 is a **major architectural refactor** of model/auth -plumbing ("Unified model runtime and provider authentication"), explicitly -listed under **Breaking Changes** in the upstream CHANGELOG. `AuthStorage` (and -its storage backends `FileAuthStorageBackend`, `InMemoryAuthStorageBackend`, -and the credential type exports) were **removed from the package's public -exports**. The class still exists internally but is no longer exported; the new -public surface is `ModelRuntime` (async) plus a synchronous compatibility -`ModelRegistry` facade with a different shape, and `readStoredCredential()` for -one-off reads. - -**Recommendation (see §5):** Do **not** attempt a dual-API compatibility shim. -The change is a deep semantic refactor (sync → async, credential store contract -change, removal of `authStorage` from services, `ModelRegistry` constructor and -method-signature changes). A clean migration to the `ModelRuntime` API, -combined with pinning the supported Pi range to `>=0.80.8 <0.81`, is the correct -fix and warrants a Pi Web version bump via a changeset. - ---- - -## 2. Where and how `AuthStorage` / `ModelRegistry` are used in `src/` - -All usage is under `src/server/sessions/`. Production files (3) and test/support -files (5). - -### Production code - -**`authService.ts`** — the central auth wiring. -- `import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent"`. -- `type ModelRegistryInstance = ReturnType`. -- `createModelRegistryForAgentDir(agentDir)`: - `AuthStorage.create(join(agentDir, "auth.json"))` then - `ModelRegistry.create(authStorage, join(agentDir, "models.json"))`. -- Constructor fallback: `ModelRegistry.create(AuthStorage.create())`. -- Reads/writes credentials through `this.modelRegistry.authStorage`: - - `.set(providerId, { type: "api_key", key })` (saveApiKey) - - `.logout(providerId)` (logoutProvider) - - `.reload()` (refreshAuthState) - - passes `this.modelRegistry.authStorage` into the OAuth login flow. -- Uses `this.modelRegistry.refresh()` (currently synchronous `void`). - -**`oauthLoginFlowService.ts`** — OAuth login orchestration for the web UI. -- `import type { AuthStorage } from "@earendil-works/pi-coding-agent"`. -- `type OAuthLoginStorage = Pick`. -- Calls `authStorage.login(providerId, callbacks)` where `callbacks` is the - old `OAuthLoginCallbacks` shape: `signal`, `onAuth`, `onDeviceCode`, - `onPrompt`, `onManualCodeInput`, `onSelect`, `onProgress`. - -**`piSessionService.ts`** — session runtime factory + warnings. -- `import { AuthStorage, ..., ModelRegistry, ... }`. -- `type ModelRegistryInstance = ReturnType`. -- `createDefaultRuntimeFactory(authStorage: AuthStorage, modelRegistry, ...)` - calls `createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry })`. -- Uses `createModelRegistryForAgentDir` fallback; passes - `this.modelRegistry.authStorage` and `this.modelRegistry` into the runtime - factory (around lines 605–612). -- `anthropicSubscriptionWarning()` reads - `session.modelRegistry.authStorage.get("anthropic")` and inspects - `credential.type` / `credential.key`. -- `PiAgentSession.modelRegistry: ModelRegistryInstance` is part of the internal - session interface. - -**`authProviderOptions.ts`** — provider enumeration (no direct SDK import; uses a -structural `AuthProviderModelRegistry` interface). Depends on the current -`ModelRegistry`/`AuthStorage` shape: -- `modelRegistry.authStorage.getOAuthProviders()` → `{ id, name }[]` -- `modelRegistry.authStorage.list()` → `string[]` -- `modelRegistry.authStorage.get(provider)` → `{ type } | undefined` -- `modelRegistry.getAll()` → `{ provider }[]` -- `modelRegistry.getProviderDisplayName(provider)` -- `modelRegistry.getProviderAuthStatus(provider)` - -### Test / support code -- `authService.test.ts` — `AuthStorage.inMemory(...)`, `ModelRegistry.create(...)`, - asserts `startOptions.authStorage`. -- `piSessionService.testSupport.ts` — `ModelRegistry.inMemory(AuthStorage.inMemory())`, - `ModelRegistry.create(AuthStorage.inMemory())`. -- `piSessionService.promptQueue.test.ts` — `AuthStorage.inMemory({ anthropic: {...} })`, - `ModelRegistry.inMemory(authStorage)`. -- `piSessionService.warnings.test.ts` — `AuthStorage.inMemory()`, - `ModelRegistry.inMemory/create`, builds anthropic credentials via - `authStorage.set(...)`. -- `oauthLoginFlowService.test.ts` — `Pick` fake. -- `authProviderOptions.test.ts` — structural `AuthProviderModelRegistry` fake - (no SDK import; must track whatever `authProviderOptions.ts` requires). - -### Other pi-coding-agent imports (unaffected — still exported in 0.80.8) -`DefaultPackageManager`, `SettingsManager` (piPackageService, piWebPluginService, -piWebStatus), `createAgentSessionServices`, `createAgentSessionFromServices`, -`createAgentSessionRuntime`, `AgentSessionRuntimeDiagnostic`, `ResourceDiagnostic`. -These remain present; only the auth/model-registry construction path is broken. - ---- - -## 3. What Pi 0.80.8 actually changed (verified against real tarballs) - -Method: downloaded and extracted the real npm tarballs for -`@earendil-works/pi-coding-agent` 0.80.7, 0.80.8, 0.80.10 and -`@earendil-works/pi-ai` 0.80.7, 0.80.8 (into `/srv/dev/pi-inspect`) and diffed -the `.d.ts` surface. (Local `node_modules` was not installed in this worktree; -the last globally installed copy elsewhere is 0.80.6.) - -### 3.1 Public export diff — `pi-coding-agent` index.d.ts (0.80.7 → 0.80.8) - -Removed: -``` -export { type ApiKeyCredential, type AuthCredential, type AuthStatus, - AuthStorage, type AuthStorageBackend, FileAuthStorageBackend, - InMemoryAuthStorageBackend, type OAuthCredential } from "./core/auth-storage.ts"; -``` -Added: -``` -export { readStoredCredential } from "./core/auth-storage.ts"; -export { type CreateModelRuntimeOptions, ModelRuntime, - type ModelRuntimeAuthOverrides } from "./core/model-runtime.ts"; -``` -`ModelRegistry` is still exported, but its class shape changed (see §3.3). -0.80.10 (current `latest`) is **byte-identical** to 0.80.8 for `index.d.ts`, -`auth-storage.d.ts`, and `model-runtime.d.ts` — the new surface is stable. - -### 3.2 Upstream CHANGELOG (0.80.8) — Breaking Changes (verbatim highlights) - -- "Replaced the SDK's `CreateAgentSessionOptions.authStorage` and - `modelRegistry` options with the async `modelRuntime` option. `AuthStorage` - and its storage backends are no longer exported; use `ModelRuntime` (or a - custom pi-ai `CredentialStore`), or `readStoredCredential()` for one-off - reads of auth.json." -- "Replaced SDK request-auth assembly through - `ModelRegistry.getApiKeyAndHeaders()` with `ModelRuntime.getAuth()`." -- "Changed extension-facing `ModelRegistry.refresh()` from synchronous `void` - to `Promise` because `models.json` loading is asynchronous. Extensions - must await it before making synchronous registry reads." -- "Moved canonical dynamic catalog refresh to async `ModelRuntime.refresh()`." - -### 3.3 The new API shape - -**`ModelRuntime`** (`core/model-runtime.d.ts`, new) — the canonical async facade: -- `static create(options?: CreateModelRuntimeOptions): Promise` - where options include `credentials?: CredentialStore`, `authPath?`, - `modelsPath?`, `modelsStore?`, `allowModelNetwork?`, etc. -- Provider/model reads: `getProviders()`, `getProvider(id)`, `getModels()`, - `getModel()`, `getAvailable()` (async) / `getAvailableSnapshot()` (sync). -- Auth: `getAuth(providerId|model, overrides?)`, `checkAuth(providerId)`, - `hasConfiguredAuth(providerId)`, `isUsingOAuth(providerId)`, - `getProviderAuthStatus(providerId)`, `listCredentials()`, - `setRuntimeApiKey`, `removeRuntimeApiKey`. -- Login/logout: `login(providerId, type, interaction): Promise`, - `logout(providerId): Promise`. -- `refresh(): Promise<...>`, `registerProvider`/`unregisterProvider`. -- Implements pi-ai `Models`. - -**`ModelRegistry`** (`core/model-registry.d.ts`, changed) — now a thin sync -compatibility facade **for extensions**, constructed from a `ModelRuntime`: -- `constructor(runtime: ModelRuntime)` — **no more `ModelRegistry.create(authStorage, ...)` - and no more `ModelRegistry.inMemory(...)`**. -- **No `authStorage` property.** (This breaks `authProviderOptions.ts`, - `authService.ts`, and `anthropicSubscriptionWarning`.) -- `refresh(): Promise` (was sync `void`). -- Keeps `getAll`, `getAvailable`, `find`, `getProviderAuthStatus`, - `getProviderDisplayName`, `getApiKeyForProvider`, `isUsingOAuth`, - `hasConfiguredAuth`, `getApiKeyAndHeaders`, `registerProvider`, etc. -- **Dropped:** the whole `authStorage`-centric credential API - (`get/set/list/logout/reload/getOAuthProviders`). - -**`AuthStorage`** (`core/auth-storage.d.ts`, still exists internally, NOT -exported): now `implements CredentialStore` with an entirely different, -**async** method set — `read()`, `modify()`, `delete()`, `list()` returning -`Promise`s of pi-ai `Credential`/`CredentialInfo`. The old -`get/set/remove/has/login/logout/getApiKey/getOAuthProviders/setRuntimeApiKey` -synchronous methods are gone. `static create/inMemory/fromStorage` remain but -the class is unexported. - -**`readStoredCredential(providerId, authPath?)`** — new synchronous one-off read -returning a pi-ai `Credential | undefined` (`{ type: "api_key", key?, env? }` or -`{ type: "oauth", ... }`). Useful for `anthropicSubscriptionWarning`. - -**pi-ai 0.80.8 auth model** (`@earendil-works/pi-ai`, `auth/types.d.ts`, -`auth/credential-store.d.ts`): -- `CredentialStore` interface: `read`, `list`, `modify`, `delete` — all async. -- `Credential = ApiKeyCredential | OAuthCredential`; `CredentialInfo`. -- `InMemoryCredentialStore` class exported — the test seam that replaces - `AuthStorage.inMemory(...)`. -- `AuthInteraction` interface replaces the old `OAuthLoginCallbacks`: - `{ signal?, prompt(prompt: AuthPrompt): Promise, notify(event: AuthEvent): void }`. - `AuthPrompt` is a discriminated union (`text`/`secret`/`select`/`manual_code`); - `AuthEvent` is `info`/`auth_url`/`device_code`/`progress`. This is a **complete - reshaping** of the OAuth login callback contract used by - `oauthLoginFlowService.ts`. -- `login(providerId, type, interaction)` now lives on `ModelRuntime`, not on a - credential store, and returns a `Credential`. -- `Provider` objects (`getProviders()`) carry `{ id, name, auth: { apiKey?, oauth? } }` - — this is the new source of truth for enumerating login providers, replacing - `authStorage.getOAuthProviders()`. - -### 3.4 Session services wiring change - -`createAgentSessionServices` options and `AgentSessionServices`: -- 0.80.7: `{ cwd, agentDir?, authStorage?, settingsManager?, modelRegistry?, ... }` - → services expose `authStorage` + `modelRegistry`. -- 0.80.8: `{ cwd, agentDir?, settingsManager?, modelRuntime?, ... }` - → services expose `modelRuntime` (no `authStorage`, no `modelRegistry`). - -So `piSessionService.ts`'s `createDefaultRuntimeFactory` must pass `modelRuntime` -instead of `authStorage` + `modelRegistry`. - ---- - -## 4. Backwards-compatibility analysis (0.80.0–0.80.7 vs 0.80.8+) - -A shim would need to bridge, simultaneously: - -1. **Construction:** `ModelRegistry.create(authStorage, modelsPath)` / - `ModelRegistry.inMemory(authStorage)` (old) vs - `await ModelRuntime.create({ credentials, authPath, modelsPath })` then - `new ModelRegistry(runtime)` (new). Old is sync; new is async. This alone - forces `AuthService` / `PiSessionService` construction to become async or to - pre-resolve a runtime, changing call sites either way. -2. **Credential access:** synchronous `authStorage.get/set/list/logout/reload/ - getOAuthProviders` (old) vs async `CredentialStore.read/modify/delete/list` - + `ModelRuntime.getProviders()/login/logout/getProviderAuthStatus` (new). - Sync→async cannot be shimmed transparently. -3. **OAuth login:** `authStorage.login(providerId, OAuthLoginCallbacks)` (old, - rich callback object) vs `modelRuntime.login(providerId, type, - AuthInteraction)` (new, `prompt`/`notify` contract). The - `oauthLoginFlowService` maps SDK callbacks onto web-UI flow state; the two - callback contracts are structurally different and would each need a distinct - adapter. -4. **`refresh()`** sync vs async. -5. **Provider enumeration** (`authProviderOptions.ts`) built on - `authStorage.getOAuthProviders()/list()/get()` — none of which exist in the - new surface; must be rederived from `getProviders()` + `listCredentials()`. - -A dual shim would therefore reimplement two full auth stacks behind a lowest- -common-denominator async interface, plus runtime detection of which export -exists — high complexity, high risk, and permanently carrying dead code for the -already-broken 0.80.0–0.80.7 line. This fails the "easy/clean" bar in the task. - -**Conclusion:** backwards compatibility with 0.80.0–0.80.7 is **not easy** and -not worth it. Pi Web should target the new (0.80.8+) API and drop support for -0.80.0–0.80.7. - ---- - -## 5. Recommendation - -**Clean migration to the `ModelRuntime` API + range correction + version bump.** - -Rationale: -- 0.80.8 is an explicit upstream breaking change; the export removal is - intentional and permanent (confirmed identical in 0.80.10 `latest`). -- The old 0.80.0–0.80.7 surface and the new 0.80.8+ surface differ across - construction, sync/async, credential access, OAuth login, and session - services — there is no small adapter that spans both cleanly. -- Pinning down to a still-working old version is a dead end: users installing - Pi Web get whatever Pi they have, and `latest` is already 0.80.10. - -### Concrete migration shape (to be executed by the relay, not now) - -1. **`authService.ts`**: hold a `ModelRuntime` (created via - `ModelRuntime.create({ authPath, modelsPath })`), optionally expose a - `ModelRegistry` wrapper for extension-facing reads. Replace credential - operations: - - `saveApiKey` → `runtime` credential `modify(providerId, async () => ({ type:"api_key", key }))` - (via the runtime's credential store / `setRuntimeApiKey` is for ephemeral; - persistence uses the `CredentialStore.modify` path). - - `logoutProvider` → `runtime.logout(providerId)`. - - `startOAuthLogin` → `runtime.login(providerId, "oauth", interaction)`. - - refresh → `await runtime.refresh()`. - - Construction becomes async (factory function returning a Promise, or an - `init()` step) — propagate to `sessiond.ts`. -2. **`authProviderOptions.ts`**: rederive login/logout options from - `runtime.getProviders()` (auth.apiKey / auth.oauth presence + names) and - `runtime.listCredentials()` / `getProviderAuthStatus()`. Update the - structural `AuthProviderModelRegistry`/`AuthProviderRuntime` interface and - its test double. -3. **`oauthLoginFlowService.ts`**: reimplement against `AuthInteraction` - (`prompt(AuthPrompt)` + `notify(AuthEvent)`) instead of `OAuthLoginCallbacks`. - Map `AuthPrompt` kinds (`text`/`secret`/`manual_code`/`select`) to the web - UI prompt/select shapes, and `AuthEvent` (`auth_url`/`device_code`/`progress`) - to the existing flow-state fields. This is the largest single slice. -4. **`piSessionService.ts`**: - - `createDefaultRuntimeFactory` passes `modelRuntime` to - `createAgentSessionServices` instead of `authStorage` + `modelRegistry`. - - `PiAgentSession` internal type: carry `modelRuntime` (or an adapted - registry) instead of the old `modelRegistry.authStorage`. - - `anthropicSubscriptionWarning`: replace - `modelRegistry.authStorage.get("anthropic")` with - `readStoredCredential("anthropic", authPath)` (sync, no `authStorage` - needed) — cleanest fit for this synchronous check. -5. **`sessiond.ts`**: adapt to async auth construction (create the runtime, - `await` init, then pass into `PiSessionService`). **This is session-daemon - code → requires a manual `pi-web-web-sessiond.service` restart after the fix - lands.** -6. **Tests / testSupport**: replace `AuthStorage.inMemory(...)` with pi-ai - `InMemoryCredentialStore` (+ `await ModelRuntime.create({ credentials })`), - and `ModelRegistry.create/inMemory(...)` accordingly. Update - `authService.test.ts`, `piSessionService.testSupport.ts`, - `piSessionService.promptQueue.test.ts`, `piSessionService.warnings.test.ts`, - `oauthLoginFlowService.test.ts`, `authProviderOptions.test.ts`. Follow the - testing-guide skill (esp. async construction, no over-mocking of SDK). - -### Dependency range correction (§6) - -- Change the three `@earendil-works/*` **peerDependencies** from - `>=0.80.0 <1` to a range that excludes the unsupported line, e.g. - `>=0.80.8 <0.81` (matching the current published minor). Keep the three - `devDependencies` on a matching `^0.80.8` (or exact `0.80.8`/`0.80.10`). -- `@earendil-works/pi-agent-core` and `@earendil-works/pi-ai` are siblings - released in lockstep with `pi-coding-agent` (coding-agent depends on - `^0.80.x` of both); correct all three ranges together. -- Rationale for the upper bound `<0.81`: the auth refactor shows this line makes - breaking changes within `0.80.x` patch releases, so a permissive `<1` is - unsafe. Pin to the known-good minor window and widen deliberately after - testing new releases. - -### Release / changeset - -- Add a **patch** (or minor, maintainer's call) `.changeset/*.md` for - `@jmfederico/pi-web` describing the user-visible fix: "Fix session daemon - crash with Pi 0.80.8+ by migrating to the new `ModelRuntime` API; require Pi - `>=0.80.8`." Do **not** edit `CHANGELOG.md` directly (Changesets generates it). -- Actual npm publish is out of scope for the fix branch; the release skill - (`npm-release-via-github-actions`) is only referenced so the changeset is - release-ready. - ---- - -## 6. Dependency range facts (current state) - -`package.json`: -``` -devDependencies: - "@earendil-works/pi-agent-core": "^0.80.6", - "@earendil-works/pi-ai": "^0.80.6", - "@earendil-works/pi-coding-agent": "^0.80.6", -peerDependencies: - "@earendil-works/pi-agent-core": ">=0.80.0 <1", - "@earendil-works/pi-ai": ">=0.80.0 <1", - "@earendil-works/pi-coding-agent": ">=0.80.0 <1", -``` -No `dependencies`/`optionalDependencies` entries for these packages. The -permissive peer range `>=0.80.0 <1` is what lets consumers' npm resolve the -breaking 0.80.8/0.80.9/0.80.10 against a Pi Web build that expects the old -export. - -Published versions (npm): 0.79.10, 0.80.1, 0.80.2, 0.80.3, 0.80.5, 0.80.6, -0.80.7, 0.80.8, 0.80.9, 0.80.10. `latest` = 0.80.10. The removal landed in -0.80.8 and persists through 0.80.10. - ---- - -## 7. Verification artifacts - -- Extracted SDK tarballs for inspection: `/srv/dev/pi-inspect/` (v0.80.7, - v0.80.8, v0.80.10 of pi-coding-agent; pi-ai0807, pi-ai0808). These are - scratch/inspection only and outside the repo. -- Key diffs reproduced in §3.1 (index exports), §3.3 (class shapes), §3.4 - (session services). 0.80.8 vs 0.80.10 `.d.ts` are identical for the affected - files → the target API is stable. - -## 8. Risks / call-outs for the fix - -- **Session daemon restart required:** changes touch `sessiond.ts` and the - session runtime path; a manual restart of the sessiond service is needed after - the fix (per AGENTS.md). -- **Async construction ripple:** moving from sync `AuthStorage/ModelRegistry` - construction to `await ModelRuntime.create(...)` changes `AuthService` / - `PiSessionService` init and their call sites; keep the async boundary - explicit and injected (code-quality-architecture skill). -- **OAuth flow contract change is the riskiest slice** — the web UI prompt/ - select/device-code mapping must be re-verified end to end. -- **No local `node_modules`** in this worktree; the relay's first implementation - leg must `npm install` (pin to 0.80.8+) before it can typecheck/test. Note the - `/tmp` quota issue observed during assessment — install in the worktree, not - `/tmp`. diff --git a/README.md b/README.md index 0642d05..ea35cf1 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Your browser is the control surface. The work stays where it can keep running. Requirements: -- Node.js 22 or newer +- Node.js 22.19.0 or newer - npm - Pi Coding Agent configured for your user - git and the development tools your agents need diff --git a/docs/faq.html b/docs/faq.html index b0f0cfa..4030bf0 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -157,7 +157,7 @@

What does pi-web doctor check?

- It keeps two kinds of checks separate. General login-shell readiness covers Node 22+, npm, Pi, and optional + It keeps two kinds of checks separate. General login-shell readiness covers Node 22.19.0 or newer, npm, Pi, and optional ripgrep. Native-service diagnostics validate only the exact prerequisites of the selected service plan in the real systemd user-manager or launchd gui/<uid> context. Development installs follow their installed checkout plan; production checks are clearly labelled prospective when the installed executable @@ -181,7 +181,7 @@

  • Prefer version-manager shims when available; for mise, use shims or enable its shim setup rather than relying only on shell activation.
  • Move any required version-manager initialization to your login shell file.
  • -
  • Make sure node --version is at least v22 from bash -lc, zsh -lc, or your detected shell.
  • +
  • Make sure node --version is at least v22.19.0 from bash -lc, zsh -lc, or your detected shell.
  • Run pi-web doctor again after changing shell files.
diff --git a/docs/index.html b/docs/index.html index 56afff4..8ab248b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -38,7 +38,7 @@ "downloadUrl": "https://www.npmjs.com/package/@jmfederico/pi-web", "codeRepository": "https://github.com/jmfederico/pi-web", "description": "PI WEB is a web UI for Pi Coding Agent that keeps persistent agent sessions running in real workspaces on your machine or server.", - "softwareRequirements": "Node.js 22 or newer and Pi Coding Agent", + "softwareRequirements": "Node.js 22.19.0 or newer and Pi Coding Agent", "license": "https://github.com/jmfederico/pi-web/blob/main/LICENSE" } @@ -138,7 +138,7 @@ # laptop, phone, tablet — same live sessions $ pi-web doctor -✓ caller login shell can find node >= 22 +✓ caller login shell can find node >= 22.19.0 ✓ native-service plan requirements pass in manager context ✓ ready for persistent agent work diff --git a/docs/install.html b/docs/install.html index 4a37f49..33423c2 100644 --- a/docs/install.html +++ b/docs/install.html @@ -106,7 +106,7 @@

Requirements

    -
  • Node.js 22 or newer and npm.
  • +
  • Node.js 22.19.0 or newer and npm.
  • Pi Coding Agent installed/configured so the pi command works for your user.
  • A shell login environment that exposes Node, npm, Pi, git, and any tools your agents need.
  • For the automatic installer: a supported per-user service manager.
  • diff --git a/package-lock.json b/package-lock.json index edd9889..62f07d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "vitest": "^4.1.10" }, "engines": { - "node": ">=22" + "node": ">=22.19.0" }, "peerDependencies": { "@earendil-works/pi-agent-core": ">=0.80.8 <0.81", diff --git a/package.json b/package.json index d06b393..e20ff17 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "access": "public" }, "engines": { - "node": ">=22" + "node": ">=22.19.0" }, "repository": { "type": "git", diff --git a/relays/issue-62-authstorage/charter.md b/relays/issue-62-authstorage/charter.md deleted file mode 100644 index 961a1f1..0000000 --- a/relays/issue-62-authstorage/charter.md +++ /dev/null @@ -1,127 +0,0 @@ -# Relay charter — issue-62-authstorage - -## Relay identity -- **Name:** `issue-62-authstorage` -- **Root path:** `relays/issue-62-authstorage/` (in the repo, on branch - `fix/issue-62-authstorage`, worktree `/srv/dev/pi-web-issue-62`) -- **Packet files:** `charter.md`, `status.md`, `log.md` (this directory). - -## Background (read once, do not re-derive) -The full technical assessment lives at the worktree root: -`ASSESSMENT-issue-62.md`. Read it once at the start of your leg for the API -migration details; do not re-investigate the SDK from scratch. Short version: -Pi `@earendil-works/pi-coding-agent` 0.80.8 removed the `AuthStorage` export and -replaced the auth/model plumbing with an async `ModelRuntime` + pi-ai -`CredentialStore` model. Pi Web imports `AuthStorage` statically and crashes at -module load with any Pi 0.80.8+. The agreed fix is a **clean migration to the -new `ModelRuntime` API** (no dual-version shim) plus dependency-range -correction, tests, and a changeset. Rationale and the per-file migration shape -are in `ASSESSMENT-issue-62.md` §5. - -## Goal / finish line -Pi Web builds, typechecks, lints, and passes its full test suite against Pi -`@earendil-works/pi-coding-agent` **0.80.8+** (target the installed 0.80.8/0.80.10 -line), with: -1. No remaining import or use of the removed `AuthStorage` export (and no - reliance on `ModelRegistry.create(authStorage)` / `.inMemory(authStorage)` / - `modelRegistry.authStorage`). -2. Auth, OAuth login, API-key save/logout, provider enumeration, and the - Anthropic subscription warning all working through the new `ModelRuntime` / - `readStoredCredential` / pi-ai `CredentialStore` APIs. -3. `package.json` peerDependencies and devDependencies for the three - `@earendil-works/*` packages corrected so npm cannot resolve an unsupported - release (target range `>=0.80.8 <0.81`; devDeps on a matching `^0.80.8`). -4. A `.changeset/*.md` fragment describing the user-visible fix (no direct - `CHANGELOG.md` edit). -5. `npm run verify` (typecheck + lint + knip + test) passing. - -Finish line reached = all of the above true and committed on the branch. **Do -not open a PR** (out of scope for this relay). - -## Sizing — one leg -One leg = **one coherent slice** from the plan below that leaves the tree in a -committed, describable state. Prefer the pre-broken-out slices in `status.md`. -A leg does not have to leave `npm run verify` fully green (the migration is -interdependent), but it MUST: -- leave a clear, honest `status.md` describing what compiles/what doesn't yet, -- commit its work with a clear message, -- not expand scope beyond its slice ("just a bit more" is the main failure mode - here — the auth surfaces are interconnected; resist rewriting everything in - one leg). - -If a slice turns out bigger than expected, split it and hand off mid-plan with -an updated `status.md` — that is expected and fine. - -## Suggested slice breakdown (task selection default) -Follow `status.md`'s named next task. If none is named, pick the lowest-numbered -incomplete slice here: - -0. **Bootstrap:** `npm install` in the worktree pinning the three `@earendil-works/*` - packages to 0.80.8+ (e.g. `npm i -D @earendil-works/pi-coding-agent@0.80.8 - @earendil-works/pi-ai@0.80.8 @earendil-works/pi-agent-core@0.80.8`), and - correct the peerDependencies range to `>=0.80.8 <0.81`. Confirm the crash - reproduces / the new exports resolve. Commit. (Install in the worktree, NOT - `/tmp` — `/tmp` has a disk quota problem, see assessment §8.) -1. **`authService.ts` core migration:** move to `ModelRuntime` (async - construction via `ModelRuntime.create({ authPath, modelsPath })`), migrate - saveApiKey/logout/refresh/credential access. Propagate async construction to - `sessiond.ts`. (Session-daemon path — see restart note.) -2. **`authProviderOptions.ts` migration:** rederive login/logout provider - options from `runtime.getProviders()` + `listCredentials()` / - `getProviderAuthStatus()`; update its structural interface + test double. -3. **`oauthLoginFlowService.ts` migration:** reimplement against pi-ai - `AuthInteraction` (`prompt`/`notify`) instead of `OAuthLoginCallbacks`; wire - `runtime.login(providerId, "oauth", interaction)`. (Riskiest slice — verify - prompt/select/device-code/auth_url mapping.) -4. **`piSessionService.ts` migration:** pass `modelRuntime` to - `createAgentSessionServices`; update `PiAgentSession` type; switch - `anthropicSubscriptionWarning` to `readStoredCredential`. -5. **Tests + testSupport:** migrate all test doubles to `InMemoryCredentialStore` - + `ModelRuntime.create`; get `npm run verify` green. Follow the testing-guide - skill. -6. **Changeset + final verify + cleanup:** add `.changeset/*.md`; run full - `npm run verify`; remove scratch (`ASSESSMENT` stays, `/srv/dev/pi-inspect` - is outside the repo). Confirm goal, hand off to a final confirmation/stop. - -Slices may merge or split. 1–4 depend on 0. Slice 5 finalizes; slice 6 closes. - -## Handover -When handing off, `spawn_session` **once** with a prompt whose first line is: -`Relay "issue-62-authstorage" leg begins now.` followed by the standard -Relay handoff body pointing at: -- `relays/issue-62-authstorage/charter.md` -- `relays/issue-62-authstorage/status.md` - -Tell the next runner not to read `log.md` end-to-end. Make all work durable -(update `status.md`, append `log.md`, commit) **before** spawning. - -## Intervention signal — stop and get the human when: -- The new SDK API does not actually provide an operation the migration needs - (e.g. no viable credential persistence path for API-key save), i.e. the - assessment's assumed mapping is wrong. -- A slice would require changing the charter's goal or the agreed "clean - migration, no shim" decision. -- `npm install` / registry access fails and cannot be resolved in-leg. -- Charter churn: if you find yourself needing to edit this charter to proceed, - stop and involve the human instead. -To raise it: set a clear `## BLOCKED` section at the top of `status.md`, append a -`log.md` entry explaining the blocker and what decision is needed, do **not** -spawn the next leg, and end your run. - -## Reading discipline -Read, in order: this `charter.md`, then `status.md`, then `ASSESSMENT-issue-62.md` -(once), then only the specific `src/server/sessions/*` files your slice touches. -Do **not** read `log.md` end-to-end — only targeted entries if `status.md` points -you there. Do not re-extract SDK tarballs unless the assessment is contradicted -by reality. - -## Standing constraints (project conventions) -- Session-daemon changes (`sessiond.ts`, session runtime / auth construction - loaded by the daemon) require the human to **manually restart the sessiond - service** to take effect. Call this out in `status.md`/handoff whenever a leg - changes that path so the human knows a restart is pending. -- Follow the skills: `code-quality-architecture` (DI, async boundaries, - testable seams), `testing-guide` (test layers, no over-mocking), and - `changeset-changelog` (changeset not CHANGELOG edit). -- Keep changes scoped to the fix; do not opportunistically refactor unrelated - code. diff --git a/relays/issue-62-authstorage/log.md b/relays/issue-62-authstorage/log.md deleted file mode 100644 index 5092fff..0000000 --- a/relays/issue-62-authstorage/log.md +++ /dev/null @@ -1,397 +0,0 @@ -# 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). - ---- - -## 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: ModelRegistryInstance` → `modelRuntime: - 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`. `warningsForSession` - passes `join(this.agentDir, "auth.json")`. -- Model reads rederived onto the runtime: `availableModels`/`setModel` → - `await modelRuntime.refresh()` + `getAvailableSnapshot()` + `getModel(...)`; - `syncCurrentModelAuthWarning` → `getModel(...)` + - `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 --noEmit` — `sessiond.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). - -## Leg 7 (slice 6 — changeset + final verify + cleanup) — RELAY COMPLETE -- **Added changeset** `.changeset/fix-pi-0-80-8-modelruntime-auth.md`: single - `patch` fragment for `@jmfederico/pi-web` describing the user-visible fix - (session daemon crash on Pi 0.80.8+ fixed by migrating auth to the new - `ModelRuntime` credential APIs; Pi Web now requires Pi `>=0.80.8`). Did NOT - edit `CHANGELOG.md`. -- **Bump type decision:** chose `patch`, not `minor`. status.md floated that a - minor was "defensible" (narrowed Pi range), but the `changeset-changelog` - skill is explicit for this CalVer repo: `patch` for all non-breaking changes, - never `minor` (the minor slot = release month), `major` only on explicit user - request. The fix is non-breaking to Pi Web consumers, so `patch` is correct. -- **Re-ran `npm run verify`:** fully GREEN — typecheck + lint + knip + 188 test - files, 1390 passed / 2 skipped. -- **Goal criteria confirmed:** (1) no live `AuthStorage`/`ModelRegistry.create| - inMemory`/`.authStorage`/`modelRegistry` use in `src/` — only remaining match - is an explanatory comment in `piSessionService.testSupport.ts`; (2) auth - surfaces all on new APIs (slices 1–5); (3) `package.json` peerDeps for the - three `@earendil-works/*` = `>=0.80.8 <0.81`, devDeps = `^0.80.8` (verified); - (4) changeset added this leg; (5) verify green. -- **Cleanup:** confirmed no scratch files in the repo (no `probe*.mjs`, - `.tmp-build/`). `ASSESSMENT-issue-62.md` intentionally kept (plan of record). -- **No PR opened** (explicitly out of scope for this relay). -- **STOP per charter:** goal reached, so no next leg was spawned. Surfaced to - the human: (a) verify green, (b) sessiond restart STILL PENDING (slices 1+4 - touched session-daemon paths; only the human clears that note after - restarting the sessiond service), (c) no PR by design. -- Committed status/log/changeset. diff --git a/relays/issue-62-authstorage/status.md b/relays/issue-62-authstorage/status.md deleted file mode 100644 index affc3f6..0000000 --- a/relays/issue-62-authstorage/status.md +++ /dev/null @@ -1,249 +0,0 @@ -# Relay status — issue-62-authstorage - -## RELAY COMPLETE — goal reached (leg 7, slice 6) -All charter goal criteria (1–5) are met and committed on branch -`fix/issue-62-authstorage`. **No PR was opened, by design (out of scope).** -The relay is finished; no further leg was spawned. - -**Surface to the human:** -- (a) `npm run verify` is fully GREEN (typecheck + lint + knip + 1390 tests, - 2 skipped). -- (b) **Sessiond restart is still PENDING** — slices 1 + 4 changed - session-daemon paths (`sessiond.ts`, `piSessionService.ts`). The human must - manually restart the sessiond service for the migration to take effect. Only - the human clears this note. -- (c) No PR opened (explicitly out of scope for this relay). - -Leg 7 (slice 6) added the changeset, re-verified green, confirmed goal -criteria, and confirmed no scratch files remain: -- **`.changeset/fix-pi-0-80-8-modelruntime-auth.md`** — a single `patch` - fragment for `@jmfederico/pi-web` describing the user-visible fix (session - daemon crash with Pi 0.80.8+ fixed by migrating to the new `ModelRuntime` - auth APIs; Pi Web now requires Pi `>=0.80.8`). Per the `changeset-changelog` - skill this repo uses **patch** for all non-breaking changes (CalVer: the - `minor` slot is the release month, not feature size; `major` only on explicit - request), so `patch` was chosen over the "minor is defensible" note. Commit - ``. -- Re-ran full `npm run verify`: GREEN. -- Double-checked `package.json`: peerDeps for the three `@earendil-works/*` - packages are `>=0.80.8 <0.81`, devDeps are `^0.80.8` — correct. -- Confirmed no scratch files in the repo (no `probe*.mjs`, `.tmp-build/`, - etc.). `ASSESSMENT-issue-62.md` intentionally stays (plan of record). -- Only `src` mention of the old API is an explanatory comment in - `piSessionService.testSupport.ts` (documents what the seam replaced) — no - live import/use. - -## Prior position (slice 5, leg 6, commit `d0cc55c`) -Slice 5 (tests + testSupport) complete and committed (`d0cc55c`). -**`npm run verify` was fully GREEN** — typecheck + lint + knip + 1390 tests -pass (2 skipped). All production code and all test/support code are off the -removed `AuthStorage` / `ModelRegistry.create|inMemory` surface. - -What slice 5 changed (all under `src/server/sessions/`): -- **`piSessionService.testSupport.ts`** (central helper): dropped - `AuthStorage`/`ModelRegistry` imports; added pi-ai `InMemoryCredentialStore`. - New seams: `createTestModelRuntime(credentials?)` (wraps - `ModelRuntime.create({ credentials })`), a shared `testModelRuntime` - (top-level `await createTestModelRuntime()` — the common no-auth catalog - runtime), and `seedCredential(store, providerId, credential)` (writes via the - `CredentialStore.modify` path). `fakeRuntime` session now carries - `modelRuntime: testModelRuntime`; `testModel()` reads - `testModelRuntime.getModel(...)`. -- Threaded `modelRuntime: testModelRuntime` into every `new PiSessionService(...)` - (now a required dep) across `archiveCleanup`/`lifecycle`/`promptQueue`/ - `spawnSession`/`spawnSubsession`/`sessionRoutes` tests, importing - `testModelRuntime` in each. -- **`piSessionService.promptQueue.test.ts`** auth-loss test rewritten: builds a - live `InMemoryCredentialStore` + `createTestModelRuntime(credentials)`, and - simulates auth changes via `credentials.delete/seedCredential` + - `modelRuntime.refresh()` + `applyAuthChange(...)` (matching AuthService's - real refresh-then-emit sequence). Removed the `modelRegistry` dep line. -- **`piSessionService.warnings.test.ts`**: `anthropicSubscriptionWarning` now - reads `readStoredCredential("anthropic", authPath)`, so the test seam is a - temp `auth.json` written per case (helper `anthropicAuthPath(...)`), passed - as the 2nd arg. `SubscriptionSession` type narrowed to - `Pick`. The "no credential" - case points at a temp dir with no auth.json (deterministic). -- **`authService.test.ts`** fully reworked to the async `AuthService.create({ - runtime | agentDir })` + `InMemoryCredentialStore` model. `saveApiKey`/ - `logoutProvider`/`startOAuthLogin` are awaited; OAuth-complete test asserts - `startOptions.runtime === runtime` and uses `vi.waitFor` for the async - refresh; credential assertions go through `credentials.read(...)`. -- Lint fixes surfaced by running lint green for the first time this relay: - `getLoginProviderOptions` made **synchronous** (it did no async work) and its - call sites in `authService.ts` + `authProviderOptions.test.ts` de-awaited; - `authRoutes.ts` handlers now `return await ...` (return-await rule); - `authService.ts` api-key interaction uses `() => Promise.resolve(key)` / - `notify: () => undefined`; test `modify` arrows use `() => Promise.resolve(...)`. - -SDK behavior verified empirically before writing doubles (throwaway probe -scripts, since removed): `ModelRuntime.create({ credentials })` exposes 36 -providers / 1072 models; `login(id, "api_key", interaction)` persists to the -store; `getModel("anthropic", "claude-sonnet-4-5-20250929")` resolves; -`hasConfiguredAuth` flips correctly across `delete`/`modify` + `refresh`; -`readStoredCredential(id, authPath)` reads a temp auth.json and returns -`undefined` for a missing file. - -### Prior position (slice 4, leg 5, commit `4ccd4f8`) -Slice 4 (`piSessionService.ts` migration) complete and committed (`4ccd4f8`). -`piSessionService.ts` now uses the new `ModelRuntime` API end to end: -- `createDefaultRuntimeFactory(modelRuntime, ...)` passes `modelRuntime` to - `createAgentSessionServices({ cwd, agentDir, modelRuntime })` (no more - `authStorage` + `modelRegistry`). -- `PiAgentSession.modelRegistry` → `PiAgentSession.modelRuntime: ModelRuntime`. -- `anthropicSubscriptionWarning(session, authPath?)` now reads via - `readStoredCredential("anthropic", authPath)` (sync); `warningsForSession` - passes `join(this.agentDir, "auth.json")`. Its `session` param narrowed to - `Pick` (no longer needs the - registry). -- Model reads rederived onto the runtime: `availableModels`/`setModel` use - `await modelRuntime.refresh()` + `getAvailableSnapshot()` + `getModel(...)`; - `syncCurrentModelAuthWarning` uses `getModel(...)` + - `hasConfiguredAuth(providerId)`. -- `applyAuthChange` no longer refreshes a registry (the shared runtime is - refreshed by AuthService before it emits, and all sessions share that - runtime), so the `auth.subscribe` callback stays synchronous. -- **`modelRuntime` is now a REQUIRED `PiSessionServiceDependencies` field** - (the old `modelRegistry?` fallback used a *sync* `ModelRegistry.create`; a - `ModelRuntime` can only be built by the async `ModelRuntime.create`, which - can't run inside a constructor). `sessiond.ts` already injects - `modelRuntime: auth.runtime` (slice 1), so it typechecks unchanged. -- Dropped the `AuthStorage` / `ModelRegistry` imports and the - `createModelRegistryForAgentDir` import (only `AuthChange` is still imported - from `authService.js`). - -`npx tsc --noEmit`: **`sessiond.ts` and `piSessionService.ts` are at 0 errors** -(production code is fully migrated; `grep -vE '\.test\.ts|testSupport\.ts'` on -tsc output is empty). All remaining errors are slice-5 test/support files. -`piSessionService.ts` lints clean. - -### Prior position (slice 3, leg 4, commit `1c3d6db`) -Slice 3 (`oauthLoginFlowService.ts` migration) complete and committed (`1c3d6db`). -`OAuthLoginFlowService` is reimplemented against the pi-ai `AuthInteraction` -contract (`{ signal?, prompt(AuthPrompt), notify(AuthEvent) }`); the old -`OAuthLoginCallbacks`/`AuthStorage` imports are gone. `start()` now takes a -`ModelRuntime` (narrowed to `Pick`) instead of -`authStorage`, and drives login via `runtime.login(providerId, "oauth", -interaction)`. Mapping: `AuthPrompt` `text`/`secret`/`manual_code` → web-UI -`prompt` (kind `prompt`, `manual_code` → kind `manual`); `select` → web-UI -`select` (options `{id,label}` → `{value,label}`, returns chosen id); -`AuthEvent` `auth_url` → `auth: {url, instructions?}`; `device_code` → reuse -`auth` field (`url: verificationUri`, `instructions: "Enter code: "`); -`info`/`progress` → append `message` to `progress`. Per-prompt -`AuthPrompt.signal` now aborts just that pending request (rejects -`"Prompt cancelled"`) without ending the overall flow — needed because a -`manual_code` prompt can race a callback server. `oauthLoginFlowService.test.ts` -rewritten to the new contract via a `fakeRuntime` login double; **9 tests pass**, -files lint clean. - -`npx tsc --noEmit` now reports **26 errors** (down from 28). `authService.ts` -is now at **0 errors** (as predicted). Remaining errors are all slice 4/5: -`sessiond.ts` (1) + `piSessionService.ts` (6) = slice 4; -`authService.test.ts` (10), `piSessionService.testSupport.ts` (3), -`.promptQueue.test.ts` (2), `.warnings.test.ts` (4) = slice 5. - -### Prior position (slice 2, leg 3, commit `d09d7cc`) -Slice 2 (`authProviderOptions.ts` migration) complete and committed (`d09d7cc`). -`authProviderOptions.ts` now derives options from a runtime-shaped -`AuthProviderRuntime` interface (`getProviders()` + `listCredentials()` + -`getProviderAuthStatus()`). `getLoginProviderOptions`/`getLogoutProviderOptions` -are now `async` (matching the `await` call sites already in `authService.ts`). -The old `AuthProviderModelRegistry` interface is gone; a real `ModelRuntime` -satisfies `AuthProviderRuntime` structurally. The test double in -`authProviderOptions.test.ts` was rewritten to the runtime shape and its 3 -tests pass. OAuth-capable = `auth.oauth` present; api-key = `auth.apiKey` -present; `OAUTH_ONLY_PROVIDERS` / `isApiKeyLoginProvider` logic preserved; -display names come from `Provider.name`. - -`npx tsc --noEmit` now reports **28 errors** (down from 31). No -`authProviderOptions` errors remain and the `getLoginProviderOptions` / -`getLogoutProviderOptions` call sites in `authService.ts` typecheck cleanly. -Remaining errors are all cross-slice: `authService.ts` (1: line-83 -`OAuthLoginFlowService.start` still expects `authStorage` not `runtime` — -slice 3), `sessiond.ts` (1) + `piSessionService.ts` (6, slice 4), and the -test/support files (slice 5): `authService.test.ts` (9), -`oauthLoginFlowService.ts`/`.test.ts` (1+1, slice 3), -`piSessionService.testSupport.ts` (3), `.promptQueue.test.ts` (2), -`.warnings.test.ts` (4). - -### Prior position (slice 1, leg 2, commit `e37148c`) -Slice 1 (`authService.ts` core migration) complete and committed (`e37148c`). -`authService.ts` now uses the async `ModelRuntime` API: `AuthService.create({ -agentDir | runtime })` factory wraps `ModelRuntime.create({ authPath, -modelsPath })`; `createModelRuntimeForAgentDir` replaces -`createModelRegistryForAgentDir`. `saveApiKey` → `runtime.login(id, "api_key", -nonInteractive)`, `logoutProvider` → `runtime.logout`, `refreshAuthState` → -`await runtime.refresh()`. `authProviders` / `requireOAuthLoginProvider` are now -async. `startOAuthLogin` passes `runtime` into `OAuthLoginFlowService.start`. -`sessiond.ts` uses async `createRuntime`, `AuthService.create`, and passes -`modelRuntime: auth.runtime` to `PiSessionService`; `sessionDaemonStartup` now -awaits `createRuntime`. - -`npx tsc --noEmit` reports **31 errors** (up from 24 — expected: the migrated -authService now calls the runtime-based interfaces that slices 2–4 haven't -exposed yet). Slice-1 files are internally consistent; every remaining error -in `authService.ts` / `sessiond.ts` is a **cross-slice** dependency: -- `authService.ts`: `getLoginProviderOptions/getLogoutProviderOptions` still - take the old `AuthProviderModelRegistry` shape (fixed in slice 2); - `OAuthLoginFlowService.start` still expects `authStorage` not `runtime` - (fixed in slice 3). -- `sessiond.ts`: `PiSessionServiceDependencies` still expects `modelRegistry` - not `modelRuntime` (fixed in slice 4). -Remaining errors otherwise live in slices 2/3/4 files and all test/support -files (slice 5). - -## Leg tracking -- **Last completed leg:** 7 (slice 6 — changeset + final verify + cleanup). **FINAL LEG.** -- **Next leg to run:** none — relay complete, no handoff spawned. - -## Next task -None — the relay goal is reached. If new work is needed (e.g. opening a PR), -that is a separate task outside this relay's charter. - -### Build/tooling note (important for every leg) -**Update (leg 2):** the human reports `/tmp` is now fully usable again, so the -previous `TMPDIR` workaround is no longer required — plain `npm install` should -work. (If a disk-quota error resurfaces, fall back to -`TMPDIR="$PWD/.tmp-build" npm install` and remove `.tmp-build` after; it is -scratch, do not commit it.) node_modules is already installed at 0.80.10, so a -fresh install is only needed if node_modules is cleared. The pre-commit hook -runs a whole-project typecheck; while the migration is incomplete, commit relay -work with `git commit --no-verify` (the charter permits legs that aren't -verify-green). Node: v24.18.0. - -## Relevant context for the next runner -- **Plan of record:** `ASSESSMENT-issue-62.md` (root) — read once. §5 has the - per-file migration shape; §3 has the exact new API shapes; §6 the dep ranges. -- **Changeset skill:** `.agents/skills/changeset-changelog/SKILL.md` (and - `changeset-changelog` in the skills list). Follow it for the fragment format. -- **The migration is done** — slice 6 is docs/changeset + confirmation only. - No further source changes are expected; if you find yourself editing - `src/`, re-check whether that's really in scope. -- **New API cheat-sheet:** `ModelRuntime.create({ authPath, modelsPath, - credentials? }): Promise`; credential persistence via the - pi-ai `CredentialStore.modify` path; `runtime.login(providerId, type, - AuthInteraction)`; `runtime.logout`; `runtime.getProviders()` / - `listCredentials()` / `getProviderAuthStatus()`; `readStoredCredential( - providerId, authPath?)` for the sync anthropic warning; pi-ai - `InMemoryCredentialStore` for tests. -- **Decision already made:** clean migration, **no dual-version compat shim** - (assessment §4/§5). Do not reopen this without the intervention signal. - -## Progress documentation expectations -Every leg: update this `status.md` (current position, leg tracking, next task, -context, blockers), append a concise `log.md` entry, make work durable, and -commit before handing off. Hand off with `spawn_session` **once** per the -charter's Handover section. - -## Blockers / intervention state -None blocking. Relay complete. Known constraints: -- **Sessiond restart pending (ACTIVE):** slice 1 (leg 2, commit `e37148c`) - changed `sessiond.ts` + the session-daemon auth construction path; slice 4 - (leg 5, commit `4ccd4f8`) added `piSessionService.ts` (a session-daemon path) - to this surface. Per AGENTS.md the human must **manually restart the sessiond - service** for these changes to take effect once the migration lands. Keep - this note until the human confirms the restart. -- `/tmp` disk-quota issue is resolved (human confirmed usable) — see the - Build/tooling note above. -- node_modules is installed (gitignored) at 0.80.10; a fresh `npm install` is - only needed if node_modules is cleared. diff --git a/src/cli.test.ts b/src/cli.test.ts index 80fa32e..843927a 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -8,6 +8,7 @@ import { doctorExitCode, isCliEntrypoint, launchdRuntimeDetails, + nodeVersionCheck, regularFileExists, serviceBackendForPlatform, } from "./cli.js"; @@ -58,6 +59,18 @@ describe("commandWithVersionCheck", () => { }); }); +describe("nodeVersionCheck", () => { + it("checks the complete supported Node version with the resolved executable", () => { + process.env["SHELL"] = "/bin/bash"; + + const command = nodeVersionCheck(); + + expect(command).toContain("22.19.0"); + expect(command).toContain("process.versions.node"); + expect(command).toContain("\"$pi_web_probe_executable\""); + }); +}); + describe("agentCommandForChecks", () => { it("reads the configured agent command for doctor checks", () => { const dir = mkdtempSync(join(tmpdir(), "pi-web-cli-test-")); diff --git a/src/cli.ts b/src/cli.ts index 3b2a0e1..d4ed1e7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,6 +15,7 @@ import { type NativeServiceInstallFailure, } from "./nativeServices/serviceInstall.js"; import { + minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, type NativeServiceBackend, @@ -772,11 +773,14 @@ export function commandWithVersionCheck(command: string): string { return `${found} && (${commandWord} --version 2>&1 || true)`; } -function nodeVersionCheck(): string { - return [ - commandCheck("node"), - "node -e \"const major = Number(process.versions.node.split('.')[0]); console.log(process.version); process.exit(major >= 22 ? 0 : 1);\"", - ].join(" && "); +export function nodeVersionCheck(): string { + return nativeServicePrerequisiteShellCheck(detectServiceShell().name, { + id: "caller.node", + kind: "node-version", + command: "node", + minimumVersion: minimumSupportedNodeVersion, + description: `node >= ${minimumSupportedNodeVersion}`, + }); } export function agentCommandForChecks(env: NodeJS.ProcessEnv = process.env): string { @@ -787,7 +791,7 @@ function generalDoctorChecks(): Check[] { const shell = serviceShellLabel(); const agentCommand = agentCommandForChecks(); return [ - [`Caller login ${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())], + [`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())], [`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))], [`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))], ]; diff --git a/src/nativeServices/servicePlan.test.ts b/src/nativeServices/servicePlan.test.ts index c8b44eb..31dee77 100644 --- a/src/nativeServices/servicePlan.test.ts +++ b/src/nativeServices/servicePlan.test.ts @@ -100,7 +100,7 @@ describe("production native service planning", () => { wants: [], prerequisites: [ { id: "sessiond.command.pi-web-sessiond", kind: "command-available", command: "pi-web-sessiond" }, - { id: "sessiond.node", kind: "node-version", command: "node", minimumMajor: 22 }, + { id: "sessiond.node", kind: "node-version", command: "node", minimumVersion: "22.19.0" }, ], }, { @@ -111,7 +111,7 @@ describe("production native service planning", () => { wants: ["sessiond"], prerequisites: [ { id: "web.command.pi-web-server", kind: "command-available", command: "pi-web-server" }, - { id: "web.node", kind: "node-version", command: "node", minimumMajor: 22 }, + { id: "web.node", kind: "node-version", command: "node", minimumVersion: "22.19.0" }, ], }, ], @@ -196,7 +196,7 @@ describe("production native service planning", () => { namedCommandFailure: "command not found", }, prerequisites: [ - { id: "sessiond.node", kind: "node-version", command: "node", minimumMajor: 22 }, + { id: "sessiond.node", kind: "node-version", command: "node", minimumVersion: "22.19.0" }, { id: "sessiond.entrypoint", kind: "readable-file", path: "/package with space/sessiond's entry.js" }, ], }); @@ -324,7 +324,7 @@ describe("development native service planning", () => { environment: { PI_WEB_CONFIG: "/tmp/config.json" }, workingDirectory: "/checkout with space", prerequisites: [ - { id: "sessiond.node", kind: "node-version", minimumMajor: 22 }, + { id: "sessiond.node", kind: "node-version", minimumVersion: "22.19.0" }, { id: "sessiond.command.npm", kind: "command-available", command: "npm" }, { id: "sessiond.package-scripts", kind: "package-scripts", scripts: ["start:sessiond"] }, ], @@ -338,7 +338,7 @@ describe("development native service planning", () => { after: ["sessiond"], wants: ["sessiond"], prerequisites: [ - { id: "uiDev.node", kind: "node-version", minimumMajor: 22 }, + { id: "uiDev.node", kind: "node-version", minimumVersion: "22.19.0" }, { id: "uiDev.command.npm", kind: "command-available", command: "npm" }, { id: "uiDev.command.bash", kind: "command-available", command: "bash" }, { id: "uiDev.package-scripts", kind: "package-scripts", scripts: ["dev:web", "dev:client"] }, diff --git a/src/nativeServices/servicePlan.ts b/src/nativeServices/servicePlan.ts index 077fc5a..af3507b 100644 --- a/src/nativeServices/servicePlan.ts +++ b/src/nativeServices/servicePlan.ts @@ -1,3 +1,5 @@ +export const minimumSupportedNodeVersion = "22.19.0"; + export type NativeServiceBackendKind = "systemd" | "launchd"; export type NativeServiceMode = "production" | "development"; export type NativeServiceId = "sessiond" | "web" | "uiDev"; @@ -64,7 +66,7 @@ export type NativeServicePrerequisite = id: string; kind: "node-version"; command: "node"; - minimumMajor: number; + minimumVersion: string; description: string; } | { @@ -571,8 +573,8 @@ function nodeRequirement(serviceId: NativeServiceId): NativeServicePrerequisite id: `${serviceId}.node`, kind: "node-version", command: "node", - minimumMajor: 22, - description: "node >= 22 is available to the service shell", + minimumVersion: minimumSupportedNodeVersion, + description: `node >= ${minimumSupportedNodeVersion} is available to the service shell`, }; } diff --git a/src/nativeServices/serviceProbe.test.ts b/src/nativeServices/serviceProbe.test.ts index bd1b491..6a3a8f8 100644 --- a/src/nativeServices/serviceProbe.test.ts +++ b/src/nativeServices/serviceProbe.test.ts @@ -1,3 +1,4 @@ +import { spawnSync } from "node:child_process"; import { describe, expect, it, vi } from "vitest"; import { LaunchdNativeServiceProbe, @@ -5,6 +6,7 @@ import { SystemdNativeServiceProbe, launchdProbePlist, nativeServicePrerequisiteShellCheck, + nodeVersionCheckScript, systemdRunArguments, type LaunchdProbeFileSystem, type ProbeCommandResult, @@ -433,13 +435,26 @@ describe("probe service definitions", () => { id: "sessiond.node", kind: "node-version", command: "node", - minimumMajor: 22, - description: "node >= 22", + minimumVersion: "22.19.0", + description: "node >= 22.19.0", }); expect(check).toContain("\"$pi_web_probe_executable\" '-e'"); + expect(check).toContain("22.19.0"); expect(check).not.toContain("&& node -e"); }); + it.each([ + { version: "21.99.99", accepted: false }, + { version: "22.18.99", accepted: false }, + { version: "22.19.0", accepted: true }, + { version: "22.19.1", accepted: true }, + { version: "23.0.0", accepted: true }, + ])("checks the complete Node version for $version", ({ version, accepted }) => { + const result = spawnSync(process.execPath, ["-e", nodeVersionCheckScript("22.19.0"), version]); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(accepted ? 0 : 1); + }); + it("requires bundled entrypoints to be readable regular files", () => { const check = nativeServicePrerequisiteShellCheck("bash", { id: "sessiond.entrypoint", diff --git a/src/nativeServices/serviceProbe.ts b/src/nativeServices/serviceProbe.ts index 8036f8b..12db593 100644 --- a/src/nativeServices/serviceProbe.ts +++ b/src/nativeServices/serviceProbe.ts @@ -434,10 +434,8 @@ export function nativeServicePrerequisiteShellCheck(shell: NativeServiceShellNam switch (prerequisite.kind) { case "command-available": return externalExecutableShellCheck(shell, prerequisite.command); - case "node-version": { - const script = `const major=Number(process.versions.node.split('.')[0]);process.exit(major>=${String(prerequisite.minimumMajor)}?0:1)`; - return externalExecutableShellCheck(shell, "node", ["-e", script]); - } + case "node-version": + return externalExecutableShellCheck(shell, "node", ["-e", nodeVersionCheckScript(prerequisite.minimumVersion)]); case "readable-file": { const path = shellQuote(shell, prerequisite.path); return `test -f ${path} && test -r ${path}`; @@ -449,6 +447,11 @@ export function nativeServicePrerequisiteShellCheck(shell: NativeServiceShellNam } } +export function nodeVersionCheckScript(minimumVersion: string): string { + const encodedMinimum = JSON.stringify(minimumVersion); + return `const version=process.argv[1]??process.versions.node;console.log(process.version);const current=version.split('.').map(Number);const minimum=${encodedMinimum}.split('.').map(Number);const length=Math.max(current.length,minimum.length);let comparison=0;for(let index=0;indexright?1:-1;break}}process.exit(comparison>=0?0:1)`; +} + function externalExecutableShellCheck( shell: NativeServiceShellName, command: string, @@ -518,7 +521,7 @@ function unsatisfiedDetail(prerequisite: NativeServicePrerequisite): string { case "command-available": return `${prerequisite.command} did not resolve to an external executable in the native service environment.`; case "node-version": - return `node >= ${String(prerequisite.minimumMajor)} was not available in the native service environment.`; + return `node >= ${prerequisite.minimumVersion} was not available in the native service environment.`; case "readable-file": return `${prerequisite.path} was not a readable regular file in the native service environment.`; case "package-scripts": diff --git a/src/server/sessions/authService.test.ts b/src/server/sessions/authService.test.ts index 599faae..bbccade 100644 --- a/src/server/sessions/authService.test.ts +++ b/src/server/sessions/authService.test.ts @@ -5,7 +5,7 @@ import { ModelRuntime } from "@earendil-works/pi-coding-agent"; import { InMemoryCredentialStore, type AuthPrompt, type Credential } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OAuthFlowState } from "../../shared/apiTypes.js"; -import { AuthService, type AuthChange, type AuthServiceLogger } from "./authService.js"; +import { AuthService, createModelRuntimeForAgentDir, type AuthChange, type AuthServiceLogger } from "./authService.js"; import { OAuthLoginFlowService } from "./oauthLoginFlowService.js"; const tempDirs: string[] = []; @@ -251,7 +251,8 @@ describe("AuthService", () => { it("stores credentials in the configured agent directory", async () => { const agentDir = await tempAgentDir(); - const auth = await AuthService.create({ agentDir }); + const runtime = await createModelRuntimeForAgentDir(agentDir, false); + const auth = await AuthService.create({ runtime }); await auth.saveApiKey("anthropic", "sk-test"); diff --git a/src/server/sessions/authService.ts b/src/server/sessions/authService.ts index 6f91d85..cd939e0 100644 --- a/src/server/sessions/authService.ts +++ b/src/server/sessions/authService.ts @@ -31,8 +31,12 @@ interface AuthChangeContext { const noopLogger: AuthServiceLogger = { error() { /* no-op */ } }; -export function createModelRuntimeForAgentDir(agentDir: string): Promise { - return ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") }); +export function createModelRuntimeForAgentDir(agentDir: string, allowModelNetwork?: boolean): Promise { + return ModelRuntime.create({ + authPath: join(agentDir, "auth.json"), + modelsPath: join(agentDir, "models.json"), + ...(allowModelNetwork === undefined ? {} : { allowModelNetwork }), + }); } export class AuthService { diff --git a/src/server/sessions/piSessionService.testSupport.ts b/src/server/sessions/piSessionService.testSupport.ts index e1d338d..5ddefa3 100644 --- a/src/server/sessions/piSessionService.testSupport.ts +++ b/src/server/sessions/piSessionService.testSupport.ts @@ -79,7 +79,7 @@ export async function seedCredential(store: InMemoryCredentialStore, providerId: * behavior (e.g. auth-loss warnings). */ export function createTestModelRuntime(credentials: CredentialStore = new InMemoryCredentialStore()): Promise { - return ModelRuntime.create({ credentials }); + return ModelRuntime.create({ credentials, modelsPath: null, allowModelNetwork: false }); } /**