@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
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { decisionText, describeThrown } from "./tool-policy.js";
|
|
2
|
+
import { askOriginOf, classifierMayAnswer } from "./ask-origin.js";
|
|
3
|
+
import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
6
|
+
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
7
|
+
import { exitGate, traceHookCrash } from "./runner/gate-exit.js";
|
|
8
|
+
function isPlainOwnRecord(x) {
|
|
9
|
+
try {
|
|
10
|
+
if (x === null || typeof x !== "object" || Array.isArray(x))
|
|
11
|
+
return false;
|
|
12
|
+
const p = Object.getPrototypeOf(x);
|
|
13
|
+
return p === Object.prototype || p === null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function ownDataValue(obj, key) {
|
|
20
|
+
try {
|
|
21
|
+
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
22
|
+
return d && "value" in d ? d.value : undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function normalizeOrgGateVerdict(answer, unreadable) {
|
|
29
|
+
const clean = (v) => Object.assign(Object.create(null), v);
|
|
30
|
+
const readable = (x) => {
|
|
31
|
+
try {
|
|
32
|
+
return x !== null && (typeof x === "object" || typeof x === "function") && !Array.isArray(x);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (!readable(answer))
|
|
39
|
+
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
40
|
+
const status = ownDataValue(answer, "status");
|
|
41
|
+
if (status === "unavailable") {
|
|
42
|
+
const disclosures = ownDataValue(answer, "disclosures");
|
|
43
|
+
let lines = [];
|
|
44
|
+
if (Array.isArray(disclosures)) {
|
|
45
|
+
try {
|
|
46
|
+
for (let i = 0; i < disclosures.length; i++) {
|
|
47
|
+
const d = disclosures[i];
|
|
48
|
+
if (typeof d === "string")
|
|
49
|
+
lines.push(d);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
lines = [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return clean({ status: "unavailable", disclosures: lines.length > 0 ? lines : [unreadable] });
|
|
57
|
+
}
|
|
58
|
+
if (status !== "available")
|
|
59
|
+
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
60
|
+
const revision = ownDataValue(answer, "revision");
|
|
61
|
+
const revisionCell = typeof revision === "number" && Number.isFinite(revision) ? { revision } : {};
|
|
62
|
+
const verdict = ownDataValue(answer, "verdict");
|
|
63
|
+
if (verdict === undefined)
|
|
64
|
+
return clean({ status: "available", ...revisionCell });
|
|
65
|
+
if (!readable(verdict))
|
|
66
|
+
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
67
|
+
const behavior = ownDataValue(verdict, "behavior");
|
|
68
|
+
const rule = ownDataValue(verdict, "rule");
|
|
69
|
+
if ((behavior !== "deny" && behavior !== "ask") || typeof rule !== "string")
|
|
70
|
+
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
71
|
+
return clean({ status: "available", verdict: clean({ behavior: behavior, rule }), ...revisionCell });
|
|
72
|
+
}
|
|
73
|
+
function copiedUsableDots(dots) {
|
|
74
|
+
let copied;
|
|
75
|
+
try {
|
|
76
|
+
if (Array.isArray(dots)) {
|
|
77
|
+
const walked = [];
|
|
78
|
+
for (let i = 0; i < dots.length; i++) {
|
|
79
|
+
const d = dots[i];
|
|
80
|
+
walked.push(isPlainOwnRecord(d) ? { actor: ownDataValue(d, "actor"), counter: ownDataValue(d, "counter") } : { actor: undefined, counter: undefined });
|
|
81
|
+
}
|
|
82
|
+
copied = walked;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
copied = undefined;
|
|
87
|
+
}
|
|
88
|
+
const usable = copied?.every((d) => typeof d.actor === "string" && typeof d.counter === "number" && Number.isFinite(d.counter)) === true && copied.length > 0;
|
|
89
|
+
return usable ? { dots: copied } : {};
|
|
90
|
+
}
|
|
91
|
+
export function normalizePersistedRuleHit(hit) {
|
|
92
|
+
if (typeof hit === "string")
|
|
93
|
+
return { hit: { rules: [{ rule: hit }] } };
|
|
94
|
+
if (!isPlainOwnRecord(hit))
|
|
95
|
+
return {};
|
|
96
|
+
if (ownDataValue(hit, "unreadable") === true)
|
|
97
|
+
return { unreadable: true };
|
|
98
|
+
const rulesRaw = ownDataValue(hit, "rules");
|
|
99
|
+
if (rulesRaw === undefined) {
|
|
100
|
+
const coverageRaw = ownDataValue(hit, "segmentCoverage");
|
|
101
|
+
let rows;
|
|
102
|
+
try {
|
|
103
|
+
if (Array.isArray(coverageRaw)) {
|
|
104
|
+
const walked = [];
|
|
105
|
+
for (let i = 0; i < coverageRaw.length; i++) {
|
|
106
|
+
const r = coverageRaw[i];
|
|
107
|
+
walked.push(isPlainOwnRecord(r) ? { segment: ownDataValue(r, "segment"), covered: ownDataValue(r, "covered") } : { segment: undefined, covered: undefined });
|
|
108
|
+
}
|
|
109
|
+
rows = walked.every((r) => typeof r.segment === "string" && typeof r.covered === "boolean") ? walked : undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
rows = undefined;
|
|
114
|
+
}
|
|
115
|
+
return rows !== undefined && rows.length > 0 ? { coverage: rows.map((r) => ({ segment: r.segment, covered: r.covered })) } : {};
|
|
116
|
+
}
|
|
117
|
+
let members;
|
|
118
|
+
try {
|
|
119
|
+
if (Array.isArray(rulesRaw)) {
|
|
120
|
+
const walked = [];
|
|
121
|
+
for (let i = 0; i < rulesRaw.length; i++) {
|
|
122
|
+
const m = rulesRaw[i];
|
|
123
|
+
walked.push(isPlainOwnRecord(m) ? { rule: ownDataValue(m, "rule"), dots: ownDataValue(m, "dots") } : { rule: undefined, dots: undefined });
|
|
124
|
+
}
|
|
125
|
+
members = walked;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
members = undefined;
|
|
130
|
+
}
|
|
131
|
+
if (members === undefined || members.length === 0)
|
|
132
|
+
return {};
|
|
133
|
+
if (!members.every((m) => typeof m.rule === "string"))
|
|
134
|
+
return {};
|
|
135
|
+
return { hit: { rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
|
|
136
|
+
}
|
|
137
|
+
export function persistedRuleMandateOf(marks) {
|
|
138
|
+
return marks.probeMandated === true
|
|
139
|
+
? "probe_mandate"
|
|
140
|
+
: marks.egress === true
|
|
141
|
+
? "tool_marks"
|
|
142
|
+
: marks.shellGated === true
|
|
143
|
+
? marks.irreversibility === "always"
|
|
144
|
+
? "operator_always"
|
|
145
|
+
: undefined
|
|
146
|
+
: marks.irreversibility === "always" || marks.irreversibility === "maybe"
|
|
147
|
+
? "tool_marks"
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
function parkWith(suspendAsk, parkArgs, carry) {
|
|
151
|
+
return suspendAsk(...parkArgs, carry);
|
|
152
|
+
}
|
|
153
|
+
export async function runGateLanes(pass) {
|
|
154
|
+
const { input, toolName, toolCallId, callSignal, ledger, notifier, screening, adjudicate, resolveAsk, suspendAsk } = pass;
|
|
155
|
+
let orgRealApprovalRequired = false;
|
|
156
|
+
const askOriginFacts = (org) => ({
|
|
157
|
+
contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
|
|
158
|
+
markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
|
|
159
|
+
org,
|
|
160
|
+
tightened: pass.tightenedBy,
|
|
161
|
+
});
|
|
162
|
+
let orgTightenCount = 0;
|
|
163
|
+
let orgEvidence = (() => {
|
|
164
|
+
const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
|
|
165
|
+
return { revisionAbsent: w, ruleAbsent: w };
|
|
166
|
+
})();
|
|
167
|
+
const mintRuleEvidence = (personal) => ({
|
|
168
|
+
...(orgEvidence.revision !== undefined ? { orgRevision: orgEvidence.revision } : { orgRevisionAbsent: orgEvidence.revisionAbsent ?? "not_reported" }),
|
|
169
|
+
...(orgEvidence.rule !== undefined ? { orgRule: orgEvidence.rule } : { orgRuleAbsent: orgEvidence.ruleAbsent ?? "not_reported" }),
|
|
170
|
+
...(personal.dots !== undefined ? { personalRuleDots: personal.dots } : { personalRuleDotsAbsent: personal.dotsAbsent ?? "not_reported" }),
|
|
171
|
+
});
|
|
172
|
+
const applyOrgLayer = async (current, args) => {
|
|
173
|
+
if (input.orgRules === undefined)
|
|
174
|
+
return { decision: current, org: undefined };
|
|
175
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
|
|
176
|
+
return { decision: current, org: undefined };
|
|
177
|
+
let decided = current;
|
|
178
|
+
let pending;
|
|
179
|
+
try {
|
|
180
|
+
pending = Promise.resolve(input.orgRules.adjudicate({ toolName, args, toolCallId }));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
pending = Promise.resolve({ status: "unavailable", disclosures: [`the org adjudication face threw before answering: ${describeThrown(err)}`] });
|
|
184
|
+
}
|
|
185
|
+
const raw = await settleOrgVerdictWithin(pending.catch((err) => ({
|
|
186
|
+
status: "unavailable",
|
|
187
|
+
disclosures: [`the org adjudication face threw: ${describeThrown(err)}`],
|
|
188
|
+
})), {
|
|
189
|
+
status: "unavailable",
|
|
190
|
+
disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
|
|
191
|
+
}, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
192
|
+
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");
|
|
193
|
+
if (answer.status === "unavailable") {
|
|
194
|
+
orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
|
|
195
|
+
orgRealApprovalRequired = true;
|
|
196
|
+
await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
|
|
197
|
+
if (decided.action === "allow") {
|
|
198
|
+
decided = {
|
|
199
|
+
action: "ask",
|
|
200
|
+
message: `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
|
|
201
|
+
`against an organization policy snapshot — every allow tightens to a real approval until it can`,
|
|
202
|
+
decisionReason: ORG_UNAVAILABLE_DECISION_REASON,
|
|
203
|
+
requiresRealApproval: true,
|
|
204
|
+
};
|
|
205
|
+
orgTightenCount += 1;
|
|
206
|
+
}
|
|
207
|
+
else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
|
|
208
|
+
decided = { ...decided, requiresRealApproval: true };
|
|
209
|
+
orgTightenCount += 1;
|
|
210
|
+
}
|
|
211
|
+
return { decision: decided, org: "unavailable" };
|
|
212
|
+
}
|
|
213
|
+
const revisionCell = answer.revision !== undefined ? { revision: answer.revision } : { revisionAbsent: "not_reported" };
|
|
214
|
+
if (answer.verdict === undefined) {
|
|
215
|
+
orgEvidence = { ...revisionCell, ruleAbsent: "no_match" };
|
|
216
|
+
return { decision: decided, org: undefined };
|
|
217
|
+
}
|
|
218
|
+
const { behavior, rule } = answer.verdict;
|
|
219
|
+
orgEvidence = { ...revisionCell, rule };
|
|
220
|
+
if (behavior === "deny") {
|
|
221
|
+
orgTightenCount += 1;
|
|
222
|
+
pass.deniedBy = "org";
|
|
223
|
+
return { decision: { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON }, org: undefined };
|
|
224
|
+
}
|
|
225
|
+
if (decided.action === "deny")
|
|
226
|
+
return { decision: decided, org: undefined };
|
|
227
|
+
orgTightenCount += 1;
|
|
228
|
+
orgRealApprovalRequired = true;
|
|
229
|
+
return {
|
|
230
|
+
decision: decided.action === "ask"
|
|
231
|
+
? { ...decided, requiresRealApproval: true }
|
|
232
|
+
: {
|
|
233
|
+
action: "ask",
|
|
234
|
+
message: `an organization policy rule (${rule}) requires approval for this call`,
|
|
235
|
+
decisionReason: ORG_RULE_DECISION_REASON,
|
|
236
|
+
requiresRealApproval: true,
|
|
237
|
+
},
|
|
238
|
+
org: "rule",
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
const orgAnswer = await applyOrgLayer(pass.decision, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
|
|
242
|
+
pass.decision = orgAnswer.decision;
|
|
243
|
+
if (pass.decision.action === "ask") {
|
|
244
|
+
if (pass.policyRewrite !== undefined) {
|
|
245
|
+
pass.currentInput = pass.policyRewrite;
|
|
246
|
+
pass.req.args = pass.policyRewrite;
|
|
247
|
+
}
|
|
248
|
+
const persistedRuleMandate = persistedRuleMandateOf({
|
|
249
|
+
egress: input.egress,
|
|
250
|
+
shellGated: input.shellGated,
|
|
251
|
+
irreversibility: input.irreversibility,
|
|
252
|
+
probeMandated: pass.decision.action === "ask" && pass.decision.probeMandated === true,
|
|
253
|
+
});
|
|
254
|
+
let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
|
|
255
|
+
let laneCoverage;
|
|
256
|
+
if (input.persistedRules &&
|
|
257
|
+
!orgRealApprovalRequired &&
|
|
258
|
+
pass.decision.action === "ask" &&
|
|
259
|
+
pass.decision.requiresRealApproval !== true &&
|
|
260
|
+
pass.decision.decisionReason !== "hook" &&
|
|
261
|
+
pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
262
|
+
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
263
|
+
const answer = await (async () => {
|
|
264
|
+
let pendingHit;
|
|
265
|
+
try {
|
|
266
|
+
pendingHit = Promise.resolve(input.persistedRules?.admits(pass.req));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return { unreadable: true };
|
|
270
|
+
}
|
|
271
|
+
return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
272
|
+
})();
|
|
273
|
+
const hitRules = answer.hit?.rules;
|
|
274
|
+
laneCoverage = answer.coverage;
|
|
275
|
+
personalEvidence = { dotsAbsent: answer.unreadable === true ? "unavailable" : "no_match" };
|
|
276
|
+
const disclosedRuleSet = (rules) => {
|
|
277
|
+
const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
|
|
278
|
+
const rest = rules.length - shown.length;
|
|
279
|
+
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
280
|
+
};
|
|
281
|
+
const unionDotsOf = (rules) => {
|
|
282
|
+
const seen = new Set();
|
|
283
|
+
const union = [];
|
|
284
|
+
for (const r of rules) {
|
|
285
|
+
if (r.dots === undefined || r.dots.length === 0)
|
|
286
|
+
return { dotsAbsent: "not_reported" };
|
|
287
|
+
for (const d of r.dots) {
|
|
288
|
+
const key = JSON.stringify([d.actor, d.counter]);
|
|
289
|
+
if (!seen.has(key)) {
|
|
290
|
+
seen.add(key);
|
|
291
|
+
union.push(d);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { dots: union };
|
|
296
|
+
};
|
|
297
|
+
if (hitRules !== undefined && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined) {
|
|
298
|
+
pass.decision = {
|
|
299
|
+
action: "allow",
|
|
300
|
+
message: hitRules.length === 1
|
|
301
|
+
? `a persisted allow rule (${disclosedRuleSet(hitRules)}) covers this call`
|
|
302
|
+
: `persisted allow rules (${disclosedRuleSet(hitRules)}) cover every segment of this call`,
|
|
303
|
+
decisionReason: "persisted_rule",
|
|
304
|
+
...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
|
|
305
|
+
};
|
|
306
|
+
await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: pass.req.toolName, toolCallId, rules: hitRules.map((r) => r.rule) }), "toolGate.persistedRuleResolved");
|
|
307
|
+
}
|
|
308
|
+
else if (hitRules !== undefined) {
|
|
309
|
+
personalEvidence = unionDotsOf(hitRules);
|
|
310
|
+
const shownRule = disclosedRuleSet(hitRules);
|
|
311
|
+
const mandateNoun = persistedRuleMandate === "probe_mandate"
|
|
312
|
+
? "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"
|
|
313
|
+
: persistedRuleMandate === "operator_always"
|
|
314
|
+
? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
|
|
315
|
+
: persistedRuleMandate !== undefined
|
|
316
|
+
? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
|
|
317
|
+
: "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
|
|
318
|
+
pass.decision = {
|
|
319
|
+
...pass.decision,
|
|
320
|
+
persistedRuleShadowed: shownRule,
|
|
321
|
+
message: hitRules.length === 1
|
|
322
|
+
? `${pass.decision.message !== undefined ? `${pass.decision.message} ` : ""}(a persisted allow rule (${shownRule}) matches this call but does not clear the ask — ${mandateNoun})`
|
|
323
|
+
: `${pass.decision.message !== undefined ? `${pass.decision.message} ` : ""}(persisted allow rules (${shownRule}) match this call but do not clear the ask — ${mandateNoun})`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const originFacts = askOriginFacts(orgAnswer.org);
|
|
328
|
+
if (pass.decision.action === "ask") {
|
|
329
|
+
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
330
|
+
pass.decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
|
|
331
|
+
}
|
|
332
|
+
if (pass.decision.action === "ask")
|
|
333
|
+
ledger.supersede();
|
|
334
|
+
if (input.autoMode && pass.decision.action === "ask" && pass.decision.origin !== undefined && classifierMayAnswer(pass.decision.origin)) {
|
|
335
|
+
const verdict = await input.autoMode.decider
|
|
336
|
+
.decide({ req: pass.req, askMessage: decisionText(pass.decision) }, input.abortSignal)
|
|
337
|
+
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
338
|
+
if (verdict.kind === "allow") {
|
|
339
|
+
input.autoMode.denialTracking?.recordAllow();
|
|
340
|
+
pass.decision = {
|
|
341
|
+
action: "allow",
|
|
342
|
+
message: "auto-mode classifier allowed this call",
|
|
343
|
+
decisionReason: "classifier",
|
|
344
|
+
...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else if (verdict.kind === "block") {
|
|
348
|
+
const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
|
|
349
|
+
const category = verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
350
|
+
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
351
|
+
if (tracked?.limitReached === true) {
|
|
352
|
+
const fallbackAsk = {
|
|
353
|
+
...pass.decision,
|
|
354
|
+
message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
|
|
355
|
+
decisionReason: "classifier",
|
|
356
|
+
requiresRealApproval: true,
|
|
357
|
+
denialLimitFallback: tracked.fallback,
|
|
358
|
+
};
|
|
359
|
+
pass.decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
pass.decision = {
|
|
363
|
+
action: "deny",
|
|
364
|
+
message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
|
|
365
|
+
decisionReason: "classifier",
|
|
366
|
+
};
|
|
367
|
+
pass.deniedBy = "classifier";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (input.sandboxAdmission !== undefined &&
|
|
372
|
+
pass.decision.action === "ask" &&
|
|
373
|
+
pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
374
|
+
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
|
|
375
|
+
pass.decision.requiresRealApproval !== true &&
|
|
376
|
+
pass.decision.decisionReason !== "hook" &&
|
|
377
|
+
pass.hookAsk === undefined &&
|
|
378
|
+
input.egress !== true &&
|
|
379
|
+
input.peerMessage !== true &&
|
|
380
|
+
input.sandboxAdmission.boundaryCapable !== true) {
|
|
381
|
+
const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
|
|
382
|
+
const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
|
|
383
|
+
if (admissible) {
|
|
384
|
+
const askClasses = recorded;
|
|
385
|
+
input.sandboxAdmission.onAdmitted({ toolCallId, toolName, askClasses, boundary: "sandbox_internal" });
|
|
386
|
+
pass.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.`);
|
|
387
|
+
pass.decision = {
|
|
388
|
+
action: "allow",
|
|
389
|
+
message: "admitted inside the deployment-declared sandbox (permission.sandbox_admitted)",
|
|
390
|
+
decisionReason: "sandbox",
|
|
391
|
+
...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const egressTool = input.egress === true;
|
|
396
|
+
const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
|
|
397
|
+
const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
|
|
398
|
+
const realApprovalOf = (d) => {
|
|
399
|
+
if (d.action !== "ask" || d.requiresRealApproval !== true)
|
|
400
|
+
return undefined;
|
|
401
|
+
if (d.origin === undefined)
|
|
402
|
+
throw new Error(`a real-approval ask for "${toolName}" reached a park site with no origin word — the origin stamp precedes every park`);
|
|
403
|
+
return { origin: d.origin, ...(originFacts.org !== undefined ? { org: originFacts.org } : {}) };
|
|
404
|
+
};
|
|
405
|
+
const carryOf = (d) => ({
|
|
406
|
+
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
407
|
+
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
408
|
+
...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
|
|
409
|
+
});
|
|
410
|
+
if (suspendAsk && pass.decision.action === "ask") {
|
|
411
|
+
const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
|
|
412
|
+
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(pass.decision));
|
|
413
|
+
if (suspended) {
|
|
414
|
+
if ("parkFailed" in suspended)
|
|
415
|
+
pass.parkFailed = suspended.parkFailed;
|
|
416
|
+
else
|
|
417
|
+
return { suspend: suspended, preToolContext: pass.preToolContext };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (pass.decision.action === "ask" && pass.req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
421
|
+
const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(pass.req, callSignal) : input.resolveContentAsk(pass.req)) : { kind: "unavailable", parkDeclined: false };
|
|
422
|
+
if (outcome.kind === "answered") {
|
|
423
|
+
pass.decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
424
|
+
}
|
|
425
|
+
else if (outcome.kind === "unavailable") {
|
|
426
|
+
if (outcome.presentedInput !== undefined) {
|
|
427
|
+
pass.currentInput = outcome.presentedInput;
|
|
428
|
+
pass.req.args = outcome.presentedInput;
|
|
429
|
+
}
|
|
430
|
+
if (suspendAsk && outcome.parkDeclined && pass.parkFailed === undefined) {
|
|
431
|
+
const parkArgs = [pass.req, pass.currentInput, safety, true, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
|
|
432
|
+
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(pass.decision));
|
|
433
|
+
if (suspended) {
|
|
434
|
+
if ("parkFailed" in suspended)
|
|
435
|
+
pass.parkFailed = suspended.parkFailed;
|
|
436
|
+
else
|
|
437
|
+
return { suspend: suspended, preToolContext: pass.preToolContext };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (pass.decision.origin === undefined)
|
|
441
|
+
throw new Error(`the content ask for "${toolName}" reached its refusal without an origin word`);
|
|
442
|
+
ledger.set({ settlement: { kind: input.resolveContentAsk !== undefined ? "approver_unavailable" : "no_approver", who: { party: "none" }, when: Date.now() }, origin: pass.decision.origin });
|
|
443
|
+
pass.deniedBy = "ask_resolution";
|
|
444
|
+
pass.decision = {
|
|
445
|
+
action: "deny",
|
|
446
|
+
message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
|
|
447
|
+
"facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
|
|
448
|
+
"the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
|
|
449
|
+
"you made.",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
if (outcome.presentedInput !== undefined)
|
|
454
|
+
pass.decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
455
|
+
else
|
|
456
|
+
pass.decision = { action: "allow" };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (pass.decision.action === "ask") {
|
|
460
|
+
const askBeforeResolve = pass.decision;
|
|
461
|
+
const resolved = await (callSignal !== undefined ? resolveAsk(pass.decision, pass.req, callSignal) : resolveAsk(pass.decision, pass.req));
|
|
462
|
+
ledger.fromResolved(resolved, askBeforeResolve, toolName);
|
|
463
|
+
if (resolved.action === "deny")
|
|
464
|
+
pass.deniedBy = "ask_resolution";
|
|
465
|
+
pass.decision = resolved;
|
|
466
|
+
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && pass.parkFailed === undefined) {
|
|
467
|
+
const parkArgs = [pass.req, pass.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];
|
|
468
|
+
const suspended = await parkWith(suspendAsk, parkArgs, carryOf(askBeforeResolve));
|
|
469
|
+
if (suspended) {
|
|
470
|
+
if ("parkFailed" in suspended)
|
|
471
|
+
pass.parkFailed = suspended.parkFailed;
|
|
472
|
+
else
|
|
473
|
+
return { suspend: suspended, preToolContext: pass.preToolContext };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (askBeforeResolve.action === "ask" && askBeforeResolve.denialLimitFallback !== undefined) {
|
|
477
|
+
if (pass.decision.action === "allow") {
|
|
478
|
+
input.autoMode?.denialTracking?.recordAllow();
|
|
479
|
+
}
|
|
480
|
+
else if ((resolved.action === "deny" &&
|
|
481
|
+
(resolved.settlement.kind === "no_approver" || resolved.settlement.kind === "blanket_allow_refused" || resolved.settlement.kind === "approver_unavailable")) ||
|
|
482
|
+
resolved.approverUnavailable === true) {
|
|
483
|
+
try {
|
|
484
|
+
input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: unarmedWindow(askBeforeResolve.denialLimitFallback) });
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (pass.decision.action === "allow" && pass.decision.updatedInput !== undefined) {
|
|
491
|
+
let editArgs = pass.decision.updatedInput;
|
|
492
|
+
let editDenied;
|
|
493
|
+
let editRewrittenSinceHuman = false;
|
|
494
|
+
for (let round = 0;; round++) {
|
|
495
|
+
if (round >= 3) {
|
|
496
|
+
editDenied = {
|
|
497
|
+
action: "deny",
|
|
498
|
+
message: `the approval-edit chain for "${toolName}" exceeded the re-approval cap — denied fail-closed; re-submit the edited action directly`,
|
|
499
|
+
};
|
|
500
|
+
pass.deniedBy = "policy";
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
if (screening !== undefined) {
|
|
504
|
+
let hr;
|
|
505
|
+
let hrUnanswered;
|
|
506
|
+
try {
|
|
507
|
+
const seat = await screening.screen(editArgs);
|
|
508
|
+
if (seat.expired)
|
|
509
|
+
hrUnanswered = seat.cause;
|
|
510
|
+
else
|
|
511
|
+
hr = screening.screened(seat.value);
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
traceHookCrash(input, err, notifier);
|
|
515
|
+
editDenied = { action: "deny", message: screening.crashReason(`the approved edit for "${toolName}"`, err) };
|
|
516
|
+
pass.deniedBy = "hook";
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
if (hrUnanswered !== undefined) {
|
|
520
|
+
editDenied = { action: "deny", message: await screening.unansweredReason(`the approved edit for "${toolName}"`, hrUnanswered) };
|
|
521
|
+
pass.deniedBy = "hook";
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
if (hr) {
|
|
525
|
+
if (hr.additionalContext)
|
|
526
|
+
pass.preToolContext.push(hr.additionalContext);
|
|
527
|
+
if (hr.action === "deny") {
|
|
528
|
+
editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
|
|
529
|
+
pass.deniedBy = "hook";
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
if (hr.action === "ask") {
|
|
533
|
+
editDenied = {
|
|
534
|
+
action: "deny",
|
|
535
|
+
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`,
|
|
536
|
+
};
|
|
537
|
+
pass.deniedBy = "hook";
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
if (hr.updatedInput !== undefined) {
|
|
541
|
+
editArgs = hr.updatedInput;
|
|
542
|
+
editRewrittenSinceHuman = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!adjudicate && input.orgRules === undefined)
|
|
547
|
+
break;
|
|
548
|
+
let recheck = { action: "allow" };
|
|
549
|
+
if (adjudicate) {
|
|
550
|
+
try {
|
|
551
|
+
recheck = await (callSignal !== undefined ? adjudicate({ toolName, args: editArgs, toolCallId }, callSignal) : adjudicate({ toolName, args: editArgs, toolCallId }));
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
recheck = {
|
|
555
|
+
action: "deny",
|
|
556
|
+
message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
{
|
|
561
|
+
const refusedBy = ledger.fromDecision(recheck);
|
|
562
|
+
if (recheck.action === "deny" && refusedBy !== undefined) {
|
|
563
|
+
editDenied = recheck;
|
|
564
|
+
pass.deniedBy = refusedBy;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (recheck.updatedInput !== undefined) {
|
|
569
|
+
editArgs = recheck.updatedInput;
|
|
570
|
+
editRewrittenSinceHuman = true;
|
|
571
|
+
}
|
|
572
|
+
const editOrg = await applyOrgLayer(recheck, editArgs);
|
|
573
|
+
recheck = editOrg.decision;
|
|
574
|
+
if (recheck.action === "deny") {
|
|
575
|
+
editDenied = recheck;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
if (recheck.action === "allow") {
|
|
579
|
+
if (recheck.updatedInput !== undefined) {
|
|
580
|
+
editArgs = recheck.updatedInput;
|
|
581
|
+
editRewrittenSinceHuman = true;
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
if (recheck.updatedInput !== undefined) {
|
|
586
|
+
editArgs = recheck.updatedInput;
|
|
587
|
+
editRewrittenSinceHuman = true;
|
|
588
|
+
}
|
|
589
|
+
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
590
|
+
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
|
|
591
|
+
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
592
|
+
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
593
|
+
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
594
|
+
if (rr.action !== "allow") {
|
|
595
|
+
editDenied = rr;
|
|
596
|
+
pass.deniedBy = "ask_resolution";
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
if (rr.updatedInput === undefined) {
|
|
600
|
+
if (rr.presentedInput !== undefined)
|
|
601
|
+
editArgs = rr.presentedInput;
|
|
602
|
+
editRewrittenSinceHuman = false;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
editArgs = rr.updatedInput;
|
|
606
|
+
editRewrittenSinceHuman = false;
|
|
607
|
+
}
|
|
608
|
+
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
609
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs);
|
|
610
|
+
if (editHit !== null) {
|
|
611
|
+
editDenied = {
|
|
612
|
+
action: "deny",
|
|
613
|
+
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`,
|
|
614
|
+
};
|
|
615
|
+
pass.deniedBy = "write_protection";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
pass.decision = editDenied ?? { ...pass.decision, updatedInput: editArgs };
|
|
619
|
+
}
|
|
620
|
+
else if (pass.decision.action === "allow" && pass.decision.updatedInput === undefined && resolved.presentedInput !== undefined) {
|
|
621
|
+
pass.decision = { ...pass.decision, updatedInput: resolved.presentedInput };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return await exitGate(pass);
|
|
626
|
+
}
|