AGENTS.md@packages/blackhole-plugin/src/server · git:20260907.33d6653 · 2026-09-07 · sha256 a432e73caa959b06
AGENTS.md@packages/blackhole-plugin/src/server git:20260907.33d6653A
Immutable. This exact content is served forever at /api/v1/blob/a432e73caa959b06.
# DOX — packages/blackhole-plugin/src/server
Files in this directory. One row per source file. See change: add-blackhole-plugin.
| File | Purpose |
|------|---------|
| `config-io.ts` | `readConfig(path) → ConfigOk \| ConfigParseError` — FAILS CLOSED (design D6): unparseable file yields the parser message and NO config object, never defaults. `saveConfig(path, managed) → { preservedUnmanagedKeys, externalWriteDetected }` — read-modify-write within the request (design D5): unmanaged + annotation keys keep value AND position, new keys append, `undefined`/`null` deletes. Throws `ConfigParseErrorOnWrite` without touching the file when unparseable. `writeAtomic` = tmp-in-same-dir + `fs.rename`, tmp cleanup on failure. |
| `config-path.ts` | `resolveBlackholeConfigPath(env)` → agent root (`PI_CODING_AGENT_DIR` trimmed/`~`-expanded/resolved, else `<home>/.pi/agent`) + fixed `BLACKHOLE_CONFIG_DIR`/`BLACKHOLE_CONFIG_FILENAME`. Mirror of blackhole `src/core/unified-config.ts` `getAgentDir`. Never from request input (no traversal). |
| `pipeline-reader.ts` | Per-session pipeline reader for `GET /session/:id` (design D5). `isValidSessionId` — RFC 4122 syntax, UNRESTRICTED version nibble (pi emits UUIDv7); pure, called before any fs access. `resolvePipelinePaths(env, id)` — builds `<agentRoot>/pi-blackhole/<id>-pending.json`, THROWS if resolved path escapes the container dir (defense in depth). `readPendingState` — absent/torn/unparseable file → `{activity:"none", pendingBatches:0, cursors:{observer,reflector,dropper}:null, tip:null}` (quiet degradation, X5); accepts numeric `entry`/`tip` when recorded (spec E11 model; pinned extension records hash `entryId` only). `readCooldownMap` — absent/garbage `pi-blackhole-cooldown.json` → `{}` (X7). `deriveWorkerViews` — chain head `<worker>Model ?? model` as `provider/id` vs active cooldown entries. `readSessionPipeline` — assembles the response. SOURCE-VERSION PIN: mirrored from `pi-blackhole@0.4.10` `src/om/pending.ts` (`PendingOMState`) + `src/om/cooldown.ts` (`CooldownMap`). Read-only: never writes. See change: add-blackhole-session-pipeline. |
| `index.ts` | `registerPlugin(ctx)` + `registerBlackholeRoutes(fastify, { logger, env, isPiExtensionInstalled? })` (factored for injected-Fastify tests). `GET`+`PUT /api/plugins/blackhole/config`. PUT validates BEFORE any disk access (400 + no write); 409 on unparseable file; 500 on an unwritable dir. NEW `GET /api/plugins/blackhole/status` (design D1): capability present → its answer ALONE (negative never overridden by config file); absent → `existsSync(configPath)` fallback; capability rejection → 503 `{error}`, NEVER `{installed:false}` (X3). NEW `GET /api/plugins/blackhole/session/:id`: validate-then-confine, read-only (GET only), quiet degradation — global config parse failure → null fields not 500 (X6). Structured logging: path, key counts, failure reason — NEVER field values. `registerPlugin` forwards `ctx.isPiExtensionInstalled`. See change: add-blackhole-plugin, add-blackhole-session-pipeline. |
| `__tests__/config-io.test.ts` | L1. Absent file returns defaults and creates nothing (E16); omitted keys report defaults (E17); unmanaged-key reporting; fail-closed parse on read + blocked write with byte-identical file (X1, X2); annotation/unknown-key and key-ORDER survival (X4-X6); request-time re-read (X7); atomic write across 200 iterations (X8); interleaved external write flagged and NOT reported as merged, via a `vi.mock("node:fs")` one-shot read hook (X9 — `vi.spyOn` cannot: ESM namespaces are not configurable); unwritable dir leaves no partial file (X11). |
| `__tests__/config-path.test.ts` | L1. `PI_CODING_AGENT_DIR` set vs unset (E15), `~`-expansion, whitespace trimming, and that the path is built only from env + the two fixed constants — nothing here can report whether the EXTENSION is installed (E23). |
| `__tests__/routes.test.ts` | L1. GET/PUT contract against an injected Fastify. Every rejection asserts the FILE as well as the status code (a 4xx that still wrote would pass a status-only test). Covers E1-E14, E16, X1, X2, X10. |
| `__tests__/pipeline-routes.test.ts` | L1 (add-blackhole-session-pipeline). Injected-Fastify contract for `/status` + `/session/:id`. Status: capability matrix E1-E4 (registry answer alone; file never overrides) + X3 (rejection → 503, never `{installed:false}`). Session: E5 UUIDv7 accepted at exact path; E6 version-nibble sweep 1/4/7/8; E7/E8 invalid + hostile ids → 4xx with ZERO fs calls (counted `node:fs` wrapper — ESM namespaces not configurable, vi.spyOn impossible); E9 confinement assert on `resolvePipelinePaths`; X5 absent/torn pending → no-activity 200; X6 malformed global config → null fields not 500; X7 absent/garbage cooldown → no advisory; X8 only GET handlers registered (mutating verbs → 404) + pending file bytes+mtime unchanged after a served request. |