UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

58 lines (57 loc) • 4.35 kB
import { deliverEngineNotice } from "../engine-notice.js"; import { unionAddOnly } from "../child-clamp-seats.js"; import { protectivePathTargetOf } from "../tool-registry.js"; import { compileLegWriteJudgment, resolveWriteProtectedTable, writeProtectionMountOver } from "../write-protect.js"; const ABSENCE_REASON = { symlink: "unresolved-symlink", target: "unresolvable-target" }; export function mountWriteProtection(input) { const announced = new Set(); const judgment = compileLegWriteJudgment(unionAddOnly("writeProtectedAdditions", resolveWriteProtectedTable(input.deps.writeProtectedPaths), input.spec.writeProtectedAdditions), { exemptRoots: input.guard.exemptRoots, ...(input.guard.rows !== undefined ? { guardRows: input.guard.rows } : {}), ...(input.deps.agentConfigDir !== undefined ? { agentConfigDir: input.deps.agentConfigDir } : {}), ...(input.env !== undefined ? { env: input.env } : {}), rootPath: input.rootPath, onViewAbsent: (absence) => { const reason = absence.unresolvedSymlink ? ABSENCE_REASON.symlink : ABSENCE_REASON.target; if (announced.has(reason)) return; announced.add(reason); deliverEngineNotice(input.deps.onNotice, { code: "config.path_view_unresolved", message: `This run's write-protection judge did not get an answer from its execution environment for one lookup of a write's judgment ` + `(${reason === ABSENCE_REASON.symlink ? "a symlink whose real target is unknowable" : "the environment refused, errored or did not answer in time"}: ${absence.cause}). ` + `A write whose target's real destination (or link chain) could not be determined is refused where the table has rows — the ` + `table cannot say it is not a protected path; a root whose key did not come back exempts and places nothing; guarded write rows ` + `a deployment's policy did not return are held, so the writes they could guard are not placed. Every row a source that did ` + `answer met stays met. An environment that cannot answer path questions is a deployment fact: fix the environment (or the ` + `link) and the same writes are judged again. Announced once per leg per reason; later writes of the same shape are judged ` + `the same way, silently.`, detail: { reason, cause: absence.cause, sessionId: input.sessionId, runId: input.runId }, }); }, }); const matcher = judgment.matcher; const leg = { mount: matcher === undefined ? undefined : writeProtectionMountOver(matcher, input.rootPath), judgeWriteTarget: judgment.judgeWriteTarget }; if (leg.mount === undefined || leg.mount.targetView !== "spelling-only") return leg; const covered = input.roster.entries .filter((e) => [e.name, ...e.aliases].some((name) => { const target = protectivePathTargetOf({ toolName: name, args: {}, face: e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {} }); return target !== undefined && target.access !== "read"; })) .map((e) => e.name) .sort(); if (covered.length === 0) return leg; deliverEngineNotice(input.deps.onNotice, { code: "config.write_protection_target_view_absent", message: `This run mounted path-confinable write tools [${covered.join(", ")}] while its write-protection judge has NO execution environment, ` + `so the judge compares path SPELLINGS against its table and nothing else. A write whose spelling misses the table but whose real target ` + `resolves onto a protected path — through a symlink, a bind mount or a ".." walk — is cleared, and cleared silently: the resolved-target ` + `half of the judgment needs a filesystem to resolve in. Nothing about this run changed. Wire an execution environment (RunnerDeps.executionEnv) ` + `to get the target view back, or accept the spelling-only floor knowingly — the leg's wiring manifest reports which one is in force at ` + `writeProtection.targetView.`, detail: { tools: covered, sessionId: input.sessionId, runId: input.runId }, }); return leg; }