@sema-agent/core 7.6.0 → 7.6.2
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 +37 -0
- package/dist/agents/agent-transcript-tool.d.ts +2 -2
- package/dist/agents/cascade.d.ts +2 -3
- package/dist/agents/repair-loop.d.ts +2 -2
- package/dist/agents/retain-ledger.d.ts +2 -3
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/session-util.d.ts +2 -2
- package/dist/agents/subagent.d.ts +3 -4
- package/dist/agents/teacher.d.ts +2 -2
- package/dist/agents/team.d.ts +2 -2
- package/dist/agents/verify.d.ts +5 -6
- package/dist/core/agent-definition.d.ts +172 -0
- package/dist/core/agent-definition.js +1 -0
- package/dist/core/checkpoint-store.d.ts +8 -4
- package/dist/core/delegation-frames.d.ts +298 -0
- package/dist/core/delegation-frames.js +21 -0
- package/dist/core/engine-notice.d.ts +555 -0
- package/dist/core/engine-notice.js +55 -0
- package/dist/core/gate-fold.d.ts +12 -0
- package/dist/core/gate-fold.js +158 -0
- package/dist/core/gate-lanes.d.ts +93 -0
- package/dist/core/gate-lanes.js +626 -0
- package/dist/core/hands-band.d.ts +134 -0
- package/dist/core/hands-band.js +1 -0
- package/dist/core/hooks.d.ts +20 -101
- package/dist/core/hooks.js +53 -854
- package/dist/core/mcp-failure.d.ts +43 -5
- package/dist/core/mcp-failure.js +31 -14
- package/dist/core/mcp-server-spec.d.ts +217 -0
- package/dist/core/mcp-server-spec.js +1 -0
- package/dist/core/model-seat.d.ts +99 -0
- package/dist/core/model-seat.js +1 -0
- package/dist/core/reminder-mint.d.ts +10 -0
- package/dist/core/reminder-mint.js +3 -0
- package/dist/core/runner/contracts.d.ts +382 -6
- package/dist/core/runner/gate-exit.d.ts +177 -9
- package/dist/core/runner/gate-exit.js +70 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
- package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
- package/dist/core/runner/prepare-run-refs.d.ts +12 -0
- package/dist/core/runner/prepare-run-refs.js +5 -0
- package/dist/core/runner/prepare-task.d.ts +2 -2
- package/dist/core/runner/runtask.d.ts +4 -71
- package/dist/core/runner/runtask.js +18 -6
- package/dist/core/runner-deps.d.ts +1416 -0
- package/dist/core/runner-deps.js +1 -0
- package/dist/core/runtime-caps.d.ts +164 -0
- package/dist/core/runtime-caps.js +1 -0
- package/dist/core/task-event.d.ts +910 -0
- package/dist/core/task-event.js +1 -0
- package/dist/core/task-limits.d.ts +110 -0
- package/dist/core/task-limits.js +1 -0
- package/dist/core/task-result.d.ts +809 -0
- package/dist/core/task-result.js +1 -0
- package/dist/core/task-spec.d.ts +1370 -0
- package/dist/core/task-spec.js +1 -0
- package/dist/core/task-stream.d.ts +382 -0
- package/dist/core/task-stream.js +1 -0
- package/dist/core/tool-spec.d.ts +1174 -0
- package/dist/core/tool-spec.js +1 -0
- package/dist/core/types.d.ts +26 -7691
- package/dist/core/types.js +2 -76
- package/dist/core/warm-resume.d.ts +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/goal.d.ts +2 -2
- package/dist/orchestration/run-spec.d.ts +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +3 -3
- package/dist/orchestration/workflow.d.ts +4 -4
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/scenarios/teacher-quickstart.d.ts +2 -2
- package/dist/server/http.d.ts +2 -2
- package/dist/stores/file/fs-atomic.d.ts +88 -12
- package/dist/stores/file/fs-atomic.js +184 -55
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +1 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
package/dist/core/hooks.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { decisionText,
|
|
2
|
-
import { createSettlementLedger,
|
|
1
|
+
import { decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
|
+
import { createSettlementLedger, engineFailClosedExit, hookDenyExit, traceHookCrash } from "./runner/gate-exit.js";
|
|
3
|
+
import { runGateFold } from "./gate-fold.js";
|
|
4
|
+
import { runGateLanes } from "./gate-lanes.js";
|
|
5
|
+
export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandateOf } from "./gate-lanes.js";
|
|
6
|
+
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
3
7
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
4
|
-
import {
|
|
5
|
-
import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
8
|
+
import { isAskOrigin } from "./ask-origin.js";
|
|
6
9
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
7
|
-
|
|
8
|
-
import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
9
|
-
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
10
|
+
export { formatHookFeedback } from "./reminder-mint.js";
|
|
10
11
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
11
|
-
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
12
12
|
export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
|
|
13
13
|
export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
|
|
14
14
|
const POST_ABORT_GRACE_MS = 1_000;
|
|
@@ -95,18 +95,6 @@ export function hookSeatExpiredError(seat, timeoutMs, cause, consequence) {
|
|
|
95
95
|
? `the deployment's ${seat} hook did not answer within its ${timeoutMs}ms bound (Hooks.timeoutMs) — ${consequence}`
|
|
96
96
|
: `the deployment's ${seat} hook had not answered when the task was cancelled — ${consequence}`);
|
|
97
97
|
}
|
|
98
|
-
export function cloneObserverInput(input) {
|
|
99
|
-
try {
|
|
100
|
-
return structuredClone(input);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
if (Array.isArray(input))
|
|
104
|
-
return [...input];
|
|
105
|
-
if (input !== null && typeof input === "object")
|
|
106
|
-
return { ...input };
|
|
107
|
-
return input;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
98
|
const reflectApply = Reflect.apply;
|
|
111
99
|
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
112
100
|
const getPrototypeOf = Object.getPrototypeOf;
|
|
@@ -180,146 +168,6 @@ export function mintHookInvocationIdentity(facts) {
|
|
|
180
168
|
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
|
|
181
169
|
}));
|
|
182
170
|
}
|
|
183
|
-
export function formatHookFeedback(text, mark) {
|
|
184
|
-
return mintSystemReminder(text, mark);
|
|
185
|
-
}
|
|
186
|
-
const PARK_FAILURE_CAUSE_MAX = 600;
|
|
187
|
-
function withParkFailureCause(reason, parkFailed) {
|
|
188
|
-
if (parkFailed === undefined)
|
|
189
|
-
return reason;
|
|
190
|
-
return (`${reason} — note: a durable approval park was attempted for this call FIRST and could not be minted ` +
|
|
191
|
-
`(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
|
|
192
|
-
`left to say, not the reason the call stopped.`);
|
|
193
|
-
}
|
|
194
|
-
function isPlainOwnRecord(x) {
|
|
195
|
-
try {
|
|
196
|
-
if (x === null || typeof x !== "object" || Array.isArray(x))
|
|
197
|
-
return false;
|
|
198
|
-
const p = Object.getPrototypeOf(x);
|
|
199
|
-
return p === Object.prototype || p === null;
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function ownDataValue(obj, key) {
|
|
206
|
-
try {
|
|
207
|
-
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
208
|
-
return d && "value" in d ? d.value : undefined;
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
export function normalizeOrgGateVerdict(answer, unreadable) {
|
|
215
|
-
const clean = (v) => Object.assign(Object.create(null), v);
|
|
216
|
-
const readable = (x) => {
|
|
217
|
-
try {
|
|
218
|
-
return x !== null && (typeof x === "object" || typeof x === "function") && !Array.isArray(x);
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
if (!readable(answer))
|
|
225
|
-
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
226
|
-
const status = ownDataValue(answer, "status");
|
|
227
|
-
if (status === "unavailable") {
|
|
228
|
-
const disclosures = ownDataValue(answer, "disclosures");
|
|
229
|
-
let lines = [];
|
|
230
|
-
if (Array.isArray(disclosures)) {
|
|
231
|
-
try {
|
|
232
|
-
for (let i = 0; i < disclosures.length; i++) {
|
|
233
|
-
const d = disclosures[i];
|
|
234
|
-
if (typeof d === "string")
|
|
235
|
-
lines.push(d);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
lines = [];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return clean({ status: "unavailable", disclosures: lines.length > 0 ? lines : [unreadable] });
|
|
243
|
-
}
|
|
244
|
-
if (status !== "available")
|
|
245
|
-
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
246
|
-
const revision = ownDataValue(answer, "revision");
|
|
247
|
-
const revisionCell = typeof revision === "number" && Number.isFinite(revision) ? { revision } : {};
|
|
248
|
-
const verdict = ownDataValue(answer, "verdict");
|
|
249
|
-
if (verdict === undefined)
|
|
250
|
-
return clean({ status: "available", ...revisionCell });
|
|
251
|
-
if (!readable(verdict))
|
|
252
|
-
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
253
|
-
const behavior = ownDataValue(verdict, "behavior");
|
|
254
|
-
const rule = ownDataValue(verdict, "rule");
|
|
255
|
-
if ((behavior !== "deny" && behavior !== "ask") || typeof rule !== "string")
|
|
256
|
-
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
257
|
-
return clean({ status: "available", verdict: clean({ behavior: behavior, rule }), ...revisionCell });
|
|
258
|
-
}
|
|
259
|
-
function copiedUsableDots(dots) {
|
|
260
|
-
let copied;
|
|
261
|
-
try {
|
|
262
|
-
if (Array.isArray(dots)) {
|
|
263
|
-
const walked = [];
|
|
264
|
-
for (let i = 0; i < dots.length; i++) {
|
|
265
|
-
const d = dots[i];
|
|
266
|
-
walked.push(isPlainOwnRecord(d) ? { actor: ownDataValue(d, "actor"), counter: ownDataValue(d, "counter") } : { actor: undefined, counter: undefined });
|
|
267
|
-
}
|
|
268
|
-
copied = walked;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
copied = undefined;
|
|
273
|
-
}
|
|
274
|
-
const usable = copied?.every((d) => typeof d.actor === "string" && typeof d.counter === "number" && Number.isFinite(d.counter)) === true && copied.length > 0;
|
|
275
|
-
return usable ? { dots: copied } : {};
|
|
276
|
-
}
|
|
277
|
-
export function normalizePersistedRuleHit(hit) {
|
|
278
|
-
if (typeof hit === "string")
|
|
279
|
-
return { hit: { rules: [{ rule: hit }] } };
|
|
280
|
-
if (!isPlainOwnRecord(hit))
|
|
281
|
-
return {};
|
|
282
|
-
if (ownDataValue(hit, "unreadable") === true)
|
|
283
|
-
return { unreadable: true };
|
|
284
|
-
const rulesRaw = ownDataValue(hit, "rules");
|
|
285
|
-
if (rulesRaw === undefined) {
|
|
286
|
-
const coverageRaw = ownDataValue(hit, "segmentCoverage");
|
|
287
|
-
let rows;
|
|
288
|
-
try {
|
|
289
|
-
if (Array.isArray(coverageRaw)) {
|
|
290
|
-
const walked = [];
|
|
291
|
-
for (let i = 0; i < coverageRaw.length; i++) {
|
|
292
|
-
const r = coverageRaw[i];
|
|
293
|
-
walked.push(isPlainOwnRecord(r) ? { segment: ownDataValue(r, "segment"), covered: ownDataValue(r, "covered") } : { segment: undefined, covered: undefined });
|
|
294
|
-
}
|
|
295
|
-
rows = walked.every((r) => typeof r.segment === "string" && typeof r.covered === "boolean") ? walked : undefined;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
rows = undefined;
|
|
300
|
-
}
|
|
301
|
-
return rows !== undefined && rows.length > 0 ? { coverage: rows.map((r) => ({ segment: r.segment, covered: r.covered })) } : {};
|
|
302
|
-
}
|
|
303
|
-
let members;
|
|
304
|
-
try {
|
|
305
|
-
if (Array.isArray(rulesRaw)) {
|
|
306
|
-
const walked = [];
|
|
307
|
-
for (let i = 0; i < rulesRaw.length; i++) {
|
|
308
|
-
const m = rulesRaw[i];
|
|
309
|
-
walked.push(isPlainOwnRecord(m) ? { rule: ownDataValue(m, "rule"), dots: ownDataValue(m, "dots") } : { rule: undefined, dots: undefined });
|
|
310
|
-
}
|
|
311
|
-
members = walked;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
members = undefined;
|
|
316
|
-
}
|
|
317
|
-
if (members === undefined || members.length === 0)
|
|
318
|
-
return {};
|
|
319
|
-
if (!members.every((m) => typeof m.rule === "string"))
|
|
320
|
-
return {};
|
|
321
|
-
return { hit: { rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
|
|
322
|
-
}
|
|
323
171
|
function preToolUseCrashReason(subject, err) {
|
|
324
172
|
const raw = err instanceof Error ? err.message.trim() || err.name : String(err);
|
|
325
173
|
const cause = inlineUntrusted(raw, 200);
|
|
@@ -383,49 +231,6 @@ export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeo
|
|
|
383
231
|
},
|
|
384
232
|
}, "hook");
|
|
385
233
|
}
|
|
386
|
-
function traceHookCrash(input, err, notifier) {
|
|
387
|
-
notifier.notify(() => input.onHookError?.(err), "toolGate.onHookError");
|
|
388
|
-
}
|
|
389
|
-
function withProbeTimeout(p, ms, signal) {
|
|
390
|
-
if (ms === undefined && signal === undefined)
|
|
391
|
-
return p;
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
let settled = false;
|
|
394
|
-
const done = (fn, v) => {
|
|
395
|
-
if (settled)
|
|
396
|
-
return;
|
|
397
|
-
settled = true;
|
|
398
|
-
if (timer !== undefined)
|
|
399
|
-
clearTimeout(timer);
|
|
400
|
-
signal?.removeEventListener("abort", onAbort);
|
|
401
|
-
fn(v);
|
|
402
|
-
};
|
|
403
|
-
const onAbort = () => done(reject, new Error("reversibilityProbe aborted (task ended)"));
|
|
404
|
-
const timer = ms !== undefined
|
|
405
|
-
? setTimeout(() => done(reject, new Error(`reversibilityProbe timed out after ${ms}ms`)), ms)
|
|
406
|
-
: undefined;
|
|
407
|
-
if (signal?.aborted) {
|
|
408
|
-
onAbort();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
signal?.addEventListener("abort", onAbort);
|
|
412
|
-
p.then((v) => done(resolve, v), (err) => done(reject, err));
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
|
|
416
|
-
export function persistedRuleMandateOf(marks) {
|
|
417
|
-
return marks.probeMandated === true
|
|
418
|
-
? "probe_mandate"
|
|
419
|
-
: marks.egress === true
|
|
420
|
-
? "tool_marks"
|
|
421
|
-
: marks.shellGated === true
|
|
422
|
-
? marks.irreversibility === "always"
|
|
423
|
-
? "operator_always"
|
|
424
|
-
: undefined
|
|
425
|
-
: marks.irreversibility === "always" || marks.irreversibility === "maybe"
|
|
426
|
-
? "tool_marks"
|
|
427
|
-
: undefined;
|
|
428
|
-
}
|
|
429
234
|
export function askCarryRowMembers(carry) {
|
|
430
235
|
if (carry === undefined)
|
|
431
236
|
return {};
|
|
@@ -434,9 +239,6 @@ export function askCarryRowMembers(carry) {
|
|
|
434
239
|
...(isAskOrigin(carry.origin) ? { origin: carry.origin } : {}),
|
|
435
240
|
};
|
|
436
241
|
}
|
|
437
|
-
function parkWith(suspendAsk, parkArgs, carry) {
|
|
438
|
-
return suspendAsk(...parkArgs, carry);
|
|
439
|
-
}
|
|
440
242
|
export async function runToolGate(input) {
|
|
441
243
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
442
244
|
const { toolCallId, toolName } = event;
|
|
@@ -452,12 +254,7 @@ export async function runToolGate(input) {
|
|
|
452
254
|
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
|
|
453
255
|
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
454
256
|
});
|
|
455
|
-
|
|
456
|
-
const preToolContext = [];
|
|
457
|
-
let hookAsk;
|
|
458
|
-
let parkFailed;
|
|
459
|
-
let deniedBy;
|
|
460
|
-
const ledger = createSettlementLedger({ toolCallId, toolName });
|
|
257
|
+
const modelInput = event.input;
|
|
461
258
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
462
259
|
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
|
|
463
260
|
const seatBound = (abortEnds) => ({
|
|
@@ -481,669 +278,71 @@ export async function runToolGate(input) {
|
|
|
481
278
|
throw hookSeatExpiredError("permissionDenied", hookSeatMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged");
|
|
482
279
|
}, "toolGate.permissionDenied");
|
|
483
280
|
};
|
|
484
|
-
|
|
281
|
+
const screening = preToolUse !== undefined
|
|
282
|
+
? {
|
|
283
|
+
screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, args, hookCtx(sig))),
|
|
284
|
+
screened: screenPreToolUseResult,
|
|
285
|
+
crashReason: preToolUseCrashReason,
|
|
286
|
+
unansweredReason: preToolUseUnansweredBlock,
|
|
287
|
+
}
|
|
288
|
+
: undefined;
|
|
289
|
+
const pass = {
|
|
290
|
+
input,
|
|
291
|
+
event,
|
|
292
|
+
adjudicate,
|
|
293
|
+
resolveAsk,
|
|
294
|
+
suspendAsk,
|
|
295
|
+
toolName,
|
|
296
|
+
toolCallId,
|
|
297
|
+
callSignal,
|
|
298
|
+
notifier,
|
|
299
|
+
ledger: createSettlementLedger({ toolCallId, toolName }),
|
|
300
|
+
notifyPermissionDenied: notifyPermissionDeniedSeat,
|
|
301
|
+
screening,
|
|
302
|
+
afterFold: runGateLanes,
|
|
303
|
+
currentInput: modelInput,
|
|
304
|
+
req: { toolName, args: modelInput, toolCallId },
|
|
305
|
+
preToolContext: [],
|
|
306
|
+
hookAsk: undefined,
|
|
307
|
+
parkFailed: undefined,
|
|
308
|
+
deniedBy: undefined,
|
|
309
|
+
decision: { action: "allow" },
|
|
310
|
+
tightenedBy: undefined,
|
|
311
|
+
policyRewrite: undefined,
|
|
312
|
+
};
|
|
313
|
+
if (screening !== undefined) {
|
|
485
314
|
let r;
|
|
486
315
|
let unanswered;
|
|
487
316
|
try {
|
|
488
|
-
const seat = await
|
|
317
|
+
const seat = await screening.screen(pass.currentInput);
|
|
489
318
|
if (seat.expired)
|
|
490
319
|
unanswered = seat.cause;
|
|
491
320
|
else
|
|
492
|
-
r =
|
|
321
|
+
r = screening.screened(seat.value);
|
|
493
322
|
}
|
|
494
323
|
catch (err) {
|
|
495
|
-
const reason =
|
|
324
|
+
const reason = screening.crashReason(`this call to "${toolName}"`, err);
|
|
496
325
|
traceHookCrash(input, err, notifier);
|
|
497
|
-
|
|
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 };
|
|
326
|
+
return await engineFailClosedExit(pass, reason);
|
|
500
327
|
}
|
|
501
328
|
if (unanswered !== undefined) {
|
|
502
|
-
const reason = await
|
|
503
|
-
|
|
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 };
|
|
329
|
+
const reason = await screening.unansweredReason(`this call to "${toolName}"`, unanswered);
|
|
330
|
+
return await engineFailClosedExit(pass, reason);
|
|
506
331
|
}
|
|
507
332
|
if (r) {
|
|
508
333
|
if (r.additionalContext) {
|
|
509
|
-
preToolContext.push(r.additionalContext);
|
|
334
|
+
pass.preToolContext.push(r.additionalContext);
|
|
510
335
|
}
|
|
511
336
|
if (r.action === "deny") {
|
|
512
|
-
return
|
|
513
|
-
block: true,
|
|
514
|
-
reason: formatHookFeedback(decisionText(r) ?? `tool "${toolName}" blocked by a PreToolUse hook`, input.reminderMark),
|
|
515
|
-
gate: directDeny("hook"),
|
|
516
|
-
preToolContext,
|
|
517
|
-
};
|
|
337
|
+
return hookDenyExit(pass, r);
|
|
518
338
|
}
|
|
519
339
|
if (r.updatedInput !== undefined) {
|
|
520
|
-
currentInput = r.updatedInput;
|
|
340
|
+
pass.currentInput = r.updatedInput;
|
|
521
341
|
}
|
|
522
342
|
if (r.action === "ask") {
|
|
523
|
-
hookAsk = r;
|
|
343
|
+
pass.hookAsk = r;
|
|
524
344
|
}
|
|
525
345
|
}
|
|
526
346
|
}
|
|
527
|
-
|
|
528
|
-
let decision = adjudicate ? await (callSignal !== undefined ? adjudicate(req, callSignal) : adjudicate(req)) : { action: "allow" };
|
|
529
|
-
{
|
|
530
|
-
const refusedBy = ledger.fromDecision(decision);
|
|
531
|
-
if (refusedBy !== undefined)
|
|
532
|
-
deniedBy = refusedBy;
|
|
533
|
-
}
|
|
534
|
-
let tightenedBy;
|
|
535
|
-
let policyRewrite;
|
|
536
|
-
if ((decision.action === "allow" || decision.action === "ask") && decision.updatedInput !== undefined) {
|
|
537
|
-
policyRewrite = decision.updatedInput;
|
|
538
|
-
}
|
|
539
|
-
else if (hookAsk?.updatedInput !== undefined) {
|
|
540
|
-
policyRewrite = hookAsk.updatedInput;
|
|
541
|
-
}
|
|
542
|
-
if (decision.action === "allow" && hookAsk) {
|
|
543
|
-
decision = {
|
|
544
|
-
action: "ask",
|
|
545
|
-
...(hookAsk.message !== undefined ? { message: hookAsk.message } : {}),
|
|
546
|
-
decisionReason: "hook",
|
|
547
|
-
...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
548
|
-
...(hookAsk.updatedInput !== undefined ? { updatedInput: hookAsk.updatedInput } : {}),
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
else if (decision.action === "ask" && hookAsk) {
|
|
552
|
-
const hookMessage = hookAsk.message;
|
|
553
|
-
const foldedMessage = hookMessage !== undefined && hookMessage !== decision.message
|
|
554
|
-
? `${decision.message !== undefined ? `${decision.message} ` : ""}(a PreToolUse hook also asks: ${hookMessage})`
|
|
555
|
-
: decision.message;
|
|
556
|
-
decision = {
|
|
557
|
-
...decision,
|
|
558
|
-
decisionReason: "hook",
|
|
559
|
-
...(foldedMessage !== undefined ? { message: foldedMessage } : {}),
|
|
560
|
-
...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
if (input.egress && decision.action === "allow") {
|
|
564
|
-
decision = {
|
|
565
|
-
action: "ask",
|
|
566
|
-
message: `tool "${toolName}" performs an external write (egress) — explicit approval required`,
|
|
567
|
-
decisionReason: "safety",
|
|
568
|
-
};
|
|
569
|
-
tightenedBy = "safety";
|
|
570
|
-
}
|
|
571
|
-
const alreadyAsking = decision.action === "ask";
|
|
572
|
-
if ((decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
|
|
573
|
-
let tighten = !alreadyAsking;
|
|
574
|
-
let probeReason;
|
|
575
|
-
let probeCause;
|
|
576
|
-
let probeMandated;
|
|
577
|
-
if (input.irreversibility === "maybe" && input.reversibilityProbe) {
|
|
578
|
-
let reversible = false;
|
|
579
|
-
const probeArgs = policyRewrite !== undefined ? policyRewrite : currentInput;
|
|
580
|
-
const suppliedProbeMs = input.approvalTimeoutMs;
|
|
581
|
-
let probeTimeoutMs;
|
|
582
|
-
if (suppliedProbeMs === undefined) {
|
|
583
|
-
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS;
|
|
584
|
-
}
|
|
585
|
-
else if (Number.isFinite(suppliedProbeMs) && suppliedProbeMs >= 0 && suppliedProbeMs <= 2_147_483_647) {
|
|
586
|
-
probeTimeoutMs = suppliedProbeMs;
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS;
|
|
590
|
-
traceHookCrash(input, new Error(`approvalTimeoutMs must be a non-negative finite number no greater than 2147483647 (got ${String(suppliedProbeMs)}) — the reversibilityProbe deadline falls back to the ${DEFAULT_PROBE_TIMEOUT_MS}ms default`), notifier);
|
|
591
|
-
}
|
|
592
|
-
try {
|
|
593
|
-
const verdict = await withProbeTimeout(Promise.resolve(input.reversibilityProbe(probeArgs)), probeTimeoutMs, input.abortSignal);
|
|
594
|
-
reversible = verdict?.reversible === true;
|
|
595
|
-
if (!reversible) {
|
|
596
|
-
const raw = verdict?.reason;
|
|
597
|
-
if (typeof raw === "string" && raw.trim().length > 0)
|
|
598
|
-
probeReason = inlineUntrusted(raw, PROBE_REASON_MAX);
|
|
599
|
-
probeCause = normalizeProbeCause(verdict?.cause);
|
|
600
|
-
if (verdict?.mandated === true)
|
|
601
|
-
probeMandated = true;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
catch (err) {
|
|
605
|
-
reversible = false;
|
|
606
|
-
if (input.abortSignal?.aborted !== true)
|
|
607
|
-
traceHookCrash(input, err, notifier);
|
|
608
|
-
}
|
|
609
|
-
tighten = !alreadyAsking && !reversible;
|
|
610
|
-
}
|
|
611
|
-
if (tighten) {
|
|
612
|
-
decision = {
|
|
613
|
-
action: "ask",
|
|
614
|
-
message: `tool "${toolName}" performs an irreversible action — explicit approval required`,
|
|
615
|
-
decisionReason: "safety",
|
|
616
|
-
...(probeReason !== undefined ? { probeReason } : {}),
|
|
617
|
-
...(probeCause !== undefined ? { probeCause } : {}),
|
|
618
|
-
...(probeMandated === true ? { probeMandated: true } : {}),
|
|
619
|
-
};
|
|
620
|
-
tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
|
|
621
|
-
}
|
|
622
|
-
else if (alreadyAsking && probeMandated === true && decision.action === "ask") {
|
|
623
|
-
decision = { ...decision, probeMandated: true };
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
const breakerKnownOpen = (d) => {
|
|
627
|
-
try {
|
|
628
|
-
return d.breakerOpen();
|
|
629
|
-
}
|
|
630
|
-
catch {
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
if (input.peerMessage === true && decision.action === "allow" && input.autoMode !== undefined && !breakerKnownOpen(input.autoMode.decider)) {
|
|
635
|
-
decision = {
|
|
636
|
-
action: "ask",
|
|
637
|
-
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
638
|
-
decisionReason: "safety",
|
|
639
|
-
};
|
|
640
|
-
tightenedBy = "safety";
|
|
641
|
-
}
|
|
642
|
-
if (decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
643
|
-
const writeProtectedHit = input.writeProtectionCheck(toolName, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
644
|
-
if (writeProtectedHit !== null) {
|
|
645
|
-
decision = {
|
|
646
|
-
action: "ask",
|
|
647
|
-
message: `tool "${toolName}" writes to a write-protected path (table entry "${writeProtectedHit.name}") — explicit approval required`,
|
|
648
|
-
decisionReason: "safety",
|
|
649
|
-
};
|
|
650
|
-
tightenedBy = "safety";
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
let orgRealApprovalRequired = false;
|
|
654
|
-
const askOriginFacts = (org) => ({
|
|
655
|
-
contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
|
|
656
|
-
markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
|
|
657
|
-
org,
|
|
658
|
-
tightened: tightenedBy,
|
|
659
|
-
});
|
|
660
|
-
let orgTightenCount = 0;
|
|
661
|
-
let orgEvidence = (() => {
|
|
662
|
-
const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
|
|
663
|
-
return { revisionAbsent: w, ruleAbsent: w };
|
|
664
|
-
})();
|
|
665
|
-
const mintRuleEvidence = (personal) => ({
|
|
666
|
-
...(orgEvidence.revision !== undefined ? { orgRevision: orgEvidence.revision } : { orgRevisionAbsent: orgEvidence.revisionAbsent ?? "not_reported" }),
|
|
667
|
-
...(orgEvidence.rule !== undefined ? { orgRule: orgEvidence.rule } : { orgRuleAbsent: orgEvidence.ruleAbsent ?? "not_reported" }),
|
|
668
|
-
...(personal.dots !== undefined ? { personalRuleDots: personal.dots } : { personalRuleDotsAbsent: personal.dotsAbsent ?? "not_reported" }),
|
|
669
|
-
});
|
|
670
|
-
const applyOrgLayer = async (current, args) => {
|
|
671
|
-
if (input.orgRules === undefined)
|
|
672
|
-
return { decision: current, org: undefined };
|
|
673
|
-
if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
|
|
674
|
-
return { decision: current, org: undefined };
|
|
675
|
-
let decided = current;
|
|
676
|
-
let pending;
|
|
677
|
-
try {
|
|
678
|
-
pending = Promise.resolve(input.orgRules.adjudicate({ toolName, args, toolCallId }));
|
|
679
|
-
}
|
|
680
|
-
catch (err) {
|
|
681
|
-
pending = Promise.resolve({ status: "unavailable", disclosures: [`the org adjudication face threw before answering: ${describeThrown(err)}`] });
|
|
682
|
-
}
|
|
683
|
-
const raw = await settleOrgVerdictWithin(pending.catch((err) => ({
|
|
684
|
-
status: "unavailable",
|
|
685
|
-
disclosures: [`the org adjudication face threw: ${describeThrown(err)}`],
|
|
686
|
-
})), {
|
|
687
|
-
status: "unavailable",
|
|
688
|
-
disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
|
|
689
|
-
}, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
690
|
-
const answer = normalizeOrgGateVerdict(raw, "the org adjudication face answered something this gate cannot read as a verdict — treated as unreadable governance, never as an empty one");
|
|
691
|
-
if (answer.status === "unavailable") {
|
|
692
|
-
orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
|
|
693
|
-
orgRealApprovalRequired = true;
|
|
694
|
-
await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
|
|
695
|
-
if (decided.action === "allow") {
|
|
696
|
-
decided = {
|
|
697
|
-
action: "ask",
|
|
698
|
-
message: `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
|
|
699
|
-
`against an organization policy snapshot — every allow tightens to a real approval until it can`,
|
|
700
|
-
decisionReason: ORG_UNAVAILABLE_DECISION_REASON,
|
|
701
|
-
requiresRealApproval: true,
|
|
702
|
-
};
|
|
703
|
-
orgTightenCount += 1;
|
|
704
|
-
}
|
|
705
|
-
else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
|
|
706
|
-
decided = { ...decided, requiresRealApproval: true };
|
|
707
|
-
orgTightenCount += 1;
|
|
708
|
-
}
|
|
709
|
-
return { decision: decided, org: "unavailable" };
|
|
710
|
-
}
|
|
711
|
-
const revisionCell = answer.revision !== undefined ? { revision: answer.revision } : { revisionAbsent: "not_reported" };
|
|
712
|
-
if (answer.verdict === undefined) {
|
|
713
|
-
orgEvidence = { ...revisionCell, ruleAbsent: "no_match" };
|
|
714
|
-
return { decision: decided, org: undefined };
|
|
715
|
-
}
|
|
716
|
-
const { behavior, rule } = answer.verdict;
|
|
717
|
-
orgEvidence = { ...revisionCell, rule };
|
|
718
|
-
if (behavior === "deny") {
|
|
719
|
-
orgTightenCount += 1;
|
|
720
|
-
deniedBy = "org";
|
|
721
|
-
return { decision: { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON }, org: undefined };
|
|
722
|
-
}
|
|
723
|
-
if (decided.action === "deny")
|
|
724
|
-
return { decision: decided, org: undefined };
|
|
725
|
-
orgTightenCount += 1;
|
|
726
|
-
orgRealApprovalRequired = true;
|
|
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
|
-
};
|
|
738
|
-
};
|
|
739
|
-
const orgAnswer = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
740
|
-
decision = orgAnswer.decision;
|
|
741
|
-
if (decision.action === "ask") {
|
|
742
|
-
if (policyRewrite !== undefined) {
|
|
743
|
-
currentInput = policyRewrite;
|
|
744
|
-
req.args = policyRewrite;
|
|
745
|
-
}
|
|
746
|
-
const persistedRuleMandate = persistedRuleMandateOf({
|
|
747
|
-
egress: input.egress,
|
|
748
|
-
shellGated: input.shellGated,
|
|
749
|
-
irreversibility: input.irreversibility,
|
|
750
|
-
probeMandated: decision.action === "ask" && decision.probeMandated === true,
|
|
751
|
-
});
|
|
752
|
-
let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
|
|
753
|
-
let laneCoverage;
|
|
754
|
-
if (input.persistedRules &&
|
|
755
|
-
!orgRealApprovalRequired &&
|
|
756
|
-
decision.action === "ask" &&
|
|
757
|
-
decision.requiresRealApproval !== true &&
|
|
758
|
-
decision.decisionReason !== "hook" &&
|
|
759
|
-
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
760
|
-
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
761
|
-
const answer = await (async () => {
|
|
762
|
-
let pendingHit;
|
|
763
|
-
try {
|
|
764
|
-
pendingHit = Promise.resolve(input.persistedRules?.admits(req));
|
|
765
|
-
}
|
|
766
|
-
catch {
|
|
767
|
-
return { unreadable: true };
|
|
768
|
-
}
|
|
769
|
-
return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
770
|
-
})();
|
|
771
|
-
const hitRules = answer.hit?.rules;
|
|
772
|
-
laneCoverage = answer.coverage;
|
|
773
|
-
personalEvidence = { dotsAbsent: answer.unreadable === true ? "unavailable" : "no_match" };
|
|
774
|
-
const disclosedRuleSet = (rules) => {
|
|
775
|
-
const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
|
|
776
|
-
const rest = rules.length - shown.length;
|
|
777
|
-
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
778
|
-
};
|
|
779
|
-
const unionDotsOf = (rules) => {
|
|
780
|
-
const seen = new Set();
|
|
781
|
-
const union = [];
|
|
782
|
-
for (const r of rules) {
|
|
783
|
-
if (r.dots === undefined || r.dots.length === 0)
|
|
784
|
-
return { dotsAbsent: "not_reported" };
|
|
785
|
-
for (const d of r.dots) {
|
|
786
|
-
const key = JSON.stringify([d.actor, d.counter]);
|
|
787
|
-
if (!seen.has(key)) {
|
|
788
|
-
seen.add(key);
|
|
789
|
-
union.push(d);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return { dots: union };
|
|
794
|
-
};
|
|
795
|
-
if (hitRules !== undefined && persistedRuleMandate === undefined && decision.matchedAskRule === undefined) {
|
|
796
|
-
decision = {
|
|
797
|
-
action: "allow",
|
|
798
|
-
message: hitRules.length === 1
|
|
799
|
-
? `a persisted allow rule (${disclosedRuleSet(hitRules)}) covers this call`
|
|
800
|
-
: `persisted allow rules (${disclosedRuleSet(hitRules)}) cover every segment of this call`,
|
|
801
|
-
decisionReason: "persisted_rule",
|
|
802
|
-
...(policyRewrite !== undefined ? { updatedInput: policyRewrite } : {}),
|
|
803
|
-
};
|
|
804
|
-
await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: req.toolName, toolCallId, rules: hitRules.map((r) => r.rule) }), "toolGate.persistedRuleResolved");
|
|
805
|
-
}
|
|
806
|
-
else if (hitRules !== undefined) {
|
|
807
|
-
personalEvidence = unionDotsOf(hitRules);
|
|
808
|
-
const shownRule = disclosedRuleSet(hitRules);
|
|
809
|
-
const mandateNoun = persistedRuleMandate === "probe_mandate"
|
|
810
|
-
? "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule"
|
|
811
|
-
: persistedRuleMandate === "operator_always"
|
|
812
|
-
? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
|
|
813
|
-
: persistedRuleMandate !== undefined
|
|
814
|
-
? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
|
|
815
|
-
: "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
|
|
816
|
-
decision = {
|
|
817
|
-
...decision,
|
|
818
|
-
persistedRuleShadowed: shownRule,
|
|
819
|
-
message: hitRules.length === 1
|
|
820
|
-
? `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${shownRule}) matches this call but does not clear the ask — ${mandateNoun})`
|
|
821
|
-
: `${decision.message !== undefined ? `${decision.message} ` : ""}(persisted allow rules (${shownRule}) match this call but do not clear the ask — ${mandateNoun})`,
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
const originFacts = askOriginFacts(orgAnswer.org);
|
|
826
|
-
if (decision.action === "ask") {
|
|
827
|
-
const stamped = { ...decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
828
|
-
decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
|
|
829
|
-
}
|
|
830
|
-
if (decision.action === "ask")
|
|
831
|
-
ledger.supersede();
|
|
832
|
-
if (input.autoMode && decision.action === "ask" && decision.origin !== undefined && classifierMayAnswer(decision.origin)) {
|
|
833
|
-
const verdict = await input.autoMode.decider
|
|
834
|
-
.decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
|
|
835
|
-
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
836
|
-
if (verdict.kind === "allow") {
|
|
837
|
-
input.autoMode.denialTracking?.recordAllow();
|
|
838
|
-
decision = {
|
|
839
|
-
action: "allow",
|
|
840
|
-
message: "auto-mode classifier allowed this call",
|
|
841
|
-
decisionReason: "classifier",
|
|
842
|
-
...(policyRewrite !== undefined ? { updatedInput: policyRewrite } : {}),
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
else if (verdict.kind === "block") {
|
|
846
|
-
const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
|
|
847
|
-
const category = verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
848
|
-
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
849
|
-
if (tracked?.limitReached === true) {
|
|
850
|
-
const fallbackAsk = {
|
|
851
|
-
...decision,
|
|
852
|
-
message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
|
|
853
|
-
decisionReason: "classifier",
|
|
854
|
-
requiresRealApproval: true,
|
|
855
|
-
denialLimitFallback: tracked.fallback,
|
|
856
|
-
};
|
|
857
|
-
decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
|
|
858
|
-
}
|
|
859
|
-
else {
|
|
860
|
-
decision = {
|
|
861
|
-
action: "deny",
|
|
862
|
-
message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
|
|
863
|
-
decisionReason: "classifier",
|
|
864
|
-
};
|
|
865
|
-
deniedBy = "classifier";
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
if (input.sandboxAdmission !== undefined &&
|
|
870
|
-
decision.action === "ask" &&
|
|
871
|
-
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
872
|
-
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
|
|
873
|
-
decision.requiresRealApproval !== true &&
|
|
874
|
-
decision.decisionReason !== "hook" &&
|
|
875
|
-
hookAsk === undefined &&
|
|
876
|
-
input.egress !== true &&
|
|
877
|
-
input.peerMessage !== true &&
|
|
878
|
-
input.sandboxAdmission.boundaryCapable !== true) {
|
|
879
|
-
const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
|
|
880
|
-
const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
|
|
881
|
-
if (admissible) {
|
|
882
|
-
const askClasses = recorded;
|
|
883
|
-
input.sandboxAdmission.onAdmitted({ toolCallId, toolName, askClasses, boundary: "sandbox_internal" });
|
|
884
|
-
preToolContext.push(`permission.sandbox_admitted: this call's pending approval was auto-admitted because the deployment declares an isolated sandbox execution env and the request stays inside it. Recorded for audit.`);
|
|
885
|
-
decision = {
|
|
886
|
-
action: "allow",
|
|
887
|
-
message: "admitted inside the deployment-declared sandbox (permission.sandbox_admitted)",
|
|
888
|
-
decisionReason: "sandbox",
|
|
889
|
-
...(policyRewrite !== undefined ? { updatedInput: policyRewrite } : {}),
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
const egressTool = input.egress === true;
|
|
894
|
-
const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
|
|
895
|
-
const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
|
|
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
|
-
};
|
|
903
|
-
const carryOf = (d) => ({
|
|
904
|
-
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
905
|
-
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
906
|
-
...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
|
|
907
|
-
});
|
|
908
|
-
if (suspendAsk && decision.action === "ask") {
|
|
909
|
-
const parkArgs = [req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
910
|
-
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(decision));
|
|
911
|
-
if (suspended) {
|
|
912
|
-
if ("parkFailed" in suspended)
|
|
913
|
-
parkFailed = suspended.parkFailed;
|
|
914
|
-
else
|
|
915
|
-
return { suspend: suspended, preToolContext };
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
919
|
-
const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(req, callSignal) : input.resolveContentAsk(req)) : { kind: "unavailable", parkDeclined: false };
|
|
920
|
-
if (outcome.kind === "answered") {
|
|
921
|
-
decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
922
|
-
}
|
|
923
|
-
else if (outcome.kind === "unavailable") {
|
|
924
|
-
if (outcome.presentedInput !== undefined) {
|
|
925
|
-
currentInput = outcome.presentedInput;
|
|
926
|
-
req.args = outcome.presentedInput;
|
|
927
|
-
}
|
|
928
|
-
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
929
|
-
const parkArgs = [req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
930
|
-
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(decision));
|
|
931
|
-
if (suspended) {
|
|
932
|
-
if ("parkFailed" in suspended)
|
|
933
|
-
parkFailed = suspended.parkFailed;
|
|
934
|
-
else
|
|
935
|
-
return { suspend: suspended, preToolContext };
|
|
936
|
-
}
|
|
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";
|
|
942
|
-
decision = {
|
|
943
|
-
action: "deny",
|
|
944
|
-
message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
|
|
945
|
-
"facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
|
|
946
|
-
"the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
|
|
947
|
-
"you made.",
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
if (outcome.presentedInput !== undefined)
|
|
952
|
-
decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
953
|
-
else
|
|
954
|
-
decision = { action: "allow" };
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
if (decision.action === "ask") {
|
|
958
|
-
const askBeforeResolve = decision;
|
|
959
|
-
const resolved = await (callSignal !== undefined ? resolveAsk(decision, req, callSignal) : resolveAsk(decision, req));
|
|
960
|
-
ledger.fromResolved(resolved, askBeforeResolve, toolName);
|
|
961
|
-
if (resolved.action === "deny")
|
|
962
|
-
deniedBy = "ask_resolution";
|
|
963
|
-
decision = resolved;
|
|
964
|
-
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
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];
|
|
966
|
-
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(askBeforeResolve));
|
|
967
|
-
if (suspended) {
|
|
968
|
-
if ("parkFailed" in suspended)
|
|
969
|
-
parkFailed = suspended.parkFailed;
|
|
970
|
-
else
|
|
971
|
-
return { suspend: suspended, preToolContext };
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
if (askBeforeResolve.action === "ask" && askBeforeResolve.denialLimitFallback !== undefined) {
|
|
975
|
-
if (decision.action === "allow") {
|
|
976
|
-
input.autoMode?.denialTracking?.recordAllow();
|
|
977
|
-
}
|
|
978
|
-
else if ((resolved.action === "deny" &&
|
|
979
|
-
(resolved.settlement.kind === "no_approver" || resolved.settlement.kind === "blanket_allow_refused" || resolved.settlement.kind === "approver_unavailable")) ||
|
|
980
|
-
resolved.approverUnavailable === true) {
|
|
981
|
-
try {
|
|
982
|
-
input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: unarmedWindow(askBeforeResolve.denialLimitFallback) });
|
|
983
|
-
}
|
|
984
|
-
catch {
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
if (decision.action === "allow" && decision.updatedInput !== undefined) {
|
|
989
|
-
let editArgs = decision.updatedInput;
|
|
990
|
-
let editDenied;
|
|
991
|
-
let editRewrittenSinceHuman = false;
|
|
992
|
-
for (let round = 0;; round++) {
|
|
993
|
-
if (round >= 3) {
|
|
994
|
-
editDenied = {
|
|
995
|
-
action: "deny",
|
|
996
|
-
message: `the approval-edit chain for "${toolName}" exceeded the re-approval cap — denied fail-closed; re-submit the edited action directly`,
|
|
997
|
-
};
|
|
998
|
-
deniedBy = "policy";
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
if (preToolUse) {
|
|
1002
|
-
let hr;
|
|
1003
|
-
let hrUnanswered;
|
|
1004
|
-
try {
|
|
1005
|
-
const seat = await runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, editArgs, hookCtx(sig)));
|
|
1006
|
-
if (seat.expired)
|
|
1007
|
-
hrUnanswered = seat.cause;
|
|
1008
|
-
else
|
|
1009
|
-
hr = screenPreToolUseResult(seat.value);
|
|
1010
|
-
}
|
|
1011
|
-
catch (err) {
|
|
1012
|
-
traceHookCrash(input, err, notifier);
|
|
1013
|
-
editDenied = { action: "deny", message: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
|
|
1014
|
-
deniedBy = "hook";
|
|
1015
|
-
break;
|
|
1016
|
-
}
|
|
1017
|
-
if (hrUnanswered !== undefined) {
|
|
1018
|
-
editDenied = { action: "deny", message: await preToolUseUnansweredBlock(`the approved edit for "${toolName}"`, hrUnanswered) };
|
|
1019
|
-
deniedBy = "hook";
|
|
1020
|
-
break;
|
|
1021
|
-
}
|
|
1022
|
-
if (hr) {
|
|
1023
|
-
if (hr.additionalContext)
|
|
1024
|
-
preToolContext.push(hr.additionalContext);
|
|
1025
|
-
if (hr.action === "deny") {
|
|
1026
|
-
editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
|
|
1027
|
-
deniedBy = "hook";
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
if (hr.action === "ask") {
|
|
1031
|
-
editDenied = {
|
|
1032
|
-
action: "deny",
|
|
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`,
|
|
1034
|
-
};
|
|
1035
|
-
deniedBy = "hook";
|
|
1036
|
-
break;
|
|
1037
|
-
}
|
|
1038
|
-
if (hr.updatedInput !== undefined) {
|
|
1039
|
-
editArgs = hr.updatedInput;
|
|
1040
|
-
editRewrittenSinceHuman = true;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (!adjudicate && input.orgRules === undefined)
|
|
1045
|
-
break;
|
|
1046
|
-
let recheck = { action: "allow" };
|
|
1047
|
-
if (adjudicate) {
|
|
1048
|
-
try {
|
|
1049
|
-
recheck = await (callSignal !== undefined ? adjudicate({ toolName, args: editArgs, toolCallId }, callSignal) : adjudicate({ toolName, args: editArgs, toolCallId }));
|
|
1050
|
-
}
|
|
1051
|
-
catch (err) {
|
|
1052
|
-
recheck = {
|
|
1053
|
-
action: "deny",
|
|
1054
|
-
message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
{
|
|
1059
|
-
const refusedBy = ledger.fromDecision(recheck);
|
|
1060
|
-
if (recheck.action === "deny" && refusedBy !== undefined) {
|
|
1061
|
-
editDenied = recheck;
|
|
1062
|
-
deniedBy = refusedBy;
|
|
1063
|
-
break;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
if (recheck.updatedInput !== undefined) {
|
|
1067
|
-
editArgs = recheck.updatedInput;
|
|
1068
|
-
editRewrittenSinceHuman = true;
|
|
1069
|
-
}
|
|
1070
|
-
const editOrg = await applyOrgLayer(recheck, editArgs);
|
|
1071
|
-
recheck = editOrg.decision;
|
|
1072
|
-
if (recheck.action === "deny") {
|
|
1073
|
-
editDenied = recheck;
|
|
1074
|
-
break;
|
|
1075
|
-
}
|
|
1076
|
-
if (recheck.action === "allow") {
|
|
1077
|
-
if (recheck.updatedInput !== undefined) {
|
|
1078
|
-
editArgs = recheck.updatedInput;
|
|
1079
|
-
editRewrittenSinceHuman = true;
|
|
1080
|
-
}
|
|
1081
|
-
break;
|
|
1082
|
-
}
|
|
1083
|
-
if (recheck.updatedInput !== undefined) {
|
|
1084
|
-
editArgs = recheck.updatedInput;
|
|
1085
|
-
editRewrittenSinceHuman = true;
|
|
1086
|
-
}
|
|
1087
|
-
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
1088
|
-
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
|
|
1089
|
-
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
1090
|
-
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
1091
|
-
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
1092
|
-
if (rr.action !== "allow") {
|
|
1093
|
-
editDenied = rr;
|
|
1094
|
-
deniedBy = "ask_resolution";
|
|
1095
|
-
break;
|
|
1096
|
-
}
|
|
1097
|
-
if (rr.updatedInput === undefined) {
|
|
1098
|
-
if (rr.presentedInput !== undefined)
|
|
1099
|
-
editArgs = rr.presentedInput;
|
|
1100
|
-
editRewrittenSinceHuman = false;
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
editArgs = rr.updatedInput;
|
|
1104
|
-
editRewrittenSinceHuman = false;
|
|
1105
|
-
}
|
|
1106
|
-
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
1107
|
-
const editHit = input.writeProtectionCheck(toolName, editArgs);
|
|
1108
|
-
if (editHit !== null) {
|
|
1109
|
-
editDenied = {
|
|
1110
|
-
action: "deny",
|
|
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`,
|
|
1112
|
-
};
|
|
1113
|
-
deniedBy = "write_protection";
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
decision = editDenied ?? { ...decision, updatedInput: editArgs };
|
|
1117
|
-
}
|
|
1118
|
-
else if (decision.action === "allow" && decision.updatedInput === undefined && resolved.presentedInput !== undefined) {
|
|
1119
|
-
decision = { ...decision, updatedInput: resolved.presentedInput };
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
if (decision.action === "deny") {
|
|
1124
|
-
const denyReason = withParkFailureCause(decisionText(decision) ?? `tool "${toolName}" denied by policy`, parkFailed);
|
|
1125
|
-
if (decision.updatedInput !== undefined) {
|
|
1126
|
-
currentInput = decision.updatedInput;
|
|
1127
|
-
}
|
|
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 } : {}) });
|
|
1132
|
-
return {
|
|
1133
|
-
block: true,
|
|
1134
|
-
reason: formatHookFeedback(denyReason, input.reminderMark),
|
|
1135
|
-
gate,
|
|
1136
|
-
preToolContext,
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
if (decision.action === "allow") {
|
|
1140
|
-
const rw = decision.updatedInput !== undefined ? decision.updatedInput : policyRewrite;
|
|
1141
|
-
if (rw !== undefined)
|
|
1142
|
-
currentInput = rw;
|
|
1143
|
-
}
|
|
1144
|
-
return {
|
|
1145
|
-
updatedInput: currentInput === event.input ? undefined : currentInput,
|
|
1146
|
-
gate: mintGateOutcome(ledger.settled !== undefined ? { settled: ledger.settled } : {}),
|
|
1147
|
-
preToolContext,
|
|
1148
|
-
};
|
|
347
|
+
return await runGateFold(pass);
|
|
1149
348
|
}
|