@sema-agent/core 7.3.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/agents/peer-admission.d.ts +18 -3
- package/dist/agents/peer-admission.js +79 -4
- package/dist/agents/peer-held-queue.d.ts +101 -0
- package/dist/agents/peer-held-queue.js +229 -0
- package/dist/agents/peer-idle.d.ts +109 -0
- package/dist/agents/peer-idle.js +240 -0
- package/dist/agents/peer-notice-route.d.ts +33 -0
- package/dist/agents/peer-notice-route.js +46 -0
- package/dist/agents/peer-notices.d.ts +103 -0
- package/dist/agents/peer-notices.js +206 -0
- package/dist/agents/peer-session-drain.d.ts +39 -4
- package/dist/agents/peer-session-drain.js +248 -42
- package/dist/agents/send-message-tool.d.ts +8 -1
- package/dist/agents/send-message-tool.js +96 -30
- package/dist/agents/subagent.js +1 -0
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- package/dist/core/auto-mode-defaults.d.ts +11 -0
- package/dist/core/auto-mode-defaults.js +2 -0
- package/dist/core/auto-mode.d.ts +59 -0
- package/dist/core/auto-mode.js +57 -1
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +30 -0
- package/dist/core/hooks.js +43 -8
- package/dist/core/mailbox-store.d.ts +33 -1
- package/dist/core/mailbox-store.js +42 -2
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/denial-limit-arms.d.ts +149 -0
- package/dist/core/runner/denial-limit-arms.js +91 -0
- package/dist/core/runner/edited-files-ledger.d.ts +33 -0
- package/dist/core/runner/edited-files-ledger.js +14 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +62 -1
- package/dist/core/runner/prepare-task.js +135 -89
- package/dist/core/runner/runtask.js +12 -0
- package/dist/core/sensitive-path-policy.d.ts +27 -6
- package/dist/core/sensitive-path-policy.js +57 -2
- package/dist/core/task-notification.d.ts +24 -2
- package/dist/core/task-notification.js +6 -1
- package/dist/core/tool-policy.d.ts +55 -4
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +251 -15
- package/dist/core/wiring-manifest.d.ts +41 -5
- package/dist/core/wiring-manifest.js +8 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +10 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/workflow.js +7 -3
- package/dist/tools/fs/fs-write.d.ts +4 -4
- package/dist/tools/fs/fs-write.js +99 -14
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +29 -8
- package/dist/tools/fs/safety.js +11 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +181 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { canonicalizeTarget, expandHomeTilde, isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
4
|
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
3
5
|
const DEFAULT_GUARDED_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
4
6
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
@@ -17,6 +19,9 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
|
17
19
|
".git/modules",
|
|
18
20
|
".mcp.json",
|
|
19
21
|
".claude*",
|
|
22
|
+
".sema",
|
|
23
|
+
".sema.*",
|
|
24
|
+
".sema-notifier-build",
|
|
20
25
|
".aws",
|
|
21
26
|
".config/gcloud",
|
|
22
27
|
".azure",
|
|
@@ -27,6 +32,33 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
|
27
32
|
".zsh_history",
|
|
28
33
|
];
|
|
29
34
|
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
|
|
35
|
+
function dataRootSpellings(explicit) {
|
|
36
|
+
const raw = explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
37
|
+
const home = homedir();
|
|
38
|
+
const tilde = expandHomeTilde(raw, home);
|
|
39
|
+
const engineForm = isAbsolute(raw) ? raw : resolve(raw);
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const v of [raw, tilde, engineForm]) {
|
|
42
|
+
const anchored = isAbsolutePathForm(v) ? v : resolve(v);
|
|
43
|
+
if (!out.includes(anchored))
|
|
44
|
+
out.push(anchored);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function relativeUnderRoot(targetKey, rootKey) {
|
|
49
|
+
const seps = isWinFormPath(rootKey) ? ["\\", "/"] : ["/"];
|
|
50
|
+
let root = rootKey;
|
|
51
|
+
while (root.length > 0 && seps.includes(root[root.length - 1]))
|
|
52
|
+
root = root.slice(0, -1);
|
|
53
|
+
if (root.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
if (targetKey === root)
|
|
56
|
+
return "";
|
|
57
|
+
for (const sep of seps)
|
|
58
|
+
if (targetKey.startsWith(root + sep))
|
|
59
|
+
return targetKey.slice(root.length + 1);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
30
62
|
function compilePatterns(patterns) {
|
|
31
63
|
const out = [];
|
|
32
64
|
for (const raw of patterns) {
|
|
@@ -44,6 +76,20 @@ function matchSensitive(canonicalKey, compiled, aliasResolved) {
|
|
|
44
76
|
}
|
|
45
77
|
export function createSensitivePathPolicy(opts) {
|
|
46
78
|
const compiled = compilePatterns(opts.patterns);
|
|
79
|
+
const dataRootPaths = dataRootSpellings(opts.dataRoot);
|
|
80
|
+
const dataRootKeys = async (signal) => {
|
|
81
|
+
const keys = [];
|
|
82
|
+
for (const spelling of dataRootPaths) {
|
|
83
|
+
try {
|
|
84
|
+
const r = await canonicalizeTarget(opts.env, spelling, signal);
|
|
85
|
+
if (r.ok && !keys.includes(r.key))
|
|
86
|
+
keys.push(r.key);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return keys;
|
|
92
|
+
};
|
|
47
93
|
const guarded = new Set(opts.tools ?? DEFAULT_GUARDED_TOOLS);
|
|
48
94
|
return {
|
|
49
95
|
async check(req, signal) {
|
|
@@ -64,7 +110,16 @@ export function createSensitivePathPolicy(opts) {
|
|
|
64
110
|
}
|
|
65
111
|
return { action: "allow" };
|
|
66
112
|
}
|
|
67
|
-
|
|
113
|
+
let relative = null;
|
|
114
|
+
for (const rootKey of await dataRootKeys(signal)) {
|
|
115
|
+
relative = relativeUnderRoot(canon.key, rootKey);
|
|
116
|
+
if (relative !== null)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const judged = relative ?? canon.key;
|
|
120
|
+
if (relative === "")
|
|
121
|
+
return { action: "allow" };
|
|
122
|
+
const hit = matchSensitive(judged, compiled, canon.aliasResolved === true);
|
|
68
123
|
if (hit) {
|
|
69
124
|
return {
|
|
70
125
|
action: "deny",
|
|
@@ -167,11 +167,27 @@ export interface TaskNotificationPayload {
|
|
|
167
167
|
crossSessionMessage?: import("../agents/cross-session-envelope.js").CrossSessionEnvelopeFields & {
|
|
168
168
|
body: string;
|
|
169
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* design/385 §4.4 / §5.2 (slice 4) — the CROSS-SESSION NOTICE carrier: a delivery receipt about one of
|
|
172
|
+
* THIS session's own outbound messages (held / denied / expired / delivered / refused / dropped) or an
|
|
173
|
+
* idle notice for a `notify_when_idle` ask (idle / exited / unavailable / expired), drained from this
|
|
174
|
+
* session's own box as a notice-kind record. Its PRESENCE is the render discriminator: the model reads
|
|
175
|
+
* the engine-rendered notice line as-is (CC injects `[Cross-session delivery notice] …` /
|
|
176
|
+
* `[Cross-session idle notice] …` as a plain user-frame line) — never a `<task-notification>` shell,
|
|
177
|
+
* never the peer-message envelope (a notice is engine speech about a peer, not a peer's words; the
|
|
178
|
+
* envelope would put it past the parity judgment as a peer's request). `text` is rendered by the
|
|
179
|
+
* drain from the record's TYPED `peerMeta.notice` (the authority); the record's own `content` mirror is
|
|
180
|
+
* never what reaches the model. Minted ONLY by the engine's session-box drain.
|
|
181
|
+
*/
|
|
182
|
+
crossSessionNotice?: {
|
|
183
|
+
kind: "delivery_notice" | "idle_notice";
|
|
184
|
+
text: string;
|
|
185
|
+
};
|
|
170
186
|
/**
|
|
171
187
|
* design/385 §1.4 d1 / §4.1 — the engine-minted PROVENANCE side record of an engine-injected peer
|
|
172
188
|
* frame, a typed key (never model text) so a host can attribute and correlate the injection on the
|
|
173
189
|
* wire. Present exactly when {@link agentMessage} OR {@link crossSessionMessage} is — one record
|
|
174
|
-
* per carrier, `kind` naming which. Its fields are read per `kind` (see {@link SemaProvenance}):
|
|
190
|
+
* per carrier (the notice carrier {@link crossSessionNotice} too), `kind` naming which. Its fields are read per `kind` (see {@link SemaProvenance}):
|
|
175
191
|
* on the agent-message lane `from` is the sender label the frame's attribute spells, `taskId` the
|
|
176
192
|
* sender's run/agent id, `seq` the producer's per-frame counter (= this payload's `seq`) and
|
|
177
193
|
* `agentType` the sender's resolved agent type when the producer knows it; on the cross-session
|
|
@@ -187,7 +203,7 @@ export interface SemaProvenance {
|
|
|
187
203
|
/** `agent_message` = the same-process uplink (§1.4 d1); `cross_session_message` = the session-box
|
|
188
204
|
* drain (§4.1) — `from` is then the sender's ADDRESS, `taskId` the recipient's own box handle,
|
|
189
205
|
* `seq` the box seq, and the typed peer record rides in `peerMeta`. */
|
|
190
|
-
kind: "agent_message" | "cross_session_message";
|
|
206
|
+
kind: "agent_message" | "cross_session_message" | "cross_session_notice";
|
|
191
207
|
from: string;
|
|
192
208
|
taskId: string;
|
|
193
209
|
seq: number;
|
|
@@ -307,6 +323,12 @@ export declare function renderAgentMessageFrame(frame: {
|
|
|
307
323
|
* level, another block — sharing either would blur the classifier's lane judgment.
|
|
308
324
|
*/
|
|
309
325
|
export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNotificationPayload["crossSessionMessage"]>): string;
|
|
326
|
+
/**
|
|
327
|
+
* design/385 §4.4 / §5.2 — render a cross-session notice line. The text is engine-authored from typed
|
|
328
|
+
* fields (every untrusted label inside it already passed the lane's label sanitizer); the reminder-tag
|
|
329
|
+
* defusal runs once more here so no carrier can smuggle a system-reminder into the model face.
|
|
330
|
+
*/
|
|
331
|
+
export declare function renderCrossSessionNoticeLine(notice: NonNullable<TaskNotificationPayload["crossSessionNotice"]>): string;
|
|
310
332
|
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
|
|
311
333
|
/**
|
|
312
334
|
* The BETWEEN-TURNS pending lane. A task notification born while NO turn is
|
|
@@ -57,11 +57,16 @@ export function renderCrossSessionMessageFrame(frame) {
|
|
|
57
57
|
const { body, ...fields } = frame;
|
|
58
58
|
return `${buildCrossSessionEnvelope(fields, body)}\n\n${CROSS_SESSION_MESSAGE_NOTICE}`;
|
|
59
59
|
}
|
|
60
|
+
export function renderCrossSessionNoticeLine(notice) {
|
|
61
|
+
return sanitizeUntrustedText(notice.text);
|
|
62
|
+
}
|
|
60
63
|
export function renderTaskNotificationXml(n) {
|
|
61
64
|
if (n.agentMessage !== undefined)
|
|
62
65
|
return renderAgentMessageFrame(n.agentMessage);
|
|
63
66
|
if (n.crossSessionMessage !== undefined)
|
|
64
67
|
return renderCrossSessionMessageFrame(n.crossSessionMessage);
|
|
68
|
+
if (n.crossSessionNotice !== undefined)
|
|
69
|
+
return renderCrossSessionNoticeLine(n.crossSessionNotice);
|
|
65
70
|
const usage = n.usage === undefined
|
|
66
71
|
? undefined
|
|
67
72
|
: (() => {
|
|
@@ -217,7 +222,7 @@ export function discloseDroppedPending(drained) {
|
|
|
217
222
|
if (dropped === undefined || disclosed.has(lane))
|
|
218
223
|
return [n];
|
|
219
224
|
disclosed.add(lane);
|
|
220
|
-
if (n.agentMessage !== undefined || n.crossSessionMessage !== undefined) {
|
|
225
|
+
if (n.agentMessage !== undefined || n.crossSessionMessage !== undefined || n.crossSessionNotice !== undefined) {
|
|
221
226
|
const line = {
|
|
222
227
|
task_id: n.task_id,
|
|
223
228
|
task_type: n.task_type,
|
|
@@ -93,14 +93,25 @@ export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
|
93
93
|
*
|
|
94
94
|
* **Which windows `"timeout"` speaks for** (#114①, 2026-08-09 — the promise this note used to make was
|
|
95
95
|
* wider than the code): the engine stamps it at the waits IT owns — `createApprovalPolicy`'s
|
|
96
|
-
* `approvalTimeoutMs` window,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* `approvalTimeoutMs` window, the durable park's TTL, and (#548) the denial-limit fallback ask's
|
|
97
|
+
* auto-deny window. The SYNCHRONOUS `onAsk` leg is otherwise not one of them: there the deployment owns
|
|
98
|
+
* the window (the engine starts no timer for a callback it does not schedule), so an unanswered card and
|
|
99
|
+
* a refused one arrive as the same `false` and the engine records
|
|
99
100
|
* `"human"` rather than inventing a cause it did not observe. A host that DOES time its own card out
|
|
100
101
|
* can say so — {@link AskOutcome}'s object arm carries an optional `settledBy` for exactly this — but a
|
|
101
102
|
* host that does not is indistinguishable, by construction. Read an absent `"timeout"` as "no window
|
|
102
103
|
* the engine owns elapsed", never as "nobody's window elapsed".
|
|
103
104
|
*
|
|
105
|
+
* **The one `onAsk` exception** (#548): a classifier DENIAL-LIMIT fallback ask — the ask that carries
|
|
106
|
+
* `denialLimitFallback`, minted when the auto-mode classifier reaches its consecutive/total bound —
|
|
107
|
+
* IS timed by the engine over the synchronous leg, because that ask exists to bound a classifier that
|
|
108
|
+
* would otherwise deny without end, and an unbounded wait would only move the "without end" onto the
|
|
109
|
+
* person. Its window elapsing produces an engine-stamped `settledBy: "timeout"` with
|
|
110
|
+
* `resolution: "window_expired"` and `autoDenied: true` on the deny — that last bit is the
|
|
111
|
+
* discriminator between core's window and a host self-report ({@link AskOutcome}'s object arm has no
|
|
112
|
+
* `autoDenied` seat, so a host cannot claim the word).
|
|
113
|
+
* `autoDenyAfterMs: 0` (or an ask with no fallback member) arms nothing, which is every other ask.
|
|
114
|
+
*
|
|
104
115
|
* The three words are exhaustive and mutually exclusive over the ways an approval can end, and the
|
|
105
116
|
* minimum discrimination a consumer needs — someone refused vs nobody answered — is `"human"` vs the
|
|
106
117
|
* other two.
|
|
@@ -197,6 +208,17 @@ export type PermissionResult = {
|
|
|
197
208
|
message?: string;
|
|
198
209
|
decisionReason?: DecisionReason;
|
|
199
210
|
requiresRealApproval?: boolean;
|
|
211
|
+
/** #548 (additive): this ask is the classifier DENIAL-LIMIT fallback — the block that reached a
|
|
212
|
+
* bound (CC 2.1.250 `FO`, count-then-judge) handed to a person instead of being denied.
|
|
213
|
+
* ENGINE-STAMPED beside `requiresRealApproval: true` and `decisionReason: "classifier"` at the
|
|
214
|
+
* classifier block sites (the gate's own and the inherited-lane arms); carries the counts and
|
|
215
|
+
* THIS ask's auto-deny window — see {@link import("./auto-mode.js").DenialLimitFallback}.
|
|
216
|
+
* Consumers: the gate's classifier step refuses to re-judge an ask carrying it (the classifier
|
|
217
|
+
* already spoke; rules and classifier stand BEFORE this ask, never after), and the ask resolver
|
|
218
|
+
* arms its deadline from `autoDenyAfterMs`. Carried onto the approval request. A policy that
|
|
219
|
+
* self-declares it only ever opts its own ask OUT of classifier resolution and INTO a bounded
|
|
220
|
+
* wait (tightening — the same safe direction as `matchedAskRule`). */
|
|
221
|
+
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
200
222
|
/** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
|
|
201
223
|
* ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
|
|
202
224
|
* egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
|
|
@@ -1163,6 +1185,13 @@ export interface AskRequest {
|
|
|
1163
1185
|
* clear this ask (see that field's doc). `readonly`, filled by the gate from the decision, never a
|
|
1164
1186
|
* caller/worker-settable field. */
|
|
1165
1187
|
readonly requiresRealApproval?: boolean;
|
|
1188
|
+
/** #548 (additive) — present ⇔ this ask is the classifier DENIAL-LIMIT fallback (see the
|
|
1189
|
+
* {@link PermissionResult} ask-arm member of the same name): the counts that tripped the bound and
|
|
1190
|
+
* this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
|
|
1191
|
+
* arms its deadline from `autoDenyAfterMs` (> 0 ⇒ an unanswered function approver auto-denies at
|
|
1192
|
+
* that deadline — `settledBy:"timeout"`, `resolution:"window_expired"`, `autoDenied:true`). Filled
|
|
1193
|
+
* by the gate from the decision, never a caller/worker-settable field. */
|
|
1194
|
+
readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1166
1195
|
/** Engine-judged risk axes of the action awaiting approval, for the human decision surface: a
|
|
1167
1196
|
* coarse `requiresRealApproval` cannot tell "cannot be undone" from "data leaves the machine",
|
|
1168
1197
|
* and those call for different scrutiny. Filled by the gate from the SAME resolved axes the
|
|
@@ -1270,7 +1299,8 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1270
1299
|
*
|
|
1271
1300
|
* #114② (2026-08-09) — `settledBy` on the object arm is the SYNCHRONOUS leg's only channel for saying
|
|
1272
1301
|
* what ended the wait. This leg's window belongs to the HOST (`onAsk` is a callback the deployment
|
|
1273
|
-
* owns; the engine starts no timer for it), so a host that timed
|
|
1302
|
+
* owns; the engine starts no timer for it — with the ONE #548 exception below), so a host that timed
|
|
1303
|
+
* its own approval card out could
|
|
1274
1304
|
* previously only report a plain `false`, which the engine correctly recorded as `"human"` — a person
|
|
1275
1305
|
* refusing. The two words the host may self-report:
|
|
1276
1306
|
* - `"human"` — a person answered (identical to omitting the field);
|
|
@@ -1282,6 +1312,15 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1282
1312
|
* (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
|
|
1283
1313
|
* self-reported one would let a host relabel its refusal as an engine failure.
|
|
1284
1314
|
*
|
|
1315
|
+
* #548 — the ONE ask on this leg the engine DOES time: a classifier denial-limit fallback (the ask
|
|
1316
|
+
* carrying `denialLimitFallback` with a non-zero `autoDenyAfterMs`). Its window is core's, because
|
|
1317
|
+
* the fallback exists to bound a classifier that would otherwise deny without end. When it elapses
|
|
1318
|
+
* the approver's promise is DETACHED (design/384: the wait is released, not cancelled — an approver
|
|
1319
|
+
* that answers afterwards is not consulted, and an `allow` it returns late becomes the
|
|
1320
|
+
* `task.late_approval` notice), and the engine's own deny carries `settledBy: "timeout"`,
|
|
1321
|
+
* `resolution: "window_expired"` and `autoDenied: true`. See {@link ApprovalSettledBy} for how a
|
|
1322
|
+
* consumer tells that window from the host's.
|
|
1323
|
+
*
|
|
1285
1324
|
* `reason` — the SYNCHRONOUS leg's seat for a model-readable reason attached to a deny, the exact
|
|
1286
1325
|
* counterpart of the durable leg's `ResumeOutcome` `policy_ask` `reason` ("Model-readable reason
|
|
1287
1326
|
* attached to a deny"): the words the decider typed with their "no" (an approval card's rejection
|
|
@@ -1416,6 +1455,12 @@ export declare const ASK_DENY_RESOLUTION_VALUES: readonly AskDenyResolution[];
|
|
|
1416
1455
|
* files or forwards the word (a policy layer could self-declare the member on its own deny; an
|
|
1417
1456
|
* out-of-vocabulary word is dropped by the carriers, never coerced or forwarded). */
|
|
1418
1457
|
export declare function isAskDenyResolution(v: unknown): v is AskDenyResolution;
|
|
1458
|
+
/** #548: the engine-attested auto-deny marker off a funneled decision, bound to the named call like
|
|
1459
|
+
* {@link coreMintedResolutionOf} (absent ⇒ not an attested auto-deny of THIS call). */
|
|
1460
|
+
export declare function coreMintedAutoDeniedOf(d: unknown, call: {
|
|
1461
|
+
toolCallId: string;
|
|
1462
|
+
toolName: string;
|
|
1463
|
+
}): boolean;
|
|
1419
1464
|
/** Read the engine-attested resolution off a funneled decision (the gate's single deny exit is the
|
|
1420
1465
|
* one consumer), FOR the named call: an attestation bound to a different toolCallId/toolName is a
|
|
1421
1466
|
* replayed object, not this call's settlement — the reader answers absence (the safe direction; the
|
|
@@ -1452,6 +1497,12 @@ export type ResolvedAsk = PermissionResult & {
|
|
|
1452
1497
|
* text; stamped only at the composing arm. Not stamped on the timeout deny (nobody answered)
|
|
1453
1498
|
* or on any engine-produced fail-closed refusal. */
|
|
1454
1499
|
humanRefusalNote?: true;
|
|
1500
|
+
/** #548 — present exactly when this deny is the classifier denial-limit fallback's AUTO-DENY: the
|
|
1501
|
+
* ask's own window (`AskRequest.denialLimitFallback.autoDenyAfterMs`, CC `AKe`) elapsed with no
|
|
1502
|
+
* answer. Rides beside `settledBy:"timeout"` / `resolution:"window_expired"` (the established approval
|
|
1503
|
+
* factory's deadline arm speaks the same two words, so a consumer classifying on them needs no new
|
|
1504
|
+
* branch) and says WHOSE window it was: core's, not the host's. Stamped only at the composing arm. */
|
|
1505
|
+
autoDenied?: true;
|
|
1455
1506
|
/** The deny-arm classification (see {@link AskDenyResolution}) — present on every deny this
|
|
1456
1507
|
* resolver composes, absent on every allow. Carried by the gate to its block exit, the
|
|
1457
1508
|
* permission-denied observer payload and the settlement sideband (thence the call's `tool_end`
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -382,6 +382,7 @@ export function combinePolicies(...policies) {
|
|
|
382
382
|
let settled;
|
|
383
383
|
let ruleAskText;
|
|
384
384
|
let probeMandateSeen = false;
|
|
385
|
+
let fallbackSeen;
|
|
385
386
|
for (const p of policies) {
|
|
386
387
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
387
388
|
if (d.action === "deny") {
|
|
@@ -411,12 +412,15 @@ export function combinePolicies(...policies) {
|
|
|
411
412
|
}
|
|
412
413
|
if (d.action === "ask" && d.probeMandated === true)
|
|
413
414
|
probeMandateSeen = true;
|
|
415
|
+
if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
|
|
416
|
+
fallbackSeen = d.denialLimitFallback;
|
|
414
417
|
}
|
|
415
418
|
if (asked) {
|
|
416
419
|
const merged = rewrite?.updatedInput;
|
|
417
420
|
const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
|
|
418
421
|
const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
|
|
419
|
-
|
|
422
|
+
const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
|
|
423
|
+
return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
|
|
420
424
|
}
|
|
421
425
|
const allowed = rewrite ?? ALLOW;
|
|
422
426
|
return settled !== undefined ? { ...allowed, settledBy: settled } : allowed;
|
|
@@ -919,10 +923,16 @@ export function isAskDenyResolution(v) {
|
|
|
919
923
|
return typeof v === "string" && ASK_DENY_RESOLUTION_SET.has(v);
|
|
920
924
|
}
|
|
921
925
|
const coreMintedResolutions = new WeakMap();
|
|
922
|
-
function withCoreMintedResolution(d, resolution, call) {
|
|
923
|
-
coreMintedResolutions.set(d, { resolution, toolCallId: call.toolCallId, toolName: call.toolName });
|
|
926
|
+
function withCoreMintedResolution(d, resolution, call, autoDenied) {
|
|
927
|
+
coreMintedResolutions.set(d, { resolution, toolCallId: call.toolCallId, toolName: call.toolName, ...(autoDenied === true ? { autoDenied: true } : {}) });
|
|
924
928
|
return d;
|
|
925
929
|
}
|
|
930
|
+
export function coreMintedAutoDeniedOf(d, call) {
|
|
931
|
+
if (typeof d !== "object" || d === null)
|
|
932
|
+
return false;
|
|
933
|
+
const v = coreMintedResolutions.get(d);
|
|
934
|
+
return v !== undefined && v.toolCallId === call.toolCallId && v.toolName === call.toolName && v.autoDenied === true;
|
|
935
|
+
}
|
|
926
936
|
export function coreMintedResolutionOf(d, call) {
|
|
927
937
|
if (typeof d !== "object" || d === null)
|
|
928
938
|
return undefined;
|
|
@@ -1006,7 +1016,7 @@ export function carriesBidiControls(value, limits) {
|
|
|
1006
1016
|
export async function resolveAsk(req, onAsk, signal, onLateSettlement) {
|
|
1007
1017
|
const r = await resolveAskArms(req, onAsk, signal, onLateSettlement);
|
|
1008
1018
|
if (r.action === "deny" && isAskDenyResolution(r.resolution))
|
|
1009
|
-
return withCoreMintedResolution(r, r.resolution, req);
|
|
1019
|
+
return withCoreMintedResolution(r, r.resolution, req, r.autoDenied === true ? true : undefined);
|
|
1010
1020
|
return r;
|
|
1011
1021
|
}
|
|
1012
1022
|
function raceAskWaitAgainstSignal(wait, signal) {
|
|
@@ -1110,7 +1120,20 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1110
1120
|
args: approverView.value,
|
|
1111
1121
|
...(bidi ? { hasBidiControls: true } : {}),
|
|
1112
1122
|
}, signal));
|
|
1113
|
-
const
|
|
1123
|
+
const windowMs = req.denialLimitFallback !== undefined && req.denialLimitFallback.autoDenyAfterMs > 0 ? req.denialLimitFallback.autoDenyAfterMs : undefined;
|
|
1124
|
+
const raced = await withTimeout(raceAskWaitAgainstSignal(wait, signal), windowMs, () => DEADLINE_ELAPSED);
|
|
1125
|
+
if (raced === DEADLINE_ELAPSED) {
|
|
1126
|
+
detachLateAskWait(wait, onLateSettlement);
|
|
1127
|
+
return {
|
|
1128
|
+
action: "deny",
|
|
1129
|
+
message: `no one answered the classifier denial-limit approval for "${req.toolName}" — the ${String(windowMs)}ms window ` +
|
|
1130
|
+
`elapsed with no answer; auto-denied: ${req.message}`,
|
|
1131
|
+
decisionReason: "mode",
|
|
1132
|
+
resolution: "window_expired",
|
|
1133
|
+
settledBy: "timeout",
|
|
1134
|
+
autoDenied: true,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1114
1137
|
if (raced.tag === "aborted") {
|
|
1115
1138
|
detachLateAskWait(wait, onLateSettlement);
|
|
1116
1139
|
return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode",
|
package/dist/core/tools.js
CHANGED
|
@@ -55,6 +55,7 @@ export function defineTool(spec, options) {
|
|
|
55
55
|
...(spec.alwaysLoad !== undefined ? { alwaysLoad: spec.alwaysLoad } : {}),
|
|
56
56
|
...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
|
|
57
57
|
...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
|
|
58
|
+
...(spec.validateInput ? { validateInput: spec.validateInput } : {}),
|
|
58
59
|
execute: async (toolCallId, rawParams, signal) => {
|
|
59
60
|
const params = spec.prepareArguments ? spec.prepareArguments(rawParams) : rawParams;
|
|
60
61
|
if (!Value.Check(spec.parameters, params)) {
|