UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

279 lines (278 loc) • 19.4 kB
/** * The permission-rule LANES the unified store feeds (prepare-path machinery): the two gate * lanes, and — since the policy-chain phase took the chain assembly — the rule-offer factory every ask-mint site calls. * * `prepareTask` used to assemble the personal lane inline (a store read + a hand-spliced session * overlay + two admission arms) and the org lane from a second seam. Both now read the ONE store query — * `PermissionRuleStore.effective({ sessionId })` — through the run's one reader of it (`person-path-rows.ts`). * * ## One read per adjudicated call * * The gate consults the org lane (`orgRules.adjudicate`) and then the personal lane * (`persistedRules.adjudicate`) on every decision, in that order (the personal lane's deny/ask rows must * be able to tighten an allow, so it can no longer wait for a surviving ask). Both take the call's ONE read, * pinned to its `toolCallId` by whichever of the call's consumers asked first (the read station ahead of them * in the fold, or the org lane): a governed deployment pays one store query — one org-source fetch — per call, * and every consumer of the call judges one snapshot, which is the honest shape for one decision. A partition * the read could not read takes the run's one failure law (the reader's): its deny and ask rows as last read, * none of its allow rows. * * ## Where the lanes do NOT go * * Neither is a `ToolPolicy`, and neither joins `policyLayers`: inside the fold an allow is the lowest * position (a permanent no-op) and a layer's mere presence flips `hasEffectAwareGate`, which would * silence the ungated-write warning for deployments that wired no policy at all. */ import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js"; import { persistedRuleMandateOf, type RuleClearanceFacts, type RuleOffersAbsence } from "../ask-origin.js"; import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js"; import type { PersistedRule, PersistedRuleVerdict, RuleOffer, ScopeRootIdentity, SegmentCoverage } from "../permission-rule-model.js"; import { type PathRuleBases } from "../permission-rule-model.js"; import { type HeadLeads } from "../path-rule-binding.js"; import type { ToolCallLike } from "../tool-registry.js"; import { type OrgRuleResolution } from "../permission-rule-org.js"; import { type EffectivePermissionRules, type PermissionRuleStoreProvider, type PersonRowsRead } from "../permission-rule-provider.js"; /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for it and the name tools); the other grammars' tools are derived (`ruleToolGrammarOf`). */ export declare const PERSISTED_RULE_TOOL = "Bash"; /** * The evidence an INHERITED-lane ask carries. * * The three inherited mint sites present an ANCESTOR policy's own decision, resolved at the ancestor's * approver, from inside the policy fold — upstream of the gate's org layer and its rule lane, neither of * which has spoken for this call at that point. So the record is stamped rather than omitted (every ask * the engine mints carries one, and a consumer never has to tell a missing field from a field that means * something), and each half names the accurate absence for THIS deployment: a partition that is not * configured at all reads `"not_wired"`, and one that exists but has not adjudicated this ask reads * `"not_adjudicated"`. Reporting the second for a deployment that has neither would assert a governance * layer that does not exist. * * FROZEN: one record is handed to every inherited ask on this leg, and `resolveAsk` shallow-spreads the * request — so an `onAsk` consumer holding `req.ruleEvidence` would otherwise be able to edit what the * next ask on this leg reports. */ export declare function inheritedAskRuleEvidence(provider: PermissionRuleStoreProvider | undefined): AskRuleEvidence; /** * The snapshot REVISION an org answer was read off, screened before it becomes * evidence. * * The org partition's own install and serving gates screen a snapshot's revision, * so a healthy in-tree partition cannot reach here with junk; a FOREIGN store (the interface is public) * can. A non-finite value is refused rather than carried — an evidence field is worthless if `NaN` can * wear the same shape as revision 41 — and the defect is announced through `onDefect` instead of quietly * becoming "this deployment reported no revision". * * The ADJUDICATION is untouched either way: the caller's verdict is the whole decision, and a * defective ATTRIBUTION must never change what a governance layer decided. */ export declare function orgRevisionEvidenceOf(resolution: OrgRuleResolution, onDefect: (message: string) => void): { revision?: number; }; /** * Project the model's verdict onto the gate's rule-lane answer: the * behavior that spoke, and each member's canonical text plus its own add DOTS. For an allow the set is * the admitting COVERAGE SET (a single rule admitting the whole command is a one-element set; a compound * admitted by the per-segment conjunction is the set of covering rules in segment order — the model's * own reporting-order contract); for a deny/ask it is the one reaching rule the precedence walk chose. * * `adds` on a listed rule are the LIVE adds — the store's read already dropped tombstoned dots — so * this is the set that actually decided the call, which is the identity a later audit reconciles a * shadowed ask against. The sanitized display text deliberately is not one. Copied rather than * aliased: the evidence must not change under a store that reuses its row objects. */ export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | undefined; /** The COMMAND arm's projection of a verdict: a call the lexer could not read against a standing deny/ask * row is the lane's UNREADABLE answer carrying the lexer's reason — it fails closed (a real-approval ask no * automatic lane clears) — never a matched ask rule, which would let a blanket `onAsk: "allow"` clear a * question nobody can answer. Every other verdict is {@link persistedRuleHitOf}'s hit. */ export declare function persistedRuleAnswerOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | PersistedRuleUnreadable | undefined; /** * The persisted-rule lane's PATH arm, for every path-targeting tool (and the entry lists' path rows): the call's target * read through the ONE reader every lane shares ({@link callTargetViewsOf} — the reading's views, else the call's own * lexical view), its rows anchored by `ctx.anchorsOf` (a stored row: a project row at its scope root, the rest at the * chain's top leg; a list's row: where the list was installed) and judged where their heads lead too (`ctx.leads`). The * precedence walk, the admission predicates and the Read-only allow are the model's (`adjudicatePersistedPathRules`); which * project rows apply is judged against the leg's own root (`ctx.root`). An ALLOW here clears ASKS only — the read fences * judge inside the tool exactly as they would with no rule anywhere — and an unresolvable path is no verdict on the allow * side and the fail-closed ask on the tightening side. * * TWO readings of the call's path slot, one per direction: the tightening arms judge the PROTECTIVE reading (the object * face's declaration, the catalog's for the name as the floor); the allow arm the DECLARED one (the face's OWN * declaration only — a same-name tool that declares no path target is unconfirmable and no grant admits it). */ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face" | "targetReading" | "root" | "cwd">, ctx: { root: string | undefined; sessionId: string | undefined; anchorsOf: (rule: PersistedRule) => PathRuleBases; leads: ReadonlyMap<string, string | undefined>; scopeIdentity?: ScopeRootIdentity; }): PersistedRuleHit | PersistedRuleUnreadable | undefined; /** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so * a deployment without one keeps a decision path byte-identical to a build without the feature. */ export interface PermissionRuleLanes { personal: { adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>; } | undefined; org: { adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>; } | undefined; } /** The ORG lane: the org's verdict about one call, off the store view `read` supplies — the gate's `orgRules` face over the * call's pinned read ({@link createPermissionRuleLanes}), and {@link orgVerdictBeforeClaim} over a view of its own. * `onDegraded` receives the snapshot's degraded line ({@link describeUnreadableOrgRows}) on every adjudication over a * snapshot carrying a row this layer cannot read. */ export declare function orgRuleLane(cfg: { read: (toolCallId: string) => Promise<Pick<EffectivePermissionRules, "org">>; questionToolMounted: boolean; questionToolName: string; onRevisionDefect: (message: string) => void; onDegraded: (message: string) => void; }): NonNullable<PermissionRuleLanes["org"]>; /** The org's CURRENT verdict about a parked call, read by the resume entry before the claim (the row's session and principal, * bounded like the gate's wait; unreadable ⇒ unavailable). The mount and a revision defect are the resumed leg's facts, so * the reserved name is exempted and the defect left to that leg: this read can leave a question unasked, never invent one. */ export declare function orgVerdictBeforeClaim(cfg: { provider: PermissionRuleStoreProvider; principal: string | undefined; localOwnerDeclared: boolean; sessionId: string; signal: AbortSignal | undefined; }, req: ToolCallRequest): Promise<OrgGateVerdict>; /** * Build the gate's two permission-rule lanes over the unified store, resolved once per task. * * The LOCAL-OWNER declaration is judged here, at prepare. It is a declaration, * so one that cannot be honored is refused LOUDLY rather than degrading into "this deployment's standing * approvals silently stopped applying" (the §7.4 org bit's rule, applied to the availability direction: * the failure is deterministic and belongs at wiring time). */ export declare function createPermissionRuleLanes(cfg: { provider: PermissionRuleStoreProvider | undefined; localOwnerDeclared: boolean; principal: string | undefined; /** The session whose session-scoped rows apply; `undefined` (a judgment no session makes) ⇒ none does. */ sessionId: string | undefined; /** The isolation-aware task root — the same value the file fence, the prompt cwd and the session rules * use, so a project-scoped rule agrees with every other place that says "this project" (which project rows apply). * `undefined` (a judgment outside any task) ⇒ no project row applies and a row anchored at the root cannot be anchored. */ root: string | undefined; /** The same root's canonical spelling (`undefined`: none could be read): a project row applies when its scope root covers * either spelling of it — the row's root resolved where it leads ({@link projectScopeIdentity}). */ rootCanonical: string | undefined; /** The anchors of the person's global and session rows and of every `~/` row: the root and home of the chain's top leg * (a project row anchors at its own scope root). */ chainTop: PathRuleBases; /** The leg's resolution of the locations rule heads pass through (the rows' twins). */ leads: HeadLeads; /** The LIVE tracked working directory: the base a relative cd resolves against * after an earlier observable `cd`; read per call. */ liveCwd: () => string | undefined; /** The EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), screened here: a declared * value that is not an absolute path is refused, loudly (a bad declaration is a wiring defect, not an absence). The * `~/` rows anchor at the chain's top leg's home (`chainTop.home`); `undefined` there = no home was declared: a `~/` * deny/ask row then reads UNREADABLE for every call (a fail-closed ask), never the engine process's home. */ home: string | undefined; /** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */ questionToolMounted: boolean; questionToolName: string; onRevisionDefect: (message: string) => void; /** Sink for a judgment against the rows that threw, and for the org lane's degraded-snapshot line. */ onDisclosure: (message: string) => void; /** The run's one reader of the person's rules: the call's ONE read, pinned, that the read station, the org lane and the * personal lane all take (the lanes consume a reader; they do not make one). */ rows: { pin(toolCallId: string): Promise<PersonRowsRead>; }; /** The allow layer's auto strip over a stored ALLOW row (true ⇒ out of this call's table BEFORE the coverage is chosen). */ stripsAllow?: (rule: PersistedRule, req: ToolCallRequest) => boolean; }): PermissionRuleLanes; /** The surviving-ask facts the rule-offer factory reads (see {@link createRuleOffersOf}): the * synchronous mint sites spread the surviving decision, which carries them; the park leg threads * each as its own parameter. The clearance half is {@link RuleClearanceFacts} — the SAME declaration * the ask's own mandate bit is derived from, so a station cannot hand one face a fact it withheld * from the other. */ interface RuleOffersAskFacts extends RuleClearanceFacts { /** The surviving ask's engine-stamped per-segment coverage table — the * batch offer carries exactly the segments this table calls uncovered. Absent ⇒ all * segments read as uncovered (over-offer, the safe direction). */ segmentCoverage?: readonly SegmentCoverage[]; } /** * The rule forms that could cover this exact call, for a surface's "stop asking me * this" option (extracted from the prepare body, the spliceSessionOverlayRows precedent; the maker * captures the per-task wiring once and the returned factory serves all five ask-mint sites — the * approval-preview field is a standing example of what happens otherwise: it reached two of the * five, so a delegated child's card was silently poorer than the top-level one). * * Empty unless a rule lane is armed — offering an option that redemption would refuse is worse than * offering none — and empty for any call the lane cannot speak for (a compound, a redirection, a * substitution, or a tool that is not the shell tool). * * Empty, too, on any ask a persisted rule could never CLEAR — "allow rules silence the classifier's * questions, never a mandated one". Two doors, matching the two halves of that boundary: * · the marks half, judged from the SAME resolved sources the gate input is built from, through the * gate's own single-source predicate (an operator's shellGate:"always", the tool's own * egress/irreversibility marks, and — separately — the PER-CALL member that predicate also takes: a * demotion this call's reversibility probe declared structural, read off the ask the gate * stamped rather than off the tool seat, because it is true of one call and not of the seat) — * a drift here would offer a rule the lane then refuses to honor; * · the decision half (`ask`, REQUIRED at every mint site so a site added later cannot skip it): * `requiresRealApproval` (org/governance — only judgment clears it), the shadowed-rule * disclosure (the standing proof that a matching rule does not clear THIS ask), a hook-raised * ask (`decisionReason === "hook"` — the lane's own clearing conjuncts refuse hook asks, so a * rule minted from one would never silence it), an EXPLICIT `ask` rule match (`matchedAskRule` * — see below), and an inherited-unresolved marked call (the * ask is an ANCESTOR's authority, which this task's rule lane never adjudicates). Offering * "stop asking me this" on a card that will keep asking lets a person mint a rule that never * takes effect where they minted it. * * (CC 2.1.245 toolPolicy 对表, F11: "wildcard `ask` … keeps every matching tool behind a * per-call prompt (no persistent always-allow)") — `matchedAskRule` was the one conjunct of the * lane's own clearing predicate this door did not mirror, and the asymmetry was live: the * persisted-rule lane refuses to clear ANY ask carrying it (`hooks.ts`, the * `decision.matchedAskRule === undefined` conjunct on its allow arm), so a person who wrote a * standing `Bash` `ask` rule and had no allow rule yet was still offered "allow, and stop asking * me this" — and the rule they minted came back on the very next call as a SHADOWED match * (the allow layer's disclosure, which this door was already refusing). The offer was only ever * suppressed one call too late, after the useless rule existed. The shadowed conjunct stays: it * covers the person who ALREADY has the allow rule; this one covers the person about to mint it. * * Every door above that answers "no offers" now also names WHICH door, on the * `ruleOffersAbsence` seat (`AskRequest`'s and the durable row's, ONE factory so the two faces * cannot disagree). The doctrine it serves is the loud-bad-value one, applied to an absence: a * surface reading an empty card could not tell "there is nothing this lane could offer" from * "offers were deliberately suppressed", and a person reading it could not tell "write the rule * yourself" from "no rule can excuse this approval". The seat is CLOSED ({@link RuleOffersAbsence}), * mutually exclusive with `ruleOffers` by construction, and structural-door silent (see the first two guards). */ export declare function createRuleOffersOf(cfg: { /** `permissionRuleLane !== undefined` at wiring time — the armed-lane structural door. */ laneArmed: boolean; principal: string | undefined; /** `deps.localOwnerRules === true` — the declared local-owner exception to the anonymous floor. */ localOwnerDeclared: boolean; /** The live mark sources the gate input is built from — passed as REFERENCES (read per call, so a * late-registered tool seat is seen exactly as the gate sees it). */ egressTools: { has(name: string): boolean; }; irreversibilityTier: { get(name: string): Parameters<typeof persistedRuleMandateOf>[0]["irreversibility"]; }; shellGatedTools: { has(name: string): boolean; }; toolMarkSources: { get(name: string): ReadonlySet<string> | undefined; }; taskRoot: string | undefined; /** The live tracked-cwd ref (read per call — the relative-cd resolution base). */ cwdRef: { current: string; } | undefined; deniesDirectoryRead: ((directory: string) => boolean) | undefined; }): (call: Pick<ToolCallLike, "toolName" | "face">, args: unknown, ask: RuleOffersAskFacts | undefined) => { ruleOffers?: readonly RuleOffer[]; ruleOffersAbsence?: RuleOffersAbsence; execCwd?: string; }; export {};