UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

90 lines (89 loc) • 5.73 kB
import { ownerStoreOf } from "../permission-rule-provider.js"; import { PATH_RULE_FACES, READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js"; import { isAbsolutePathForm, pathFamilyOf } from "../../tools/fs/safety.js"; import { pathRuleDenyRowOf } from "../permission-rules.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js"; import { thrownText } from "../thrown-value.js"; import { emitTrace } from "../trace.js"; import { deliverEngineNotice } from "../engine-notice.js"; export async function persistedPathRowsOf(cfg) { const none = { readDirectories: [], denyRows: [] }; const provider = cfg.provider; if (provider === undefined || !readsPersistedPathRows(provider, cfg.principal, cfg.localOwnerDeclared)) return none; let view; try { view = await withGateTimeout(ownerStoreOf(provider, cfg.principal, cfg.localOwnerDeclared).effective({ sessionId: cfg.sessionId }), DEFAULT_GATE_AWAIT_TIMEOUT_MS, cfg.signal, "persistedPathRows"); } catch (err) { return gateWaitFailureOf(err) === "aborted" ? none : skipped(cfg, thrownText(err)); } if (view.unreadable !== undefined) skipped(cfg, view.unreadable, view.unreadableSources); const call = { tool: READ_RULE_TOOL, cwd: cfg.root, sessionId: cfg.sessionId }; const bases = { root: cfg.root, ...(cfg.home !== undefined && isAbsolutePathForm(cfg.home) ? { home: cfg.home } : {}) }; const family = pathFamilyOf(bases); const readDirectories = []; const denyRows = []; for (const rule of view.rules) { if (rule.status !== "live") continue; if (rule.behavior === "allow" && rule.tool === READ_RULE_TOOL && rule.match === "subpath" && eligiblePersisted(rule, call, "allow")) { if (!readDirectories.includes(rule.command)) readDirectories.push(rule.command); continue; } const faces = PATH_RULE_FACES[rule.tool]?.deny; if (rule.behavior !== "deny" || faces === undefined || (rule.match !== "path" && rule.match !== "subpath") || !eligiblePersisted(rule, { ...call, tool: rule.tool }, "deny")) continue; const missing = ruleBasesNeeded(rule).filter((b) => !isUsablePathBase(b === "cwd" ? bases.root : bases[b], family)); if (missing.length > 0) { cfg.onDisclosure(`the persisted deny rule ${inlineUntrusted(rule.rule, 200)} needs a ${missing.join(" / ")} base this task does not declare — the shell's file commands and the search tools do not judge it on this leg`); continue; } denyRows.push(pathRuleDenyRowOf({ parsed: rule, ruleText: rule.rule, faces }, bases)); } return { readDirectories, denyRows }; } function skipped(cfg, why, sources) { cfg.onDisclosure(`the persisted path rules could not be read at prepare, so this run's read directories and read / write deny tables take none of ${sources !== undefined ? `the ${sourcesWord(sources)} rows` : "their rows"} (the next prepare reads again): ${why}`); cfg.onUnread("prepare", sources); return { readDirectories: [], denyRows: [] }; } function sourcesWord(sources) { return sources.length <= 1 ? sources.join("") : `${sources.slice(0, -1).join(", ")} and ${sources[sources.length - 1]}`; } const SKIPPED_STORE_SENTENCE = { prepare: (which) => `${which} could not be read when this run started, so this run's file and shell checks skip their path rows (Read / Edit deny rows and Read directory rows) — a deny row among them protects nothing on this run. Each call's own rule check reads the rules again (a call whose own read fails too is judged without them), and the next run reads the path rows again.`, call: (which) => `${which} could not be read for a call on this run, so that call's own rule check skipped them: none of their allow, ask or deny rows took part in it, and a deny row among them protected nothing there (path rows the run took when it started still apply to its file and shell checks). Every later call reads them again, and they apply again as soon as a read succeeds.`, }; export function ruleStoreUnreadableNotice(onNotice, ids) { let sent = false; return (at, sources) => { if (sent) return; sent = true; const which = sources !== undefined && sources.length > 0 ? `your persisted permission rules from the ${sourcesWord(sources)} sources` : "your persisted permission rules"; deliverEngineNotice(onNotice, { code: "permission.rule_store_unavailable", message: SKIPPED_STORE_SENTENCE[at](which), detail: { at, sessionId: ids.sessionId, runId: ids.runId } }); }; } function readsPersistedPathRows(provider, principal, localOwnerDeclared) { return provider.partitions.durable && ((principal !== undefined && principal !== "") || localOwnerDeclared); } export function persistedPathRowsOfTask(t) { const provider = t.deps.permissionRuleStore; if (!t.handsEnabled || provider === undefined || !readsPersistedPathRows(provider, t.principal, t.deps.localOwnerRules === true)) return undefined; return persistedPathRowsOf({ provider, localOwnerDeclared: t.deps.localOwnerRules === true, principal: t.principal, sessionId: t.sessionId, root: t.root, home: t.home, onDisclosure: (message) => emitTrace(t.deps.tracer, () => ({ kind: "permission.rule_store_unreadable", version: 1, taskId: t.taskId, message, ts: Date.now() })), onUnread: t.onUnread, signal: AbortSignal.any([...t.signals]), }); }