Add fixture-only execution materialization and refresh (#1500)

This commit is contained in:
2026-09-10 20:20:44 -05:00
parent 27e4873acc
commit 3daee5ad89
10 changed files with 1163 additions and 5 deletions
@@ -1,7 +1,7 @@
# Increment 2 charter (#1500)
Status: draft, independent review requested before owner implementation approval.
Baseline b3fa221060abb820cd809f15c3a54ba84518b7a1. Owner requested faster continuation of charter drafting. Author/coordinator Darkwing; independent reviewer Filbert; Jason approves implementation. No source changes are authorized by this document yet.
Status: owner APPROVED the fixture-only implementation after prerequisite correction was independently verified and published. Implementation baseline: 27e4873accd4179acad61f1c4673fc65a4fa5f8b. Original draft and prerequisite context follow.
Baseline b3fa221060abb820cd809f15c3a54ba84518b7a1. Owner requested faster continuation of charter drafting. Author/coordinator Darkwing; independent reviewer Filbert; Jason approves implementation. The subsequent owner approval authorizes only the fixture-only stages and allowed paths below; no live activation is included.
## Outcome and boundary
@@ -0,0 +1,599 @@
# 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.
+17 -3
View File
@@ -1,6 +1,6 @@
# Registry metadata validation
Read-only metadata CLI. No materialization, live credential handling or refresh service.
Read-only metadata CLI plus fixture-only resolution, generation and refresh APIs. No live credential handling or refresh service.
```
node packages/mosaic/src/cli/main.mjs validate --registry-root /absolute/fixture-root
@@ -12,7 +12,7 @@ node --test packages/mosaic/tests/
The reader currently supports Linux with procfs and descriptor-relative paths. Other platforms refuse explicitly rather than falling back to unsafe traversal. Root and all traversed registry directories must belong to the invoking user and have mode 0700. Metadata files must be regular, owned by that user and mode 0600. Symlinks are rejected in the root path, directories and metadata files. Required directories are auth/providers, auth/accounts, auth/settings and harnesses. A missing or empty registry is invalid, not an empty successful listing.
Metadata is limited to 1 MiB per file. Directory handles anchor child access while paths are being checked, preventing a renamed ancestor from redirecting later opens. Files are opened without following symlinks and checked against their prior inode/device and permissions. This is not a claim of transactionally consistent multi-record snapshots under concurrent writers; registry mutation and materialization are outside this package slice.
Metadata is limited to 1 MiB per file. Directory handles anchor child access while paths are being checked, preventing a renamed ancestor from redirecting later opens. Files are opened without following symlinks and checked against their prior inode/device and permissions. This is not a claim of transactionally consistent multi-record snapshots under concurrent writers; registry mutation and production materialization remain outside this package slice.
Account metadata lives at auth/accounts/<provider>/<account>/account.json. The credential.json sibling is never opened or inspected. No real credentials are needed for tests. Public Git fixtures do not preserve private modes: the tests copy them into temporary directories and set 0700/0600 before invoking the reader. Do not run the CLI against your real credential stores for a smoke test.
@@ -22,4 +22,18 @@ Record version fields are numeric 1. Unknown versions, top-level/nested unknown
Profiles may only default to an enrolled account. Account types must be supported by their providers, and references must resolve. Failed validation returns no partial entries or CLI listing. Diagnostics use fixed paths/codes, never input values, unknown keys or JSON parser excerpts. Invalid input exits 1; command usage errors exit 2.
The safety regression suite covers independently reproduced findings D1-D10 from issue #1500. Passing it establishes those tested properties, not a production security certification or authorization to build materialization on unreviewed code.
The safety regression suite covers independently reproduced findings D1-D10 from issue #1500. Passing it establishes those tested properties, not a production security certification.
## Fixture-only execution and refresh
`resolveFixtureExecution`, `createFixtureCredentialStore` and `createFixtureWorkspace` are exported from src/index.mjs. Supply a validated registry snapshot, explicit scope and marker-only synthetic credentials. Scope is fixture input, not proof of actual project membership. Fork pins and enrollment constrain selection; native model ceilings that cannot be enforced are refused.
`workspace.generate(registry, request, store, options)` creates distinct private execution generations in a dedicated /tmp root. It accepts only the branded in-memory store. No production backend or output directory can be injected. Call `workspace.close()` when done. Published files are never rewritten by this API. Exclusive claims survive failures and prohibit retrying the same execution ID.
Expired or near-expiry OAuth fixtures automatically run the fixed fake refresh program under the store transaction. Each invocation has isolated PI_CODING_AGENT_DIR, HOME and cwd, with no inherited environment. No real Pi executable, shell, print command or network client is used. Both credential fields rotate together; successful generation commits the in-memory draft. Child output is discarded, the child is killed on timeout, and cleanup waits for child closure. Output size, type, mode and fixture markers are validated before use.
Tests can supply `refresh: { mode, timeoutMs }`. Modes are rotate, unchanged, failure, timeout and malformed. Timeout is 1010000 ms, default 2000. Supplying refresh forces the fake check even for currently valid credentials. Generation fault hooks are after-auth, before-publish and after-publish. No arbitrary executable, environment or callback can be supplied.
Limits: store locks are in-process, not a production distributed lock. Credential-store commit and filesystem publication are not crash-atomic together. A post-publication failure records uncertainty, retains the generation and rolls back the in-memory draft; this simulator has no external token issuer to reconcile. A real refresh backend would need a separate reconciliation protocol before reuse. Filesystem ownership does not defend against arbitrary same-UID tampering or establish hardlink provenance.
Exact Pi 0.85.1 isolation is static-source evidence only. Its model-catalog network flag does NOT suppress OAuth refresh networking. Never substitute real Pi in these tests, even with expired synthetic OAuth tokens. Final whole-increment review is required before publication.
+96
View File
@@ -0,0 +1,96 @@
// Fixture-only execution resolution. Not a membership/registration authority.
import { validId, validateProvider, validateAccount, validateSettingsProfile,
validateHarnessManifest } from './records.mjs';
export class FixtureError extends Error {
constructor(code) { super(code); this.name = 'FixtureError'; this.code = code; }
}
export const refuse = code => { throw new FixtureError(code); };
const obj = v => v !== null && typeof v === 'object' && !Array.isArray(v);
export function exact(v, required, optional = []) {
if (!obj(v) || required.some(k => !Object.hasOwn(v, k)) ||
Object.keys(v).some(k => ![...required, ...optional].includes(k))) refuse('invalid-fixture-input');
}
function checkRegistry(result) {
if (!obj(result) || !Array.isArray(result.errors) || result.errors.length || !obj(result.entries)) refuse('invalid-registry');
const entries = result.entries;
for (const [name, validate] of [['providers', validateProvider], ['accounts', validateAccount],
['profiles', validateSettingsProfile], ['harnesses', validateHarnessManifest]]) {
if (!obj(entries[name])) refuse('invalid-registry');
for (const [key, record] of Object.entries(entries[name])) {
if (validate(record).length) refuse('invalid-registry');
if (key !== (name === 'accounts' ? `${record.provider}/${record.id}` : record.id)) refuse('invalid-registry');
}
}
return entries;
}
export function resolveFixtureExecution(result, request) {
// Clone immediately: later caller mutation cannot change this resolution.
let r, registry;
try { r = structuredClone(request); registry = structuredClone(result); }
catch { refuse('invalid-fixture-input'); }
const entries = checkRegistry(registry);
exact(r, ['fixtureOnly', 'agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId', 'profile'], ['accounts', 'fork']);
if (r.fixtureOnly !== true) refuse('fixture-only');
for (const key of ['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId', 'profile'])
if (!validId(r[key])) refuse('invalid-scope');
if (!Object.hasOwn(entries.profiles, r.profile)) refuse('missing-profile');
const profile = entries.profiles[r.profile];
const overrides = r.accounts ?? {};
if (!obj(overrides)) refuse('invalid-selection');
const providers = new Set([...(profile.providers ?? []), ...profile.allowedAccounts.map(ref => ref.split('/')[0])]);
for (const key of Object.keys(overrides)) if (!providers.has(key)) refuse('provider-not-enrolled');
if (Object.hasOwn(r, 'fork')) {
exact(r.fork, ['sourceSessionId', 'accounts']);
if (!validId(r.fork.sourceSessionId) || r.fork.sourceSessionId === r.sessionId || !obj(r.fork.accounts)) refuse('invalid-fork-pin');
for (const id of Object.keys(r.fork.accounts)) if (!providers.has(id)) refuse('revoked-fork-pin');
}
const accounts = Object.create(null), models = { providers: Object.create(null) }, types = Object.create(null);
for (const id of [...providers].sort()) {
if (!Object.hasOwn(entries.providers, id)) refuse('missing-provider');
const provider = entries.providers[id], cfg = provider.harnesses.pi;
if (!cfg || !entries.harnesses.pi) refuse('unsupported-harness');
let ref;
if (r.fork) {
ref = r.fork.accounts[id];
if (Object.hasOwn(overrides, id) && overrides[id] !== ref) refuse('fork-account-change');
} else ref = Object.hasOwn(overrides, id) ? overrides[id] : profile.defaultAccounts?.[id];
if (ref === undefined) {
if (!provider.credentialTypes.includes('none')) refuse(r.fork ? 'missing-fork-pin' : 'missing-account-selection');
} else {
if (typeof ref !== 'string' || !profile.allowedAccounts.includes(ref) || ref.split('/')[0] !== id)
refuse('account-not-enrolled');
if (!Object.hasOwn(entries.accounts, ref)) refuse('missing-account');
const account = entries.accounts[ref];
if (account.provider !== id || !provider.credentialTypes.includes(account.type)) refuse('credential-type-not-supported');
accounts[id] = ref; types[id] = account.type;
}
if (provider.kind === 'native') {
// No aliasing to another provider's credential slot.
if (cfg.providerId !== id) refuse('provider-alias-unsupported');
if (profile.models?.[id]) refuse('native-model-enforcement-unavailable');
} else {
if (!['openai-completions', 'openai-responses', 'anthropic-messages', 'google-generative-ai'].includes(cfg.api)) refuse('unsupported-model-api');
const selected = profile.models?.[id] ?? cfg.models;
if (!selected.length || selected.some(m => !cfg.models.includes(m))) refuse('model-not-enrolled');
models.providers[id] = { api: cfg.api, baseUrl: cfg.baseUrl, models: selected.map(model => ({ id: model })) };
if (!ref || types[id] === 'none') models.providers[id].apiKey = 'FIXTURE_NONE';
}
}
const scope = Object.fromEntries(['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId'].map(k => [k, r[k]]));
return { fixtureOnly: true, scope, profile: r.profile, accounts, types, models,
fork: r.fork ? { sourceSessionId: r.fork.sourceSessionId, accounts: { ...accounts } } : null };
}
// Real keys/tokens are deliberately outside this slice's accepted input language.
const marker = v => typeof v === 'string' && /^FIXTURE_[A-Z0-9_-]{1,128}$/.test(v);
export function validateFixtureCredential(value, type) {
if (type === 'none') { if (value !== null) refuse('invalid-fixture-credential'); return null; }
if (type === 'api_key') {
exact(value, ['type', 'key']);
if (value.type !== type || !marker(value.key)) refuse('invalid-fixture-credential');
} else if (type === 'oauth') {
exact(value, ['type', 'access', 'refresh', 'expires']);
if (value.type !== type || !marker(value.access) || !marker(value.refresh) ||
!Number.isSafeInteger(value.expires) || value.expires < 0) refuse('invalid-fixture-credential');
} else refuse('unsupported-fixture-credential');
return structuredClone(value);
}
+31
View File
@@ -0,0 +1,31 @@
// In-memory synthetic store only; no filesystem or production backend.
import { refuse, validateFixtureCredential } from './execution.mjs';
const stores = new WeakSet();
export const isFixtureStore = store => stores.has(store);
export function createFixtureCredentialStore(initial) {
const records = new Map(), locks = new Set();
for (const [ref, value] of Object.entries(initial)) {
if (!/^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(ref)) refuse('invalid-account-ref');
records.set(ref, validateFixtureCredential(value, value?.type ?? 'none'));
}
const store = Object.freeze({
fixtureOnly: true,
async transaction(refs, callback) {
const keys = [...new Set(refs)].sort();
for (const key of keys) {
if (!records.has(key)) refuse('missing-fixture-credential');
if (locks.has(key)) refuse('credential-busy');
}
keys.forEach(key => locks.add(key));
try {
const draft = new Map(keys.map(key => [key, structuredClone(records.get(key))]));
const result = await callback(draft);
for (const key of keys) validateFixtureCredential(draft.get(key), records.get(key)?.type ?? 'none');
keys.forEach(key => records.set(key, structuredClone(draft.get(key))));
return result;
} finally { keys.forEach(key => locks.delete(key)); }
},
});
stores.add(store);
return store;
}
+3
View File
@@ -2,3 +2,6 @@ import { validateProvider, validateAccount, validateSettingsProfile, validateSea
import { loadRegistry } from "./registry.mjs";
export { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest, ValidationError, loadRegistry };
export { resolveFixtureExecution, FixtureError } from './execution.mjs';
export { createFixtureCredentialStore } from './fixture-store.mjs';
export { createFixtureWorkspace } from './materialize-fixture.mjs';
+127
View File
@@ -0,0 +1,127 @@
// Synthetic-only publication simulator. No caller-selected filesystem root,
// production credential adapter, real Pi runner, service or live activation.
import { mkdtemp, mkdir, open, rename, rm, lstat } from 'node:fs/promises';
import { constants } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { FixtureError, exact, refuse, resolveFixtureExecution, validateFixtureCredential } from './execution.mjs';
import { isFixtureStore } from './fixture-store.mjs';
import { refreshFixtureCredential, validateRefreshOptions } from './refresh-fixture.mjs';
const anchor = h => `/proc/self/fd/${h.fd}`;
async function openDir(path) { return open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); }
async function verifyJson(dir, name, value) {
const expected = Buffer.from(JSON.stringify(value, null, 2) + '\n');
const h = await open(`${anchor(dir)}/${name}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const s = await h.stat();
if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o600 || s.size !== expected.length) refuse('generation-validation-failed');
const actual = Buffer.alloc(expected.length + 1);
let size = 0;
while (size < actual.length) {
const { bytesRead } = await h.read(actual, size, actual.length - size, null);
if (!bytesRead) break;
size += bytesRead;
}
if (size !== expected.length || !actual.subarray(0, size).equals(expected)) refuse('generation-validation-failed');
} finally { await h.close(); }
}
async function writeJson(dir, name, value) {
const content = JSON.stringify(value, null, 2) + '\n';
if (Buffer.byteLength(content) > 1024 * 1024) refuse('generation-too-large');
const h = await open(`${anchor(dir)}/${name}`, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
try { await h.writeFile(content); await h.sync(); }
finally { await h.close(); }
}
export async function createFixtureWorkspace() {
if (process.platform !== 'linux') refuse('unsupported-platform');
const path = await mkdtemp('/tmp/mosaic-materialization-fixture-');
const root = await openDir(path), identity = await root.stat();
let closed = false, active = 0;
const handles = {};
try {
for (const name of ['claims', 'pending', 'generations']) {
await mkdir(`${anchor(root)}/${name}`, { mode: 0o700 });
handles[name] = await openDir(`${anchor(root)}/${name}`);
}
} catch (e) {
for (const h of Object.values(handles)) await h.close();
await root.close(); await rm(path, { recursive: true, force: true }); throw e;
}
return Object.freeze({
fixtureOnly: true, path,
async generate(registry, request, store, options = {}) {
if (closed) refuse('workspace-closed');
if (!isFixtureStore(store)) refuse('fixture-store-required');
exact(options, [], ['fault', 'refresh']);
if (options.fault !== undefined && !['after-auth', 'before-publish', 'after-publish'].includes(options.fault)) refuse('invalid-fixture-option');
options = { fault: options.fault, refresh: options.refresh === undefined ? undefined : validateRefreshOptions(options.refresh) };
const plan = resolveFixtureExecution(registry, request), id = plan.scope.executionId;
const pending = `${anchor(handles.pending)}/${id}`, claimPath = `${anchor(handles.claims)}/${id}`;
let claim, generation, claimed = false, published = false;
active++;
try {
try { await mkdir(claimPath, { mode: 0o700 }); claimed = true; }
catch { refuse('execution-already-claimed'); }
claim = await openDir(claimPath);
await writeJson(claim, 'started.json', { fixtureOnly: true, executionId: id, state: 'started' });
const result = await store.transaction(Object.values(plan.accounts), async draft => {
const auth = Object.create(null);
for (const [provider, ref] of Object.entries(plan.accounts)) {
let credential = validateFixtureCredential(draft.get(ref), plan.types[provider]);
if (credential !== null && (options.refresh !== undefined ||
(credential.type === 'oauth' && credential.expires <= Date.now() + 300000))) {
credential = await refreshFixtureCredential(provider, credential, options.refresh ?? {});
draft.set(ref, credential);
}
if (credential !== null) auth[provider] = credential;
}
await mkdir(pending, { mode: 0o700 }); generation = await openDir(pending);
await writeJson(generation, 'auth.json', auth);
if (options.fault === 'after-auth') refuse('injected-generation-failure');
await writeJson(generation, 'models.json', plan.models);
const manifest = { manifestVersion: 1, fixtureOnly: true, manifestId: randomUUID(),
scope: plan.scope, profile: plan.profile, accounts: plan.accounts, fork: plan.fork,
artifacts: ['auth.json', 'models.json'], state: 'ready' };
// Manifest contains no token, expiry or credential-content digest.
await writeJson(generation, 'manifest.json', manifest);
if (options.fault === 'before-publish') refuse('injected-generation-failure');
await verifyJson(generation, 'auth.json', auth);
await verifyJson(generation, 'models.json', plan.models);
await verifyJson(generation, 'manifest.json', manifest);
await generation.sync(); await generation.close(); generation = undefined;
const target = `${anchor(handles.generations)}/${id}`;
try { await lstat(target); refuse('generation-exists'); }
catch (e) { if (e.code !== 'ENOENT') throw e; }
// Retained exclusive claim serializes this execution ID, including
// failed attempts. Published generations are never replaced by API.
await rename(pending, target); published = true;
if (options.fault === 'after-publish') refuse('injected-generation-failure');
await handles.generations.sync();
await writeJson(claim, 'result.json', { fixtureOnly: true, state: 'published', manifestId: manifest.manifestId });
return { fixtureOnly: true, manifest, generationPath: `${path}/generations/${id}` };
});
return result;
} catch (e) {
if (generation) { await generation.close(); generation = undefined; }
if (claimed && !published) await rm(pending, { recursive: true, force: true });
const code = published ? 'publication-uncertain' : e instanceof FixtureError ? e.code : 'generation-failed';
if (claim) {
try { await writeJson(claim, 'failure.json', { fixtureOnly: true, state: published ? 'uncertain' : 'failed', code }); }
catch { throw new FixtureError('recording-failed'); }
}
throw new FixtureError(code);
} finally {
if (claim) await claim.close(); active--;
}
},
async close() {
if (closed) return;
if (active) refuse('workspace-busy');
closed = true;
for (const h of Object.values(handles)) await h.close();
await root.close();
const current = await lstat(path);
if (current.dev !== identity.dev || current.ino !== identity.ino || current.isSymbolicLink()) refuse('workspace-path-changed');
await rm(path, { recursive: true, force: true });
},
});
}
+80
View File
@@ -0,0 +1,80 @@
// Deliberately executes a fixed fake program, never Pi or caller-supplied code.
// PI_CODING_AGENT_DIR matches the statically verified 0.85.1 auth boundary.
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, writeFile, open, rm } from 'node:fs/promises';
import { constants } from 'node:fs';
import { validId } from './records.mjs';
import { exact, refuse, validateFixtureCredential } from './execution.mjs';
const fake = `
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const [command, subcommand, flag, provider] = process.argv.slice(1);
if (command !== 'auth' || subcommand !== 'check' || flag !== '--provider') process.exit(2);
if (Object.keys(process.env).some(k => !['HOME','PI_CODING_AGENT_DIR','FIXTURE_MODE'].includes(k))) process.exit(2);
const path = join(process.env.PI_CODING_AGENT_DIR, 'auth.json');
const mode = process.env.FIXTURE_MODE;
if (mode === 'timeout') await new Promise(() => setInterval(() => {}, 1000));
if (mode === 'failure') { process.stderr.write('FIXTURE_PRIVATE_DIAGNOSTIC'); process.exit(1); }
if (mode === 'malformed') { await writeFile(path, '{', { mode: 0o600 }); process.exit(0); }
const auth = JSON.parse(await readFile(path, 'utf8'));
const c = auth[provider];
if (mode === 'rotate' && c.type === 'oauth') {
c.access = 'FIXTURE_ROTATED_ACCESS'; c.refresh = 'FIXTURE_ROTATED_REFRESH';
c.expires = Date.now() + 3600000;
}
await writeFile(path, JSON.stringify(auth), { mode: 0o600 });
process.stdout.write('ready');
`;
export function validateRefreshOptions(options = {}) {
exact(options, [], ['mode', 'timeoutMs']);
const mode = options.mode ?? 'rotate', timeoutMs = options.timeoutMs ?? 2000;
if (!['rotate', 'unchanged', 'failure', 'timeout', 'malformed'].includes(mode) ||
!Number.isInteger(timeoutMs) || timeoutMs < 10 || timeoutMs > 10000) refuse('invalid-refresh-option');
return { mode, timeoutMs };
}
export async function refreshFixtureCredential(provider, credential, options = {}) {
if (!validId(provider)) refuse('invalid-provider');
const { mode, timeoutMs } = validateRefreshOptions(options);
const input = validateFixtureCredential(credential, credential?.type);
const root = await mkdtemp('/tmp/mosaic-refresh-fixture-');
try {
const agent = `${root}/agent`, home = `${root}/home`, cwd = `${root}/cwd`;
for (const dir of [agent, home, cwd]) await mkdir(dir, { mode: 0o700 });
const file = `${agent}/auth.json`;
await writeFile(file, JSON.stringify({ [provider]: input }), { mode: 0o600, flag: 'wx' });
const outcome = await new Promise(resolve => {
let timedOut = false, spawnFailed = false;
const child = spawn(process.execPath, ['--input-type=module', '-e', fake, 'auth', 'check', '--provider', provider], {
cwd, env: { HOME: home, PI_CODING_AGENT_DIR: agent, FIXTURE_MODE: mode },
// Child output is discarded, never buffered, parsed, logged or returned.
stdio: 'ignore', shell: false,
});
const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
child.on('error', () => { spawnFailed = true; });
child.on('close', (code, signal) => {
clearTimeout(timer); resolve({ code, signal, timedOut, spawnFailed });
});
});
if (outcome.timedOut) refuse('refresh-timeout');
if (outcome.spawnFailed || outcome.code !== 0 || outcome.signal) refuse('refresh-failed');
const h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
let result;
try {
const s = await h.stat();
if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o600 || s.size > 4096) refuse('invalid-refresh-output');
const buf = Buffer.alloc(4097); let size = 0;
while (size < buf.length) {
const { bytesRead } = await h.read(buf, size, buf.length - size, null);
if (!bytesRead) break;
size += bytesRead;
}
if (size > 4096) refuse('invalid-refresh-output');
try { result = JSON.parse(buf.subarray(0, size).toString('utf8')); }
catch { refuse('invalid-refresh-output'); }
} finally { await h.close(); }
exact(result, [provider]);
const output = validateFixtureCredential(result[provider], input.type);
if (output.type === 'oauth' && output.expires <= Date.now() + 300000) refuse('refresh-not-ready');
return output;
} finally { await rm(root, { recursive: true, force: true }); }
}
+184
View File
@@ -0,0 +1,184 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFile, readdir, stat, lstat, writeFile, mkdir, symlink } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { resolveFixtureExecution } from '../src/execution.mjs';
import { createFixtureCredentialStore } from '../src/fixture-store.mjs';
import { createFixtureWorkspace } from '../src/materialize-fixture.mjs';
const base = resolve(import.meta.dirname, 'fixtures/valid');
const json = name => JSON.parse(readFileSync(`${base}/${name}`, 'utf8'));
function registry() {
const account = json('auth/accounts/openai-codex/homelab-openai/account.json');
const profile = json('auth/settings/research-default.json');
profile.allowedAccounts.push('openai-codex/personal');
return { errors: [], entries: {
providers: { 'openai-codex': json('auth/providers/openai-codex.json'), 'ollama-remote': json('auth/providers/ollama-remote.json') },
profiles: { 'research-default': profile }, harnesses: { pi: json('harnesses/pi.json') }, selections: {},
accounts: { 'openai-codex/homelab-openai': account, 'openai-codex/personal': { ...account, id: 'personal', name: 'Personal' } },
} };
}
const credential = suffix => ({ type: 'oauth', access: `FIXTURE_ACCESS_${suffix}`, refresh: `FIXTURE_REFRESH_${suffix}`, expires: 9000000000000 });
const store = () => createFixtureCredentialStore({ 'openai-codex/homelab-openai': credential('A'), 'openai-codex/personal': credential('B') });
const request = (executionId = 'exec-a') => ({ fixtureOnly: true, agentId: 'rocko', projectId: 'project-a', workspaceId: 'workspace-a', sessionId: 'session-a', executionId, profile: 'research-default' });
const read = async p => JSON.parse(await readFile(p, 'utf8'));
async function workspace(t) { const w = await createFixtureWorkspace(); t.after(() => w.close()); return w; }
test('pure resolution selects current default or explicit enrolled account', () => {
const r = registry(), req = request();
const p = resolveFixtureExecution(r, req);
assert.equal(p.accounts['openai-codex'], 'openai-codex/homelab-openai');
assert.equal(p.models.providers['ollama-remote'].models[0].id, 'qwen2.5-coder:7b');
r.entries.profiles['research-default'].defaultAccounts['openai-codex'] = 'openai-codex/personal';
assert.equal(resolveFixtureExecution(r, req).accounts['openai-codex'], 'openai-codex/personal');
assert.equal(p.accounts['openai-codex'], 'openai-codex/homelab-openai');
});
test('scope is explicit, bounded and never inferred', () => {
for (const key of ['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId']) {
const r = request(); delete r[key]; assert.throws(() => resolveFixtureExecution(registry(), r), /invalid-fixture-input/);
r[key] = '../outside'; assert.throws(() => resolveFixtureExecution(registry(), r), /invalid-scope/);
}
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), fixtureOnly: false }), /fixture-only/);
});
test('fork pin is preserved against default change, override, missing account and revocation', () => {
const r = registry(), q = { ...request(), fork: { sourceSessionId: 'source', accounts: { 'openai-codex': 'openai-codex/homelab-openai' } } };
r.entries.profiles['research-default'].defaultAccounts['openai-codex'] = 'openai-codex/personal';
assert.equal(resolveFixtureExecution(r, q).accounts['openai-codex'], 'openai-codex/homelab-openai');
assert.throws(() => resolveFixtureExecution(r, { ...q, accounts: { 'openai-codex': 'openai-codex/personal' } }), /fork-account-change/);
assert.throws(() => resolveFixtureExecution(r, { ...q, fork: { sourceSessionId: 'source', accounts: {} } }), /missing-fork-pin/);
delete r.entries.accounts['openai-codex/homelab-openai'];
assert.throws(() => resolveFixtureExecution(r, q), /missing-account/);
r.entries.profiles['research-default'].allowedAccounts = ['openai-codex/personal'];
assert.throws(() => resolveFixtureExecution(r, q), /account-not-enrolled/);
});
test('unenrolled account/provider, missing harness, model expansion and native model ceiling refuse', () => {
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), accounts: { unknown: 'unknown/a' } }), /provider-not-enrolled/);
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), accounts: { 'openai-codex': 'other/a' } }), /account-not-enrolled/);
const r = registry(); delete r.entries.harnesses.pi;
assert.throws(() => resolveFixtureExecution(r, request()), /unsupported-harness/);
const s = registry(); s.entries.profiles['research-default'].models['ollama-remote'] = ['not-allowed'];
assert.throws(() => resolveFixtureExecution(s, request()), /model-not-enrolled/);
s.entries.profiles['research-default'].models = { 'openai-codex': ['not-enforceable'] };
assert.throws(() => resolveFixtureExecution(s, request()), /native-model-enforcement-unavailable/);
});
test('only explicit synthetic credential forms and internal fixture stores admitted', async t => {
assert.throws(() => createFixtureCredentialStore({ 'openai-codex/homelab-openai': { type: 'oauth', access: 'not-fixture', refresh: 'not-fixture', expires: 3 } }), /invalid-fixture-credential/);
const w = await workspace(t);
await assert.rejects(w.generate(registry(), request(), { fixtureOnly: true, transaction() { throw Error('must not execute'); } }), /fixture-store-required/);
});
test('two concurrent workspaces of the same agent publish distinct complete private generations', async t => {
const w = await workspace(t), s = store();
const a = request('one'), b = { ...request('two'), sessionId: 'session-b', workspaceId: 'workspace-b', accounts: { 'openai-codex': 'openai-codex/personal' } };
const [x, y] = await Promise.all([w.generate(registry(), a, s), w.generate(registry(), b, s)]);
assert.notEqual(x.generationPath, y.generationPath);
assert.equal((await read(`${x.generationPath}/auth.json`))['openai-codex'].access, 'FIXTURE_ACCESS_A');
assert.equal((await read(`${y.generationPath}/auth.json`))['openai-codex'].access, 'FIXTURE_ACCESS_B');
for (const g of [x, y]) {
assert.deepEqual((await readdir(g.generationPath)).sort(), ['auth.json', 'manifest.json', 'models.json']);
assert.equal((await stat(g.generationPath)).mode & 0o777, 0o700);
for (const file of ['auth.json', 'manifest.json', 'models.json']) assert.equal((await stat(`${g.generationPath}/${file}`)).mode & 0o777, 0o600);
const manifest = await readFile(`${g.generationPath}/manifest.json`, 'utf8');
assert.ok(!/FIXTURE_ACCESS|FIXTURE_REFRESH|expires|sha256|credentialHash/.test(manifest));
assert.equal(g.manifest.state, 'ready');
}
assert.deepEqual(await readdir(`${w.path}/pending`), []);
});
test('same execution ID is exclusively claimed and cannot overwrite a published generation', async t => {
const w = await workspace(t), s = store();
const attempts = await Promise.allSettled([w.generate(registry(), request(), s), w.generate(registry(), request(), s)]);
assert.equal(attempts.filter(a => a.status === 'fulfilled').length, 1);
const g = attempts.find(a => a.status === 'fulfilled').value;
const before = await readFile(`${g.generationPath}/auth.json`);
await assert.rejects(w.generate(registry(), request(), s), /execution-already-claimed/);
assert.deepEqual(await readFile(`${g.generationPath}/auth.json`), before);
});
for (const fault of ['after-auth', 'before-publish']) test(`failed generation ${fault} preserves prior files, records failure and refuses blind same-ID retry`, async t => {
const w = await workspace(t), s = store(); const first = await w.generate(registry(), request('good'), s);
const before = await readFile(`${first.generationPath}/auth.json`);
await assert.rejects(w.generate(registry(), request('bad'), s, { fault }), /injected-generation-failure/);
assert.deepEqual(await readdir(`${w.path}/generations`), ['good']);
assert.deepEqual(await readdir(`${w.path}/pending`), []);
assert.deepEqual(await readFile(`${first.generationPath}/auth.json`), before);
assert.equal((await read(`${w.path}/claims/bad/failure.json`)).state, 'failed');
await assert.rejects(w.generate(registry(), request('bad'), s), /execution-already-claimed/);
});
test('credential lock contention refuses without duplicate side effects', async t => {
const w = await workspace(t), s = store(); let release, entered;
const ready = new Promise(r => { entered = r; });
const held = s.transaction(['openai-codex/homelab-openai'], async () => { entered(); await new Promise(r => { release = r; }); });
await ready;
try { await assert.rejects(w.generate(registry(), request(), s), /credential-busy/); }
finally { release(); await held; }
assert.deepEqual(await readdir(`${w.path}/generations`), []);
});
test('symlinked pre-existing final target is refused and never followed', async t => {
const w = await workspace(t); await mkdir(`${w.path}/outside`, { mode: 0o700 });
await writeFile(`${w.path}/outside/sentinel`, 'unchanged');
await symlink(`${w.path}/outside`, `${w.path}/generations/exec-a`);
await assert.rejects(w.generate(registry(), request(), store()), /generation-exists/);
assert.equal(await readFile(`${w.path}/outside/sentinel`, 'utf8'), 'unchanged');
assert.ok((await lstat(`${w.path}/generations/exec-a`)).isSymbolicLink());
});
test('invalid registry cannot resolve; no fallback to supplied partial entries', () => {
const r = registry(); r.errors = [{ code: 'refused' }];
assert.throws(() => resolveFixtureExecution(r, request()), /invalid-registry/);
});
test('post-publication failure records uncertainty, preserves complete generation and prevents replay', async t => {
const w = await workspace(t), s = store();
await assert.rejects(w.generate(registry(), request('uncertain'), s, { fault: 'after-publish' }), /publication-uncertain/);
const dir = `${w.path}/generations/uncertain`;
assert.deepEqual((await readdir(dir)).sort(), ['auth.json', 'manifest.json', 'models.json']);
const before = await readFile(`${dir}/auth.json`);
assert.equal((await read(`${w.path}/claims/uncertain/failure.json`)).state, 'uncertain');
await assert.rejects(w.generate(registry(), request('uncertain'), s), /execution-already-claimed/);
assert.deepEqual(await readFile(`${dir}/auth.json`), before);
assert.deepEqual(await readdir(`${w.path}/pending`), []);
});
test('expired credentials refresh under transaction and subsequent generation reuses rotation', async t => {
const w = await workspace(t), s = createFixtureCredentialStore({
'openai-codex/homelab-openai': { ...credential('OLD'), expires: 1 },
});
const first = await w.generate(registry(), request('rotate'), s);
const auth = await read(`${first.generationPath}/auth.json`);
assert.equal(auth['openai-codex'].access, 'FIXTURE_ROTATED_ACCESS');
assert.equal(auth['openai-codex'].refresh, 'FIXTURE_ROTATED_REFRESH');
const second = await w.generate(registry(), request('reuse'), s);
assert.deepEqual(await read(`${second.generationPath}/auth.json`), auth);
await s.transaction(['openai-codex/homelab-openai'], draft => {
assert.equal(draft.get('openai-codex/homelab-openai').refresh, 'FIXTURE_ROTATED_REFRESH');
});
});
for (const mode of ['failure', 'timeout', 'malformed']) test(`refresh ${mode} retains prior generation and store state`, async t => {
const w = await workspace(t), s = store();
const prior = await w.generate(registry(), request('prior'), s);
const bytes = await readFile(`${prior.generationPath}/auth.json`);
await assert.rejects(w.generate(registry(), request('failed-refresh'), s,
{ refresh: { mode, timeoutMs: mode === 'timeout' ? 100 : 2000 } }));
assert.deepEqual(await readdir(`${w.path}/generations`), ['prior']);
assert.deepEqual(await readFile(`${prior.generationPath}/auth.json`), bytes);
await s.transaction(['openai-codex/homelab-openai'], draft => {
assert.equal(draft.get('openai-codex/homelab-openai').access, 'FIXTURE_ACCESS_A');
});
await assert.rejects(w.generate(registry(), request('failed-refresh'), s), /execution-already-claimed/);
});
test('concurrent refresh on same account refuses contention while unrelated account proceeds', async t => {
const w = await workspace(t), s = store();
const results = await Promise.allSettled([
w.generate(registry(), request('busy-a'), s, { refresh: { mode: 'timeout', timeoutMs: 200 } }),
w.generate(registry(), request('busy-b'), s, { refresh: { mode: 'timeout', timeoutMs: 200 } }),
w.generate(registry(), { ...request('independent'), accounts: { 'openai-codex': 'openai-codex/personal' } }, s),
]);
const codes = results.slice(0, 2).map(r => r.reason?.code).sort();
assert.deepEqual(codes, ['credential-busy', 'refresh-timeout']);
assert.equal(results[2].status, 'fulfilled');
assert.deepEqual(await readdir(`${w.path}/generations`), ['independent']);
});
test('invalid refresh options refuse before burning claim', async t => {
const w = await workspace(t), s = store();
await assert.rejects(w.generate(registry(), request(), s, { refresh: { executable: '/bin/false' } }), /invalid-fixture-input/);
await w.generate(registry(), request(), s);
});
+24
View File
@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { refreshFixtureCredential } from '../src/refresh-fixture.mjs';
const expired = () => ({ type: 'oauth', access: 'FIXTURE_OLD_ACCESS', refresh: 'FIXTURE_OLD_REFRESH', expires: 1 });
test('fixed fake process rotates both OAuth fields without mutating caller input', async () => {
const input = expired(), result = await refreshFixtureCredential('openai-codex', input);
assert.ok(result.access !== input.access && result.refresh !== input.refresh);
assert.ok(result.expires > Date.now() + 300000);
assert.equal(input.expires, 1);
});
test('concurrent isolated processes preserve separate provider credentials', async () => {
const a = { type: 'api_key', key: 'FIXTURE_ACCOUNT_A' }, b = { type: 'api_key', key: 'FIXTURE_ACCOUNT_B' };
const results = await Promise.all([refreshFixtureCredential('provider-a', a), refreshFixtureCredential('provider-b', b)]);
assert.deepEqual(results, [a, b]);
});
for (const [mode, code] of [['failure', 'refresh-failed'], ['malformed', 'invalid-refresh-output'], ['timeout', 'refresh-timeout'], ['unchanged', 'refresh-not-ready']]) {
test(`fake ${mode} is refused with fixed diagnostics`, async () => {
await assert.rejects(refreshFixtureCredential('openai-codex', expired(), { mode, timeoutMs: mode === 'timeout' ? 100 : 2000 }), e => e.message === code);
});
}
test('caller executable/environment injection is rejected before spawning', async () => {
await assert.rejects(refreshFixtureCredential('openai-codex', expired(), { executable: '/bin/false' }), /invalid-fixture-input/);
await assert.rejects(refreshFixtureCredential('openai-codex', expired(), { env: {} }), /invalid-fixture-input/);
});