@sema-agent/core
Version:
Stateless, task-oriented AI agent core
58 lines (57 loc) • 4.35 kB
JavaScript
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;
}