/** * Where an MCP `writeToDir` may put files. * * **The threat.** `pull_context` / `pull_inbox` take a `writeToDir` the *agent* * names, and the server's `read-only` flag deliberately does not cover it — it * gates context-layer writes, and a local materialization of already-readable * context is not one. That reasoning is sound about the server and silent about * the disk: nothing constrained the path, so a prompt-injected or simply mistaken * agent could have Auden write into `~/.claude/`, `~/.ssh/`, or any other * directory the user's shell can reach, with no gate and no record. * * Two distinct harms, and they want different rules: * * 1. **Outside the working tree.** Writing to `~/.claude/CLAUDE.md` puts * attacker-chosen text into a file every future agent session loads as * standing instructions. Auden must not be the mechanism that places it. The * rule is containment: the destination is inside the current working * directory or it is refused. * * 2. **A discovery root inside the working tree.** `/.claude/skills` passes * containment and is still wrong: a guide dropped there is indistinguishable * from repo-authored guidance, so the next `auden import` sends it up * as a *new* guide and forks it on the first edit. The bundle-managed * skip-list cannot save it, because a `writeToDir` records no placement. * * **What stays allowed is the actual feature**: an agent dropping context into * its own working tree — `./context/`, `.auden/context/`, `docs/` — which is * what the tool is for. This narrows a capability rather than removing it. * * Refusal is the whole mechanism deliberately: there is no flag, prompt or * persistent grant that widens it. A tool that let an agent ask for the wider * scope would be the consent bypass in a different shape, since the agent is * precisely the party the guard exists to constrain. */ export type WriteScopeRefusal = { /** One code per guard, so a test names the guard rather than the wording. */ code: 'outside-cwd' | 'discovery-root' | 'escaping-link'; message: string; }; export type WriteScopeDeps = { /** Injectable so the symlink guard tests without creating real links. */ realpathFn?: (path: string) => Promise; }; /** * Refuse a path that is not contained in the working tree — the rule both * directions of the boundary share. * * **Why this is extracted rather than inlined twice.** Outbound, it answers * "may Auden write here?" (`refuseUnsafeWriteDir`, below). Inbound, it answers * "may Auden read this and upload it?" (the MCP `import` tool, * `mcp/import-tool.ts`) — the outbound twin's mirror, since an agent naming * `~/.claude/CLAUDE.md` to *read* leaks it to the account just as surely as * naming it to write plants instructions. Those are the same question about the * same boundary, so they get one definition and cannot drift * (`auden-import-plan.md` → *Security*). * * **What is deliberately NOT in here: the discovery-root guard.** That one is * outbound-only and stays in `refuseUnsafeWriteDir`. Its reasoning — a guide * written into `.agents/` comes back as a new guide on the next import — is a * statement about *writing*. Inbound, `.agents/`, `.claude/` and `.cursor/` are * precisely the locations an import exists to read, so applying it here would * refuse every guide the tool is for. Widening this predicate to cover it would * break import; narrowing it in the caller would let containment drift. Hence * the split. * * `cwd` is passed rather than read from `process.cwd()` so this is testable and * so there is exactly one place that decides what "the working tree" means. */ export declare function refuseOutsideTree(path: string, cwd: string, deps?: WriteScopeDeps): Promise; /** * Decide whether `dir` is a legal `writeToDir`, returning `null` when it is. * * Containment (shared, above) then the discovery-root guard (outbound-only). * The order matters only for a path that fails both, and containment is the * security boundary, so it answers first. */ export declare function refuseUnsafeWriteDir(dir: string, cwd: string, deps?: WriteScopeDeps): Promise; /** * True when a **file** path would land on an auto-discovered guide file at the * repo root — `AGENTS.md` and friends. Separate from the directory check because * the directory guard cannot see the filenames a write will derive from item * names, and `writeToDir: "."` is otherwise legal and common. */ export declare function isDiscoveredGuideFile(fileName: string): boolean; //# sourceMappingURL=write-scope.d.ts.map