/** * The agent path into adoption — the MCP `import` tool. * * **This is the inbound twin of `mcp-write-to-dir-unscoped` (#625).** That change * constrained what an agent can have Auden *write into* the tree. This is what an * agent can have Auden *read out of* it, and the asymmetry is the whole reason it * needs its own guard: reading `~/.claude/CLAUDE.md` and sending it uploads the * user's standing instructions to their account just as surely as writing there * plants them. An unconstrained import is an exfiltration primitive with a * friendly name. * * Three constraints, each with a reason that is not "defence in depth": * * 1. **Containment to the cwd subtree**, decided by `refuseOutsideTree` — the * *same* predicate the write path uses, extracted for exactly this so the two * directions of one boundary cannot drift (`mcp/write-scope.ts`). Note it is * deliberately not the whole write guard: `refuseUnsafeWriteDir` also refuses * `.agents/`, `.claude/` and `.cursor/`, which inbound are precisely the * locations an import exists to read. * 2. **`paths` is required, non-empty, and may not name the working tree * itself.** Requiring the argument is not sufficient on its own, and the * first version of this tool wrongly claimed it was (caught in review on * #640): `resolveNamedPaths` treats a path that resolves to the repo root as * "everything the scan found" (`rules/import-paths.ts`, the `rel === ''` * branch), so `paths: ["."]` reproduced the whole-repo sweep exactly, one * keystroke instead of zero. The gate is therefore on the *meaning* of the * argument, not on its presence: an agent must name what it adopts, and the * whole tree is refused however it is spelled — `.`, `./`, or the working * directory given absolutely. That branch stays for `auden import .`, which * is a person choosing it in their own terminal, with a picker in front of * them; neither of those is true of an agent. * 3. **No home scanning at any point.** `discoverGuideFiles` is called without * `global`, so `~/.claude/CLAUDE.md` is not even in the population this tool * selects from. `--global` stays human-only: an agent has no business scanning * the user's home directory, and a containment check alone would not say so — * it would only refuse the paths it happened to be handed. * * **Guides do not ride `push_context`.** Its `contentType` enum is * `['document', 'reference', 'asset']` with no `guide` * (`packages/protocol/src/schemas.ts` → `PushContentTypeSchema`), and adding one * is not the one-line change it looks like: a guide carries `body_item_id` and * resolves through that indirection (`apps/dashboard/src/server/context/bundleItems.ts` * → `resolveBundleItemTarget`). So this wraps the rules-import endpoint through * `importDiscoveredGuides`, the same function `auden import` and `auden init` * reach the server through — which is also what makes the managed check * unforgettable here: that function refuses a bundle-managed file itself * (`rules/init-import.ts`), so this tool could not fork a guide even if it * skipped candidate resolution. */ import { importDiscoveredGuides } from '../rules/init-import.js'; import type { DiscoveredGuideFile } from '../rules/discover.js'; import type { ImportCandidate } from '../rules/import-candidates.js'; import type { ImportRulesResult } from '../rules/import-client.js'; /** Credentials the tool needs; the context client does not expose its own. */ export type ImportToolAuth = { dashboardUrl: string; token: string; }; export type ImportToolDeps = { /** The tree the agent is working in — the boundary every path is checked against. */ cwd: string; /** * Null when the server was started without a resolvable token. The tool then * refuses by name rather than failing at the HTTP call, since "no token" is a * setup answer and a 401 is not. */ auth: ImportToolAuth | null; /** Injected by tests so the tool runs without a repo on disk or a network. */ discoverFn?: (options: { cwd: string; }) => Promise; resolveCandidatesFn?: (files: ReadonlyArray, root: string) => Promise; importFn?: typeof importDiscoveredGuides; repoIdFn?: (root: string) => Promise; }; export type ImportToolOutcome = { /** Repo-relative display paths actually sent. */ imported: string[]; /** One line per path that yielded nothing, each naming why. */ refused: string[]; result?: ImportRulesResult; }; /** * Adopt the named paths, or explain every path that yielded nothing. * * Refusals accumulate rather than short-circuit: an agent that names four files * and gets one error for the first has to guess about the other three, and * guessing is what makes it re-run the call with a wider argument. */ export declare function runImportTool(paths: ReadonlyArray, deps: ImportToolDeps): Promise; //# sourceMappingURL=import-tool.d.ts.map