import { SandboxProvider, SandboxMode } from '@theokit/sdk/sandbox'; /** * M78 — the tool scope binder: bind `{ projectRoot, writeRoot, sandbox }` once, so an unconfined * shell is unrepresentable. * * ## The defect this closes * * The framework shipped the ingredients — `createSandboxBackend`, `resolveSandboxPosture` — and no * binder. Measured against `@theokit/sdk-tools@0.26.1`: `projectRoot` is REQUIRED on eleven * factories, while `sandbox` is **optional** on three — `createGitDiffTool`, `createGitStatusTool` * and, the one that matters, `createShellTool`. * * An optional sandbox on a shell factory means a scope assembled without one produces an UNCONFINED * SHELL with no error and no warning. The consumer that prompted this milestone documented exactly * that outcome. And because nothing bound the roots either, every product rediscovered per tool * which root each factory accepts — with silence as the failure mode for getting it wrong. * * ## Why this is framework and not runtime * * It runs nothing. It applies configuration to factories the SDK owns — the boundary/home half of * ADR-0040 § D2, the same category as the approval gate and the ask channel. * * ## Why ONE generic `bind` and not eleven named wrappers * * The milestone wants the consumer's registry to become "the declarative map of ten entries it * should be". A generic `bind` IS that map: * * ```ts * const registry = { * shell: scope.bind(createShellTool)(), * read_file: scope.bind(createReadFileTool)(), * } * ``` * * Eleven named wrappers would add nothing to that and cost two things: a twelfth tool in `sdk-tools` * would need a change here (breaking OCP), and each wrapper is one more place to forget the sandbox — * the exact failure being closed. Parsimony rung 1: they do not need to exist. */ /** What a sandbox mode permits, in the two terms a tool factory actually needs. */ interface SandboxWritePolicy { /** Whether ANY write is permitted. */ readonly writes: boolean; /** Whether a path outside the project root may be honoured. */ readonly allowAbsolute: boolean; } /** * The write policy of a sandbox mode. * * A PROJECTION of the SDK's own `writableRootsFor`, never a second source of truth (G12): products * were re-deriving this by hand, and a function that disagreed with the SDK would be a third * derivation — worse than the two it replaces. `tool-scope.test.ts` asserts the agreement for every * mode. * * The mapping is not guessable, which is why it is measured rather than assumed: * * | mode | `writableRootsFor` | meaning | * |---|---|---| * | `read-only` | `[]` | no writable root | * | `workspace-write` | `[cwd, '/tmp']` | writes, confined | * | `danger-full-access` | `null` | **unrestricted** — not "no writes" | * * Reading `null` as "nothing is writable" would forbid writes exactly where everything is allowed. */ declare function sandboxWritePolicy(mode: SandboxMode, cwd: string): SandboxWritePolicy; /** The scope fields a bound factory receives. Every field is non-optional by the time it is applied. */ interface ScopeFields { readonly projectRoot: string; readonly writeRoot: string; readonly sandbox: SandboxProvider; } interface ToolScopeInput { /** Root the tools read from. */ readonly projectRoot: string; /** * Root the tools write to. Defaults to `projectRoot` — the conservative reading: a product that * never distinguishes the two should not have to say so twice, and the default must not be broader * than what was asked for. */ readonly writeRoot?: string; /** * The confinement. REQUIRED, and that is the entire milestone. * * Making it optional is what let a scope produce an unconfined shell in silence. A caller who * genuinely wants no confinement must say so by passing a `danger-full-access` sandbox — a * decision that appears in the code, in a review, and in a log, rather than an omission that * appears nowhere. */ readonly sandbox: SandboxProvider; /** * The mode `policy` is computed from. Optional because the policy is a convenience: a scope whose * product never asks about write policy needs no mode. */ readonly mode?: SandboxMode; } /** Any `sdk-tools` factory: takes an options object, returns a tool. */ type ToolFactory = (options: TOptions) => TTool; /** * Per-tool overrides. * * The scope fields are optional here — a caller may replace them — but `undefined` is treated as * "not supplied", never as "unset". See {@link ToolScope.bind}. */ type Overrides = Omit & Partial; interface ToolScope extends ScopeFields { /** The write policy of `mode`, when one was supplied. */ readonly policy: SandboxWritePolicy; /** * Scope a factory. * * Returns a function taking that tool's own options. Scope fields are injected; an override may * REPLACE one, but an `undefined` override falls back to the scope rather than clearing it. * * That asymmetry is the point. Variation is legitimate — one tool may need stricter confinement * than the rest, which is the risk the milestone flags. Absence is not: `{ sandbox: maybeSandbox }` * with an undefined variable is ordinary code, and honouring it would silently unconfine a shell * through the one door the type system cannot close. */ bind, TTool>(factory: ToolFactory): (overrides?: Overrides) => TTool; } /** * Bind a tool scope. * * ```ts * const scope = bindToolScope({ projectRoot, sandbox: await resolveSandbox({ mode }) , mode }) * const shell = scope.bind(createShellTool)({ name: 'run_shell' }) * ``` * * Omitting `sandbox` does not compile — see `tests/type/tool-scope.test-d.ts`. */ declare function bindToolScope(input: ToolScopeInput): ToolScope; export { type SandboxWritePolicy, type ToolScope, type ToolScopeInput, bindToolScope, sandboxWritePolicy };