import { type LiveCheckoutGuard } from './live-checkout-guard.service.js'; import { type DestructiveGitGuard } from './destructive-git-guard.service.js'; /** * Control-plane guard for Claude Code agent sessions. * * Request 72c9427a, spec `specs/2026-09-24-control-plane-isolation.md` Part 3. * The backend writes two files per session under * `~/.crewly/runtime/control-plane/`: * * - `.settings.json` — a Claude Code settings file with * `permissions.deny` rules for the control-plane paths and a PreToolUse * hook on the Bash tool. It is passed with `claude --settings `. * - `.paths` — the protected-path list the hook script reads. * * Both files, and the hook script itself, are on their own deny list. * * Coverage (stated in the hook's output as well): this stops an agent editing * the stop/start/config mechanisms with its normal tools, and accidental * edits. It does NOT stop an interpreter one-liner, a runtime-built path, or a * call to the loopback API. That needs OS-level isolation (spec Part 4). */ /** Roots the protected paths are resolved against. */ export interface ControlPlaneRoots { /** Crewly home directory (`~/.crewly`, or `$CREWLY_HOME`). */ crewlyHome: string; /** Crewly install root (package dir or checkout) that holds `config/` and `dist/`. */ installRoot: string; /** The agent's working directory / project path, when known. */ projectPath?: string; } /** Protected paths, split by what is denied. */ export interface ControlPlanePaths { /** Paths agents may not write. Directories protect their whole subtree. */ writeDenied: Array<{ path: string; isDirectory: boolean; }>; /** Files agents may not read with the built-in Read/Grep/Glob tools. */ readDenied: string[]; } /** One Claude Code hook group: an optional tool matcher and its commands. */ export interface HookGroup { matcher?: string; hooks: Array<{ type: 'command'; command: string; }>; } /** Shape of the generated Claude Code settings file (the subset we write). */ export interface ControlPlaneSettings { permissions: { deny: string[]; }; /** * Claude Code prompt suggestions stay off: a faint predicted user message * in an empty input, accepted by Tab, must never reach a harness-driven * session (2026-10-03 phantom owner input). Mirrors the env var set at launch. */ promptSuggestionEnabled: false; /** Tokens at which Claude Code auto-compacts (absent: Claude Code's default). */ autoCompactWindow?: number; hooks: { /** The control-plane guard's Bash hook first; the agent-status hook (all tools) after it, when given. */ PreToolUse: Array; /** Agent-status hook events (#815) and subagent-guard events (#852), present only when those hooks are given. */ [event: string]: HookGroup[]; }; } /** Result of preparing the guard for one session. */ export type ControlPlaneGuardResult = { enabled: true; settingsPath: string; pathsPath: string; protectedCount: number; } | { enabled: false; reason: string; }; /** * Whether the guard is on for sessions launched by this backend. * * Read from the backend's own environment at launch, so an agent session * cannot switch it off: exporting the variable inside a session changes that * shell, not the backend. * * @param env - Environment to read (defaults to the backend's process.env) * @returns false only when `CREWLY_CONTROL_PLANE_GUARD=0` */ export declare function isControlPlaneGuardEnabled(env?: NodeJS.ProcessEnv): boolean; /** * Resolve every control-plane path (spec Part 2 table) to an absolute path. * * Duplicates are dropped, e.g. when the project path is the install root. * * @param roots - Crewly home, install root and optional project path * @param existingTeamIds - Team directory names to protect `config.json` for * (spec Part 2 protects `teams/*\/config.json`, not the whole `teams/` * subtree — a team directory also holds `norms/`, `wiki/`, `prompts/`, * `sops/` and `cron-tasks.json`, which agents write routinely). Pass the * ids returned by {@link listExistingTeamIds}; defaults to none, so * calling this without them protects no team config (documented, not a * silent gap — `prepareControlPlaneGuard` always supplies them). * @returns Write-denied paths (with a directory flag) and read-denied files * * @example * ```ts * const { writeDenied } = resolveControlPlanePaths( * { crewlyHome: '/h/.crewly', installRoot: '/opt/crewly' }, * ['team-0001'], * ); * // writeDenied includes { path: '/h/.crewly/teams/team-0001/config.json', isDirectory: false } * // writeDenied does NOT include '/h/.crewly/teams' itself — reads and * // writes to team-0001/wiki, norms, prompts, sops, cron-tasks.json stay allowed. * ``` */ export declare function resolveControlPlanePaths(roots: ControlPlaneRoots, existingTeamIds?: readonly string[]): ControlPlanePaths; /** * Format an absolute path as a Claude Code permission-rule specifier. * * In Claude Code rules `//x` is an absolute path, while `/x` is relative to * the settings file, so an absolute path gets one extra leading slash. * * @param absPath - Absolute filesystem path * @param isDirectory - Append `/**` to cover the subtree * @returns Specifier such as `//Users/me/.crewly/teams/**` */ export declare function toRuleSpecifier(absPath: string, isDirectory: boolean): string; /** * Build the Claude Code settings object for one session. * * Deny rules use `Edit(...)`, which Claude Code applies to every built-in * file-editing tool (Edit, Write, NotebookEdit), and `Read(...)` for the API * token. Only documented rule forms are emitted: an invalid rule could get * the whole settings file rejected, and the Bash hook with it. * * The agent-status hook (#815) is merged into the same file, because Claude * Code takes one `--settings`. It is registered on its own events * (Notification, PermissionRequest, Stop, UserPromptSubmit, PreToolUse, * PostToolUse, SubagentStart, SubagentStop, SessionStart). On PreToolUse it is a second * group after the guard's Bash group, and on the subagent events it sits next * to the subagent guard: groups are appended, never replaced, so the guard's * entry and the deny list are identical with or without it. * * @param paths - Resolved control-plane paths * @param hookCommand - Shell command that runs the PreToolUse Bash hook * @param statusHookCommand - Shell command that runs the agent-status hook; omit to leave it out * @param subagentHookCommand - Shell command that runs the subagent guard (#852); omit to leave it out * @param credentialGuard - The credential guard (specs/2026-10-04-agent-credential-isolation.md): * its hook command (a PreToolUse group on Bash/Read/Grep/Glob, right after the * control-plane group) and its `Read(...)` deny rules; omit to leave it out * @param liveCheckoutGuard - The live-checkout guard hook (a PreToolUse group on * Bash and the file-editing tools); omit to leave it out * @param destructiveGitGuard - The destructive-git guard hook (a PreToolUse group on * Bash); omit to leave it out * @returns Settings object ready to serialise */ export declare function buildControlPlaneSettings(paths: ControlPlanePaths, hookCommand: string, statusHookCommand?: string, subagentHookCommand?: string, credentialGuard?: { hookCommand: string; matcher: string; denyRules: string[]; }, liveCheckoutGuard?: Pick | null, destructiveGitGuard?: Pick | null): ControlPlaneSettings; /** * Make a session name safe to use as a file name. * * @param sessionName - Crewly session name * @returns Name with anything outside `[A-Za-z0-9._-]` replaced by `_` */ export declare function toSafeFileStem(sessionName: string): string; /** * Write the per-session settings file and protected-path list. * * @param sessionName - Crewly session name (used for the file names) * @param roots - Crewly home, install root and optional project path * @param env - Environment holding the kill switch (defaults to process.env) * @param credentialGuard - Credential guard hook + deny rules to merge in (optional) * @returns Where the files were written, or why the guard is off * @throws When the files cannot be written (the caller decides whether to launch unguarded) */ export declare function prepareControlPlaneGuard(sessionName: string, roots: ControlPlaneRoots, env?: NodeJS.ProcessEnv, credentialGuard?: { hookCommand: string; matcher: string; denyRules: string[]; }): Promise; /** * Append `--settings ` to a Claude Code launch command. * * Only commands that carry `--dangerously-skip-permissions` (the Crewly agent * launch line) are changed. A command that already passes `--settings` is * left alone: the owner configured their own, and Claude Code takes one. * * @param cmd - Launch command line * @param settingsPath - Generated settings file * @returns The command with the flag appended, or unchanged */ export declare function applyControlPlaneSettingsFlag(cmd: string, settingsPath: string): string; //# sourceMappingURL=control-plane-guard.service.d.ts.map