@sema-agent/core 7.8.0 → 7.9.1
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 +75 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +16 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +36 -35
- package/dist/core/effective-path-target.d.ts +43 -0
- package/dist/core/effective-path-target.js +56 -0
- package/dist/core/engine-notice.d.ts +8 -0
- package/dist/core/fs-write-gate-policy.js +2 -1
- package/dist/core/gate-lanes.d.ts +1 -0
- package/dist/core/gate-lanes.js +60 -28
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +66 -32
- package/dist/core/mcp-injection-drop.d.ts +74 -0
- package/dist/core/mcp-injection-drop.js +27 -0
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +311 -96
- package/dist/core/permission-rule-model.js +309 -50
- package/dist/core/permission-rule-org.d.ts +19 -12
- package/dist/core/permission-rule-org.js +13 -16
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +56 -33
- package/dist/core/permission-rules.js +88 -55
- package/dist/core/persisted-rule-arms.d.ts +61 -0
- package/dist/core/persisted-rule-arms.js +50 -0
- package/dist/core/runner/active-skill-scope.js +2 -1
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
- package/dist/core/runner/permission-rule-lanes.js +25 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-face-overlay.js +22 -3
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.js +5 -3
- package/dist/core/shell-lexer.d.ts +47 -0
- package/dist/core/shell-lexer.js +478 -0
- package/dist/core/shell-scan.d.ts +60 -0
- package/dist/core/shell-scan.js +183 -0
- package/dist/core/shell-wrapper-table.d.ts +297 -0
- package/dist/core/shell-wrapper-table.js +58 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-catalog-entries.js +6 -6
- package/dist/core/tool-face.d.ts +80 -4
- package/dist/core/tool-face.js +10 -0
- package/dist/core/tool-policy.d.ts +1 -6
- package/dist/core/tool-registry.d.ts +8 -11
- package/dist/core/tool-registry.js +5 -2
- package/dist/core/tool-roster.d.ts +20 -2
- package/dist/core/tool-roster.js +36 -8
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/fs-search-tools.js +17 -9
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +0 -8
- package/dist/tools/fs/search.js +0 -23
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -11
package/dist/core/gate-lanes.js
CHANGED
|
@@ -2,7 +2,9 @@ import { decisionText, describeThrown } from "./tool-policy.js";
|
|
|
2
2
|
import { askOriginOf, classifierMayAnswer } from "./ask-origin.js";
|
|
3
3
|
import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
|
+
import { isRuleBehavior } from "./permission-rule-model.js";
|
|
5
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
7
|
+
import { applyPersistedTightening, disclosedRuleSet } from "./persisted-rule-arms.js";
|
|
6
8
|
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
7
9
|
import { exitGate, traceHookCrash } from "./runner/gate-exit.js";
|
|
8
10
|
function isPlainOwnRecord(x) {
|
|
@@ -68,7 +70,9 @@ export function normalizeOrgGateVerdict(answer, unreadable) {
|
|
|
68
70
|
const rule = ownDataValue(verdict, "rule");
|
|
69
71
|
if ((behavior !== "deny" && behavior !== "ask") || typeof rule !== "string")
|
|
70
72
|
return clean({ status: "unavailable", disclosures: [unreadable] });
|
|
71
|
-
|
|
73
|
+
const reason = ownDataValue(verdict, "unreadable");
|
|
74
|
+
const unreadableCell = behavior === "ask" && typeof reason === "string" ? { unreadable: reason } : {};
|
|
75
|
+
return clean({ status: "available", verdict: clean({ behavior: behavior, rule, ...unreadableCell }), ...revisionCell });
|
|
72
76
|
}
|
|
73
77
|
function copiedUsableDots(dots) {
|
|
74
78
|
let copied;
|
|
@@ -89,12 +93,12 @@ function copiedUsableDots(dots) {
|
|
|
89
93
|
return usable ? { dots: copied } : {};
|
|
90
94
|
}
|
|
91
95
|
export function normalizePersistedRuleHit(hit) {
|
|
92
|
-
if (typeof hit === "string")
|
|
93
|
-
return { hit: { rules: [{ rule: hit }] } };
|
|
94
96
|
if (!isPlainOwnRecord(hit))
|
|
95
97
|
return {};
|
|
96
|
-
if (ownDataValue(hit, "unreadable") === true)
|
|
97
|
-
|
|
98
|
+
if (ownDataValue(hit, "unreadable") === true) {
|
|
99
|
+
const reason = ownDataValue(hit, "reason");
|
|
100
|
+
return typeof reason === "string" ? { unreadable: true, reason } : { unreadable: true };
|
|
101
|
+
}
|
|
98
102
|
const rulesRaw = ownDataValue(hit, "rules");
|
|
99
103
|
if (rulesRaw === undefined) {
|
|
100
104
|
const coverageRaw = ownDataValue(hit, "segmentCoverage");
|
|
@@ -132,7 +136,10 @@ export function normalizePersistedRuleHit(hit) {
|
|
|
132
136
|
return {};
|
|
133
137
|
if (!members.every((m) => typeof m.rule === "string"))
|
|
134
138
|
return {};
|
|
135
|
-
|
|
139
|
+
const behavior = ownDataValue(hit, "behavior");
|
|
140
|
+
if (!isRuleBehavior(behavior))
|
|
141
|
+
return {};
|
|
142
|
+
return { hit: { behavior, rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
|
|
136
143
|
}
|
|
137
144
|
export function persistedRuleMandateOf(marks) {
|
|
138
145
|
return marks.probeMandated === true
|
|
@@ -153,10 +160,11 @@ function parkWith(suspendAsk, parkArgs, carry) {
|
|
|
153
160
|
export async function runGateLanes(pass) {
|
|
154
161
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, screening, adjudicate, resolveAsk, suspendAsk } = pass;
|
|
155
162
|
let orgRealApprovalRequired = false;
|
|
156
|
-
const askOriginFacts = (org) => ({
|
|
163
|
+
const askOriginFacts = (org, ruleStore) => ({
|
|
157
164
|
contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
|
|
158
165
|
markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
|
|
159
166
|
org,
|
|
167
|
+
...(ruleStore !== undefined ? { ruleStore } : {}),
|
|
160
168
|
tightened: pass.tightenedBy,
|
|
161
169
|
});
|
|
162
170
|
let orgTightenCount = 0;
|
|
@@ -215,7 +223,7 @@ export async function runGateLanes(pass) {
|
|
|
215
223
|
orgEvidence = { ...revisionCell, ruleAbsent: "no_match" };
|
|
216
224
|
return { decision: decided, org: undefined };
|
|
217
225
|
}
|
|
218
|
-
const { behavior, rule } = answer.verdict;
|
|
226
|
+
const { behavior, rule, unreadable } = answer.verdict;
|
|
219
227
|
orgEvidence = { ...revisionCell, rule };
|
|
220
228
|
if (behavior === "deny") {
|
|
221
229
|
orgTightenCount += 1;
|
|
@@ -231,7 +239,9 @@ export async function runGateLanes(pass) {
|
|
|
231
239
|
? { ...decided, requiresRealApproval: true }
|
|
232
240
|
: {
|
|
233
241
|
action: "ask",
|
|
234
|
-
message:
|
|
242
|
+
message: unreadable !== undefined
|
|
243
|
+
? `an organization policy rule (${rule}) may reach this call, which could not be read against it (${unreadable}) — a person must decide`
|
|
244
|
+
: `an organization policy rule (${rule}) requires approval for this call`,
|
|
235
245
|
decisionReason: ORG_RULE_DECISION_REASON,
|
|
236
246
|
requiresRealApproval: true,
|
|
237
247
|
},
|
|
@@ -240,6 +250,24 @@ export async function runGateLanes(pass) {
|
|
|
240
250
|
};
|
|
241
251
|
const orgAnswer = await applyOrgLayer(pass.decision, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
|
|
242
252
|
pass.decision = orgAnswer.decision;
|
|
253
|
+
const readPersistedRules = async (req) => {
|
|
254
|
+
let pendingHit;
|
|
255
|
+
try {
|
|
256
|
+
pendingHit = Promise.resolve(input.persistedRules?.adjudicate(req));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return { unreadable: true };
|
|
260
|
+
}
|
|
261
|
+
return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
262
|
+
};
|
|
263
|
+
const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
|
|
264
|
+
const laneAnswer = input.persistedRules !== undefined && pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
|
|
265
|
+
{
|
|
266
|
+
const tightened = applyPersistedTightening(pass.decision, laneAnswer);
|
|
267
|
+
pass.decision = tightened.decision;
|
|
268
|
+
if (tightened.tightened === "deny")
|
|
269
|
+
pass.deniedBy = "persisted_rule";
|
|
270
|
+
}
|
|
243
271
|
if (pass.decision.action === "ask") {
|
|
244
272
|
if (pass.policyRewrite !== undefined) {
|
|
245
273
|
pass.currentInput = pass.policyRewrite;
|
|
@@ -252,6 +280,8 @@ export async function runGateLanes(pass) {
|
|
|
252
280
|
probeMandated: pass.decision.action === "ask" && pass.decision.probeMandated === true,
|
|
253
281
|
});
|
|
254
282
|
let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
|
|
283
|
+
if (laneAnswer?.unreadable === true)
|
|
284
|
+
personalEvidence = { dotsAbsent: "unavailable" };
|
|
255
285
|
let laneCoverage;
|
|
256
286
|
if (input.persistedRules &&
|
|
257
287
|
!orgRealApprovalRequired &&
|
|
@@ -260,24 +290,10 @@ export async function runGateLanes(pass) {
|
|
|
260
290
|
pass.decision.decisionReason !== "hook" &&
|
|
261
291
|
pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
262
292
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
263
|
-
const answer =
|
|
264
|
-
|
|
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;
|
|
293
|
+
const answer = laneAnswer ?? {};
|
|
294
|
+
const hitRules = answer.hit?.behavior === "allow" ? answer.hit.rules : undefined;
|
|
274
295
|
laneCoverage = answer.coverage;
|
|
275
296
|
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
297
|
const unionDotsOf = (rules) => {
|
|
282
298
|
const seen = new Set();
|
|
283
299
|
const union = [];
|
|
@@ -294,6 +310,8 @@ export async function runGateLanes(pass) {
|
|
|
294
310
|
}
|
|
295
311
|
return { dots: union };
|
|
296
312
|
};
|
|
313
|
+
if (answer.hit?.behavior === "ask")
|
|
314
|
+
personalEvidence = unionDotsOf(answer.hit.rules);
|
|
297
315
|
if (hitRules !== undefined && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined) {
|
|
298
316
|
pass.decision = {
|
|
299
317
|
action: "allow",
|
|
@@ -324,7 +342,7 @@ export async function runGateLanes(pass) {
|
|
|
324
342
|
};
|
|
325
343
|
}
|
|
326
344
|
}
|
|
327
|
-
const originFacts = askOriginFacts(orgAnswer.org);
|
|
345
|
+
const originFacts = askOriginFacts(orgAnswer.org, laneAnswer?.unreadable === true ? "unavailable" : undefined);
|
|
328
346
|
if (pass.decision.action === "ask") {
|
|
329
347
|
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
330
348
|
pass.decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
|
|
@@ -374,6 +392,7 @@ export async function runGateLanes(pass) {
|
|
|
374
392
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
|
|
375
393
|
pass.decision.requiresRealApproval !== true &&
|
|
376
394
|
pass.decision.decisionReason !== "hook" &&
|
|
395
|
+
pass.decision.origin !== "ask_rule" &&
|
|
377
396
|
pass.hookAsk === undefined &&
|
|
378
397
|
input.egress !== true &&
|
|
379
398
|
input.peerMessage !== true &&
|
|
@@ -543,7 +562,7 @@ export async function runGateLanes(pass) {
|
|
|
543
562
|
}
|
|
544
563
|
}
|
|
545
564
|
}
|
|
546
|
-
if (!adjudicate && input.orgRules === undefined)
|
|
565
|
+
if (!adjudicate && input.orgRules === undefined && input.persistedRules === undefined)
|
|
547
566
|
break;
|
|
548
567
|
let recheck = { action: "allow" };
|
|
549
568
|
if (adjudicate) {
|
|
@@ -576,6 +595,19 @@ export async function runGateLanes(pass) {
|
|
|
576
595
|
editDenied = recheck;
|
|
577
596
|
break;
|
|
578
597
|
}
|
|
598
|
+
let editRuleStore;
|
|
599
|
+
if (input.persistedRules !== undefined && toolName !== ASK_USER_QUESTION_TOOL_NAME) {
|
|
600
|
+
const editLane = await readPersistedRules({ toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
|
|
601
|
+
editRuleStore = editLane.unreadable === true ? "unavailable" : undefined;
|
|
602
|
+
const editTightened = applyPersistedTightening(recheck, editLane);
|
|
603
|
+
recheck = editTightened.decision;
|
|
604
|
+
if (editTightened.tightened === "deny")
|
|
605
|
+
pass.deniedBy = "persisted_rule";
|
|
606
|
+
if (recheck.action === "deny") {
|
|
607
|
+
editDenied = recheck;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
579
611
|
if (recheck.action === "allow") {
|
|
580
612
|
if (recheck.updatedInput !== undefined) {
|
|
581
613
|
editArgs = recheck.updatedInput;
|
|
@@ -588,7 +620,7 @@ export async function runGateLanes(pass) {
|
|
|
588
620
|
editRewrittenSinceHuman = true;
|
|
589
621
|
}
|
|
590
622
|
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
591
|
-
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
|
|
623
|
+
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org, editRuleStore)) };
|
|
592
624
|
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
593
625
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
594
626
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
@@ -129,18 +129,22 @@ export type SettlementRefusalTableCoversEveryKind = AssertAllKeysHandled<Exclude
|
|
|
129
129
|
* - `ask_resolution` — the ask's own settlement is the refusal (a person said no, a window elapsed,
|
|
130
130
|
* nobody could be asked…); the detail is on {@link GateOutcome.settlement}.
|
|
131
131
|
*/
|
|
132
|
-
export declare const DENIED_BY_VALUES: readonly ["policy", "hook", "org", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
132
|
+
export declare const DENIED_BY_VALUES: readonly ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
133
133
|
export type DeniedBy = (typeof DENIED_BY_VALUES)[number];
|
|
134
134
|
/** Membership test for {@link DeniedBy}. */
|
|
135
135
|
export declare function isDeniedBy(v: unknown): v is DeniedBy;
|
|
136
136
|
/** Which layers may VETO an approval a person already gave (invariant I3: a `human_allowed` settlement
|
|
137
|
-
* beside a `denied` disposition names one of these). The approval-edit re-screen (hook), the policy
|
|
138
|
-
*
|
|
139
|
-
* and compliance run before any ask exists, and `ask_resolution` IS the
|
|
137
|
+
* beside a `denied` disposition names one of these). The approval-edit re-screen (hook), the policy, org
|
|
138
|
+
* and persisted-rule re-checks, and the write-protection re-judge run AFTER the person's yes; the
|
|
139
|
+
* classifier, plan mode and compliance run before any ask exists, and `ask_resolution` IS the
|
|
140
|
+
* settlement, not a veto of it. `persisted_rule` = a person's own standing DENY row (their settings'
|
|
141
|
+
* deny list, imported) — the personal-store sibling of `org`, and like it re-judged on an edited
|
|
142
|
+
* command. */
|
|
140
143
|
export declare const DENIED_BY_MAY_VETO: {
|
|
141
144
|
readonly policy: true;
|
|
142
145
|
readonly hook: true;
|
|
143
146
|
readonly org: true;
|
|
147
|
+
readonly persisted_rule: true;
|
|
144
148
|
readonly classifier: false;
|
|
145
149
|
readonly plan_mode: false;
|
|
146
150
|
readonly compliance: false;
|
|
@@ -31,7 +31,7 @@ export const SETTLEMENT_IS_REFUSAL = {
|
|
|
31
31
|
blanket_allow_refused: true,
|
|
32
32
|
task_aborted: true,
|
|
33
33
|
};
|
|
34
|
-
export const DENIED_BY_VALUES = ["policy", "hook", "org", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
34
|
+
export const DENIED_BY_VALUES = ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
35
35
|
const DENIED_BY_SET = new Set(DENIED_BY_VALUES);
|
|
36
36
|
export function isDeniedBy(v) {
|
|
37
37
|
return DENIED_BY_SET.has(v);
|
|
@@ -40,6 +40,7 @@ export const DENIED_BY_MAY_VETO = {
|
|
|
40
40
|
policy: true,
|
|
41
41
|
hook: true,
|
|
42
42
|
org: true,
|
|
43
|
+
persisted_rule: true,
|
|
43
44
|
classifier: false,
|
|
44
45
|
plan_mode: false,
|
|
45
46
|
compliance: false,
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -113,6 +113,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
113
113
|
"classifier.denial_limit",
|
|
114
114
|
"checkpoint.execution_outcome_unrecorded",
|
|
115
115
|
"delegation.transcript_integrity",
|
|
116
|
+
"mcp.injection_dropped",
|
|
116
117
|
"mcp.revocation_probe_failed",
|
|
117
118
|
"workflow.governance_key_stripped",
|
|
118
119
|
"workflow.agent_option_ignored",
|
|
@@ -177,6 +178,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
177
178
|
"peer.idle_subscription": "user",
|
|
178
179
|
"delegation.transcript_integrity": "operator",
|
|
179
180
|
"checkpoint.execution_outcome_unrecorded": "operator",
|
|
181
|
+
"mcp.injection_dropped": "user",
|
|
180
182
|
"mcp.revocation_probe_failed": "operator",
|
|
181
183
|
"workflow.governance_key_stripped": "operator",
|
|
182
184
|
"workflow.agent_option_ignored": "operator",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -993,10 +993,13 @@ export interface ParkAttemptFailed {
|
|
|
993
993
|
*/
|
|
994
994
|
export type OrgGateVerdict = {
|
|
995
995
|
status: "available";
|
|
996
|
-
/** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket.
|
|
996
|
+
/** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket. An
|
|
997
|
+
* `unreadable` member (the lexer's reason) says the call could not be READ against `rule` — an
|
|
998
|
+
* org rule that may reach it — so `behavior` is `ask` and the message names the reason. */
|
|
997
999
|
verdict?: {
|
|
998
1000
|
behavior: "deny" | "ask";
|
|
999
1001
|
rule: string;
|
|
1002
|
+
unreadable?: string;
|
|
1000
1003
|
};
|
|
1001
1004
|
/** design/252 G-2 (additive): the REVISION of the snapshot this answer was read off — the
|
|
1002
1005
|
* published-policy version an auditor reconciles the resulting decision against. Present on the
|
|
@@ -1022,22 +1025,27 @@ export interface PersistedRuleHitRule {
|
|
|
1022
1025
|
readonly dots?: readonly import("./permission-rule-model.js").RuleDot[];
|
|
1023
1026
|
}
|
|
1024
1027
|
/**
|
|
1025
|
-
* design/252 G-2, re-formed by design/375
|
|
1026
|
-
* gives:
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1028
|
+
* design/252 G-2, re-formed by design/375 and again by the three-behavior model (BREAKING) — the
|
|
1029
|
+
* structured answer a persisted-rule lane gives: WHICH behavior spoke for the call, and the rule set it
|
|
1030
|
+
* stands on. For an `allow` the set is the COVERAGE SET that admits the call — a single rule admitting
|
|
1031
|
+
* the whole command is a one-element set (the historical case); a compound admitted by the conjunction
|
|
1032
|
+
* of per-segment rules (design/375 §5.1) is the set of those rules, in segment order. For a `deny` or an
|
|
1033
|
+
* `ask` the set is the one rule of that behavior that reaches the call (the lane's precedence walk —
|
|
1034
|
+
* deny > ask > allow — already chose it). `rules` is non-empty by contract — an empty set covers nothing
|
|
1035
|
+
* and is not a hit (the normalizer reads it as a clean negative).
|
|
1030
1036
|
*
|
|
1031
|
-
* Why dots and not an id: a persisted
|
|
1037
|
+
* Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose
|
|
1032
1038
|
* identity is the set of add dots that redeemed it, and two concurrently approved records for one text
|
|
1033
1039
|
* each carry their own. The set is the identity; a joined string would not be one.
|
|
1034
1040
|
*
|
|
1035
|
-
* The pre-375 single-rule object shape (`{ rule, dots? }`)
|
|
1036
|
-
* unrecognized answer degrades to a clean negative (more asks),
|
|
1037
|
-
* shape for a lane that missed the same-window upgrade.
|
|
1041
|
+
* The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are
|
|
1042
|
+
* NOT accepted: on this loosening seam an unrecognized answer degrades to a clean negative (more asks),
|
|
1043
|
+
* never to a match — the safe failure shape for a lane that missed the same-window upgrade.
|
|
1038
1044
|
*/
|
|
1039
1045
|
export interface PersistedRuleHit {
|
|
1040
|
-
/**
|
|
1046
|
+
/** What the deciding rule(s) say about the call — the closed set's word. */
|
|
1047
|
+
readonly behavior: import("./permission-rule-model.js").RuleBehavior;
|
|
1048
|
+
/** The rule set the verdict stands on. Non-empty. */
|
|
1041
1049
|
readonly rules: readonly PersistedRuleHitRule[];
|
|
1042
1050
|
}
|
|
1043
1051
|
/**
|
|
@@ -1052,6 +1060,12 @@ export interface PersistedRuleHit {
|
|
|
1052
1060
|
*/
|
|
1053
1061
|
export interface PersistedRuleUnreadable {
|
|
1054
1062
|
readonly unreadable: true;
|
|
1063
|
+
/** WHY the lane could not read: absent for a store read failure; for a call the lane could not READ
|
|
1064
|
+
* against the person's standing deny/ask rows (an expansion where a rule reads a word, an unterminated
|
|
1065
|
+
* quote, a syntax error — `programRunReachOf`'s `unreadable` word), the lexer's reason. The DECISION
|
|
1066
|
+
* is the same fail-closed one either way (the question "does one of my rules forbid this?" is
|
|
1067
|
+
* unanswered); the reason is what the ask's message says instead of "the store could not be read". */
|
|
1068
|
+
readonly reason?: string;
|
|
1055
1069
|
}
|
|
1056
1070
|
/**
|
|
1057
1071
|
* design/375 §5.2② — the CLEAN NEGATIVE that still carries the per-segment coverage table: no rule
|
|
@@ -1065,8 +1079,9 @@ export interface PersistedRuleUnreadable {
|
|
|
1065
1079
|
export interface PersistedRuleCoverage {
|
|
1066
1080
|
readonly segmentCoverage: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
1067
1081
|
}
|
|
1068
|
-
/** Every shape a lane may answer with.
|
|
1069
|
-
|
|
1082
|
+
/** Every shape a lane may answer with. The retired bare-string form (a single allow rule's text) is read
|
|
1083
|
+
* by the normalizer as a clean negative, like every other unrecognized shape. */
|
|
1084
|
+
export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | PersistedRuleUnreadable | undefined;
|
|
1070
1085
|
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1071
1086
|
* signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
|
|
1072
1087
|
* additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
|
|
@@ -1404,15 +1419,27 @@ export interface ToolGateInput {
|
|
|
1404
1419
|
*/
|
|
1405
1420
|
isMarkedUnresolvable?: (toolCallId: string) => boolean;
|
|
1406
1421
|
/**
|
|
1407
|
-
* design/179 — the persisted
|
|
1408
|
-
*
|
|
1422
|
+
* design/179 — the persisted-rule lane. Present only when a deployment wired a rule store; absent it,
|
|
1423
|
+
* this whole step does not exist and the gate's decision path is byte-identical to what it was.
|
|
1409
1424
|
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
* the
|
|
1425
|
+
* The lane answers with the BEHAVIOR that spoke ({@link PersistedRuleHit}), and the gate consumes the
|
|
1426
|
+
* three behaviors at two different places — the upstream arbiter's order, deny > ask > allow, across
|
|
1427
|
+
* the person's own standing rules:
|
|
1428
|
+
* · a DENY row refuses the call outright, on EVERY decision (an allow from the policy fold included):
|
|
1429
|
+
* it is the person's own "never run this" (their settings' deny list, imported), the personal-store
|
|
1430
|
+
* sibling of an org deny, and it runs right after the org layer — nothing downstream re-opens it;
|
|
1431
|
+
* · an ASK row is the person's standing "ask me each time": it turns a surviving allow into an ask
|
|
1432
|
+
* and stamps `matchedAskRule` on it (the same marker an explicit `ask` permission rule stamps), so
|
|
1433
|
+
* the ask's origin is `ask_rule` — the auto-mode classifier never answers it and a persisted ALLOW
|
|
1434
|
+
* never clears it; on a call that is already an ask it stamps the marker and leaves the rest;
|
|
1435
|
+
* · an ALLOW row is the standing form of ONE recorded human approval, so it resolves the asks a real
|
|
1436
|
+
* person saying "allow" would have resolved — and nothing else. It sits post-fold, at the entry of
|
|
1437
|
+
* the `ask` branch, BEFORE the classifier and AFTER every tighten: earlier and a tighten would
|
|
1438
|
+
* simply put the ask back; later and the classifier would already have spoken.
|
|
1414
1439
|
*
|
|
1415
|
-
*
|
|
1440
|
+
* The deny/ask arms tighten and take no conjunct but the two structural ones every lane shares (the
|
|
1441
|
+
* engine's own content-ask tool is exempt; the lane must be wired). The ALLOW arm's consumption
|
|
1442
|
+
* predicate is its own, not the classifier's blanket flip:
|
|
1416
1443
|
* · `requiresRealApproval` asks are never consumed — the two integrity gates are the floor, and the
|
|
1417
1444
|
* same flag is what a v2 organization-level ask will carry;
|
|
1418
1445
|
* · a PreToolUse hook's ask is never consumed — that is the deployment's own adjudication, and a rule
|
|
@@ -1441,19 +1468,26 @@ export interface ToolGateInput {
|
|
|
1441
1468
|
* bullets above are what keep integrity/hook/mandated asks out of that set.
|
|
1442
1469
|
*/
|
|
1443
1470
|
persistedRules?: {
|
|
1444
|
-
/** The
|
|
1445
|
-
*
|
|
1471
|
+
/** The lane's verdict for this call — a {@link PersistedRuleHit} naming the behavior that spoke and
|
|
1472
|
+
* the rule set it stands on, a coverage-carrying clean negative, an unreadable-store report, or
|
|
1473
|
+
* `undefined` (the store was READ and no rule speaks). Must not throw — and a store that cannot be
|
|
1474
|
+
* read answers `{ unreadable: true }`, NEVER `undefined`: the gate reads the unreadable report as
|
|
1475
|
+
* fail-closed (an allow tightens to a real-approval ask of origin `rule_store_unavailable`), while
|
|
1476
|
+
* `undefined` is a successful no-match that leaves an allow standing. An adapter that answers
|
|
1477
|
+
* `undefined` on a read failure silently disarms every deny/ask row it holds.
|
|
1446
1478
|
*
|
|
1447
|
-
*
|
|
1448
|
-
* single
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
|
|
1479
|
+
* For an ALLOW the set is the COVERAGE SET — each member's canonical text plus its add DOTS. A
|
|
1480
|
+
* single rule admitting the whole command is a one-element set; a compound admitted by the
|
|
1481
|
+
* per-segment conjunction (design/375 §5.1) is the set of the covering rules. The dots are a rule's
|
|
1482
|
+
* durable identity, and the text is not one (it reaches a card sanitized and capped); a lane that
|
|
1483
|
+
* can name them lets the shadowed-ask evidence say WHICH stored rules matched rather than merely
|
|
1484
|
+
* what they looked like. The retired shapes — a bare rule TEXT, the pre-375 `{ rule, dots? }` object,
|
|
1485
|
+
* a `{ rules }` hit without its behavior — are read as a clean negative by the normalizer: a stale
|
|
1486
|
+
* lane degrades toward asking, never toward an allow. Consulted on EVERY decision (a deny/ask row
|
|
1487
|
+
* must be able to tighten an allow), so the lane pays one store read per call on the tools it
|
|
1488
|
+
* speaks for; the runner's lane hands the org layer's read to it, so a governed deployment still
|
|
1489
|
+
* pays one. */
|
|
1490
|
+
adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
|
|
1457
1491
|
/** Observation sink for an ask a rule coverage resolved — the allow-side disclosure. `rules` is
|
|
1458
1492
|
* the coverage set's canonical texts in reporting order (design/375: a conjunction names every
|
|
1459
1493
|
* rule the decision stands on). Never affects the outcome. */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcp.injection_dropped` — the one mint of the notice a host raises when an entry of the user's own
|
|
3
|
+
* MCP injection (a `.mcp.json` row handed to the engine through the host's request lane) was NOT
|
|
4
|
+
* mounted for the leg. L-167: three drop sites in the host's intake and one cap site each produced
|
|
5
|
+
* their own wording and none reached the user; the engine did not see the entry at all (it never
|
|
6
|
+
* reached `prepareTask`), so no core-side notice could stand for it.
|
|
7
|
+
*
|
|
8
|
+
* Division of labour, the settlement pattern (`settlement.single_mint`): the HOST supplies FACTS
|
|
9
|
+
* (which session, which server name, which of four reasons, optionally which field was bad) and
|
|
10
|
+
* CORE composes the notice — the code word, the audience row, the message text per reason. A host
|
|
11
|
+
* never spells the code or the text itself: a second wording is a second source that drifts, and a
|
|
12
|
+
* host-spelled code is not in {@link import("./governance-codes.js").ENGINE_NOTICE_CODES}'s
|
|
13
|
+
* mint scan, so its audience row could silently rot.
|
|
14
|
+
*
|
|
15
|
+
* Audience `"user"`: the dropped entry is the user's own configuration and the recovery (rename the
|
|
16
|
+
* server, fix the field, trim the list) is in their hands — an operator channel does not reach them.
|
|
17
|
+
* `sessionId` is therefore REQUIRED at the mint (the routing half of a user row: a user disclosure
|
|
18
|
+
* that cannot name its session has nowhere to be delivered) — a host with no session has no user to
|
|
19
|
+
* tell and must not mint. Dedup unit: per leg per server name — the HOST's obligation (it owns the
|
|
20
|
+
* leg's intake loop); this module is pure.
|
|
21
|
+
*/
|
|
22
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
23
|
+
import type { EngineNotice } from "./engine-notice.js";
|
|
24
|
+
/**
|
|
25
|
+
* The closed set of drop reasons ({@link McpInjectionDropFacts.reason}) — the host's intake
|
|
26
|
+
* enumerates exactly these, and a consumer rendering the notice keys on the word:
|
|
27
|
+
* · `malformed_entry` — the entry did not parse as a server declaration (a missing
|
|
28
|
+
* `command`/`url`, a `toolFaces` value of the wrong shape, …);
|
|
29
|
+
* `field` names the offending key when the host knows it;
|
|
30
|
+
* · `name_reserved_by_deployment` — the entry's server name collides with one the deployment
|
|
31
|
+
* itself mounts, and the deployment's wins;
|
|
32
|
+
* · `gate_closed` — the deployment does not accept request-lane MCP injection on
|
|
33
|
+
* this leg at all (a multi-tenant posture): the whole list is
|
|
34
|
+
* ignored, one notice per entry;
|
|
35
|
+
* · `over_cap` — the entry sits beyond the deployment's injection cap.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MCP_INJECTION_DROP_REASONS: readonly ["malformed_entry", "name_reserved_by_deployment", "gate_closed", "over_cap"];
|
|
38
|
+
export type McpInjectionDropReason = (typeof MCP_INJECTION_DROP_REASONS)[number];
|
|
39
|
+
/**
|
|
40
|
+
* The user-facing sentence per reason — the disposition table over {@link MCP_INJECTION_DROP_REASONS}
|
|
41
|
+
* (one reason, one wording; a consumer that wants its own phrasing keys on `detail.reason`, never on
|
|
42
|
+
* this text). Each names the recovery, because the reader is the person who can perform it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MCP_INJECTION_DROP_TEXT: {
|
|
45
|
+
readonly malformed_entry: "its declaration could not be read — fix the entry and start a new session";
|
|
46
|
+
readonly name_reserved_by_deployment: "the deployment mounts a server of that name and its own wins — rename the entry";
|
|
47
|
+
readonly gate_closed: "this deployment does not accept MCP servers from the request on this leg — ask the operator, or use the deployment's servers";
|
|
48
|
+
readonly over_cap: "the request named more MCP servers than this deployment mounts per leg — trim the list";
|
|
49
|
+
};
|
|
50
|
+
export type DropTextCoversEveryReason = AssertAllKeysHandled<Exclude<McpInjectionDropReason, keyof typeof MCP_INJECTION_DROP_TEXT>>;
|
|
51
|
+
/** What a host knows at the drop site — facts only, no words of the engine's vocabulary composed. */
|
|
52
|
+
export interface McpInjectionDropFacts {
|
|
53
|
+
/** The session whose injection carried the entry — the notice's routing key, required (see module note). */
|
|
54
|
+
readonly sessionId: string;
|
|
55
|
+
/** The entry's server name as the user spelled it (the message quotes it verbatim). */
|
|
56
|
+
readonly server: string;
|
|
57
|
+
readonly reason: McpInjectionDropReason;
|
|
58
|
+
/** `malformed_entry` only, optional: the key the host found bad, so the user knows what to edit. */
|
|
59
|
+
readonly field?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compose the `mcp.injection_dropped` notice from a host's facts.
|
|
63
|
+
*
|
|
64
|
+
* @contract mcp.injection_dropped.single_mint — the `mcp.injection_dropped` notice is composed by CORE
|
|
65
|
+
* from a host's drop FACTS (session, server name, one of four reasons, optionally the bad field); a
|
|
66
|
+
* host never spells the code, the audience or the sentence, and a fact outside the vocabulary (an
|
|
67
|
+
* unlisted reason word, an empty session or server name) is refused at the mint with a `TypeError`
|
|
68
|
+
* rather than shaped into a notice that would route nowhere or read as an unknown word downstream.
|
|
69
|
+
*
|
|
70
|
+
* `detail: { sessionId, server, reason, field? }` — `sessionId` is lifted onto the typed key by
|
|
71
|
+
* {@link import("./engine-notice.js").deliverEngineNotice}, the one throat, so the two spellings
|
|
72
|
+
* cannot disagree; `field` is present only when the host supplied a non-empty one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mcpInjectionDroppedNotice(facts: McpInjectionDropFacts): EngineNotice;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const MCP_INJECTION_DROP_REASONS = ["malformed_entry", "name_reserved_by_deployment", "gate_closed", "over_cap"];
|
|
2
|
+
export const MCP_INJECTION_DROP_TEXT = {
|
|
3
|
+
malformed_entry: "its declaration could not be read — fix the entry and start a new session",
|
|
4
|
+
name_reserved_by_deployment: "the deployment mounts a server of that name and its own wins — rename the entry",
|
|
5
|
+
gate_closed: "this deployment does not accept MCP servers from the request on this leg — ask the operator, or use the deployment's servers",
|
|
6
|
+
over_cap: "the request named more MCP servers than this deployment mounts per leg — trim the list",
|
|
7
|
+
};
|
|
8
|
+
const isReason = (x) => typeof x === "string" && MCP_INJECTION_DROP_REASONS.includes(x);
|
|
9
|
+
export function mcpInjectionDroppedNotice(facts) {
|
|
10
|
+
const { sessionId, server, reason, field } = facts;
|
|
11
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
12
|
+
throw new TypeError("mcp.injection_dropped: sessionId is required — a user-audience notice with no session routes nowhere");
|
|
13
|
+
}
|
|
14
|
+
if (typeof server !== "string" || server.length === 0) {
|
|
15
|
+
throw new TypeError("mcp.injection_dropped: server name is required");
|
|
16
|
+
}
|
|
17
|
+
if (!isReason(reason)) {
|
|
18
|
+
throw new TypeError(`mcp.injection_dropped: reason ${JSON.stringify(reason)} is not one of ${MCP_INJECTION_DROP_REASONS.join("/")}`);
|
|
19
|
+
}
|
|
20
|
+
const fieldSeat = typeof field === "string" && field.length > 0 ? { field } : {};
|
|
21
|
+
const where = reason === "malformed_entry" && typeof field === "string" && field.length > 0 ? ` (field \`${field}\`)` : "";
|
|
22
|
+
return {
|
|
23
|
+
code: "mcp.injection_dropped",
|
|
24
|
+
message: `MCP server "${server}" from this request was not mounted: ${MCP_INJECTION_DROP_TEXT[reason]}${where}.`,
|
|
25
|
+
detail: { sessionId, server, reason, ...fieldSeat },
|
|
26
|
+
};
|
|
27
|
+
}
|