@sema-agent/core 7.7.0 → 7.9.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 +88 -0
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/agents/subagent.js +2 -3
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/ask-question.js +2 -2
- package/dist/core/checkpoint-store.d.ts +64 -24
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +6 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +53 -26
- 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 +6 -0
- package/dist/core/hooks.d.ts +64 -33
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- 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 +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- 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 +85 -33
- package/dist/core/permission-rules.js +104 -88
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +46 -3
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +4 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-run-refs.d.ts +16 -0
- package/dist/core/runner/prepare-run-refs.js +6 -0
- package/dist/core/runner/prepare-safety-scan.js +48 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- 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.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +129 -1774
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +8 -7
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +14 -1
- package/dist/core/runner/tool-output-projection.js +24 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +459 -0
- package/dist/core/tool-roster.js +507 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +17 -6
- package/dist/index.js +17 -6
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- 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-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +3 -2
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +189 -17
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isWithin } from "./runner/session-rule-policy.js";
|
|
3
|
+
import { declaredPathTargetOf, isProtectedWrite } from "./tool-registry.js";
|
|
3
4
|
const ask = (message) => ({ action: "ask", message, decisionReason: "rule" });
|
|
4
5
|
export function createFsWriteGatePolicy(opts) {
|
|
5
6
|
const { env, rootPath, defaultWrite } = opts;
|
|
6
|
-
const gated = PATH_CONFINABLE_WRITE_TOOLS;
|
|
7
7
|
const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
|
|
8
8
|
const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
|
|
9
9
|
const guardedCapable = env.writeFileGuarded !== undefined;
|
|
@@ -12,8 +12,7 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
12
12
|
: " (advisory adjudication: this environment provides no guarded write, so approval is judged before a separate, non-atomic write step)";
|
|
13
13
|
return {
|
|
14
14
|
async check(req, signal) {
|
|
15
|
-
|
|
16
|
-
if (!gated.has(canonical))
|
|
15
|
+
if (!isProtectedWrite(req))
|
|
17
16
|
return { action: "allow" };
|
|
18
17
|
if (opts.requireGuardedWrite === true && !guardedCapable) {
|
|
19
18
|
return {
|
|
@@ -22,9 +21,10 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
22
21
|
decisionReason: "rule",
|
|
23
22
|
};
|
|
24
23
|
}
|
|
25
|
-
const
|
|
24
|
+
const declared = declaredPathTargetOf(req);
|
|
25
|
+
const path = declared === undefined ? undefined : writeTargetPath(req, declared);
|
|
26
26
|
if (path === undefined) {
|
|
27
|
-
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${advisory}`);
|
|
27
|
+
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${declared === undefined ? " (the tool declares no path target)" : ""}${advisory}`);
|
|
28
28
|
}
|
|
29
29
|
const canon = await canonicalizeTarget(env, path, signal, req.cwd ?? rootPath);
|
|
30
30
|
if (!canon.ok) {
|
|
@@ -42,7 +42,7 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
42
42
|
if (opts.isExempt) {
|
|
43
43
|
let exempt = false;
|
|
44
44
|
try {
|
|
45
|
-
exempt = (await opts.isExempt(
|
|
45
|
+
exempt = (await opts.isExempt(req.toolName, canon.key, req.face !== undefined ? { capabilityId: req.face.capabilityId, contractId: req.face.contractId, shapeDigest: req.face.shapeDigest } : undefined)) === true;
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
48
|
exempt = false;
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -30,7 +30,7 @@ function withProbeTimeout(p, ms, signal) {
|
|
|
30
30
|
const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
|
|
31
31
|
export async function runGateFold(pass) {
|
|
32
32
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
|
|
33
|
-
pass.req = { toolName, args: pass.currentInput, toolCallId };
|
|
33
|
+
pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
34
34
|
pass.decision = adjudicate ? await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req)) : { action: "allow" };
|
|
35
35
|
{
|
|
36
36
|
const refusedBy = ledger.fromDecision(pass.decision);
|
|
@@ -144,7 +144,7 @@ export async function runGateFold(pass) {
|
|
|
144
144
|
pass.tightenedBy = "safety";
|
|
145
145
|
}
|
|
146
146
|
if (pass.decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
147
|
-
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
|
|
147
|
+
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face);
|
|
148
148
|
if (writeProtectedHit !== null) {
|
|
149
149
|
pass.decision = {
|
|
150
150
|
action: "ask",
|
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) {
|
|
@@ -89,8 +91,6 @@ function copiedUsableDots(dots) {
|
|
|
89
91
|
return usable ? { dots: copied } : {};
|
|
90
92
|
}
|
|
91
93
|
export function normalizePersistedRuleHit(hit) {
|
|
92
|
-
if (typeof hit === "string")
|
|
93
|
-
return { hit: { rules: [{ rule: hit }] } };
|
|
94
94
|
if (!isPlainOwnRecord(hit))
|
|
95
95
|
return {};
|
|
96
96
|
if (ownDataValue(hit, "unreadable") === true)
|
|
@@ -132,7 +132,10 @@ export function normalizePersistedRuleHit(hit) {
|
|
|
132
132
|
return {};
|
|
133
133
|
if (!members.every((m) => typeof m.rule === "string"))
|
|
134
134
|
return {};
|
|
135
|
-
|
|
135
|
+
const behavior = ownDataValue(hit, "behavior");
|
|
136
|
+
if (!isRuleBehavior(behavior))
|
|
137
|
+
return {};
|
|
138
|
+
return { hit: { behavior, rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
|
|
136
139
|
}
|
|
137
140
|
export function persistedRuleMandateOf(marks) {
|
|
138
141
|
return marks.probeMandated === true
|
|
@@ -153,10 +156,11 @@ function parkWith(suspendAsk, parkArgs, carry) {
|
|
|
153
156
|
export async function runGateLanes(pass) {
|
|
154
157
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, screening, adjudicate, resolveAsk, suspendAsk } = pass;
|
|
155
158
|
let orgRealApprovalRequired = false;
|
|
156
|
-
const askOriginFacts = (org) => ({
|
|
159
|
+
const askOriginFacts = (org, ruleStore) => ({
|
|
157
160
|
contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
|
|
158
161
|
markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
|
|
159
162
|
org,
|
|
163
|
+
...(ruleStore !== undefined ? { ruleStore } : {}),
|
|
160
164
|
tightened: pass.tightenedBy,
|
|
161
165
|
});
|
|
162
166
|
let orgTightenCount = 0;
|
|
@@ -240,6 +244,24 @@ export async function runGateLanes(pass) {
|
|
|
240
244
|
};
|
|
241
245
|
const orgAnswer = await applyOrgLayer(pass.decision, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
|
|
242
246
|
pass.decision = orgAnswer.decision;
|
|
247
|
+
const readPersistedRules = async (req) => {
|
|
248
|
+
let pendingHit;
|
|
249
|
+
try {
|
|
250
|
+
pendingHit = Promise.resolve(input.persistedRules?.adjudicate(req));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return { unreadable: true };
|
|
254
|
+
}
|
|
255
|
+
return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
256
|
+
};
|
|
257
|
+
const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
|
|
258
|
+
const laneAnswer = input.persistedRules !== undefined && pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
|
|
259
|
+
{
|
|
260
|
+
const tightened = applyPersistedTightening(pass.decision, laneAnswer);
|
|
261
|
+
pass.decision = tightened.decision;
|
|
262
|
+
if (tightened.tightened === "deny")
|
|
263
|
+
pass.deniedBy = "persisted_rule";
|
|
264
|
+
}
|
|
243
265
|
if (pass.decision.action === "ask") {
|
|
244
266
|
if (pass.policyRewrite !== undefined) {
|
|
245
267
|
pass.currentInput = pass.policyRewrite;
|
|
@@ -252,6 +274,8 @@ export async function runGateLanes(pass) {
|
|
|
252
274
|
probeMandated: pass.decision.action === "ask" && pass.decision.probeMandated === true,
|
|
253
275
|
});
|
|
254
276
|
let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
|
|
277
|
+
if (laneAnswer?.unreadable === true)
|
|
278
|
+
personalEvidence = { dotsAbsent: "unavailable" };
|
|
255
279
|
let laneCoverage;
|
|
256
280
|
if (input.persistedRules &&
|
|
257
281
|
!orgRealApprovalRequired &&
|
|
@@ -260,24 +284,10 @@ export async function runGateLanes(pass) {
|
|
|
260
284
|
pass.decision.decisionReason !== "hook" &&
|
|
261
285
|
pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
262
286
|
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;
|
|
287
|
+
const answer = laneAnswer ?? {};
|
|
288
|
+
const hitRules = answer.hit?.behavior === "allow" ? answer.hit.rules : undefined;
|
|
274
289
|
laneCoverage = answer.coverage;
|
|
275
290
|
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
291
|
const unionDotsOf = (rules) => {
|
|
282
292
|
const seen = new Set();
|
|
283
293
|
const union = [];
|
|
@@ -294,6 +304,8 @@ export async function runGateLanes(pass) {
|
|
|
294
304
|
}
|
|
295
305
|
return { dots: union };
|
|
296
306
|
};
|
|
307
|
+
if (answer.hit?.behavior === "ask")
|
|
308
|
+
personalEvidence = unionDotsOf(answer.hit.rules);
|
|
297
309
|
if (hitRules !== undefined && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined) {
|
|
298
310
|
pass.decision = {
|
|
299
311
|
action: "allow",
|
|
@@ -324,7 +336,7 @@ export async function runGateLanes(pass) {
|
|
|
324
336
|
};
|
|
325
337
|
}
|
|
326
338
|
}
|
|
327
|
-
const originFacts = askOriginFacts(orgAnswer.org);
|
|
339
|
+
const originFacts = askOriginFacts(orgAnswer.org, laneAnswer?.unreadable === true ? "unavailable" : undefined);
|
|
328
340
|
if (pass.decision.action === "ask") {
|
|
329
341
|
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
330
342
|
pass.decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
|
|
@@ -374,6 +386,7 @@ export async function runGateLanes(pass) {
|
|
|
374
386
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
|
|
375
387
|
pass.decision.requiresRealApproval !== true &&
|
|
376
388
|
pass.decision.decisionReason !== "hook" &&
|
|
389
|
+
pass.decision.origin !== "ask_rule" &&
|
|
377
390
|
pass.hookAsk === undefined &&
|
|
378
391
|
input.egress !== true &&
|
|
379
392
|
input.peerMessage !== true &&
|
|
@@ -543,12 +556,13 @@ export async function runGateLanes(pass) {
|
|
|
543
556
|
}
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
|
-
if (!adjudicate && input.orgRules === undefined)
|
|
559
|
+
if (!adjudicate && input.orgRules === undefined && input.persistedRules === undefined)
|
|
547
560
|
break;
|
|
548
561
|
let recheck = { action: "allow" };
|
|
549
562
|
if (adjudicate) {
|
|
550
563
|
try {
|
|
551
|
-
|
|
564
|
+
const editReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
565
|
+
recheck = await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq));
|
|
552
566
|
}
|
|
553
567
|
catch (err) {
|
|
554
568
|
recheck = {
|
|
@@ -575,6 +589,19 @@ export async function runGateLanes(pass) {
|
|
|
575
589
|
editDenied = recheck;
|
|
576
590
|
break;
|
|
577
591
|
}
|
|
592
|
+
let editRuleStore;
|
|
593
|
+
if (input.persistedRules !== undefined && toolName !== ASK_USER_QUESTION_TOOL_NAME) {
|
|
594
|
+
const editLane = await readPersistedRules({ toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
|
|
595
|
+
editRuleStore = editLane.unreadable === true ? "unavailable" : undefined;
|
|
596
|
+
const editTightened = applyPersistedTightening(recheck, editLane);
|
|
597
|
+
recheck = editTightened.decision;
|
|
598
|
+
if (editTightened.tightened === "deny")
|
|
599
|
+
pass.deniedBy = "persisted_rule";
|
|
600
|
+
if (recheck.action === "deny") {
|
|
601
|
+
editDenied = recheck;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
578
605
|
if (recheck.action === "allow") {
|
|
579
606
|
if (recheck.updatedInput !== undefined) {
|
|
580
607
|
editArgs = recheck.updatedInput;
|
|
@@ -587,8 +614,8 @@ export async function runGateLanes(pass) {
|
|
|
587
614
|
editRewrittenSinceHuman = true;
|
|
588
615
|
}
|
|
589
616
|
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 };
|
|
617
|
+
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org, editRuleStore)) };
|
|
618
|
+
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
592
619
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
593
620
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
594
621
|
if (rr.action !== "allow") {
|
|
@@ -606,7 +633,7 @@ export async function runGateLanes(pass) {
|
|
|
606
633
|
editRewrittenSinceHuman = false;
|
|
607
634
|
}
|
|
608
635
|
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
609
|
-
const editHit = input.writeProtectionCheck(toolName, editArgs);
|
|
636
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face);
|
|
610
637
|
if (editHit !== null) {
|
|
611
638
|
editDenied = {
|
|
612
639
|
action: "deny",
|
|
@@ -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.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.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. */
|
|
@@ -101,6 +101,9 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
101
101
|
"config.tool_model_gate_removed",
|
|
102
102
|
"config.tool_model_gate_unknown_class",
|
|
103
103
|
"config.tool_model_gate_env_invalid",
|
|
104
|
+
"config.tool_card_undeclared",
|
|
105
|
+
"config.tool_face_undeclared",
|
|
106
|
+
"config.tool_face_invalid",
|
|
104
107
|
"config.durable_gate_unavailable",
|
|
105
108
|
"config.peer_admission_out_of_range",
|
|
106
109
|
"config.peer_lane_unmounted",
|
|
@@ -164,6 +167,9 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
164
167
|
"config.tool_model_gate_removed": "operator",
|
|
165
168
|
"config.tool_model_gate_unknown_class": "operator",
|
|
166
169
|
"config.tool_model_gate_env_invalid": "operator",
|
|
170
|
+
"config.tool_card_undeclared": "operator",
|
|
171
|
+
"config.tool_face_undeclared": "operator",
|
|
172
|
+
"config.tool_face_invalid": "operator",
|
|
167
173
|
"config.peer_lane_unmounted": "operator",
|
|
168
174
|
"config.peer_admission_out_of_range": "operator",
|
|
169
175
|
"peer.inbound_disposition": "user",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
|
-
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
3
|
+
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import type { GateOutcome } from "./gate-outcome.js";
|
|
5
5
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandateOf } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
@@ -1022,22 +1022,27 @@ export interface PersistedRuleHitRule {
|
|
|
1022
1022
|
readonly dots?: readonly import("./permission-rule-model.js").RuleDot[];
|
|
1023
1023
|
}
|
|
1024
1024
|
/**
|
|
1025
|
-
* design/252 G-2, re-formed by design/375
|
|
1026
|
-
* gives:
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1025
|
+
* design/252 G-2, re-formed by design/375 and again by the three-behavior model (BREAKING) — the
|
|
1026
|
+
* structured answer a persisted-rule lane gives: WHICH behavior spoke for the call, and the rule set it
|
|
1027
|
+
* stands on. For an `allow` the set is the COVERAGE SET that admits the call — a single rule admitting
|
|
1028
|
+
* the whole command is a one-element set (the historical case); a compound admitted by the conjunction
|
|
1029
|
+
* of per-segment rules (design/375 §5.1) is the set of those rules, in segment order. For a `deny` or an
|
|
1030
|
+
* `ask` the set is the one rule of that behavior that reaches the call (the lane's precedence walk —
|
|
1031
|
+
* deny > ask > allow — already chose it). `rules` is non-empty by contract — an empty set covers nothing
|
|
1032
|
+
* and is not a hit (the normalizer reads it as a clean negative).
|
|
1030
1033
|
*
|
|
1031
|
-
* Why dots and not an id: a persisted
|
|
1034
|
+
* Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose
|
|
1032
1035
|
* identity is the set of add dots that redeemed it, and two concurrently approved records for one text
|
|
1033
1036
|
* each carry their own. The set is the identity; a joined string would not be one.
|
|
1034
1037
|
*
|
|
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.
|
|
1038
|
+
* The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are
|
|
1039
|
+
* NOT accepted: on this loosening seam an unrecognized answer degrades to a clean negative (more asks),
|
|
1040
|
+
* never to a match — the safe failure shape for a lane that missed the same-window upgrade.
|
|
1038
1041
|
*/
|
|
1039
1042
|
export interface PersistedRuleHit {
|
|
1040
|
-
/**
|
|
1043
|
+
/** What the deciding rule(s) say about the call — the closed set's word. */
|
|
1044
|
+
readonly behavior: import("./permission-rule-model.js").RuleBehavior;
|
|
1045
|
+
/** The rule set the verdict stands on. Non-empty. */
|
|
1041
1046
|
readonly rules: readonly PersistedRuleHitRule[];
|
|
1042
1047
|
}
|
|
1043
1048
|
/**
|
|
@@ -1065,8 +1070,9 @@ export interface PersistedRuleUnreadable {
|
|
|
1065
1070
|
export interface PersistedRuleCoverage {
|
|
1066
1071
|
readonly segmentCoverage: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
1067
1072
|
}
|
|
1068
|
-
/** Every shape a lane may answer with.
|
|
1069
|
-
|
|
1073
|
+
/** Every shape a lane may answer with. The retired bare-string form (a single allow rule's text) is read
|
|
1074
|
+
* by the normalizer as a clean negative, like every other unrecognized shape. */
|
|
1075
|
+
export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | PersistedRuleUnreadable | undefined;
|
|
1070
1076
|
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1071
1077
|
* signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
|
|
1072
1078
|
* additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
|
|
@@ -1115,6 +1121,12 @@ export interface ToolGateInput {
|
|
|
1115
1121
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
1116
1122
|
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
1117
1123
|
hookEnv?: HookEnvCapabilities;
|
|
1124
|
+
/**
|
|
1125
|
+
* design/388 B6 — the leg's live roster lookup: the face of the mounted tool a call dispatches to, stamped
|
|
1126
|
+
* on the {@link ToolCallRequest} every policy sees (read at the gate entry, so a run-time roster change is
|
|
1127
|
+
* honoured on the next call). Absent ⇒ requests carry no face (a bare producer).
|
|
1128
|
+
*/
|
|
1129
|
+
toolFaceOf?: (toolName: string) => ToolCallFace | undefined;
|
|
1118
1130
|
/**
|
|
1119
1131
|
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1120
1132
|
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
@@ -1332,7 +1344,7 @@ export interface ToolGateInput {
|
|
|
1332
1344
|
* (the write-protection tighten below). Absent ⇒ the deployment replaced the table with `[]` (or
|
|
1333
1345
|
* the caller runs the gate without one) and the decision path is byte-identical.
|
|
1334
1346
|
*/
|
|
1335
|
-
writeProtectionCheck?: (toolName: string, args: unknown) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1347
|
+
writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"]) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1336
1348
|
/**
|
|
1337
1349
|
* design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
1338
1350
|
* small-model policy CLASSIFIER before any human/durable resolution:
|
|
@@ -1398,15 +1410,27 @@ export interface ToolGateInput {
|
|
|
1398
1410
|
*/
|
|
1399
1411
|
isMarkedUnresolvable?: (toolCallId: string) => boolean;
|
|
1400
1412
|
/**
|
|
1401
|
-
* design/179 — the persisted
|
|
1402
|
-
*
|
|
1413
|
+
* design/179 — the persisted-rule lane. Present only when a deployment wired a rule store; absent it,
|
|
1414
|
+
* this whole step does not exist and the gate's decision path is byte-identical to what it was.
|
|
1403
1415
|
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
* the
|
|
1416
|
+
* The lane answers with the BEHAVIOR that spoke ({@link PersistedRuleHit}), and the gate consumes the
|
|
1417
|
+
* three behaviors at two different places — the upstream arbiter's order, deny > ask > allow, across
|
|
1418
|
+
* the person's own standing rules:
|
|
1419
|
+
* · a DENY row refuses the call outright, on EVERY decision (an allow from the policy fold included):
|
|
1420
|
+
* it is the person's own "never run this" (their settings' deny list, imported), the personal-store
|
|
1421
|
+
* sibling of an org deny, and it runs right after the org layer — nothing downstream re-opens it;
|
|
1422
|
+
* · an ASK row is the person's standing "ask me each time": it turns a surviving allow into an ask
|
|
1423
|
+
* and stamps `matchedAskRule` on it (the same marker an explicit `ask` permission rule stamps), so
|
|
1424
|
+
* the ask's origin is `ask_rule` — the auto-mode classifier never answers it and a persisted ALLOW
|
|
1425
|
+
* never clears it; on a call that is already an ask it stamps the marker and leaves the rest;
|
|
1426
|
+
* · an ALLOW row is the standing form of ONE recorded human approval, so it resolves the asks a real
|
|
1427
|
+
* person saying "allow" would have resolved — and nothing else. It sits post-fold, at the entry of
|
|
1428
|
+
* the `ask` branch, BEFORE the classifier and AFTER every tighten: earlier and a tighten would
|
|
1429
|
+
* simply put the ask back; later and the classifier would already have spoken.
|
|
1408
1430
|
*
|
|
1409
|
-
*
|
|
1431
|
+
* The deny/ask arms tighten and take no conjunct but the two structural ones every lane shares (the
|
|
1432
|
+
* engine's own content-ask tool is exempt; the lane must be wired). The ALLOW arm's consumption
|
|
1433
|
+
* predicate is its own, not the classifier's blanket flip:
|
|
1410
1434
|
* · `requiresRealApproval` asks are never consumed — the two integrity gates are the floor, and the
|
|
1411
1435
|
* same flag is what a v2 organization-level ask will carry;
|
|
1412
1436
|
* · a PreToolUse hook's ask is never consumed — that is the deployment's own adjudication, and a rule
|
|
@@ -1435,19 +1459,26 @@ export interface ToolGateInput {
|
|
|
1435
1459
|
* bullets above are what keep integrity/hook/mandated asks out of that set.
|
|
1436
1460
|
*/
|
|
1437
1461
|
persistedRules?: {
|
|
1438
|
-
/** The
|
|
1439
|
-
*
|
|
1462
|
+
/** The lane's verdict for this call — a {@link PersistedRuleHit} naming the behavior that spoke and
|
|
1463
|
+
* the rule set it stands on, a coverage-carrying clean negative, an unreadable-store report, or
|
|
1464
|
+
* `undefined` (the store was READ and no rule speaks). Must not throw — and a store that cannot be
|
|
1465
|
+
* read answers `{ unreadable: true }`, NEVER `undefined`: the gate reads the unreadable report as
|
|
1466
|
+
* fail-closed (an allow tightens to a real-approval ask of origin `rule_store_unavailable`), while
|
|
1467
|
+
* `undefined` is a successful no-match that leaves an allow standing. An adapter that answers
|
|
1468
|
+
* `undefined` on a read failure silently disarms every deny/ask row it holds.
|
|
1440
1469
|
*
|
|
1441
|
-
*
|
|
1442
|
-
* single
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
|
|
1470
|
+
* For an ALLOW the set is the COVERAGE SET — each member's canonical text plus its add DOTS. A
|
|
1471
|
+
* single rule admitting the whole command is a one-element set; a compound admitted by the
|
|
1472
|
+
* per-segment conjunction (design/375 §5.1) is the set of the covering rules. The dots are a rule's
|
|
1473
|
+
* durable identity, and the text is not one (it reaches a card sanitized and capped); a lane that
|
|
1474
|
+
* can name them lets the shadowed-ask evidence say WHICH stored rules matched rather than merely
|
|
1475
|
+
* what they looked like. The retired shapes — a bare rule TEXT, the pre-375 `{ rule, dots? }` object,
|
|
1476
|
+
* a `{ rules }` hit without its behavior — are read as a clean negative by the normalizer: a stale
|
|
1477
|
+
* lane degrades toward asking, never toward an allow. Consulted on EVERY decision (a deny/ask row
|
|
1478
|
+
* must be able to tighten an allow), so the lane pays one store read per call on the tools it
|
|
1479
|
+
* speaks for; the runner's lane hands the org layer's read to it, so a governed deployment still
|
|
1480
|
+
* pays one. */
|
|
1481
|
+
adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
|
|
1451
1482
|
/** Observation sink for an ask a rule coverage resolved — the allow-side disclosure. `rules` is
|
|
1452
1483
|
* the coverage set's canonical texts in reporting order (design/375: a conjunction names every
|
|
1453
1484
|
* rule the decision stands on). Never affects the outcome. */
|
package/dist/core/hooks.js
CHANGED
|
@@ -301,7 +301,7 @@ export async function runToolGate(input) {
|
|
|
301
301
|
screening,
|
|
302
302
|
afterFold: runGateLanes,
|
|
303
303
|
currentInput: modelInput,
|
|
304
|
-
req: { toolName, args: modelInput, toolCallId },
|
|
304
|
+
req: { toolName, args: modelInput, toolCallId, ...(() => { const f = input.toolFaceOf?.(toolName); return f !== undefined ? { face: f } : {}; })() },
|
|
305
305
|
preToolContext: [],
|
|
306
306
|
hookAsk: undefined,
|
|
307
307
|
parkFailed: undefined,
|
package/dist/core/lsp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { uriToPath } from "./lsp-protocol.js";
|
|
|
2
2
|
import { foldRedundantPathSegments, isAbsolutePathForm } from "../tools/fs/safety.js";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { defineTool, errorResult } from "./tools.js";
|
|
5
|
+
import { toolFace } from "./tool-catalog-entries.js";
|
|
5
6
|
export const LSP_OPERATIONS = [
|
|
6
7
|
"goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol",
|
|
7
8
|
"goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
|
|
@@ -145,7 +146,7 @@ const POSITION_OPS = new Set([
|
|
|
145
146
|
export function createLspTool(manager, opts = {}) {
|
|
146
147
|
return defineTool({
|
|
147
148
|
name: "LSP",
|
|
148
|
-
|
|
149
|
+
...toolFace("lsp"),
|
|
149
150
|
description: "Interact with Language Server Protocol (LSP) servers to get code intelligence features.\n" +
|
|
150
151
|
"\n" +
|
|
151
152
|
"Supported operations:\n" +
|
|
@@ -172,7 +173,6 @@ export function createLspTool(manager, opts = {}) {
|
|
|
172
173
|
character: Type.Optional(Type.Number({ description: "The character offset (1-based, as shown in editors)." })),
|
|
173
174
|
query: Type.Optional(Type.String({ description: "Symbol query for workspaceSymbol (may be empty to browse broadly)." })),
|
|
174
175
|
}),
|
|
175
|
-
effect: "read",
|
|
176
176
|
execute: async (args, ctx) => {
|
|
177
177
|
const { operation, filePath, line, character, query } = args;
|
|
178
178
|
if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* a longer name. Layer 0 vocabulary; `types.ts` re-exports every name below.
|
|
7
7
|
*/
|
|
8
8
|
import type { ToolContentOrigin, ToolEffect } from "./tool-spec.js";
|
|
9
|
+
import type { ToolFamily, ToolPathTarget, ToolRenderHints, ToolRuleFace } from "./tool-face.js";
|
|
9
10
|
/** Definition of an MCP server to connect to for the duration of one task, then discard. */
|
|
10
11
|
export interface McpServerSpec {
|
|
11
12
|
/** Stable name; tool names are namespaced as `<name>__<tool>` to avoid collisions. */
|
|
@@ -71,6 +72,22 @@ export interface McpServerSpec {
|
|
|
71
72
|
egress?: boolean;
|
|
72
73
|
irreversibility?: "always" | "never";
|
|
73
74
|
}>;
|
|
75
|
+
/**
|
|
76
|
+
* design/388 §1.1 / §2.7 — the OPTIONAL per-tool FACE overlay for this server's tools (keyed by the server's
|
|
77
|
+
* RAW tool name, like {@link allowTools} / {@link toolAxes}): a deployment lets an MCP tool enter a specialised
|
|
78
|
+
* approval card, be confirmed by path, carry a display name / summary, or narrow its rule vocabulary. TRUSTED
|
|
79
|
+
* (the deployment writes it), but validated against the MATERIALIZED schema at every listing (first mount and
|
|
80
|
+
* every refresh): a `pathTarget.param` that is not a top-level property, an `approvalCard` whose keys the schema
|
|
81
|
+
* lacks, a `ruleFace` naming a non-scalar key ⇒ the tool mounts WITHOUT the overlay and the leg announces
|
|
82
|
+
* `config.tool_face_invalid` once; a key naming a tool the listing does not carry announces
|
|
83
|
+
* `config.tool_face_undeclared` once. Applied whole or not at all.
|
|
84
|
+
*/
|
|
85
|
+
toolFaces?: Record<string, {
|
|
86
|
+
family?: ToolFamily;
|
|
87
|
+
pathTarget?: ToolPathTarget;
|
|
88
|
+
renderHints?: ToolRenderHints;
|
|
89
|
+
ruleFace?: ToolRuleFace;
|
|
90
|
+
}>;
|
|
74
91
|
/**
|
|
75
92
|
* design/378 — declare {@link ToolSpec.contentOrigin} on behalf of THIS SERVER'S ENTIRE TOOL SET
|
|
76
93
|
* (tools a mid-task refresh adds included), with the same authority and the same responsibility a
|
package/dist/core/mcp.js
CHANGED
|
@@ -18,6 +18,7 @@ import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disc
|
|
|
18
18
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
19
19
|
import { snapshotMcpEntries } from "./locked-config.js";
|
|
20
20
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
21
|
+
import { schemaKeyBoundProblem } from "./tool-roster.js";
|
|
21
22
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
22
23
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
23
24
|
const MCP_CHARS_PER_TOKEN = 4;
|
|
@@ -1513,7 +1514,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1513
1514
|
effectiveInputSchema = normalized.schema;
|
|
1514
1515
|
effectiveDescription = normalized.note + (effectiveDescription ? `\n\n${effectiveDescription}` : "");
|
|
1515
1516
|
}
|
|
1516
|
-
const schemaProblem = mcpToolSchemaProblem(effectiveInputSchema);
|
|
1517
|
+
const schemaProblem = mcpToolSchemaProblem(effectiveInputSchema) ?? schemaKeyBoundProblem(effectiveInputSchema);
|
|
1517
1518
|
if (schemaProblem !== undefined) {
|
|
1518
1519
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaProblem, 240) });
|
|
1519
1520
|
continue;
|
|
@@ -1551,6 +1552,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1551
1552
|
parameters: (effectiveInputSchema ?? { type: "object" }),
|
|
1552
1553
|
...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
|
|
1553
1554
|
...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
|
|
1555
|
+
mcpRemoteName: remoteName,
|
|
1554
1556
|
execute: async (_toolCallId, params, signal) => {
|
|
1555
1557
|
const what = `The call to tool ${inlineUntrusted(remoteName)}`;
|
|
1556
1558
|
if (isServerRevoked?.(spec.name) === true) {
|