UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

85 lines (84 loc) • 5.95 kB
/** * The two rule LANES' answers — the org overlay's and the person's persisted rules' — screened and read for one * call, shared by every consumer that judges a call against them (the live gate's lane station, the resumed * execution boundary, the synthetic-Read judge). One normalizer and one bounded read per lane: a second copy of * either would be a second answer to "what did the lane say", and the gate's fail-closed (org) / skip * (persisted store) readings would drift apart between consumers. */ import type { ToolCallRequest } from "./tool-policy.js"; import type { OrgGateVerdict, PersistedRuleAnswer, PersistedRuleHit } from "./hooks.js"; /** * design/252 review r3 — read a foreign {@link OrgGateVerdict} the way the personal-rule answer is read: * OWN DATA properties only, never the prototype chain, never an accessor. * * Unlike the personal-rule normalizer below, this one accepts ANY non-array object as the carrier — * not only a plain record. The two seams tighten in opposite directions when a shape is refused: the * personal lane is a LOOSENING seam, so refusing a class instance degrades toward asking; this answer * carries the org's DENY, and folding a structurally valid `{status:"available", verdict:{behavior: * "deny"}}` class instance into `unavailable` would LOOSEN it — from a deny nobody can approve into a * real-approval ask a person can clear. A class instance's fields are its own data properties, so the * own-data read below already gives the full pollution guarantee (inherited members never authorize); * the prototype test added nothing here but the downgrade. * * The two seams are the same class of trust boundary and were not being read the same way. What that * cost here is worse than on the personal lane, because this answer is GOVERNANCE: * · an inherited `revision` (a polluted `Object.prototype`) stamped a fabricated snapshot version onto * a human's approval request — a WRONG record, which is worse than an absent one; * · an inherited `verdict` invented an org rule, and with it an org ask or deny the lane never gave; * · an answer that is not a record at all read as "available, nothing to say", i.e. the governance * fail-open the availability contract exists to prevent. * Anything this function cannot read as a well-formed answer becomes `unavailable` — the fail-CLOSED * word, never the empty one. */ export declare function normalizeOrgGateVerdict(answer: unknown, unreadable: string): OrgGateVerdict; /** * Normalize the accepted {@link ToolGateInput.persistedRules} answers into one reading. * * `{}` = a clean negative (no rule admits this call). `{ unreadable: true }` = the lane could not read * its source. `{ hit }` = a match, whose `rules` is the non-empty coverage set (design/375). An answer * outside every accepted shape — a number, `null`, an object with neither `rules` nor `unreadable`, an * EMPTY `rules` array, the retired pre-375 `{ rule, dots? }` single-rule object — is read as a clean * negative rather than a match: this is a LOOSENING seam, so an answer nobody can name degrades toward * asking, never toward an allow built on it. All-or-nothing across the set's TEXTS for the same * reason: a set with one unreadable member is a different (smaller) claim than the lane made, and a * decision must not stand on a claim nobody made — one bad member drops the whole answer to the clean * negative. Per-member DOTS stay individually optional (identity lost ⇒ `"not_reported"`, exactly the * single-rule contract). * * The hit's dots are COPIED, not aliased. The array travels onto an ask that may sit in front of a * person for a long time; a lane that retains and mutates its own array would otherwise change what the * approver is looking at, and what an audit later reads, after the evidence was stamped. */ export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): { hit?: PersistedRuleHit; unreadable?: true; reason?: string; coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[]; }; /** * The ORG lane's one read for one call — every consumer's (the live gate's org layer, the synthetic-Read * judge). The CALL is inside the boundary (a synchronous throw — a non-function face, a revoked proxy — * happens before any `.catch` on its result could be installed), it is invoked directly (a lane that reads * its own state before its first `await` sees the same turn), it is BOUNDED by the dependency timeout and * the signal, and everything that is not a well-formed answer — a throw, a hang, a garbage value — reads * as `unavailable`, the fail-closed word, never as "no org rule spoke". */ export declare function readOrgRuleLane(org: { adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>; }, req: ToolCallRequest, signal: AbortSignal | undefined, timeoutMs?: number): Promise<OrgGateVerdict>; /** * The persisted-rule lane's one read for one call — every consumer's (the live gate, the synthetic-Read * judge), normalized once. A lane that throws, hangs past the dependency timeout, or answers something * unreadable is a lane that could not answer: for the DECISION its rows take no part (the tightening arms * do not fire — `applyPersistedTightening` reads `{ unreadable }` without a reason that way), and for * the RECORD it is `"unavailable"`. A call the lane could not read against the rows (`reason`) fails closed * in the arms. Absent lane ⇒ the clean negative. */ export declare function readPersistedRuleLane(lane: { adjudicate: (req: ToolCallRequest) => PersistedRuleAnswer | Promise<PersistedRuleAnswer>; } | undefined, req: ToolCallRequest, signal: AbortSignal | undefined, timeoutMs?: number): Promise<{ hit?: PersistedRuleHit; unreadable?: true; reason?: string; coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[]; }>;