# 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). |
| `index.ts` | `registerPlugin(ctx)` + `registerBlackholeRoutes(fastify, { logger, env })` (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. Structured logging: path, key counts, failure reason — NEVER field values (config holds provider/model hints). |
| `__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. |
