@sema-agent/core
Version:
Stateless, task-oriented AI agent core
85 lines (84 loc) • 6.02 kB
TypeScript
/**
* 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: a row nobody could read may forbid the call, so for the DECISION it
* fails closed (`applyPersistedTightening` reads `{ unreadable }` without a reason as the store form: a real-approval
* ask), and for the RECORD it is `"unavailable"`. A call the lane could not read against the rows (`reason`) fails
* closed in the arms the same way, as the call form. 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[];
}>;