@sema-agent/core
Version:
Stateless, task-oriented AI agent core
54 lines (53 loc) • 3.92 kB
JavaScript
import { deliverEngineNotice } from "../engine-notice.js";
import { protectivePathTargetOf } from "../tool-registry.js";
import { compileWriteProtection, writeProtectionMountOver } from "../write-protect.js";
const ABSENCE_REASON = { symlink: "unresolved-symlink", target: "unresolvable-target" };
export function mountWriteProtection(input) {
const announced = new Set();
const matcher = compileWriteProtection(input.deps.writeProtectedPaths, {
...(input.deps.memoryEngineDir !== undefined ? { dataRoot: input.deps.memoryEngineDir } : {}),
...(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.write_protection_unresolved",
message: `This run's write-protection judge could not resolve a write target in its execution environment ` +
`(${reason === ABSENCE_REASON.symlink ? "a symlink whose real target is unknowable" : "the environment refused or errored on the spelling"}: ${absence.cause}). ` +
`A view that cannot answer does not vote, so that write was judged by its SPELLING against the table and nothing else — ` +
`a target reaching a protected path only through an alias, a bind mount or a ".." walk is cleared. Nothing about this run was ` +
`refused because of it, and nothing here is a defect in the write itself: an environment that cannot answer path questions is a ` +
`deployment fact. 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 },
});
},
});
if (matcher === undefined)
return undefined;
const mount = { ...writeProtectionMountOver(matcher, input.rootPath), judgeTarget: async (target, opts) => (await matcher.judge(target, { ...(opts?.signal !== undefined ? { signal: opts.signal } : {}), ...(opts?.canonical === true ? { canonical: true } : {}) })).kind === "hit" };
if (mount.targetView !== "spelling-only")
return mount;
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 mount;
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 mount;
}