# M20-I2-ISOLATION-ROCKO-1 — pi auth check/print isolation findings Bounded read-only investigation (#1500). Brief: `docs/plans/reviews/2026-09-10_m20-refresh-isolation-request.md`. Package under investigation: `@earendil-works/pi-coding-agent@0.85.1` (registry), the same version currently pinned at the repo root (`package.json:8`; verified directly for this correction — see section 0). Method: `npm pack`/`npm view` against the public registry, local re-hash, static reading of the extracted public package only. No CLI execution, no credentials, no network calls, no installs. ## 0. Correction receipt (post-review, 2026-09-10) Darkwing's review identified three evidence issues in the version of this file at hash `89a9578c1a5982a2ef085e1ebff3990d66e8c237732397e2aba1c3b84d081ce9`. Original claims are quoted verbatim below, followed by the correction. Nothing in the original file has been deleted; corrected passages are updated in place in the sections referenced. **(1) Package pin.** Original (section 11): *"Did not compare 0.85.1's auth-check isolation surface against the currently pinned 0.84.4 (`package.json:8`) line-by-line."* This was stale. Re-checked directly against the current working tree: `package.json:8` now reads `"@earendil-works/pi-coding-agent": "0.85.1"`, and `package-lock.json:12,533` agree. `git log --oneline -- package.json` shows the pin was moved from `0.84.4` to `0.85.1` by commit `557aba0f` ("Add packages/mosaic registry schemas, validation, read-only CLI; pin pi 0.85.1 (#1499)"), which landed after my original PI-REFRESH-ROCKO-1 read of `package.json:8` (when it still said `0.84.4`) and before this review. Corrected in section 11 below: there is now no version delta between the pinned package and the package under investigation — both are `0.85.1`. **(2) Snapshot path.** Original (section 10): a timestamp placeholder, `/tmp/m20-pi0851-isolation-/`. Corrected to the concrete, still-present path used for every citation in this file: `/tmp/m20-pi0851-isolation-20260910T222007Z/` (verified present with `ls` at the time of this correction). Section 10 updated below. **(3) `allowModelNetwork:false` scope.** Original claims (sections 5 and 9) stated flatly that "no live network call occurs" for any of the three auth subcommands because `allowModelNetwork` is hard-coded `false`. This was **incorrect as a general claim** — `allowModelNetwork`/`allowNetwork` only gates `ModelRuntime.refresh()`'s **model-catalog** refresh path (`dist/core/model-runtime.js`). It does **not** reach OAuth **credential** refresh, which is a separate, unconditional path traced fresh for this correction in section 5a below and was not part of the original findings. Sections 5, 8, and 9 are corrected below to bound the no-network claim to model-catalog traffic only, and to state the OAuth-refresh network exposure explicitly. No CLI or network credential experiment was run to produce this correction — the finding is from static reading of `dist/bundle/chunks/chunk-IDDQWTHI.js` only, in the same snapshot already cited. ## 1. Package identity/integrity (re-verified for this task) Reused method from `docs/plans/reviews/2026-09-10_pi0851-refresh-investigation.md` (hash `cfab6c006a5aeba1734390c01efa8231e201e6560bf422938385448b24054ac6`). The original session-scratchpad extraction from that task was no longer present (cleaned up at that task's close-out); Darkwing acknowledged this before I re-fetched (reaction message, see brief context). Two independent fetches into the shared snapshot (path below): | Fetch | Command | Tarball shasum (npm pack manifest) | |---|---|---| | 1 | `npm pack @earendil-works/pi-coding-agent@0.85.1 --json` | `4cd00f653c3dabeb193b46f511044e7fbfe0f947` | | 2 (addendum: lifecycle scripts disabled) | `npm pack @earendil-works/pi-coding-agent@0.85.1 --ignore-scripts --json` | `4cd00f653c3dabeb193b46f511044e7fbfe0f947` | Identical shasum both times, and identical to the shasum recorded in the prior PI-REFRESH-ROCKO-1 findings — the "0.85.1" identity used here is byte-for-byte the same package. Also cross-checked against `registry-dist.json` (`npm view @earendil-works/pi-coding-agent@0.85.1 dist --json`), whose `shasum`/`integrity` match the local re-hash. Note on lifecycle scripts: `npm pack ` against a remote registry package resolves and downloads the tarball directly — it does not run an `npm install` and therefore never executes install lifecycle scripts, independent of `--ignore-scripts`. The identical shasum with and without the flag is expected and is not by itself proof the package has no lifecycle scripts (`extracted/package/package.json` `scripts` block was not separately audited here, out of scope); it is proof the fetch method used for this snapshot did not execute any. ## 2. Isolation lever: `PI_CODING_AGENT_DIR` `extracted/package/dist/config.js:406-427`: ```js export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; export function getAgentDir() { const envDir = process.env[ENV_AGENT_DIR]; if (envDir) { return expandTildePath(envDir); } return join(homedir(), CONFIG_DIR_NAME, "agent"); } ``` `APP_NAME` is `"pi"` (`dist/config.js:401`, `piConfigName || "pi"`), so `ENV_AGENT_DIR` resolves to the literal string `PI_CODING_AGENT_DIR`. `CONFIG_DIR_NAME` is `.pi` by default (`dist/config.js:403`). With the env var unset, the fallback is `homedir()` (Node's `os.homedir()`, itself `HOME`-driven on POSIX) joined with `.pi/agent` — so `HOME` also moves the default location, but `PI_CODING_AGENT_DIR` is the explicit, single-purpose override and does not disturb any other `HOME`-dependent behavior. Every agent-state path is derived from `getAgentDir()` and therefore moves together under one env var (`dist/config.js:433-461`): ```js export function getModelsPath() { return join(getAgentDir(), "models.json"); } export function getAuthPath() { return join(getAgentDir(), "auth.json"); } export function getSettingsPath() { return join(getAgentDir(), "settings.json"); } export function getToolsDir() { return join(getAgentDir(), "tools"); } export function getBinDir() { return join(getAgentDir(), "bin"); } export function getPromptsDir() { return join(getAgentDir(), "prompts"); } export function getSessionsDir() { return join(getAgentDir(), "sessions"); } export function getDebugLogPath() { return join(getAgentDir(), `${APP_NAME}-debug.log`); } ``` `getPackageDir()` (`dist/config.js:311-313`) is a **separate**, unrelated env var (`PI_PACKAGE_DIR`) that locates the *installed package itself* (themes, docs, README, CHANGELOG bundled with the npm package) — it has no bearing on agent state and is not part of the isolation boundary being evaluated. **No dedicated CLI flag exists for auth path override.** `auth-command.js` (`dist/cli/auth-command.js`) defines only `--provider`, `--model`, `--json`, `--credentials`, `--no-refresh` (check) and `--min-expiry` (print-bearer-token) — see `AUTH_COMMAND_USAGE` at `dist/cli/auth-command.js:4-8` and the flag parser at `dist/cli/auth-command.js:27-75`. There is no `--auth-path`, `--config-dir`, or similar. `PI_CODING_AGENT_DIR` (optionally combined with a scratch `HOME`) is the only supported isolation mechanism. ## 3. CLI dispatch order — auth commands run before cwd/settings/extension/session/trust setup Confirmed directly from the bundled entry point, not inferred from naming. `dist/bundle/cli.js` is the npm `bin.pi` shim; it calls `main(process.argv.slice(2))` from the single bundled chunk `dist/bundle/chunks/chunk-JVUZSMYM.js`. That chunk is a single minified line (4,006,956 bytes); functions were located by string search (`indexOf`) rather than line numbers — anchors below are function names, which are preserved (not mangled) in this build. `async function main(args, options)` (found at byte offset 3990752), head of body, statement order preserved verbatim except formatting: ```js async function main(args,options){ resetTimings(); let extensionFactories=[...builtInExtensions,...options?.extensionFactories??[]], offlineMode=args.includes("--offline")||isTruthyEnvFlag2(process.env.PI_OFFLINE); if(offlineMode&&(process.env.PI_OFFLINE="1",process.env.PI_SKIP_VERSION_CHECK="1"), await runAuthCommand(args)) return; process.platform==="win32"&&cleanupWindowsSelfUpdateQuarantine(getPackageDir()), cleanupManagedInstall(); let cwd=process.cwd(), agentDir=getAgentDir(), bootstrapSettingsManager=SettingsManager.create(cwd,agentDir,{projectTrusted:!1}); if(applyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy), configureHttpDispatcher(), await handlePackageCommand(args,{extensionFactories})){ ... } if(await handleConfigCommand(args,{extensionFactories}))return; let parsed=parseArgs(args); ... ``` This settles the brief's explicit question with evidence, not an inference from command naming: `await runAuthCommand(args)` is the **first** awaited call in `main()`, and a truthy return **exits `main()` before**: - `process.cwd()` is read (`cwd` is assigned only after the auth branch), - `SettingsManager.create(cwd, agentDir, {projectTrusted:false})` runs (project trust / project settings discovery), - `handlePackageCommand`/`handleConfigCommand` run, - `parseArgs(args)` runs (the general CLI arg parser, session/model/tool flags), - any session manager, migrations, or interactive-mode setup is reached. The only thing evaluated *before* `runAuthCommand` is the `extensionFactories` array assembly: `builtInExtensions` is a static in-memory literal — `builtInExtensions=[{name:"llama.cpp",factory:llamaExtension,hidden:!0}]` — with no disk I/O at construction, and `options?.extensionFactories` are factories the *host process* passes in (not loaded from a project directory or config file). So no project-scoped extension or config discovery occurs ahead of auth dispatch either. `runAuthCommand(args)` itself (byte offset 3981368) calls `getAgentDir()` indirectly (via `AuthStorage.create()` / `ReadOnlyAuthStorage` / `ModelRuntime.create()` default paths — section 4) but never reads `cwd`, never touches `SettingsManager`, and never resolves project-level extensions. ## 4. Auth storage isolation specifics `dist/core/auth-storage.js`: - `AuthStorage.create(authPath = join(getAgentDir(), "auth.json"))` (line 281) — used by `check` unless `--no-refresh` is given. - `ReadOnlyAuthStorage` constructor: `constructor(authPath = join(getAgentDir(), "auth.json"))` (line 161) — used by `check --no-refresh`. - `FileAuthStorageBackend` (line 17) is the backing store for `AuthStorage`; its lock file is acquired directly on the auth path via `proper-lockfile`: `lockfile.lockSync(path, {realpath:false})` (line 39) and `lockfile.lock(this.authPath, ...)` (line 85). `proper-lockfile` places its lock next to the target file, so the lock stays inside `getAgentDir()` — no isolation-breaking temp path outside the agent dir. - Neither constructor, nor any auth-path CLI flag, exists to point `check`/`print-*` at a path outside `getAgentDir()` independent of the env var — confirming `PI_CODING_AGENT_DIR` (optionally with a scratch `HOME`) is the complete and only isolation lever for credentials. `print-api-key`/`print-bearer-token` (the `command.kind!=="check"` branch inside `runAuthCommand`, same byte-offset function) call `ModelRuntime.create({allowModelNetwork:false, signal})` with no explicit `credentials`/`authPath` option, so `ModelRuntime.create` (`dist/core/model-runtime.js:74`) falls back to its own default: `DefaultAuthStorage.create(options.authPath)` with `options.authPath` undefined — which resolves to the same `getAgentDir()`-derived `auth.json` path. Credential *file location* is identical across all three subcommands; the models-catalog behavior differs (section 5), and so does credential-refresh network exposure across `check`/`check --no-refresh`/`print-*` (section 5a). ## 5. `check` vs `print-*`: models.json / models-store.json asymmetry, and model-catalog network scope (corrected) `dist/cli/auth-check.js:42-49`: ```js export async function createAuthCheckModelRuntime(credentials) { return ModelRuntime.create({ credentials, modelsStore: new InMemoryCodingAgentModelsStore(), allowModelNetwork: false, refreshOnCreate: false, }); } ``` `check` passes an explicit in-memory models store and disables `refreshOnCreate` — it never reads `models.json`/`models-store.json` from disk and never runs `runtime.refresh()` at construction. `print-api-key`/`print-bearer-token`, by contrast, call `ModelRuntime.create({allowModelNetwork:false, signal})` with no `modelsStore`/`modelsPath`/`refreshOnCreate` override. `ModelRuntime.create` defaults (`dist/core/model-runtime.js:74-108`): ```js static async create(options = {}) { const credentials = new RuntimeCredentials(options.credentials ?? DefaultAuthStorage.create(options.authPath)); const modelsPath = options.modelsPath === null ? undefined : (options.modelsPath ?? join(getAgentDir(), "models.json")); const config = await ModelConfig.load(modelsPath); const modelsStore = options.modelsStore ?? (modelsPath ? new FileModelsStore(options.modelsStorePath ?? join(dirname(modelsPath), "models-store.json")) : new InMemoryCodingAgentModelsStore()); ... const refreshFromNetwork = runtime.modelNetworkEnabled && options.allowModelNetwork === true; ... if (options.refreshOnCreate !== false) { await runtime.refresh({ allowNetwork: refreshFromNetwork, signal }); } ... } ``` So `print-*` **does** read `models.json` and `models-store.json` from `getAgentDir()` (both env-overridable together with everything else), and **does** call `runtime.refresh()` at construction (since `refreshOnCreate` is not passed as `false` here) — this is a real behavioral difference from `check`. `allowModelNetwork` is fixed to `false` for both `check`'s in-memory runtime and `print-*`'s runtime, so `refreshFromNetwork = runtime.modelNetworkEnabled && options.allowModelNetwork === true` evaluates to `false` regardless of `modelNetworkEnabled`. `refresh()` (`dist/core/model-runtime.js:504-517`) forwards this as `allowNetwork: options.allowNetwork ?? this.modelNetworkEnabled` into the underlying `this.models.refresh(refreshOptions)` (the `pi-ai` models store) — **this specific call path** performs no network-based **model-catalog** refresh for any of the three auth subcommands. The only disk reads triggered by it are local: `ModelConfig.load(modelsPath)` (`models.json`) and whatever `FileModelsStore` does with `models-store.json`, both under `getAgentDir()`. **Practical isolation consequence (catalog only):** an isolated `PI_CODING_AGENT_DIR` needs, at minimum, a valid (possibly empty-provider) `models.json` to exist or be absent-tolerant for `print-*` to construct cleanly, since it does load that file; `check` has no such dependency. **This "no network" finding is scoped to model-catalog refresh only and does not extend to OAuth credential refresh — see section 5a, added on review correction.** The original text here claimed "no live network call occurs under an isolated, offline-style setup" without that scope qualifier; that unqualified claim was incorrect and is retracted in favor of section 5a. ### 5a. OAuth credential refresh is a *separate*, unconditional network path (correction — traced 2026-09-10) `ModelRuntime.getAuth()`/`checkAuth()` (`dist/core/model-runtime.js:280,339`) delegate to `this.models.getAuth`/`checkAuth` — methods on the `pi-ai`-level models store, implemented in `dist/bundle/chunks/chunk-IDDQWTHI.js` (same snapshot, `extracted/package/dist/bundle/chunks/chunk-IDDQWTHI.js`), **not** in `dist/core/model-runtime.js`. Neither `allowModelNetwork` nor `allowNetwork` is threaded into this call at all — `getAuth` (offset 15594) calls `resolveProviderAuth(provider, this.credentials, this.authContext, {...overrides, signal})` with no network flag among `overrides`. `resolveProviderAuth` → `resolveStoredOAuth` (offset 4923 in that chunk) is where an OAuth credential's freshness is enforced, **unconditionally, with no network gate anywhere in the function**: ```js var DEFAULT_OAUTH_MINIMUM_VALIDITY_MS=300*1e3, DEFAULT_OAUTH_REFRESH_TIMEOUT_MS=15e3; async function resolveStoredOAuth(credentials,providerId,oauth,stored,signal,minOAuthValidityMs){ let minimumValidityMs=Math.max(DEFAULT_OAUTH_MINIMUM_VALIDITY_MS,minOAuthValidityMs??0), expiresSoon=credential2=>Date.now()+minimumValidityMs>=credential2.expires, credential=stored; if(expiresSoon(credential)){ let post; try{ post=await credentials.modify(providerId,async current=>{ if(current?.type==="oauth"&&expiresSoon(current)) try{ let refreshSignal=AbortSignal.any([signal,AbortSignal.timeout(DEFAULT_OAUTH_REFRESH_TIMEOUT_MS)]); return await oauth.refresh(current,refreshSignal) }catch(error){throw new ModelsError("oauth",`OAuth refresh failed for ${providerId}`,{cause:error})} },{signal}) }catch(error){ ... } ... } ... } ``` If a stored OAuth credential's `expires` timestamp is within `DEFAULT_OAUTH_MINIMUM_VALIDITY_MS` (300,000 ms / 5 minutes) of `Date.now()` — including already expired — this path calls `oauth.refresh(current, refreshSignal)` **regardless of `allowModelNetwork`/`allowNetwork`, which this function never receives**. `oauth.refresh` is the per-provider implementation; for Anthropic it is `refreshAnthropicToken` posting to `TOKEN_URL="https://platform.claude.com/v1/oauth/token"` (`dist/bundle/chunks/anthropic.js`, confirmed present in this snapshot with that exact literal), and equivalent live token endpoints exist for other bundled providers (`openai-codex.js` → `auth.openai.com/oauth/token`, `openrouter.js` → `openrouter.ai/api/v1/auth/keys`, `xai.js` → `auth.x.ai/oauth2/token`). This is a real outbound HTTPS call, not a local operation. **Which subcommands are exposed, and the one mitigation that exists:** - `check` (default, no `--no-refresh`): uses `AuthStorage.create()` (mutable) → `credentials.modify()` succeeds → `oauth.refresh()` runs if a stored OAuth credential is near/at expiry. - `check --no-refresh`: uses `ReadOnlyAuthStorage`, whose `modify()` **throws immediately** (`dist/core/auth-storage.js:227`, `"Read-only credential storage cannot modify auth.json"`) *before* `oauth.refresh()` can be reached. The thrown error propagates out of `resolveStoredOAuth`/`getAuth` and is caught by `checkProviderAuth`'s try/catch (`dist/cli/auth-check.js:32-34`), which reports `{status:"invalid", reason:"invalid_state"}` instead. **This is the only one of the three subcommands with a supported way to guarantee no OAuth-refresh network attempt.** - `print-api-key` / `print-bearer-token`: always construct `ModelRuntime.create({allowModelNetwork:false, signal})` with no `credentials` override, which resolves to `DefaultAuthStorage.create()` — the same **mutable** `AuthStorage` as default `check`. There is **no `--no-refresh` equivalent flag** for either print command (`AUTH_COMMAND_USAGE`/flag parser, `dist/cli/auth-command.js:4-8,27-75` restrict `--no-refresh` to `kind==="check"`). So both print commands will attempt a live OAuth-refresh network call whenever the isolated `auth.json` holds a stored OAuth credential within 5 minutes of expiry (or already expired), with no way to suppress it via flags. **Bounded conclusion:** for an *empty* isolated `auth.json` (or one containing only `api_key`-type credentials, which never enter `resolveStoredOAuth` at all — `readCredential` in `resolveProviderAuthWithSignal` branches on `stored.type==="oauth"` before this path is reached), none of the three subcommands makes any network call, matching the original (uncorrected) intent. The risk is specific and load-bearing: **never place a real, or a synthetic near-expiry, OAuth-type credential in an isolated `PI_CODING_AGENT_DIR` used for `print-api-key`/`print-bearer-token` testing, or in `check` without `--no-refresh`** — doing so can trigger a genuine outbound request to the provider's live OAuth token endpoint, independent of `allowModelNetwork`. No such credential or request was created, read, or attempted in this investigation; this is a static-source-derived bound, not an observed network call. ## 6. `models.json` custom-provider schema (Darkwing addendum) `docs/models.md` (top-level `docs/` in the extracted 0.85.1 tarball, included in the shared snapshot) documents the format; the authoritative schema is `dist/core/model-config.js:139-187` (TypeBox), reproduced in relevant part: ```js const ModelDefinitionSchema = Type.Object({ id: Type.String({ minLength: 1 }), name: Type.Optional(...), api: Type.Optional(...), baseUrl: Type.Optional(...), reasoning: Type.Optional(...), thinkingLevelMap: Type.Optional(...), input: Type.Optional(...), cost: Type.Optional(...), contextWindow: Type.Optional(...), maxTokens: Type.Optional(...), samplingParams: Type.Optional(...), headers: Type.Optional(...), compat: Type.Optional(...), }); const ProviderConfigSchema = Type.Object({ name: Type.Optional(...), baseUrl: Type.Optional(Type.String({minLength:1})), apiKey: Type.Optional(Type.String({minLength:1})), api: Type.Optional(Type.String({minLength:1})), oauth: Type.Optional(Type.Literal("radius")), headers: Type.Optional(...), compat: Type.Optional(...), authHeader: Type.Optional(...), models: Type.Optional(Type.Array(ModelDefinitionSchema)), modelOverrides: Type.Optional(...), }); const ModelsConfigSchema = Type.Object({ providers: Type.Record(Type.String(), ProviderConfigSchema), }); ``` This confirms Darkwing's proposed minimal shape is schema-valid against 0.85.1's actual `models.json` validator: ```json { "providers": { "": { "api": "...", "baseUrl": "...", "models": [{ "id": "..." }], "apiKey": "... (optional)" } } } ``` `models.id` is the only field in the entire schema with no `Type.Optional` wrapper anywhere in the object graph — every other field at every level is optional. `docs/models.md`'s own "Minimal Example" uses exactly this shape (an Ollama provider with `api`, `baseUrl`, `apiKey`, and a `models` array of bare `{id}` objects), matching the schema reading. `apiKey` is confirmed optional both in the schema and in `docs/models.md`'s notes on keyless local servers. ## 7. Non-secret output and exit codes Source: `runAuthCommand` (byte offset 3981368) and `resolveCredentialForPrint` (found by name in the same chunk), plus `dist/cli/auth-command.js` / `dist/cli/auth-check.js`. **Parse-level errors (any subcommand, before dispatch):** - Unknown `auth` subcommand or unparseable args → `console.error` in red (`Error: `), `process.exitCode = 1`. - Unknown flag → `console.error` (`Unknown option -- for "".`) + a dim usage hint, `process.exitCode = 1`. - `--help`/`-h`/bare `auth` → `printAuthCommandHelp()` prints the usage block to stdout via `console.log`, no exit code forced (defaults to 0). **`print-api-key` / `print-bearer-token`:** - Success: the resolved credential/token string is written to stdout via `process.stdout.write(`${credential}\n`)`. No JSON mode, no structured status — bare secret value only. `process.exitCode` is left at its default (0) on success. - Failure (`AuthCommandError` or unexpected throw, e.g. unknown provider, no credential configured, model not found): `console.error` in red (`Error: `), `process.exitCode = 1` (kind is not `"check"`, so the kind-dependent exit branch sets `1`). **`check`:** - `result.status` is one of `"ready" | "not_ready" | "invalid"` (`checkProviderAuth`, `dist/cli/auth-check.js:5-35`), with a `reason` code among `invalid_state | provider_not_found | credentials_not_configured | credential_not_available` when not ready. - Default (non-`--json`) output: `credential ?? result.status` written to stdout (i.e. the bare status word unless `--credentials` requested and status is `ready`, in which case the raw credential is printed instead — operators should not pass `--credentials` in a context where stdout is logged/shared if avoiding secret material matters). - `--json` output: `JSON.stringify({...result, ...(credential ? {credentials: credential} : {})})` — i.e. `{"status":"ready","provider":"...","authType":"..."}` (plus a `credentials` field only when `--credentials` was also passed). - Exit codes: `result.status==="ready" ? 0 : result.status==="not_ready" ? 1 : 2`. - On a thrown error during `check` (e.g. missing `--provider`/`--model`): `console.error` (`Error: `), `process.exitCode = 2` (kind is `"check"`). No path observed prints raw OAuth `refresh` tokens or API keys unless the operator explicitly passes `--credentials` (`check`) or invokes `print-api-key`/`print-bearer-token` directly, which is their documented purpose. This finding itself contains no credential material — all values above are format descriptions only. ## 8. Direct answers to the brief - **Exact supported environment/flags for isolation:** set `PI_CODING_AGENT_DIR` to a dedicated empty directory (this alone isolates `auth.json`, `models.json`, `models-store.json`, `settings.json`, sessions, tools, bin, prompts, and the debug log). Optionally also set a scratch `HOME` as defense-in-depth, since it is the fallback base if `PI_CODING_AGENT_DIR` were ever unset by mistake, but it is not required given `PI_CODING_AGENT_DIR` takes precedence unconditionally. No CLI flag exists for this; it is env-only. - **Working directory relevance:** none, for auth commands specifically — proven in section 3 (`cwd` is not read until after `runAuthCommand` returns). - **Does auth-check load extensions/project packages/settings before handling the command?** No — proven in section 3 by direct reading of `main()`'s statement order, not inferred from naming. - **Is `PI_CODING_AGENT_DIR` (or another override) supported?** Yes, confirmed at `dist/config.js:406-427`; it is the sole override and controls the entire agent directory as one unit. - **Non-secret output and exit codes:** enumerated in section 7. - **Network scope (corrected):** `allowModelNetwork:false` blocks only the model-catalog refresh path (section 5); it does not gate OAuth credential refresh, which is a separate, unconditional path that can make a live call to the provider's OAuth token endpoint if a stored OAuth credential is near/at expiry (section 5a). ## 9. Blocker determination (corrected) **No blocker identified for the isolation mechanism itself**, with one load-bearing constraint on credential content that was missing from the original version of this section. `PI_CODING_AGENT_DIR` fully isolates the auth/config directory: - Credential resolution (`auth.json`) is isolated identically across all three subcommands (section 4). - No project/cwd/extension/settings discovery occurs before or during auth-command handling (section 3). - Model-catalog refresh performs no network call under `allowModelNetwork:false`, which is hard-coded for all three subcommands, not operator-controlled (section 5). - The one residual gap for `print-*` is that it also touches `models.json`/`models-store.json` under the same isolated directory (section 5) — this is a same-directory read, not an isolation leak, but callers should ensure the isolated `PI_CODING_AGENT_DIR` either omits `models.json` (tolerated — `ModelConfig.load` handles a missing file) or contains only the intended synthetic provider (section 6 shape) if `print-*` isolation tests need deterministic provider resolution. - **Corrected/added:** credential *content* placed in the isolated `auth.json` is load-bearing for network isolation, independent of `PI_CODING_AGENT_DIR`. An empty `auth.json`, or one containing only `api_key`-type entries, triggers no network call from any of the three subcommands. A stored `oauth`-type credential within 5 minutes of its `expires` timestamp (or already expired) will trigger a live refresh request to the provider's real OAuth token endpoint for default `check` and for both `print-*` commands (no flag suppresses this for `print-*`; `check --no-refresh` is the one subcommand/flag combination that provably cannot reach the network for this reason — section 5a). This is a constraint on test fixture design, not a defect in `PI_CODING_AGENT_DIR` isolation, but it must be honored for any isolation test that populates `auth.json` with OAuth-shaped data. ## 10. Shared snapshot (corrected — concrete path) `/tmp/m20-pi0851-isolation-20260910T222007Z/` (verified present on this host at the time of this correction; coordinator/reviewer-accessible for inspection). Contents: tarball, `pack.json`/`pack-ignore-scripts.json` (npm pack manifests), `registry-dist.json` (npm view dist metadata), `tarball.sha256`, and `extracted/package/` (full public tarball contents: `dist/`, `docs/` including `docs/models.md`, `CHANGELOG.md`, `README.md`, `package.json`). See `SNAPSHOT-INDEX.md` inside the snapshot for a file-by-file map to the citations above. ## 11. What this investigation did not do - Did not run `pi` (any subcommand), install the package, or use real or fake credentials. - Did not audit `extracted/package/package.json`'s `scripts` block for the presence/absence of lifecycle scripts (noted as out of scope in section 1's caveat). - Did not trace `FileModelsStore`/`ModelConfig.load`'s exact disk-miss behavior line-by-line beyond confirming it is called and is local (no network) under `allowModelNetwork:false` for the model-catalog path specifically (section 5). - Did not verify behavior on Windows (`getAgentDir`'s `expandTildePath`/ path-join behavior assumed POSIX-equivalent per this host). - **Corrected:** the package pin comparison this bullet originally described is moot — `package.json:8` now pins `0.85.1`, the same version investigated throughout this file (see section 0(1)). The pin was `0.84.4` when read during the prior PI-REFRESH-ROCKO-1 task and was moved to `0.85.1` by commit `557aba0f` (#1499) before this task ran. There is no version delta left to compare for the auth/isolation surface; PI-REFRESH-ROCKO-1's changelog analysis (0.84.4→0.85.1, no auth-related changes) remains the relevant prior evidence for anything that predates the pin bump. - Did not run any command, real or synthetic, that would populate `auth.json` with an OAuth-type credential to observe the refresh path in section 5a directly — that section is derived entirely from static reading of `chunk-IDDQWTHI.js`, per the brief's "no CLI/network credential experiment authorized" constraint from the review.