UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

119 lines (118 loc) • 6.98 kB
import { canonicalizeTarget, lexicalViewOf, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js"; import { isWithin } from "./runner/session-rule-policy.js"; import { swallowedFenceRoot } from "./runner/advertised-writable-dirs.js"; import { declaredPathTargetOf, isProtectedWrite } from "./tool-registry.js"; import { pathTargetBaseOf } from "./effective-path-target.js"; const ask = (message) => ({ action: "ask", message, decisionReason: "rule" }); function absoluteRootOf(rootPath, envCwd) { return rootPath === undefined ? undefined : lexicalViewOf(rootPath, envCwd); } export const WRITE_EXEMPT_DIR_COVERS_ROOT_CODE = "config.write_exempt_dir_covers_root"; function assertExemptDirsDoNotCoverRoot(exemptDirs, rootLexical) { if (exemptDirs === undefined || rootLexical === undefined) return; for (const raw of exemptDirs) { if (!raw) continue; const dirLexical = lexicalViewOf(raw, rootLexical); const swallowed = swallowedFenceRoot(dirLexical, [rootLexical]); if (swallowed === undefined) continue; const e = new Error(`createFsWriteGatePolicy: exemptDirs entry "${raw}" resolves to ${dirLexical}, which covers this gate's root ${swallowed} — ` + `an exemption that holds the root auto-approves every write under it, which is the whole workspace, so the gate refuses to be built rather than run with write governance silently off. ` + `If that IS what this deployment wants, say it with the knob that means it: defaultWrite: "allow", an open write face, or a permission rule.`); e.code = WRITE_EXEMPT_DIR_COVERS_ROOT_CODE; e.detail = { exemptDir: raw, exemptDirResolved: dirLexical, root: swallowed }; throw e; } } export function createFsWriteGatePolicy(opts) { const { env, rootPath, defaultWrite } = opts; const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined; const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined; const rootLexical = absoluteRootOf(rootPath, env.cwd); assertExemptDirsDoNotCoverRoot(exemptDirs, rootLexical); const guardedCapable = env.writeFileGuarded !== undefined; const advisory = guardedCapable ? "" : " (advisory adjudication: this environment provides no guarded write, so approval is judged before a separate, non-atomic write step)"; return { async check(req, signal) { if (!isProtectedWrite(req)) return { action: "allow" }; if (opts.requireGuardedWrite === true && !guardedCapable) { return { action: "deny", message: `write tool "${req.toolName}" refused: this deployment requires guarded (atomic precondition-verified) writes (requireGuardedWrite), but the execution environment provides no writeFileGuarded capability — refusing rather than falling back to a non-atomic write`, decisionReason: "rule", }; } const declared = declaredPathTargetOf(req); const path = declared === undefined ? undefined : writeTargetPath(req, declared); if (path === undefined) { return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${declared === undefined ? " (the tool declares no path target)" : ""}${advisory}`); } const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declared, { root: rootPath, cwd: req.cwd })); if (!canon.ok) { return ask(`write to "${path}" requires approval: its real target could not be resolved (${canon.message})${advisory}`); } for (const root of req.advertisedWritableRoots ?? []) { if (isWithin(root, canon.key)) return { action: "allow" }; } let fenceRootsCanonical; let refusedDomain; for (const [dirs, structural] of [ [exemptDirs, true], [acceptDirs, false], ]) { if (!dirs) continue; for (const raw of dirs) { const dir = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath); if (!dir.ok) continue; if (structural) { if (fenceRootsCanonical === undefined) { const r = rootPath === undefined ? undefined : await canonicalizeTarget(env, rootPath, signal, pathFamilyOf({ root: rootPath }), undefined); fenceRootsCanonical = r === undefined ? [] : r.ok ? [r.key] : "unresolved"; } if (fenceRootsCanonical === "unresolved") { refusedDomain ??= `"${raw}" (this task's root ${rootPath} could not be resolved, so a structural domain that may cover it grants nothing)`; continue; } const swallowed = swallowedFenceRoot(dir.key, fenceRootsCanonical); if (swallowed !== undefined) { refusedDomain ??= `"${raw}" (resolves to ${dir.key}, which contains this task's root ${swallowed})`; continue; } } if (isWithin(dir.key, canon.key)) return { action: "allow" }; } } if (opts.isExempt) { let exempt = false; try { exempt = (await opts.isExempt(req.toolName, canon.key, req.face !== undefined ? { capabilityId: req.face.capabilityId, contractId: req.face.contractId, shapeDigest: req.face.shapeDigest } : undefined)) === true; } catch { exempt = false; } if (exempt) { return { action: "allow", message: `write to "${path}" allowed by session exemption (previously approved for this session)`, decisionReason: "rule", }; } } if (defaultWrite === "allow") return { action: "allow" }; const note = refusedDomain === undefined ? undefined : ` — note: the configured write domain ${refusedDomain} was not honored; a domain that holds the root would exempt every write under it`; if (opts.defaultAsk !== undefined) return opts.defaultAsk(path, note); return ask(`approve write to "${path}"?${advisory}${note ?? ""}`); }, }; }