30 KiB
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/[email protected] (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-<timestamp>/. 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/[email protected] --json |
4cd00f653c3dabeb193b46f511044e7fbfe0f947 |
| 2 (addendum: lifecycle scripts disabled) | npm pack @earendil-works/[email protected] --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/[email protected] dist --json), whose shasum/integrity match the local re-hash.
Note on lifecycle scripts: npm pack <spec> 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:
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):
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:
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 (cwdis assigned only after the auth branch),SettingsManager.create(cwd, agentDir, {projectTrusted:false})runs (project trust / project settings discovery),handlePackageCommand/handleConfigCommandrun,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 bycheckunless--no-refreshis given.ReadOnlyAuthStorageconstructor:constructor(authPath = join(getAgentDir(), "auth.json"))(line 161) — used bycheck --no-refresh.FileAuthStorageBackend(line 17) is the backing store forAuthStorage; its lock file is acquired directly on the auth path viaproper-lockfile:lockfile.lockSync(path, {realpath:false})(line 39) andlockfile.lock(this.authPath, ...)(line 85).proper-lockfileplaces its lock next to the target file, so the lock stays insidegetAgentDir()— 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 outsidegetAgentDir()independent of the env var — confirmingPI_CODING_AGENT_DIR(optionally with a scratchHOME) 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:
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):
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:
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): usesAuthStorage.create()(mutable) →credentials.modify()succeeds →oauth.refresh()runs if a stored OAuth credential is near/at expiry.check --no-refresh: usesReadOnlyAuthStorage, whosemodify()throws immediately (dist/core/auth-storage.js:227,"Read-only credential storage cannot modify auth.json") beforeoauth.refresh()can be reached. The thrown error propagates out ofresolveStoredOAuth/getAuthand is caught bycheckProviderAuth'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 constructModelRuntime.create({allowModelNetwork:false, signal})with nocredentialsoverride, which resolves toDefaultAuthStorage.create()— the same mutableAuthStorageas defaultcheck. There is no--no-refreshequivalent flag for either print command (AUTH_COMMAND_USAGE/flag parser,dist/cli/auth-command.js:4-8,27-75restrict--no-refreshtokind==="check"). So both print commands will attempt a live OAuth-refresh network call whenever the isolatedauth.jsonholds 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:
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:
{
"providers": {
"<id>": {
"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
authsubcommand or unparseable args →console.errorin red (Error: <message>),process.exitCode = 1. - Unknown flag →
console.error(Unknown option --<flag> for "<command name>".) + a dim usage hint,process.exitCode = 1. --help/-h/bareauth→printAuthCommandHelp()prints the usage block to stdout viaconsole.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.exitCodeis left at its default (0) on success. - Failure (
AuthCommandErroror unexpected throw, e.g. unknown provider, no credential configured, model not found):console.errorin red (Error: <message>),process.exitCode = 1(kind is not"check", so the kind-dependent exit branch sets1).
check:
result.statusis one of"ready" | "not_ready" | "invalid"(checkProviderAuth,dist/cli/auth-check.js:5-35), with areasoncode amonginvalid_state | provider_not_found | credentials_not_configured | credential_not_availablewhen not ready.- Default (non-
--json) output:credential ?? result.statuswritten to stdout (i.e. the bare status word unless--credentialsrequested and status isready, in which case the raw credential is printed instead — operators should not pass--credentialsin a context where stdout is logged/shared if avoiding secret material matters). --jsonoutput:JSON.stringify({...result, ...(credential ? {credentials: credential} : {})})— i.e.{"status":"ready","provider":"...","authType":"..."}(plus acredentialsfield only when--credentialswas 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: <message>),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_DIRto a dedicated empty directory (this alone isolatesauth.json,models.json,models-store.json,settings.json, sessions, tools, bin, prompts, and the debug log). Optionally also set a scratchHOMEas defense-in-depth, since it is the fallback base ifPI_CODING_AGENT_DIRwere ever unset by mistake, but it is not required givenPI_CODING_AGENT_DIRtakes precedence unconditionally. No CLI flag exists for this; it is env-only. - Working directory relevance: none, for auth commands specifically —
proven in section 3 (
cwdis not read until afterrunAuthCommandreturns). - 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 atdist/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:falseblocks 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 touchesmodels.json/models-store.jsonunder the same isolated directory (section 5) — this is a same-directory read, not an isolation leak, but callers should ensure the isolatedPI_CODING_AGENT_DIReither omitsmodels.json(tolerated —ModelConfig.loadhandles a missing file) or contains only the intended synthetic provider (section 6 shape) ifprint-*isolation tests need deterministic provider resolution. - Corrected/added: credential content placed in the isolated
auth.jsonis load-bearing for network isolation, independent ofPI_CODING_AGENT_DIR. An emptyauth.json, or one containing onlyapi_key-type entries, triggers no network call from any of the three subcommands. A storedoauth-type credential within 5 minutes of itsexpirestimestamp (or already expired) will trigger a live refresh request to the provider's real OAuth token endpoint for defaultcheckand for bothprint-*commands (no flag suppresses this forprint-*;check --no-refreshis 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 inPI_CODING_AGENT_DIRisolation, but it must be honored for any isolation test that populatesauth.jsonwith 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'sscriptsblock 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) underallowModelNetwork:falsefor the model-catalog path specifically (section 5). - Did not verify behavior on Windows (
getAgentDir'sexpandTildePath/ path-join behavior assumed POSIX-equivalent per this host). - Corrected: the package pin comparison this bullet originally
described is moot —
package.json:8now pins0.85.1, the same version investigated throughout this file (see section 0(1)). The pin was0.84.4when read during the prior PI-REFRESH-ROCKO-1 task and was moved to0.85.1by commit557aba0f(#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.jsonwith an OAuth-type credential to observe the refresh path in section 5a directly — that section is derived entirely from static reading ofchunk-IDDQWTHI.js, per the brief's "no CLI/network credential experiment authorized" constraint from the review.