600 lines
30 KiB
Markdown
600 lines
30 KiB
Markdown
# 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`:
|
|
|
|
```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": {
|
|
"<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 `auth` subcommand or unparseable args → `console.error` in 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`/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: <message>`), `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: <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_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.
|