@sema-agent/core 7.36.0 → 7.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/core/ask-origin.d.ts +35 -5
- package/dist/core/ask-origin.js +23 -4
- package/dist/core/checkpoint-store.d.ts +13 -3
- package/dist/core/decision-receipt.d.ts +21 -0
- package/dist/core/decision-receipt.js +63 -0
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/gate-fold.js +2 -1
- package/dist/core/gate-lanes.js +8 -13
- package/dist/core/gate-outcome.d.ts +7 -5
- package/dist/core/gate-outcome.js +5 -1
- package/dist/core/hooks.d.ts +11 -2
- package/dist/core/hooks.js +3 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/runner/denial-limit-arms.d.ts +28 -18
- package/dist/core/runner/denial-limit-arms.js +11 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
- package/dist/core/runner/inherited-ask-grants.js +5 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +1 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -8
- package/dist/core/runner/prepare-policy-chain.js +43 -25
- package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +74 -84
- package/dist/core/tool-policy.js +71 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.37.0.json +405 -0
|
@@ -381,6 +381,24 @@ export async function checkpointStoreContract(make, runAssertion) {
|
|
|
381
381
|
assert.equal("mandate" in byId.get("mw-absent"), false, "absent must project the key OMITTED");
|
|
382
382
|
assert.equal("mandate" in byId.get("mw-bogus"), false, "a word outside the closed set must read as absent, never project verbatim");
|
|
383
383
|
});
|
|
384
|
+
run("#1214 ask class pair: a tool_approval row's `askClass` / `classes` survive the round-trip verbatim (the array whole, in its order); an absent pair stays absent", async () => {
|
|
385
|
+
const store = make();
|
|
386
|
+
const single = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-single" });
|
|
387
|
+
Object.assign(single.pendingAction, { askClass: "denial_limit_fallback", classes: ["denial_limit_fallback"] });
|
|
388
|
+
const several = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-several" });
|
|
389
|
+
Object.assign(several.pendingAction, { askClass: "transcript_write", classes: ["transcript_write", "org_rule"] });
|
|
390
|
+
const absent = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-absent" });
|
|
391
|
+
for (const cp of [single, several, absent])
|
|
392
|
+
await store.put(cp.token, cp);
|
|
393
|
+
for (const [cp, askClass, classes] of [[single, "denial_limit_fallback", ["denial_limit_fallback"]], [several, "transcript_write", ["transcript_write", "org_rule"]]]) {
|
|
394
|
+
const back = (await store.get(cp.token)).pendingAction;
|
|
395
|
+
assert.equal(back.askClass, askClass, "the row's askClass must survive the round-trip verbatim");
|
|
396
|
+
assert.deepEqual(back.classes, classes, "the row's classes must survive the round-trip as the same array, in the same order");
|
|
397
|
+
}
|
|
398
|
+
const bare = (await store.get(absent.token)).pendingAction;
|
|
399
|
+
assert.equal("askClass" in bare, false, "an absent askClass must stay OMITTED on the row");
|
|
400
|
+
assert.equal("classes" in bare, false, "an absent classes must stay OMITTED on the row");
|
|
401
|
+
});
|
|
384
402
|
await settle();
|
|
385
403
|
}
|
|
386
404
|
const sortByToken = (a, b) => a.token.localeCompare(b.token);
|
|
@@ -203,6 +203,10 @@ export declare const NAME_READINGS: {
|
|
|
203
203
|
readonly rank: "member";
|
|
204
204
|
readonly file: string;
|
|
205
205
|
};
|
|
206
|
+
readonly "report.markSources": {
|
|
207
|
+
readonly kind: "any";
|
|
208
|
+
readonly file: string;
|
|
209
|
+
};
|
|
206
210
|
readonly "verify.wroteThisRun": {
|
|
207
211
|
readonly kind: "wire";
|
|
208
212
|
readonly file: string;
|
|
@@ -50,6 +50,7 @@ export const NAME_READINGS = {
|
|
|
50
50
|
"report.riskAxes": seat("tier", CHAIN),
|
|
51
51
|
"report.riskAxes.egress": any(CHAIN),
|
|
52
52
|
"report.riskAxes.negatives": seat("member", CHAIN),
|
|
53
|
+
"report.markSources": any(CHAIN),
|
|
53
54
|
"verify.wroteThisRun": wire(HANDLERS),
|
|
54
55
|
"verify.wroteThisRunSeed": wire("src/core/runner/run-attachment-seats.ts"),
|
|
55
56
|
"event.toolStartEnd": spelling(HANDLERS),
|
|
@@ -262,18 +262,9 @@ export declare function screenApproverAttribution(v: unknown): {
|
|
|
262
262
|
* or `approver` member on its verdict is putting an unread property on it.
|
|
263
263
|
* RB-479-B① (ruled 2026-08-02): the legacy `reason` field is REMOVED — one name for one thing.
|
|
264
264
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* = {…}`, or a `check` written with an explicit `: PermissionResult` return annotation. The ordinary
|
|
269
|
-
* policy shape, `{ check: () => ({…}) }` with an INFERRED return type (also the method-syntax and
|
|
270
|
-
* assign-then-return forms), compiles GREEN with a stray `reason`. That is not a hole to plug in the
|
|
271
|
-
* type system — it is how contextual typing works — so the enforcement is at RUNTIME instead: every
|
|
272
|
-
* point that CONSUMES a decision REFUSES one carrying an own `reason` property, fail-closed (deny, with
|
|
273
|
-
* a migration message) — the {@link combinePolicies} fold, the PreToolUse hook face, and the durable
|
|
274
|
-
* resume-edit rechecks (see {@link refuseOutOfContractDecision}). Same doctrine as the strict `approve`
|
|
275
|
-
* / `onAsk` folds in this file: an out-of-contract value on the permission face is a loud rejection,
|
|
276
|
-
* never a best-effort read.
|
|
265
|
+
* The TYPE carries that retirement only where a decision is a literal in an annotated position; an inferred `check`
|
|
266
|
+
* return compiles green with a stray `reason`, so the enforcement is at RUNTIME — every point that consumes a decision
|
|
267
|
+
* refuses one carrying an own `reason` property, fail-closed ({@link refuseOutOfContractDecision}).
|
|
277
268
|
*/
|
|
278
269
|
export type PermissionResult = {
|
|
279
270
|
action: "allow";
|
|
@@ -387,6 +378,13 @@ export type PermissionResult = {
|
|
|
387
378
|
* render the card by it. Optional on the TYPE only because a decision is caller-composable; every ask
|
|
388
379
|
* the gate hands to its resolver carries it. */
|
|
389
380
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
381
|
+
/** The ask's CLASS — the first member of {@link classes} (see {@link AskRequest.askClass}). ENGINE-STAMPED at the
|
|
382
|
+
* same single stamp as {@link origin}, from the class set the gate's lanes read; a policy that self-declares it is
|
|
383
|
+
* overwritten there, and no lane reads it back (they read the classification itself). */
|
|
384
|
+
askClass?: import("./ask-origin.js").AskClass;
|
|
385
|
+
/** The ask's whole class set, in {@link import("./ask-origin.js").ASK_CLASSES} order (see
|
|
386
|
+
* {@link AskRequest.classes}). Stamped beside {@link askClass}, under the same rule. */
|
|
387
|
+
classes?: readonly import("./ask-origin.js").AskClass[];
|
|
390
388
|
/** WHICH integrity guard minted this ask — the unverifiable recursive delete or the session-transcript
|
|
391
389
|
* write. ENGINE-STAMPED by the two guard policies; a fold-internal fact the class derivation reads
|
|
392
390
|
* ({@link import("./ask-origin.js").classifyAsk}) so the two guards, which mint the same
|
|
@@ -737,63 +735,53 @@ export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintCha
|
|
|
737
735
|
* different frozen deny data now digest differently.
|
|
738
736
|
*/
|
|
739
737
|
export declare function constraintChainDigest(chain: readonly ConstraintChainEntry[]): string;
|
|
738
|
+
/** Every member of the decision union — a member the union gains and this list lacks is a type error at `satisfies`.
|
|
739
|
+
* The receipt walks it: a contract member not held as an own enumerable data property is refused if a descriptor on the
|
|
740
|
+
* prototype chain holds one (an `undefined` data value spells absence). Not re-exported from `src/index.ts`. */
|
|
741
|
+
export type DecisionMember = PermissionResult extends infer U ? (U extends unknown ? keyof U : never) : never;
|
|
742
|
+
export declare const DECISION_MEMBERS: readonly DecisionMember[];
|
|
740
743
|
/**
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
* the
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* SCOPE of the carrier check (ruled after three review rounds converged on the same root): it screens
|
|
784
|
-
* STRUCTURALLY unstable carriers — getters, prototype-supplied values — i.e. shapes an ordinary
|
|
785
|
-
* deployment can write by accident. It does NOT try to defeat a Proxy whose descriptor trap reports a
|
|
786
|
-
* data property while its get trap stays stateful: same-process JavaScript has no trust boundary a
|
|
787
|
-
* function can enforce (an untrusted party who ships such a Proxy can as easily patch this module), so
|
|
788
|
-
* chasing that shape adds complexity without adding a guarantee. The screen's promise is against
|
|
789
|
-
* drift and accident, not against a untrusted co-resident.
|
|
790
|
-
*
|
|
791
|
-
* `reasonIsNonInput` (the HOOK seam's spelling — merged-code scan, 5.28 window): at that seam the
|
|
792
|
-
* field is documented as DISCARDED — every mint point downstream unconditionally re-stamps
|
|
793
|
-
* `decisionReason:"hook"` (fold, deny arm, delegated twin), and the allow path never reads it — so
|
|
794
|
-
* both reason arms are skipped there: refusing a value that cannot travel would fail-close a call
|
|
795
|
-
* over a field with no consumer, which inverted the discard contract the stamp exists to enforce.
|
|
796
|
-
* The retired-`reason` arm still applies (that one is about the MESSAGE channel, which does travel).
|
|
744
|
+
* The RECEIPT of a permission decision — the one point at which the engine takes a decision in from whoever composed it,
|
|
745
|
+
* on every lane: each layer of {@link combinePolicies}' fold, the gate's adjudication entry and its approved-edit re-check,
|
|
746
|
+
* the inherited lane's receipts of an ancestor policy's decision, the final-form re-check, the direct `check()` rechecks
|
|
747
|
+
* (the resume edit, the park re-projection, the changed-files probe), and the PreToolUse hook's result seam (its own
|
|
748
|
+
* reach, below). NOT re-exported from `src/index.ts`: an internal screen on inputs the engine receives.
|
|
749
|
+
*
|
|
750
|
+
* WHAT COMES BACK is the engine's own frozen copy, never the caller's object. A decision is caller-supplied data: a
|
|
751
|
+
* stateful accessor, a Proxy, or a policy that keeps and mutates an object it returned could otherwise answer two
|
|
752
|
+
* consumers with two values, and which consumer read which value would decide the call. So the caller's object is read
|
|
753
|
+
* ONCE, here, and every station after the receipt (the fold's ranking, the origin stamp, the classifier's eligibility, the
|
|
754
|
+
* mode arbitration, the approval request, the rewrite that executes) reads the copy; a copy handed to the screen again
|
|
755
|
+
* comes back as it is, unread. In order: (1) the CARRIER's type — a decision that is not an object (a string, a function
|
|
756
|
+
* holding the members) has no `action`; (2) the ONE READ — ONE function ({@link receiveContract}) for the decision and for the
|
|
757
|
+
* records it nests ({@link NESTED_CONTRACTS}), on an ORDINARY object (a Proxy, as the carrier or anywhere on its prototype
|
|
758
|
+
* chain, is refused before a trap of its could run): each contract member absent or an own enumerable DATA property, its value
|
|
759
|
+
* taken off the carrier's own descriptor (a member outside the contract is neither read nor carried — a policy's self-declared
|
|
760
|
+
* record on another name finds no reader); any other spelling refused by name — an accessor (never invoked), a member the
|
|
761
|
+
* prototype or a non-enumerable property supplies unless that descriptor spells absence (an `undefined` data value) — judged by
|
|
762
|
+
* descriptor, never read; (3) the retired `reason` field as an OWN property, judged without a read (authorship of the old
|
|
763
|
+
* spelling: a prototype exposing it never wrote it; ignoring it silently dropped the policy's text); (4) the VALUES, off the engine's
|
|
764
|
+
* one-read record, never a second read of the caller's object — the retired window absent, `action` in
|
|
765
|
+
* {@link DECISION_ACTIONS} (a consumer ranks a decision by testing two of its words, so any other value used to rank as an
|
|
766
|
+
* allow; a `canUseTool` result outside its set is a failure in CC) and `decisionReason` in {@link DECISION_REASONS} when
|
|
767
|
+
* present (absence stays legal); (5) the DETACH —
|
|
768
|
+
* the contract members' values structured-cloned ({@link tryCloneArgs}; one that cannot be, a function inside a rewrite,
|
|
769
|
+
* is refused), the engine's identity-keyed records carried across ({@link carryEngineAttestations}), the copy frozen and
|
|
770
|
+
* marked as this module's. A refusal is a deny in the screen's words, one code for every arm (`decisionReason: "rule"`),
|
|
771
|
+
* carrying no `updatedInput` from the refused object (a rewrite from an EARLIER, contract-shaped layer still rides out of
|
|
772
|
+
* {@link combinePolicies}' deny path). `null` / `undefined` carry nothing and take the carrier's refusal (the caller's own
|
|
773
|
+
* read of `.action` would throw instead, and that throw was being reported as an aborted check).
|
|
774
|
+
*
|
|
775
|
+
* SCOPE: the receipt runs NONE of the carrier's code — a Proxy (its traps are the caller's code) is refused unread, and on an
|
|
776
|
+
* ordinary object descriptor looks and `Object.getPrototypeOf` invoke nothing. What the receipt does not reach: code inside a
|
|
777
|
+
* member's VALUE, run by the detach itself (an accessor inside a rewrite is invoked by the structured clone, and what it answers
|
|
778
|
+
* there is what is cloned and what runs — one read, consistent) and by a refusal's rendering of a foreign word (`String(v)` on an
|
|
779
|
+
* out-of-set `action`, after the deny is decided). Same-process JavaScript has no trust boundary a function can enforce.
|
|
780
|
+
*
|
|
781
|
+
* `reasonIsNonInput` (the HOOK seam): a hook's `decisionReason` is DISCARDED there — every mint point downstream re-stamps
|
|
782
|
+
* `"hook"`, and the seam maps a hook's deny and ask and reads anything else as no objection, the policy still adjudicating
|
|
783
|
+
* the call — so that seam takes the two retired-field arms alone and its object back unchanged (`undefined` = no
|
|
784
|
+
* opinion): refusing a value that cannot travel would fail-close a call over a field with no consumer.
|
|
797
785
|
*/
|
|
798
786
|
export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
|
|
799
787
|
reasonIsNonInput?: boolean;
|
|
@@ -911,22 +899,11 @@ export declare function createApprovalPolicy(opts: {
|
|
|
911
899
|
*/
|
|
912
900
|
export declare function combinePolicies(...policies: ToolPolicy[]): ToolPolicy;
|
|
913
901
|
/**
|
|
914
|
-
* The shell tools whose `{ command }` arg a coarse command-name policy parses
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* tighten (else it becomes a policy side-door around a gated Bash)". The shellGate half was implemented;
|
|
920
|
-
* this half — the two ALWAYS-ON, core-owned parity gates that a caller's policy cannot remove — was keyed
|
|
921
|
-
* on a list containing only "Bash". The probe drove the whole thing end to end: the identical
|
|
922
|
-
* `rm -rf $(cat targets)` was denied as Bash and executed as Monitor, deleting a real directory with no
|
|
923
|
-
* gate at all; the same substitution makes `rm -rf ~/.ai-agent/sessions` a way to erase the audit trail.
|
|
924
|
-
*/
|
|
925
|
-
/**
|
|
926
|
-
* EXPORTED. A downstream governance layer keeps its own copy of this list so its
|
|
927
|
-
* per-command ask-list gates the same tools core does, and it had no way to read the real one — its guard
|
|
928
|
-
* had to probe a hand-written candidate list of names, which stays green for any name nobody thought to
|
|
929
|
-
* put in it. The list itself is the fact; publishing it removes the guess.
|
|
902
|
+
* The shell tools whose `{ command }` arg a coarse command-name policy parses; other tools are out of scope. `Monitor`
|
|
903
|
+
* belongs here: it runs an arbitrary shell `command` through the same env seam as Bash, so the always-on parity gates
|
|
904
|
+
* keyed on this list reach it too (a list of `Bash` alone let the identical command run ungated as `Monitor`). EXPORTED:
|
|
905
|
+
* a downstream governance layer keeps its own copy of this list so its per-command ask-list gates the same tools core
|
|
906
|
+
* does — the list itself is the fact, and publishing it removes the guess.
|
|
930
907
|
*/
|
|
931
908
|
export declare const COARSE_SHELL_TOOLS: string[];
|
|
932
909
|
/**
|
|
@@ -1389,6 +1366,19 @@ export interface AskRequest {
|
|
|
1389
1366
|
* therefore reading the deployment's own claim about its own ask, which is the direction that cannot
|
|
1390
1367
|
* loosen anything. */
|
|
1391
1368
|
readonly origin?: import("./ask-origin.js").AskOrigin;
|
|
1369
|
+
/** The ask's CLASS: the first member of {@link classes} in {@link import("./ask-origin.js").ASK_CLASSES} order — the
|
|
1370
|
+
* row key of {@link import("./ask-origin.js").MODE_ARBITRATION} a permission mode reads. Filled by every station that
|
|
1371
|
+
* fills {@link origin}, from the same classification the gate's lanes arbitrated this ask by; never written by a
|
|
1372
|
+
* caller or worker (a policy's self-declared value is replaced at the stamp), and a policy can move it only through
|
|
1373
|
+
* the tighten-only inputs the classification reads. ABSENT = unclassified: a request from a station that fills no
|
|
1374
|
+
* {@link origin} (the shared-artifact read confirmation), or a request built by a deployment itself. An approver
|
|
1375
|
+
* that answers by class treats absence as a class it will not answer. */
|
|
1376
|
+
readonly askClass?: import("./ask-origin.js").AskClass;
|
|
1377
|
+
/** Every class this ask belongs to, in {@link import("./ask-origin.js").ASK_CLASSES} order, `classes[0] ===
|
|
1378
|
+
* askClass`. One ask can carry several (an org verdict beside a hook's question); an approver that answers by class
|
|
1379
|
+
* reads the whole set — a class it will not answer anywhere in it is a question it does not answer. Present exactly
|
|
1380
|
+
* when {@link askClass} is. */
|
|
1381
|
+
readonly classes?: readonly import("./ask-origin.js").AskClass[];
|
|
1392
1382
|
/** Engine-judged risk axes of the action awaiting approval, for the human decision surface: a
|
|
1393
1383
|
* coarse `requiresRealApproval` cannot tell "cannot be undone" from "data leaves the machine",
|
|
1394
1384
|
* and those call for different scrutiny. Filled by the gate from the SAME resolved axes the
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -13,6 +13,7 @@ import { boundInputHashOf } from "./canonical-json.js";
|
|
|
13
13
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
14
14
|
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
15
15
|
import { thrownText } from "./thrown-value.js";
|
|
16
|
+
import { receiveContract, refusedOutOfContract } from "./decision-receipt.js";
|
|
16
17
|
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
17
18
|
import { protocolOf } from "./protocol-table.js";
|
|
18
19
|
import { isAbsoluteForFamily, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
|
|
@@ -244,56 +245,90 @@ function stableJson(v) {
|
|
|
244
245
|
export function constraintChainDigest(chain) {
|
|
245
246
|
return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
|
|
246
247
|
}
|
|
247
|
-
const ALLOW = { action: "allow" };
|
|
248
|
+
const ALLOW = Object.freeze({ action: "allow" });
|
|
248
249
|
const RETIRED_TEXT_FIELD = "reason";
|
|
249
250
|
const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs";
|
|
250
251
|
const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` +
|
|
251
252
|
`the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` +
|
|
252
253
|
`declared by a policy; denied fail-closed rather than silently replacing the declared number`;
|
|
254
|
+
const carriesRetiredWindow = (fallback) => typeof fallback === "object" && fallback !== null && fallback[RETIRED_WINDOW_MEMBER] !== undefined;
|
|
255
|
+
const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
|
|
256
|
+
const DECISION_ACTION_SET = new Set(DECISION_ACTIONS);
|
|
257
|
+
export const DECISION_MEMBERS = Object.keys({
|
|
258
|
+
action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, persistedRuleShadowed: 0, probeReason: 0,
|
|
259
|
+
probeCause: 0, probeMandated: 0, mandated: 0, mandate: 0, probeReadRootCandidate: 0, matchedAskRule: 0, ruleEvidence: 0, segmentCoverage: 0,
|
|
260
|
+
origin: 0, askClass: 0, classes: 0, guard: 0, ruleUnreadable: 0, authoredBy: 0, foldedAskClasses: 0,
|
|
261
|
+
});
|
|
262
|
+
const OPERAND_MEMBERS = Object.keys({ shown: 0, total: 0 });
|
|
263
|
+
const NESTED_CONTRACTS = [
|
|
264
|
+
["", "denialLimitFallback", Object.keys({ consecutive: 0, total: 0, limit: 0, autoDenyAfterMs: 0 })],
|
|
265
|
+
["", "probeCause", Object.keys({ code: 0, roots: 0, further: 0 })],
|
|
266
|
+
["probeCause.", "roots", OPERAND_MEMBERS],
|
|
267
|
+
["probeCause.", "further", OPERAND_MEMBERS],
|
|
268
|
+
];
|
|
269
|
+
const refusedAction = (what) => refusedOutOfContract(`a permission decision's "action" is ${what} — the closed set is ${DECISION_ACTIONS.join("/")}; denied fail-closed rather than reading a word outside it as an allow`);
|
|
270
|
+
function renderForeignValue(v) {
|
|
271
|
+
try {
|
|
272
|
+
return JSON.stringify(String(v).slice(0, 64));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return `[unprintable ${typeof v}]`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
253
278
|
const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
|
|
254
279
|
`a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
|
|
280
|
+
const ENGINE_RECEIPTS = new WeakSet();
|
|
255
281
|
export function refuseOutOfContractDecision(d, opts) {
|
|
282
|
+
if (opts?.reasonIsNonInput === true) {
|
|
283
|
+
if (d === null || d === undefined)
|
|
284
|
+
return d;
|
|
285
|
+
if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
|
|
286
|
+
return refusedOutOfContract(RETIRED_TEXT_FIELD_DENY_MESSAGE);
|
|
287
|
+
return carriesRetiredWindow(d.denialLimitFallback) ? refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE) : d;
|
|
288
|
+
}
|
|
256
289
|
if (typeof d !== "object" || d === null)
|
|
290
|
+
return refusedAction(`absent (the decision is ${d === null || d === undefined ? String(d) : `a ${typeof d}`}, not an object)`);
|
|
291
|
+
if (ENGINE_RECEIPTS.has(d))
|
|
257
292
|
return d;
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
const decision = receiveContract(d, DECISION_MEMBERS, "");
|
|
294
|
+
if (decision.kind === "refused")
|
|
295
|
+
return decision.refused;
|
|
296
|
+
if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
|
|
297
|
+
return refusedOutOfContract(RETIRED_TEXT_FIELD_DENY_MESSAGE);
|
|
298
|
+
const contract = decision.held;
|
|
299
|
+
const records = Object.assign(Object.create(null), { "": contract });
|
|
300
|
+
for (const [holder, member, members] of NESTED_CONTRACTS) {
|
|
301
|
+
const held = records[holder];
|
|
302
|
+
if (held === undefined || typeof held[member] !== "object" || held[member] === null)
|
|
303
|
+
continue;
|
|
304
|
+
const nested = receiveContract(held[member], members, `${holder}${member}.`);
|
|
305
|
+
if (nested.kind === "refused")
|
|
306
|
+
return nested.refused;
|
|
307
|
+
held[member] = records[`${holder}${member}.`] = nested.held;
|
|
260
308
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
309
|
+
if (carriesRetiredWindow(contract.denialLimitFallback))
|
|
310
|
+
return refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE);
|
|
311
|
+
if (!DECISION_ACTION_SET.has(contract.action))
|
|
312
|
+
return refusedAction(contract.action === undefined ? "absent" : renderForeignValue(contract.action));
|
|
313
|
+
if (contract.decisionReason !== undefined && !DECISION_REASON_SET.has(contract.decisionReason)) {
|
|
314
|
+
return refusedOutOfContract(`a permission decision carries an unrecognized "decisionReason" value (${renderForeignValue(contract.decisionReason)}) — ` +
|
|
315
|
+
`the closed set is ${DECISION_REASONS.join("/")}; denied fail-closed rather than letting consumers branch on a value this layer cannot classify`);
|
|
264
316
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return d;
|
|
270
|
-
if (!DECISION_REASON_SET.has(dr)) {
|
|
271
|
-
let rendered;
|
|
272
|
-
try {
|
|
273
|
-
rendered = JSON.stringify(String(dr).slice(0, 64));
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
rendered = `[unprintable ${typeof dr}]`;
|
|
277
|
-
}
|
|
278
|
-
return {
|
|
279
|
-
action: "deny",
|
|
280
|
-
message: `a permission decision carries an unrecognized "decisionReason" value (${rendered}) — ` +
|
|
281
|
-
`the closed set is ${DECISION_REASONS.join("/")}; denied fail-closed rather than letting consumers ` +
|
|
282
|
-
`branch on a value this layer cannot classify`,
|
|
283
|
-
decisionReason: "rule",
|
|
284
|
-
};
|
|
317
|
+
const detached = tryCloneArgs(contract);
|
|
318
|
+
if (!detached.ok) {
|
|
319
|
+
return refusedOutOfContract(`a permission decision's members could not be detached from the policy that returned them (${detached.reason}) — ` +
|
|
320
|
+
`the engine keeps its own copy of what it read; denied fail-closed`);
|
|
285
321
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`different value than this screen validated; denied fail-closed`,
|
|
293
|
-
decisionReason: "rule",
|
|
294
|
-
};
|
|
322
|
+
const receipt = Object.freeze(detached.value);
|
|
323
|
+
const ownRecord = (o, k) => (typeof o === "object" && o !== null && Object.hasOwn(o, k) ? o[k] : undefined);
|
|
324
|
+
for (const [holder, member] of NESTED_CONTRACTS) {
|
|
325
|
+
const record = ownRecord(holder.split(".").filter(Boolean).reduce(ownRecord, receipt), member);
|
|
326
|
+
if (typeof record === "object" && record !== null)
|
|
327
|
+
Object.freeze(record);
|
|
295
328
|
}
|
|
296
|
-
|
|
329
|
+
carryEngineAttestations(d, receipt);
|
|
330
|
+
ENGINE_RECEIPTS.add(receipt);
|
|
331
|
+
return receipt;
|
|
297
332
|
}
|
|
298
333
|
const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
|
|
299
334
|
function withTimeout(p, ms, onTimeout) {
|
package/dist/index.d.ts
CHANGED
|
@@ -192,7 +192,7 @@ export { createAllowDenyPolicy, createApprovalPolicy,
|
|
|
192
192
|
type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
|
|
193
193
|
export { type ReadRootGrantScreen } from "./core/read-root-grant-screen.js";
|
|
194
194
|
export { type AskOrigin, ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate } from "./core/ask-origin.js";
|
|
195
|
-
export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, RELEASABLE_ASK_CLASSES, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
|
|
195
|
+
export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, type ModeReleaseReason, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
|
|
196
196
|
export { modePolicyOf, type ModePolicy, PERMISSION_MODE_FOLD_REASONS, type PermissionModeFoldReason, type PermissionModeReceipt, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
|
|
197
197
|
export { readDenyClassifierRules, renderClassifierDenyRules, type DerivedDenyRule } from "./core/auto-mode-prompt.js";
|
|
198
198
|
export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource, type AutoModeDenySource } from "./core/runtime-caps.js";
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.
|
|
|
157
157
|
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
|
|
158
158
|
export {} from "./core/read-root-grant-screen.js";
|
|
159
159
|
export { ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, isPersistedRuleMandate } from "./core/ask-origin.js";
|
|
160
|
-
export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, RELEASABLE_ASK_CLASSES, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
|
|
160
|
+
export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
|
|
161
161
|
export { modePolicyOf, PERMISSION_MODE_FOLD_REASONS, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
|
|
162
162
|
export { readDenyClassifierRules, renderClassifierDenyRules } from "./core/auto-mode-prompt.js";
|
|
163
163
|
export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./core/runtime-caps.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 2540,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -713,6 +713,7 @@
|
|
|
713
713
|
"MEMORY_SEARCH_TOOL_NAME": "variable",
|
|
714
714
|
"MODEL_ROLES": "variable",
|
|
715
715
|
"MODE_ARBITRATION": "variable",
|
|
716
|
+
"MODE_RELEASE_REASONS": "variable",
|
|
716
717
|
"MONITOR_BATCH_WINDOW_MS": "variable",
|
|
717
718
|
"MONITOR_DEFAULT_TIMEOUT_MS": "variable",
|
|
718
719
|
"MONITOR_MAX_BATCHES_PER_MINUTE": "variable",
|
|
@@ -822,6 +823,7 @@
|
|
|
822
823
|
"ModePolicy": "interface",
|
|
823
824
|
"ModeRefusal": "interface",
|
|
824
825
|
"ModeRelease": "interface",
|
|
826
|
+
"ModeReleaseReason": "type",
|
|
825
827
|
"Model": "interface",
|
|
826
828
|
"ModelCriteria": "interface",
|
|
827
829
|
"ModelMention": "interface",
|
|
@@ -2094,6 +2096,7 @@
|
|
|
2094
2096
|
"isGateKind": "function",
|
|
2095
2097
|
"isInstructionEntry": "function",
|
|
2096
2098
|
"isIsolated": "function",
|
|
2099
|
+
"isModeReleaseReason": "function",
|
|
2097
2100
|
"isModelGatedForClass": "function",
|
|
2098
2101
|
"isModelPriced": "function",
|
|
2099
2102
|
"isNamespacedCoveringRuleName": "function",
|
|
@@ -3252,6 +3255,7 @@
|
|
|
3252
3255
|
"MEMORY_SEARCH_TOOL_NAME": "advanced",
|
|
3253
3256
|
"MODEL_ROLES": "advanced",
|
|
3254
3257
|
"MODE_ARBITRATION": "advanced",
|
|
3258
|
+
"MODE_RELEASE_REASONS": "advanced",
|
|
3255
3259
|
"MONITOR_BATCH_WINDOW_MS": "advanced",
|
|
3256
3260
|
"MONITOR_DEFAULT_TIMEOUT_MS": "advanced",
|
|
3257
3261
|
"MONITOR_MAX_BATCHES_PER_MINUTE": "advanced",
|
|
@@ -3361,6 +3365,7 @@
|
|
|
3361
3365
|
"ModePolicy": "advanced",
|
|
3362
3366
|
"ModeRefusal": "advanced",
|
|
3363
3367
|
"ModeRelease": "advanced",
|
|
3368
|
+
"ModeReleaseReason": "advanced",
|
|
3364
3369
|
"Model": "stable",
|
|
3365
3370
|
"ModelCriteria": "advanced",
|
|
3366
3371
|
"ModelMention": "advanced",
|
|
@@ -4633,6 +4638,7 @@
|
|
|
4633
4638
|
"isGateKind": "advanced",
|
|
4634
4639
|
"isInstructionEntry": "advanced",
|
|
4635
4640
|
"isIsolated": "advanced",
|
|
4641
|
+
"isModeReleaseReason": "advanced",
|
|
4636
4642
|
"isModelGatedForClass": "advanced",
|
|
4637
4643
|
"isModelPriced": "advanced",
|
|
4638
4644
|
"isNamespacedCoveringRuleName": "advanced",
|