UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

54 lines (53 loc) • 3.92 kB
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; }