@sema-agent/core 7.5.2 → 7.6.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 +41 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/contracts.d.ts +46 -64
- package/dist/core/runner/denial-limit-arms.d.ts +1 -1
- package/dist/core/runner/denial-limit-arms.js +3 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/park-commit.d.ts +17 -23
- package/dist/core/runner/park-commit.js +14 -15
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
- package/dist/core/runner/prepare-ask-lane.js +3 -5
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
- package/dist/core/runner/prepare-boundary-parks.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
- package/dist/core/runner/prepare-gate-stations.js +29 -54
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +2 -4
- package/dist/core/runner/prepare-park-ask.js +5 -5
- package/dist/core/runner/prepare-task.js +8 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
- package/dist/core/runner/prepare-wiring-manifest.js +9 -10
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +109 -120
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -22
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
|
+
import type { GateOutcome } from "./gate-outcome.js";
|
|
4
5
|
import { type AskClass } from "./ask-class.js";
|
|
5
6
|
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
6
7
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
@@ -385,30 +386,7 @@ export declare function runHookSeat<T>(seat: HookSeatName, bound: HookSeatBound,
|
|
|
385
386
|
* expiry, including the ones where the bound was never reached.)
|
|
386
387
|
*/
|
|
387
388
|
export declare function hookSeatExpiredError(seat: HookSeatName, timeoutMs: number, cause: "timeout" | "aborted", consequence: string): Error;
|
|
388
|
-
/**
|
|
389
|
-
* Where a {@link Hooks.permissionDenied} deny came from — our gate-source enum standing in for CC's
|
|
390
|
-
* `decision_reason_type` ('classifier'|'asyncAgent'|'mode'|'rule'), named after OUR adjudicate-chain
|
|
391
|
-
* sources (design/134 R5):
|
|
392
|
-
* - `"policy"` — the ToolPolicy denied directly, or a policy-raised `ask` resolved to deny
|
|
393
|
-
* (headless auto-deny / approver said no / approval aborted).
|
|
394
|
-
* - `"hook"` — a PreToolUse hook `ask` (folded allow→ask) resolved to deny, OR the engine's fail-closed
|
|
395
|
-
* interception of a hook that THREW (RB-442). (A hook's own `deny` short-circuit is EXCLUDED from the
|
|
396
|
-
* event — a decision the hook made; the crash interception is a decision the ENGINE made. The
|
|
397
|
-
* exclusion is ours on that merit; see {@link Hooks.permissionDenied} for why it must NOT be read
|
|
398
|
-
* as a parity claim about this seat's trigger surface.)
|
|
399
|
-
* - `"safety"` — the gate's deterministic egress/irreversibility tighten (design/70 / design/77 §4)
|
|
400
|
-
* raised the `ask` that resolved to deny, from an explicit per-tool mark.
|
|
401
|
-
* - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
|
|
402
|
-
* `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark.
|
|
403
|
-
* - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate).
|
|
404
|
-
* - `"org"` (design/182 §7) — the ORG layer: an organization deny rule, or the fail-closed tighten an
|
|
405
|
-
* org-governed deployment applies while it cannot adjudicate against a snapshot (that tighten's ask
|
|
406
|
-
* then resolving to deny). Its own word rather than `"policy"` because the authority is the ORG's
|
|
407
|
-
* published snapshot, not this deployment's `ToolPolicy` — an operator reading "policy" on a call
|
|
408
|
-
* their own policy allowed would be looking in the wrong place.
|
|
409
|
-
*/
|
|
410
|
-
export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier" | "org";
|
|
411
|
-
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
|
|
389
|
+
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + the gate outcome). */
|
|
412
390
|
export interface PermissionDeniedPayload {
|
|
413
391
|
/** #281 件A — the run/leg identity envelope. Present on every ENGINE emission (the gate's single
|
|
414
392
|
* deny exit, the hook-crash intercept, the plan-mode and compliance denies); absent only when a
|
|
@@ -421,19 +399,15 @@ export interface PermissionDeniedPayload {
|
|
|
421
399
|
toolCallId: string;
|
|
422
400
|
/** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT `<system-reminder>`-wrapped. */
|
|
423
401
|
reason: string;
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
/** #548 — the deny is the classifier denial-limit fallback's AUTO-DENY (core's own window elapsed;
|
|
434
|
-
* see {@link import("./tool-policy.js").ResolvedAsk.autoDenied}). Carried verbatim from the resolver;
|
|
435
|
-
* absent on every other deny. */
|
|
436
|
-
autoDenied?: true;
|
|
402
|
+
/**
|
|
403
|
+
* The gate outcome this deny is — the SAME object the call's `tool_end` frame carries as `gate` and
|
|
404
|
+
* the engine's per-call sideband records (one mint, three faces). `disposition.deniedBy` says which
|
|
405
|
+
* LAYER refused (our `decision_reason_type` analog for the refusing half); when the refusal came through
|
|
406
|
+
* an ask resolution, `settlement` says how the wait ended and `origin` who asked. Nothing is read out of
|
|
407
|
+
* an absence: a direct policy / hook / plan-mode / compliance deny carries no settlement because it
|
|
408
|
+
* settled none.
|
|
409
|
+
*/
|
|
410
|
+
gate: GateOutcome;
|
|
437
411
|
/** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
|
|
438
412
|
* already happened and nothing this callback does can change it, so the signal says exactly one
|
|
439
413
|
* thing: stop reading, nobody is waiting for your answer any more. */
|
|
@@ -943,46 +917,14 @@ export interface ToolGateResult {
|
|
|
943
917
|
/** Model-readable block reason (already `<system-reminder>`-wrapped). */
|
|
944
918
|
reason?: string;
|
|
945
919
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
* allow. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
|
|
953
|
-
* frame: the machine-readable half of the disclosure `reason` makes in words, on a channel no tool
|
|
954
|
-
* and no post-tool hook can write.
|
|
955
|
-
*/
|
|
956
|
-
settledBy?: import("./tool-policy.js").ApprovalSettledBy;
|
|
957
|
-
/**
|
|
958
|
-
* The ask resolver's deny-arm classification (see {@link import("./tool-policy.js").AskDenyResolution}),
|
|
959
|
-
* present only on a BLOCK whose deny came through an ask resolution and passed the closed-vocabulary
|
|
960
|
-
* screen at the deny exit (a self-declared word on a policy's own deny is dropped there, never
|
|
961
|
-
* forwarded). Rides beside {@link settledBy} to the caller's per-call sideband and the call's
|
|
962
|
-
* `tool_end` frame — the machine-readable "why was this refused" a consumer classifies on.
|
|
963
|
-
*/
|
|
964
|
-
resolution?: import("./tool-policy.js").AskDenyResolution;
|
|
965
|
-
/**
|
|
966
|
-
* `true` ⇔ this BLOCK is the classifier denial-limit fallback's own auto-deny: the ask's window
|
|
967
|
-
* (`AskRequest.denialLimitFallback.autoDenyAfterMs`) elapsed with nobody answering and the resolver
|
|
968
|
-
* refused it itself — the same fact the `permissionDenied` observer payload's `autoDenied` carries,
|
|
969
|
-
* read from the same two places (this gate's own resolution, or the module-private attestation an
|
|
970
|
-
* inherited arm bound to THIS call). Rides to the call's `tool_end` frame so a consumer can tell
|
|
971
|
-
* "the classifier fallback's own 120 s window auto-denied" from "the approval factory's own window
|
|
972
|
-
* (`approvalTimeoutMs`) elapsed" — both settle as `settledBy:"timeout"` / `resolution:"window_expired"`,
|
|
973
|
-
* and the factory's deadline NEVER carries this. A durable park's expiry settles on the decide lane
|
|
974
|
-
* with no `resolution` word, so it is told apart by that absence, not by this bit.
|
|
975
|
-
* Absent everywhere else (a person's no, a policy deny, an executing call).
|
|
976
|
-
*/
|
|
977
|
-
autoDenied?: true;
|
|
978
|
-
/**
|
|
979
|
-
* design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported, carried
|
|
980
|
-
* out verbatim beside {@link settledBy}. This layer authenticates nothing and compares nothing; the
|
|
981
|
-
* value was screened for shape at the seam that received it (`screenApproverAttribution`) and is a
|
|
982
|
-
* transcription from there on. Present only where the caller settled something AND reported an
|
|
983
|
-
* identity — absent everywhere else, and absence never means "nobody": it means nobody said.
|
|
920
|
+
* The record of this pass ({@link GateOutcome}): its final disposition, the ask it settled (if any) and
|
|
921
|
+
* that ask's origin. Present on BOTH terminal exits, the block and the allow — an approved call is a
|
|
922
|
+
* settled approval too, and a caller that saw the record only on refusals would read its absence on an
|
|
923
|
+
* execution as "nobody approved this". Absent only on the durable-suspend exit, where the pass decided
|
|
924
|
+
* nothing yet. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
|
|
925
|
+
* frame, and the deny observer receives the same object: one mint, three faces.
|
|
984
926
|
*/
|
|
985
|
-
|
|
927
|
+
gate?: GateOutcome;
|
|
986
928
|
/** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */
|
|
987
929
|
updatedInput?: unknown;
|
|
988
930
|
/**
|
|
@@ -1767,20 +1709,4 @@ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
|
|
|
1767
1709
|
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1768
1710
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
1769
1711
|
};
|
|
1770
|
-
/**
|
|
1771
|
-
* The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
|
|
1772
|
-
* structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
|
|
1773
|
-
*
|
|
1774
|
-
* 1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
|
|
1775
|
-
* and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
|
|
1776
|
-
* `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it).
|
|
1777
|
-
* 2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args),
|
|
1778
|
-
* fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask`
|
|
1779
|
-
* through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may
|
|
1780
|
-
* itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite.
|
|
1781
|
-
*
|
|
1782
|
-
* Because both phases run in this one linear function, the ordering — and the "policy is final"
|
|
1783
|
-
* invariant — is enforced by the call stack, not by registration convention. Returns the gate result
|
|
1784
|
-
* for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result.
|
|
1785
|
-
*/
|
|
1786
1712
|
export declare function runToolGate(input: ToolGateInput): Promise<ToolGateResult>;
|
package/dist/core/hooks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
|
+
import { createSettlementLedger, directDeny, mintGateOutcome } from "./runner/gate-exit.js";
|
|
2
3
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
3
4
|
import { askOriginOf, classifierMayAnswer, isAskOrigin } from "./ask-origin.js";
|
|
4
5
|
import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
@@ -455,8 +456,8 @@ export async function runToolGate(input) {
|
|
|
455
456
|
const preToolContext = [];
|
|
456
457
|
let hookAsk;
|
|
457
458
|
let parkFailed;
|
|
458
|
-
let
|
|
459
|
-
|
|
459
|
+
let deniedBy;
|
|
460
|
+
const ledger = createSettlementLedger({ toolCallId, toolName });
|
|
460
461
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
461
462
|
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
|
|
462
463
|
const seatBound = (abortEnds) => ({
|
|
@@ -493,13 +494,15 @@ export async function runToolGate(input) {
|
|
|
493
494
|
catch (err) {
|
|
494
495
|
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
495
496
|
traceHookCrash(input, err, notifier);
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
const gate = directDeny("hook");
|
|
498
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
499
|
+
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, preToolContext };
|
|
498
500
|
}
|
|
499
501
|
if (unanswered !== undefined) {
|
|
500
502
|
const reason = await preToolUseUnansweredBlock(`this call to "${toolName}"`, unanswered);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
+
const gate = directDeny("hook");
|
|
504
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
505
|
+
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, preToolContext };
|
|
503
506
|
}
|
|
504
507
|
if (r) {
|
|
505
508
|
if (r.additionalContext) {
|
|
@@ -509,6 +512,7 @@ export async function runToolGate(input) {
|
|
|
509
512
|
return {
|
|
510
513
|
block: true,
|
|
511
514
|
reason: formatHookFeedback(decisionText(r) ?? `tool "${toolName}" blocked by a PreToolUse hook`, input.reminderMark),
|
|
515
|
+
gate: directDeny("hook"),
|
|
512
516
|
preToolContext,
|
|
513
517
|
};
|
|
514
518
|
}
|
|
@@ -522,7 +526,12 @@ export async function runToolGate(input) {
|
|
|
522
526
|
}
|
|
523
527
|
const req = { toolName, args: currentInput, toolCallId };
|
|
524
528
|
let decision = adjudicate ? await (callSignal !== undefined ? adjudicate(req, callSignal) : adjudicate(req)) : { action: "allow" };
|
|
525
|
-
|
|
529
|
+
{
|
|
530
|
+
const refusedBy = ledger.fromDecision(decision);
|
|
531
|
+
if (refusedBy !== undefined)
|
|
532
|
+
deniedBy = refusedBy;
|
|
533
|
+
}
|
|
534
|
+
let tightenedBy;
|
|
526
535
|
let policyRewrite;
|
|
527
536
|
if ((decision.action === "allow" || decision.action === "ask") && decision.updatedInput !== undefined) {
|
|
528
537
|
policyRewrite = decision.updatedInput;
|
|
@@ -538,7 +547,6 @@ export async function runToolGate(input) {
|
|
|
538
547
|
...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
539
548
|
...(hookAsk.updatedInput !== undefined ? { updatedInput: hookAsk.updatedInput } : {}),
|
|
540
549
|
};
|
|
541
|
-
denySource = "hook";
|
|
542
550
|
}
|
|
543
551
|
else if (decision.action === "ask" && hookAsk) {
|
|
544
552
|
const hookMessage = hookAsk.message;
|
|
@@ -558,7 +566,7 @@ export async function runToolGate(input) {
|
|
|
558
566
|
message: `tool "${toolName}" performs an external write (egress) — explicit approval required`,
|
|
559
567
|
decisionReason: "safety",
|
|
560
568
|
};
|
|
561
|
-
|
|
569
|
+
tightenedBy = "safety";
|
|
562
570
|
}
|
|
563
571
|
const alreadyAsking = decision.action === "ask";
|
|
564
572
|
if ((decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
|
|
@@ -609,7 +617,7 @@ export async function runToolGate(input) {
|
|
|
609
617
|
...(probeCause !== undefined ? { probeCause } : {}),
|
|
610
618
|
...(probeMandated === true ? { probeMandated: true } : {}),
|
|
611
619
|
};
|
|
612
|
-
|
|
620
|
+
tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
|
|
613
621
|
}
|
|
614
622
|
else if (alreadyAsking && probeMandated === true && decision.action === "ask") {
|
|
615
623
|
decision = { ...decision, probeMandated: true };
|
|
@@ -629,7 +637,7 @@ export async function runToolGate(input) {
|
|
|
629
637
|
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
630
638
|
decisionReason: "safety",
|
|
631
639
|
};
|
|
632
|
-
|
|
640
|
+
tightenedBy = "safety";
|
|
633
641
|
}
|
|
634
642
|
if (decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
635
643
|
const writeProtectedHit = input.writeProtectionCheck(toolName, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
@@ -639,18 +647,17 @@ export async function runToolGate(input) {
|
|
|
639
647
|
message: `tool "${toolName}" writes to a write-protected path (table entry "${writeProtectedHit.name}") — explicit approval required`,
|
|
640
648
|
decisionReason: "safety",
|
|
641
649
|
};
|
|
642
|
-
|
|
650
|
+
tightenedBy = "safety";
|
|
643
651
|
}
|
|
644
652
|
}
|
|
645
653
|
let orgRealApprovalRequired = false;
|
|
646
|
-
|
|
647
|
-
const askOriginFacts = () => ({
|
|
654
|
+
const askOriginFacts = (org) => ({
|
|
648
655
|
contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
|
|
649
656
|
markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
|
|
650
|
-
org
|
|
657
|
+
org,
|
|
658
|
+
tightened: tightenedBy,
|
|
651
659
|
});
|
|
652
660
|
let orgTightenCount = 0;
|
|
653
|
-
let resolvedApprover;
|
|
654
661
|
let orgEvidence = (() => {
|
|
655
662
|
const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
|
|
656
663
|
return { revisionAbsent: w, ruleAbsent: w };
|
|
@@ -662,9 +669,9 @@ export async function runToolGate(input) {
|
|
|
662
669
|
});
|
|
663
670
|
const applyOrgLayer = async (current, args) => {
|
|
664
671
|
if (input.orgRules === undefined)
|
|
665
|
-
return current;
|
|
672
|
+
return { decision: current, org: undefined };
|
|
666
673
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
|
|
667
|
-
return current;
|
|
674
|
+
return { decision: current, org: undefined };
|
|
668
675
|
let decided = current;
|
|
669
676
|
let pending;
|
|
670
677
|
try {
|
|
@@ -684,7 +691,6 @@ export async function runToolGate(input) {
|
|
|
684
691
|
if (answer.status === "unavailable") {
|
|
685
692
|
orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
|
|
686
693
|
orgRealApprovalRequired = true;
|
|
687
|
-
orgAskOrigin = "unavailable";
|
|
688
694
|
await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
|
|
689
695
|
if (decided.action === "allow") {
|
|
690
696
|
decided = {
|
|
@@ -695,42 +701,43 @@ export async function runToolGate(input) {
|
|
|
695
701
|
requiresRealApproval: true,
|
|
696
702
|
};
|
|
697
703
|
orgTightenCount += 1;
|
|
698
|
-
denySource = "org";
|
|
699
704
|
}
|
|
700
705
|
else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
|
|
701
706
|
decided = { ...decided, requiresRealApproval: true };
|
|
702
707
|
orgTightenCount += 1;
|
|
703
708
|
}
|
|
704
|
-
return decided;
|
|
709
|
+
return { decision: decided, org: "unavailable" };
|
|
705
710
|
}
|
|
706
711
|
const revisionCell = answer.revision !== undefined ? { revision: answer.revision } : { revisionAbsent: "not_reported" };
|
|
707
712
|
if (answer.verdict === undefined) {
|
|
708
713
|
orgEvidence = { ...revisionCell, ruleAbsent: "no_match" };
|
|
709
|
-
return decided;
|
|
714
|
+
return { decision: decided, org: undefined };
|
|
710
715
|
}
|
|
711
716
|
const { behavior, rule } = answer.verdict;
|
|
712
717
|
orgEvidence = { ...revisionCell, rule };
|
|
713
718
|
if (behavior === "deny") {
|
|
714
719
|
orgTightenCount += 1;
|
|
715
|
-
|
|
716
|
-
return { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON };
|
|
720
|
+
deniedBy = "org";
|
|
721
|
+
return { decision: { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON }, org: undefined };
|
|
717
722
|
}
|
|
718
723
|
if (decided.action === "deny")
|
|
719
|
-
return decided;
|
|
724
|
+
return { decision: decided, org: undefined };
|
|
720
725
|
orgTightenCount += 1;
|
|
721
|
-
denySource = "org";
|
|
722
726
|
orgRealApprovalRequired = true;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
727
|
+
return {
|
|
728
|
+
decision: decided.action === "ask"
|
|
729
|
+
? { ...decided, requiresRealApproval: true }
|
|
730
|
+
: {
|
|
731
|
+
action: "ask",
|
|
732
|
+
message: `an organization policy rule (${rule}) requires approval for this call`,
|
|
733
|
+
decisionReason: ORG_RULE_DECISION_REASON,
|
|
734
|
+
requiresRealApproval: true,
|
|
735
|
+
},
|
|
736
|
+
org: "rule",
|
|
737
|
+
};
|
|
732
738
|
};
|
|
733
|
-
|
|
739
|
+
const orgAnswer = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
740
|
+
decision = orgAnswer.decision;
|
|
734
741
|
if (decision.action === "ask") {
|
|
735
742
|
if (policyRewrite !== undefined) {
|
|
736
743
|
currentInput = policyRewrite;
|
|
@@ -815,11 +822,13 @@ export async function runToolGate(input) {
|
|
|
815
822
|
};
|
|
816
823
|
}
|
|
817
824
|
}
|
|
818
|
-
const originFacts = askOriginFacts();
|
|
825
|
+
const originFacts = askOriginFacts(orgAnswer.org);
|
|
819
826
|
if (decision.action === "ask") {
|
|
820
827
|
const stamped = { ...decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
821
828
|
decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
|
|
822
829
|
}
|
|
830
|
+
if (decision.action === "ask")
|
|
831
|
+
ledger.supersede();
|
|
823
832
|
if (input.autoMode && decision.action === "ask" && decision.origin !== undefined && classifierMayAnswer(decision.origin)) {
|
|
824
833
|
const verdict = await input.autoMode.decider
|
|
825
834
|
.decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
|
|
@@ -846,7 +855,6 @@ export async function runToolGate(input) {
|
|
|
846
855
|
denialLimitFallback: tracked.fallback,
|
|
847
856
|
};
|
|
848
857
|
decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
|
|
849
|
-
denySource = "classifier";
|
|
850
858
|
}
|
|
851
859
|
else {
|
|
852
860
|
decision = {
|
|
@@ -854,7 +862,7 @@ export async function runToolGate(input) {
|
|
|
854
862
|
message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
|
|
855
863
|
decisionReason: "classifier",
|
|
856
864
|
};
|
|
857
|
-
|
|
865
|
+
deniedBy = "classifier";
|
|
858
866
|
}
|
|
859
867
|
}
|
|
860
868
|
}
|
|
@@ -885,9 +893,13 @@ export async function runToolGate(input) {
|
|
|
885
893
|
const egressTool = input.egress === true;
|
|
886
894
|
const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
|
|
887
895
|
const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
|
|
888
|
-
const realApprovalOf = (d) =>
|
|
889
|
-
|
|
890
|
-
|
|
896
|
+
const realApprovalOf = (d) => {
|
|
897
|
+
if (d.action !== "ask" || d.requiresRealApproval !== true)
|
|
898
|
+
return undefined;
|
|
899
|
+
if (d.origin === undefined)
|
|
900
|
+
throw new Error(`a real-approval ask for "${toolName}" reached a park site with no origin word — the origin stamp precedes every park`);
|
|
901
|
+
return { origin: d.origin, ...(originFacts.org !== undefined ? { org: originFacts.org } : {}) };
|
|
902
|
+
};
|
|
891
903
|
const carryOf = (d) => ({
|
|
892
904
|
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
893
905
|
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
@@ -923,6 +935,10 @@ export async function runToolGate(input) {
|
|
|
923
935
|
return { suspend: suspended, preToolContext };
|
|
924
936
|
}
|
|
925
937
|
}
|
|
938
|
+
if (decision.origin === undefined)
|
|
939
|
+
throw new Error(`the content ask for "${toolName}" reached its refusal without an origin word`);
|
|
940
|
+
ledger.set({ settlement: { kind: input.resolveContentAsk !== undefined ? "approver_unavailable" : "no_approver", who: { party: "none" }, when: Date.now() }, origin: decision.origin });
|
|
941
|
+
deniedBy = "ask_resolution";
|
|
926
942
|
decision = {
|
|
927
943
|
action: "deny",
|
|
928
944
|
message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
|
|
@@ -941,12 +957,9 @@ export async function runToolGate(input) {
|
|
|
941
957
|
if (decision.action === "ask") {
|
|
942
958
|
const askBeforeResolve = decision;
|
|
943
959
|
const resolved = await (callSignal !== undefined ? resolveAsk(decision, req, callSignal) : resolveAsk(decision, req));
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
askDenyResolution = resolved.resolution;
|
|
948
|
-
if (resolved.action === "deny" && resolved.autoDenied === true)
|
|
949
|
-
askAutoDenied = true;
|
|
960
|
+
ledger.fromResolved(resolved, askBeforeResolve, toolName);
|
|
961
|
+
if (resolved.action === "deny")
|
|
962
|
+
deniedBy = "ask_resolution";
|
|
950
963
|
decision = resolved;
|
|
951
964
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
952
965
|
const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
|
|
@@ -962,9 +975,8 @@ export async function runToolGate(input) {
|
|
|
962
975
|
if (decision.action === "allow") {
|
|
963
976
|
input.autoMode?.denialTracking?.recordAllow();
|
|
964
977
|
}
|
|
965
|
-
else if (
|
|
966
|
-
|
|
967
|
-
askDenyResolution === "approver_unavailable" ||
|
|
978
|
+
else if ((resolved.action === "deny" &&
|
|
979
|
+
(resolved.settlement.kind === "no_approver" || resolved.settlement.kind === "blanket_allow_refused" || resolved.settlement.kind === "approver_unavailable")) ||
|
|
968
980
|
resolved.approverUnavailable === true) {
|
|
969
981
|
try {
|
|
970
982
|
input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: unarmedWindow(askBeforeResolve.denialLimitFallback) });
|
|
@@ -983,7 +995,7 @@ export async function runToolGate(input) {
|
|
|
983
995
|
action: "deny",
|
|
984
996
|
message: `the approval-edit chain for "${toolName}" exceeded the re-approval cap — denied fail-closed; re-submit the edited action directly`,
|
|
985
997
|
};
|
|
986
|
-
|
|
998
|
+
deniedBy = "policy";
|
|
987
999
|
break;
|
|
988
1000
|
}
|
|
989
1001
|
if (preToolUse) {
|
|
@@ -999,12 +1011,12 @@ export async function runToolGate(input) {
|
|
|
999
1011
|
catch (err) {
|
|
1000
1012
|
traceHookCrash(input, err, notifier);
|
|
1001
1013
|
editDenied = { action: "deny", message: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
|
|
1002
|
-
|
|
1014
|
+
deniedBy = "hook";
|
|
1003
1015
|
break;
|
|
1004
1016
|
}
|
|
1005
1017
|
if (hrUnanswered !== undefined) {
|
|
1006
1018
|
editDenied = { action: "deny", message: await preToolUseUnansweredBlock(`the approved edit for "${toolName}"`, hrUnanswered) };
|
|
1007
|
-
|
|
1019
|
+
deniedBy = "hook";
|
|
1008
1020
|
break;
|
|
1009
1021
|
}
|
|
1010
1022
|
if (hr) {
|
|
@@ -1012,7 +1024,7 @@ export async function runToolGate(input) {
|
|
|
1012
1024
|
preToolContext.push(hr.additionalContext);
|
|
1013
1025
|
if (hr.action === "deny") {
|
|
1014
1026
|
editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
|
|
1015
|
-
|
|
1027
|
+
deniedBy = "hook";
|
|
1016
1028
|
break;
|
|
1017
1029
|
}
|
|
1018
1030
|
if (hr.action === "ask") {
|
|
@@ -1020,7 +1032,7 @@ export async function runToolGate(input) {
|
|
|
1020
1032
|
action: "deny",
|
|
1021
1033
|
message: `the approved edit for "${toolName}" raises a new PreToolUse approval requirement the original approval does not cover — denied fail-closed; re-submit the edited action directly`,
|
|
1022
1034
|
};
|
|
1023
|
-
|
|
1035
|
+
deniedBy = "hook";
|
|
1024
1036
|
break;
|
|
1025
1037
|
}
|
|
1026
1038
|
if (hr.updatedInput !== undefined) {
|
|
@@ -1043,19 +1055,20 @@ export async function runToolGate(input) {
|
|
|
1043
1055
|
};
|
|
1044
1056
|
}
|
|
1045
1057
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1058
|
+
{
|
|
1059
|
+
const refusedBy = ledger.fromDecision(recheck);
|
|
1060
|
+
if (recheck.action === "deny" && refusedBy !== undefined) {
|
|
1061
|
+
editDenied = recheck;
|
|
1062
|
+
deniedBy = refusedBy;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1050
1065
|
}
|
|
1051
1066
|
if (recheck.updatedInput !== undefined) {
|
|
1052
1067
|
editArgs = recheck.updatedInput;
|
|
1053
1068
|
editRewrittenSinceHuman = true;
|
|
1054
1069
|
}
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
recheck = await applyOrgLayer(recheck, editArgs);
|
|
1058
|
-
const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
|
|
1070
|
+
const editOrg = await applyOrgLayer(recheck, editArgs);
|
|
1071
|
+
recheck = editOrg.decision;
|
|
1059
1072
|
if (recheck.action === "deny") {
|
|
1060
1073
|
editDenied = recheck;
|
|
1061
1074
|
break;
|
|
@@ -1072,16 +1085,13 @@ export async function runToolGate(input) {
|
|
|
1072
1085
|
editRewrittenSinceHuman = true;
|
|
1073
1086
|
}
|
|
1074
1087
|
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
1075
|
-
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts()) };
|
|
1088
|
+
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
|
|
1076
1089
|
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
1077
1090
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
1078
|
-
|
|
1091
|
+
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
1079
1092
|
if (rr.action !== "allow") {
|
|
1080
|
-
if (rr.action === "deny" && isAskDenyResolution(rr.resolution))
|
|
1081
|
-
askDenyResolution = rr.resolution;
|
|
1082
1093
|
editDenied = rr;
|
|
1083
|
-
|
|
1084
|
-
denySource = "policy";
|
|
1094
|
+
deniedBy = "ask_resolution";
|
|
1085
1095
|
break;
|
|
1086
1096
|
}
|
|
1087
1097
|
if (rr.updatedInput === undefined) {
|
|
@@ -1100,7 +1110,7 @@ export async function runToolGate(input) {
|
|
|
1100
1110
|
action: "deny",
|
|
1101
1111
|
message: `the approved edit for "${toolName}" was rewritten by the restriction chain onto a write-protected path (table entry "${editHit.name}") that no approval covers — denied fail-closed; re-submit the edited action directly`,
|
|
1102
1112
|
};
|
|
1103
|
-
|
|
1113
|
+
deniedBy = "write_protection";
|
|
1104
1114
|
}
|
|
1105
1115
|
}
|
|
1106
1116
|
decision = editDenied ?? { ...decision, updatedInput: editArgs };
|
|
@@ -1115,18 +1125,14 @@ export async function runToolGate(input) {
|
|
|
1115
1125
|
if (decision.updatedInput !== undefined) {
|
|
1116
1126
|
currentInput = decision.updatedInput;
|
|
1117
1127
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
1128
|
+
if (deniedBy === undefined)
|
|
1129
|
+
throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
|
|
1130
|
+
const gate = mintGateOutcome({ deniedBy, ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
|
|
1131
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
1123
1132
|
return {
|
|
1124
1133
|
block: true,
|
|
1125
1134
|
reason: formatHookFeedback(denyReason, input.reminderMark),
|
|
1126
|
-
|
|
1127
|
-
...(denyResolution !== undefined ? { resolution: denyResolution } : {}),
|
|
1128
|
-
...(denyAutoDenied ? { autoDenied: true } : {}),
|
|
1129
|
-
...(denyApprover !== undefined ? { approver: denyApprover } : {}),
|
|
1135
|
+
gate,
|
|
1130
1136
|
preToolContext,
|
|
1131
1137
|
};
|
|
1132
1138
|
}
|
|
@@ -1135,12 +1141,9 @@ export async function runToolGate(input) {
|
|
|
1135
1141
|
if (rw !== undefined)
|
|
1136
1142
|
currentInput = rw;
|
|
1137
1143
|
}
|
|
1138
|
-
const allowSettledBy = decision.action === "allow" ? decision.settledBy : undefined;
|
|
1139
|
-
const allowApprover = decision.action === "allow" && allowSettledBy !== undefined ? resolvedApprover : undefined;
|
|
1140
1144
|
return {
|
|
1141
1145
|
updatedInput: currentInput === event.input ? undefined : currentInput,
|
|
1142
|
-
|
|
1143
|
-
...(allowApprover !== undefined ? { approver: allowApprover } : {}),
|
|
1146
|
+
gate: mintGateOutcome(ledger.settled !== undefined ? { settled: ledger.settled } : {}),
|
|
1144
1147
|
preToolContext,
|
|
1145
1148
|
};
|
|
1146
1149
|
}
|