@sema-agent/core 7.39.2 → 7.40.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 +60 -0
- package/dist/core/a2a.js +1 -0
- package/dist/core/ask-origin.d.ts +19 -6
- package/dist/core/ask-origin.js +6 -0
- package/dist/core/gate-fold.js +5 -0
- package/dist/core/gate-lanes.js +8 -4
- package/dist/core/hands-band.d.ts +0 -12
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/hooks.js +4 -1
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +17 -2
- package/dist/core/mode-policy.js +13 -6
- package/dist/core/permission-rule-model.d.ts +29 -0
- package/dist/core/permission-rule-model.js +15 -2
- package/dist/core/permission-rules.d.ts +2 -10
- package/dist/core/permission-rules.js +22 -19
- package/dist/core/runner/denial-limit-arms.js +1 -1
- package/dist/core/runner/gate-exit.d.ts +5 -1
- package/dist/core/runner/permission-rule-lanes.js +7 -5
- package/dist/core/runner/persisted-path-rows.js +3 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
- package/dist/core/runner/prepare-gate-stations.js +2 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +1 -1
- package/dist/core/runner/prepare-hands-readface.js +0 -1
- package/dist/core/runner/prepare-policy-chain.js +11 -7
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +10 -5
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/tool-policy.d.ts +14 -0
- package/dist/core/tool-policy.js +15 -6
- package/dist/core/tool-spec.d.ts +10 -0
- package/dist/core/tools.js +1 -0
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/orchestration/workflow.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +3 -5
- package/dist/tools/fs/fs-shared.d.ts +0 -27
- package/dist/tools/fs/fs-shared.js +0 -3
- package/dist/tools/fs/index.d.ts +1 -9
- package/dist/tools/fs/index.js +1 -1
- package/package.json +1 -1
- package/wire-changes/core@7.40.0.json +376 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
3
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
4
|
-
import { PATH_RULE_BASE_LABEL,
|
|
4
|
+
import { PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
5
|
import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
|
|
6
6
|
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
7
7
|
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
@@ -82,7 +82,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
|
|
|
82
82
|
const face = catalogRuleFaceOf(canonicalTool);
|
|
83
83
|
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
|
|
84
84
|
};
|
|
85
|
-
export const PATH_DENY_RULE_FACES = Object.freeze({ [READ_RULE_TOOL]: ["read", "write"], Edit: ["write"] });
|
|
86
85
|
export function pathRuleDenyRowOf(rule, bases) {
|
|
87
86
|
const family = pathFamilyOf(bases);
|
|
88
87
|
const tree = bases.root ?? bases.cwd;
|
|
@@ -99,10 +98,6 @@ export function pathRuleDenyRowOf(rule, bases) {
|
|
|
99
98
|
...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
|
|
100
99
|
};
|
|
101
100
|
}
|
|
102
|
-
function callPathFaceOf(req) {
|
|
103
|
-
const target = protectivePathTargetOf(req);
|
|
104
|
-
return target === undefined ? undefined : target.access === "read" ? "read" : "write";
|
|
105
|
-
}
|
|
106
101
|
function countStars(s) {
|
|
107
102
|
let n = 0;
|
|
108
103
|
for (const c of s)
|
|
@@ -218,7 +213,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
218
213
|
}
|
|
219
214
|
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
220
215
|
continue;
|
|
221
|
-
const faces = r.behavior
|
|
216
|
+
const faces = PATH_RULE_FACES[toolName]?.[r.behavior];
|
|
222
217
|
entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
|
|
223
218
|
byTool.set(toolName, entry);
|
|
224
219
|
continue;
|
|
@@ -340,8 +335,15 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
340
335
|
}
|
|
341
336
|
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
|
|
342
337
|
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
|
|
343
|
-
const
|
|
344
|
-
|
|
338
|
+
const faceRules = {
|
|
339
|
+
deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
|
|
340
|
+
ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
|
|
341
|
+
};
|
|
342
|
+
const pathDenyRules = faceRules.deny.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
|
|
343
|
+
const contentLaneOf = (behavior, entry, callFace) => [
|
|
344
|
+
...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
|
|
345
|
+
...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
|
|
346
|
+
];
|
|
345
347
|
const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
|
|
346
348
|
const contentHit = (lane, req) => {
|
|
347
349
|
if (lane.length === 0)
|
|
@@ -410,8 +412,8 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
410
412
|
if (coveringDeny)
|
|
411
413
|
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
|
|
412
414
|
}
|
|
413
|
-
const callFace =
|
|
414
|
-
const denyLane =
|
|
415
|
+
const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
|
|
416
|
+
const denyLane = contentLaneOf("deny", entry, callFace);
|
|
415
417
|
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
|
|
416
418
|
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
417
419
|
return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
@@ -434,23 +436,24 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
434
436
|
if (coveringAsk)
|
|
435
437
|
return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
|
|
436
438
|
}
|
|
439
|
+
const askLane = contentLaneOf("ask", entry, callFace);
|
|
440
|
+
const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
|
|
441
|
+
if (contentAsk !== undefined) {
|
|
442
|
+
if (contentAsk.unreadable !== undefined)
|
|
443
|
+
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
|
|
444
|
+
return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
|
|
445
|
+
}
|
|
437
446
|
if (entry) {
|
|
438
|
-
const contentAsk = contentHit(entry.content.ask, req);
|
|
439
|
-
if (contentAsk !== undefined) {
|
|
440
|
-
if (contentAsk.unreadable !== undefined)
|
|
441
|
-
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
|
|
442
|
-
return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
|
|
443
|
-
}
|
|
444
447
|
const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
|
|
445
448
|
if (paramAsk) {
|
|
446
449
|
return { action: "ask", message: ruleAskMessage(paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
|
|
447
450
|
}
|
|
448
451
|
if (entry.bare.allow) {
|
|
449
|
-
return { action: "allow" };
|
|
452
|
+
return { action: "allow", decisionReason: "rule" };
|
|
450
453
|
}
|
|
451
454
|
}
|
|
452
455
|
if (covering && coveringHit(namespacedCovering.allow, req.toolName)) {
|
|
453
|
-
return { action: "allow" };
|
|
456
|
+
return { action: "allow", decisionReason: "rule" };
|
|
454
457
|
}
|
|
455
458
|
if (defaultAction === "allow")
|
|
456
459
|
return { action: "allow" };
|
|
@@ -103,7 +103,7 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
103
103
|
const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d);
|
|
104
104
|
if (verdict.kind === "allow") {
|
|
105
105
|
autoMode.denialTracking?.recordAllow();
|
|
106
|
-
return { kind: "allow", result: attributed({ action: "allow", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
|
|
106
|
+
return { kind: "allow", result: attributed({ action: "allow", decisionReason: "classifier", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
|
|
107
107
|
}
|
|
108
108
|
if (verdict.kind === "unavailable") {
|
|
109
109
|
const arm = classifierUnavailableArm({ ask, req, cause: verdict.cause, seat: opts.unavailableSeat, aborted: opts.signal.aborted });
|
|
@@ -214,6 +214,10 @@ export interface GatePass {
|
|
|
214
214
|
hookAsk: Extract<PermissionResult, {
|
|
215
215
|
action: "ask";
|
|
216
216
|
}> | undefined;
|
|
217
|
+
/** borrowed-mutable — the phase-1 hook answered its own `allow` (the exact word): a statement about this call, read by
|
|
218
|
+
* the fold's last station only while the policy left the arguments as the hook saw them. Writer: the dispatch (phase 1).
|
|
219
|
+
* Reader: the fold's unstated station. */
|
|
220
|
+
hookAllowed?: true;
|
|
217
221
|
/** borrowed-mutable — set by a park attempt that FAILED (see `ParkAttemptFailed`). Read at the single deny
|
|
218
222
|
* exit — whatever the fallback chain then decides to say, it says it knowing the park was tried first.
|
|
219
223
|
* Writers: the three park sites. */
|
|
@@ -256,7 +260,7 @@ export interface GatePass {
|
|
|
256
260
|
* policy's answer; then rewritten by every station that changes the verdict (fold, tightens, org, rule
|
|
257
261
|
* lane, stamp, classifier, sandbox, content lane, resolution, edit chain). */
|
|
258
262
|
decision: PermissionResult;
|
|
259
|
-
/** borrowed-mutable — which of the gate's OWN post-fold tightens minted the standing ask, if one did — an
|
|
263
|
+
/** borrowed-mutable — which of the gate's OWN post-fold stations (the tightens and the unstated station) minted the standing ask, if one did — an
|
|
260
264
|
* input of the ask-origin derivation (`AskOriginFacts.tightened`). Written at most once per pass: every
|
|
261
265
|
* tighten fires only over a surviving `allow`, and the first one to fire leaves an `ask` the others do not
|
|
262
266
|
* touch. Recorded by the gate rather than read off `decisionReason` because that member is a policy's to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
|
|
2
2
|
import {} from "../hooks.js";
|
|
3
|
-
import { adjudicatePersistedPathRules, adjudicatePersistedRules, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
3
|
+
import { adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
4
4
|
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
5
5
|
import { effectivePathTargetOf } from "../effective-path-target.js";
|
|
6
6
|
import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
|
|
@@ -45,7 +45,7 @@ export function pathRuleLaneAnswer(table, req, ctx) {
|
|
|
45
45
|
const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
|
|
46
46
|
if (tighten === undefined && allow === undefined)
|
|
47
47
|
return undefined;
|
|
48
|
-
return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
|
|
48
|
+
return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
|
|
49
49
|
}
|
|
50
50
|
export function orgRuleLane(cfg) {
|
|
51
51
|
return {
|
|
@@ -127,8 +127,10 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
127
127
|
const personalAdjudicate = async (req) => {
|
|
128
128
|
if (anonymous && !cfg.localOwnerDeclared)
|
|
129
129
|
return undefined;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
130
|
+
const judged = laneToolNamesOf(req).map((n) => [n, ruleToolGrammarOf(n)]);
|
|
131
|
+
if (!judged.some(([, g]) => g === "path") && faceAskSpeaksFor(callPathFaceOf(req)))
|
|
132
|
+
judged.push([req.toolName, "path"]);
|
|
133
|
+
if (judged.length === 0)
|
|
132
134
|
return undefined;
|
|
133
135
|
let view;
|
|
134
136
|
try {
|
|
@@ -140,7 +142,7 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
140
142
|
if (view.unreadable !== undefined)
|
|
141
143
|
skipped(view.unreadable, view.unreadableSources);
|
|
142
144
|
try {
|
|
143
|
-
const answer = strongestAnswer(
|
|
145
|
+
const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules, { ...req, toolName: n }, grammar)));
|
|
144
146
|
return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
|
|
145
147
|
}
|
|
146
148
|
catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ownerStoreOf } from "../permission-rule-provider.js";
|
|
2
|
-
import { READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
|
|
2
|
+
import { PATH_RULE_FACES, READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
|
|
3
3
|
import { isAbsolutePathForm, pathFamilyOf } from "../../tools/fs/safety.js";
|
|
4
|
-
import {
|
|
4
|
+
import { pathRuleDenyRowOf } from "../permission-rules.js";
|
|
5
5
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
6
|
import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
|
|
7
7
|
import { thrownText } from "../thrown-value.js";
|
|
@@ -34,7 +34,7 @@ export async function persistedPathRowsOf(cfg) {
|
|
|
34
34
|
readDirectories.push(rule.command);
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
|
-
const faces =
|
|
37
|
+
const faces = PATH_RULE_FACES[rule.tool]?.deny;
|
|
38
38
|
if (rule.behavior !== "deny" || faces === undefined || (rule.match !== "path" && rule.match !== "subpath") || !eligiblePersisted(rule, { ...call, tool: rule.tool }, "deny"))
|
|
39
39
|
continue;
|
|
40
40
|
const missing = ruleBasesNeeded(rule).filter((b) => !isUsablePathBase(b === "cwd" ? bases.root : bases[b], family));
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import type { AgentHarness, AgentTool } from "../../internal/harness.js";
|
|
26
26
|
import type { AskClass } from "../ask-class.js";
|
|
27
27
|
import type { Arbitration, ToolMarkSource } from "../ask-origin.js";
|
|
28
|
-
import type
|
|
28
|
+
import { type PermissionModeController } from "../mode-policy.js";
|
|
29
29
|
import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
|
|
30
30
|
import { type ComplianceCapability } from "../compliance.js";
|
|
31
31
|
import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
|
|
2
|
+
import { mountedToolPermissionSeat } from "../mode-policy.js";
|
|
2
3
|
import { complianceCallDenial } from "../compliance.js";
|
|
3
4
|
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, runHookSeat, runToolGate } from "../hooks.js";
|
|
4
5
|
import { screenGateOutcome } from "../gate-outcome.js";
|
|
@@ -137,6 +138,7 @@ export function prepareGateStations(input) {
|
|
|
137
138
|
...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
|
|
138
139
|
permissionMode: () => permissionMode.current,
|
|
139
140
|
...(markSources.size > 0 ? { toolMarkSources: markSources } : {}),
|
|
141
|
+
...(mountedToolPermissionSeat(tools, e.toolName) === "passthrough" ? { permissionSeat: "passthrough" } : {}),
|
|
140
142
|
...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
|
|
141
143
|
onModeReleased,
|
|
142
144
|
ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
|
|
@@ -149,7 +149,7 @@ export interface PrepareHandsReadFaceInput {
|
|
|
149
149
|
reversibilityProbes: ReversibilityProbes;
|
|
150
150
|
/** borrowed-readonly — the run's reminder provenance mark (minted/adopted
|
|
151
151
|
* by the driver's adoption ladder); threaded into the hands band so the fs-side engine mints
|
|
152
|
-
* (the Read
|
|
152
|
+
* (the Read dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
|
|
153
153
|
* system-prompt declaration names. */
|
|
154
154
|
reminderMark: string;
|
|
155
155
|
/** borrowed-mutable — the leg's disclosure trigger counters; the hands
|
|
@@ -366,7 +366,6 @@ export async function prepareHandsMount(input) {
|
|
|
366
366
|
...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
|
|
367
367
|
...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
|
|
368
368
|
...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
|
|
369
|
-
...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
|
|
370
369
|
beforeWrite: async (w) => {
|
|
371
370
|
const deploymentGate = deps.hands?.beforeWrite;
|
|
372
371
|
if (deploymentGate) {
|
|
@@ -364,7 +364,8 @@ export async function preparePolicyChain(input) {
|
|
|
364
364
|
if (re.action === "deny")
|
|
365
365
|
return vetoed(re);
|
|
366
366
|
if (re.action === "allow") {
|
|
367
|
-
const
|
|
367
|
+
const stated = re.updatedInput === undefined ? "mode" : re.decisionReason;
|
|
368
|
+
const out = { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs, ...(stated !== undefined ? { decisionReason: stated } : {}) };
|
|
368
369
|
carryEngineAttestations(re, out);
|
|
369
370
|
carryPrecedingSettlement(settledBy, out);
|
|
370
371
|
return out;
|
|
@@ -384,7 +385,7 @@ export async function preparePolicyChain(input) {
|
|
|
384
385
|
re = editAsk;
|
|
385
386
|
const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged);
|
|
386
387
|
if (editArbitrated !== undefined) {
|
|
387
|
-
const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
|
|
388
|
+
const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs, decisionReason: "mode" } : editArbitrated;
|
|
388
389
|
carryPrecedingSettlement(settledBy, out);
|
|
389
390
|
return out;
|
|
390
391
|
}
|
|
@@ -408,7 +409,7 @@ export async function preparePolicyChain(input) {
|
|
|
408
409
|
return rr;
|
|
409
410
|
settledBy = rr;
|
|
410
411
|
if (rr.updatedInput === undefined) {
|
|
411
|
-
const out = { action: "allow", updatedInput: editArgs };
|
|
412
|
+
const out = { action: "allow", updatedInput: editArgs, decisionReason: "mode" };
|
|
412
413
|
carryPrecedingSettlement(rr, out);
|
|
413
414
|
return out;
|
|
414
415
|
}
|
|
@@ -461,7 +462,7 @@ export async function preparePolicyChain(input) {
|
|
|
461
462
|
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
462
463
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
|
|
463
464
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
464
|
-
return { action: "allow" };
|
|
465
|
+
return { action: "allow", decisionReason: "sandbox" };
|
|
465
466
|
}
|
|
466
467
|
const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged);
|
|
467
468
|
if (arbitrated !== undefined)
|
|
@@ -496,7 +497,7 @@ export async function preparePolicyChain(input) {
|
|
|
496
497
|
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
|
|
497
498
|
}
|
|
498
499
|
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
|
|
499
|
-
return { action: "allow" };
|
|
500
|
+
return { action: "allow", decisionReason: "mode" };
|
|
500
501
|
},
|
|
501
502
|
}
|
|
502
503
|
: wrapToolPolicy(pc.policy, async (creq, csignal) => {
|
|
@@ -535,7 +536,7 @@ export async function preparePolicyChain(input) {
|
|
|
535
536
|
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
536
537
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
|
|
537
538
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
538
|
-
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
539
|
+
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput, decisionReason: "sandbox" } : { action: "allow", decisionReason: "sandbox" };
|
|
539
540
|
}
|
|
540
541
|
const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged);
|
|
541
542
|
if (arbitrated !== undefined)
|
|
@@ -635,6 +636,7 @@ export async function preparePolicyChain(input) {
|
|
|
635
636
|
const finalReq = { ...req, args: decision.updatedInput };
|
|
636
637
|
let asked;
|
|
637
638
|
const revalidated = {};
|
|
639
|
+
let restated;
|
|
638
640
|
try {
|
|
639
641
|
for (const layer of recheckLayers) {
|
|
640
642
|
const d = refuseOutOfContractDecision(await layer.check(finalReq, signal));
|
|
@@ -653,6 +655,8 @@ export async function preparePolicyChain(input) {
|
|
|
653
655
|
}
|
|
654
656
|
if (d.action === "ask" && asked === undefined)
|
|
655
657
|
asked = d;
|
|
658
|
+
if (d.action === "allow" && restated === undefined && d.decisionReason !== undefined)
|
|
659
|
+
restated = d.decisionReason;
|
|
656
660
|
carryEngineAttestations(d, revalidated);
|
|
657
661
|
}
|
|
658
662
|
}
|
|
@@ -665,7 +669,7 @@ export async function preparePolicyChain(input) {
|
|
|
665
669
|
return surfaced;
|
|
666
670
|
}
|
|
667
671
|
if (decision.action === "allow") {
|
|
668
|
-
const owned = { ...decision };
|
|
672
|
+
const owned = { ...decision, ...(decision.decisionReason === undefined && restated !== undefined ? { decisionReason: restated } : {}) };
|
|
669
673
|
carryEngineAttestations(decision, owned);
|
|
670
674
|
carryEngineAttestations(revalidated, owned);
|
|
671
675
|
return owned;
|
|
@@ -33,7 +33,7 @@ import { type LegWriteProtection } from "./write-protection-station.js";
|
|
|
33
33
|
import { type UsageWindow } from "../usage-window-store.js";
|
|
34
34
|
import type { ToolRoster } from "../tool-roster.js";
|
|
35
35
|
import { type AutoModeArmFact, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
|
|
36
|
-
import type
|
|
36
|
+
import { type PermissionModeController } from "../mode-policy.js";
|
|
37
37
|
import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, UsageGovernance } from "./contracts.js";
|
|
38
38
|
export interface PrepareWiringManifestInput {
|
|
39
39
|
/** borrowed-readonly — the REBOUND spec. Read: `onAsk` (the own seat), `onQuestion` (the question seam), `handsReadOnly` / `tools` (the ungated scan),
|
|
@@ -12,6 +12,7 @@ import { mountWriteProtection } from "./write-protection-station.js";
|
|
|
12
12
|
import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
|
|
13
13
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
14
14
|
import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
15
|
+
import { modePolicyOf } from "../mode-policy.js";
|
|
15
16
|
import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
|
|
16
17
|
export function mcpManifestEntries(entries, statuses) {
|
|
17
18
|
return (entries ?? []).map((entry) => {
|
|
@@ -120,15 +121,18 @@ export function prepareWiringManifest(input) {
|
|
|
120
121
|
const onAsk = spec.onAsk ?? deps.onAsk;
|
|
121
122
|
const handWriteTools = toolRoster.entries.filter((e) => e.mountedBy === "hands" && (e.effect === "write" || e.effect === "idempotent")).map((e) => e.name);
|
|
122
123
|
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
|
|
123
|
-
const
|
|
124
|
+
const peerWriteUngated = modePolicyOf(permissionMode.current).unstated === "ask"
|
|
125
|
+
? []
|
|
126
|
+
: mcp.tools.filter((t) => t.permissionSeat === "passthrough" && !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read").map((t) => t.name);
|
|
124
127
|
const firstPartyWriteUngated = (spec.tools ?? [])
|
|
125
128
|
.map((t) => t.name)
|
|
126
129
|
.filter((name) => !irreversibleTools.has(name) && !egressTools.has(name) && toolEffects.get(name) === "write");
|
|
127
|
-
|
|
130
|
+
const handOrFirstPartyUngated = !hasEffectAwareGate && (handWriteTools.length > 0 || firstPartyWriteUngated.length > 0);
|
|
131
|
+
if (handOrFirstPartyUngated || peerWriteUngated.length > 0) {
|
|
128
132
|
const what = [
|
|
129
|
-
handWriteTools.length > 0 ? `hand tools [${handWriteTools.join(", ")}]` : undefined,
|
|
130
|
-
|
|
131
|
-
firstPartyWriteUngated.length > 0 ? `write tools [${firstPartyWriteUngated.join(", ")}]` : undefined,
|
|
133
|
+
!hasEffectAwareGate && handWriteTools.length > 0 ? `hand tools [${handWriteTools.join(", ")}]` : undefined,
|
|
134
|
+
peerWriteUngated.length > 0 ? `server write tools under ${permissionMode.current} [${peerWriteUngated.join(", ")}]` : undefined,
|
|
135
|
+
!hasEffectAwareGate && firstPartyWriteUngated.length > 0 ? `write tools [${firstPartyWriteUngated.join(", ")}]` : undefined,
|
|
132
136
|
]
|
|
133
137
|
.filter(Boolean)
|
|
134
138
|
.join(" + ");
|
|
@@ -141,6 +145,7 @@ export function prepareWiringManifest(input) {
|
|
|
141
145
|
shapes.add(what);
|
|
142
146
|
deps.onError?.(new Error(`write-capable ${what} are present but UNGATED — wire an effect-aware tool policy (or a PreToolUse hook). ` +
|
|
143
147
|
`bash/MCP irreversible actions are otherwise unadjudicated; the deployment sandbox/egress boundary is the only protection (design/77 §4, design/53 §2.H). ` +
|
|
148
|
+
(peerWriteUngated.length > 0 ? `Under ${permissionMode.current} a server's tools run with nobody stating them; a deny or ask rule naming them still gates them. ` : "") +
|
|
144
149
|
`(This warning fires once per deployment per tool-set shape.)`), { phase: "config", sessionId });
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -3,6 +3,7 @@ const RETIRED_RUNNER_DEPS_KEYS = {
|
|
|
3
3
|
legacyRewindBoundaryProbe: "the rewind refusal always names both possibilities (never existed / retired whole-tree era); a deployment that holds that era's table answers the question on its own surface",
|
|
4
4
|
peerAdmission: "the peer lane's knob table is `RunnerDeps.peerLane` (the same fields: admission gate, delivery budgets, listing window)",
|
|
5
5
|
graderEnvFactory: "the grader env reaches the repair loop as `RepairLoopConfig.graderEnv`, minted by the trusted control plane (`assertOracleIsolation` / `GraderEnv` unchanged)",
|
|
6
|
+
"hands.readCyberReminder": "a Read result carries only the reminders about that read (an empty file, an offset past the end, a file already in context) — no standing content-safety tail, so there is nothing to switch; a deployment that wants that sentence states it in its own `appendSystemPrompt`",
|
|
6
7
|
};
|
|
7
8
|
export function assertNoRetiredRunnerDeps(deps) {
|
|
8
9
|
assertNoRetiredKeys(deps, RETIRED_RUNNER_DEPS_KEYS, "RunnerDeps");
|
|
@@ -850,6 +850,13 @@ export declare function createAllowDenyPolicy(opts: {
|
|
|
850
850
|
list: "allow" | "deny";
|
|
851
851
|
message: string;
|
|
852
852
|
}) => void;
|
|
853
|
+
/**
|
|
854
|
+
* What an `allow` list HIT means. `"grant"` (default): the deployment named this tool as permitted — a statement
|
|
855
|
+
* (`decisionReason: "rule"`) that satisfies the fold's unstated rule for a passthrough-seat tool. `"narrowing"`: the
|
|
856
|
+
* list only bounds WHICH tools a lane may see (an agent definition's `allowTools`); a hit is scope, not consent, and
|
|
857
|
+
* answers a bare allow — the mode's unstated question still goes to a person. The deny list is unaffected.
|
|
858
|
+
*/
|
|
859
|
+
scope?: "grant" | "narrowing";
|
|
853
860
|
}): NamedToolPolicy;
|
|
854
861
|
/**
|
|
855
862
|
* What `createApprovalPolicy`'s `approve` may resolve to: a boolean, or the `AskOutcome` OBJECT minus the
|
|
@@ -918,6 +925,13 @@ export declare function createApprovalPolicy(opts: {
|
|
|
918
925
|
* short-circuit — scanning continues, because a later policy may still `deny` (which outranks ask).
|
|
919
926
|
* With no deny, the first `ask` wins; with neither, `allow`.
|
|
920
927
|
*
|
|
928
|
+
* **An allow is STATED or not.** A layer that names the call (an allow rule, a list entry, an approver's yes, a mode's
|
|
929
|
+
* release) says WHY on its allow (`decisionReason`); a bare allow is "no objection" — an empty table, a list the call is
|
|
930
|
+
* not on, a layer that does not know the tool. The combined allow carries the FIRST stated reason in layer order, and a
|
|
931
|
+
* statement binds the arguments its layer judged: a layer that rewrites the arguments voids every statement before it (its
|
|
932
|
+
* own allow, if it states one, speaks for the new arguments). For a tool whose permission seat is `passthrough` an
|
|
933
|
+
* unstated allow is not a permission — the gate puts the mode's question to it; for every other tool the two forms run alike.
|
|
934
|
+
*
|
|
921
935
|
* **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a
|
|
922
936
|
* policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be
|
|
923
937
|
* idempotent — repeated calls with the same {@link ToolCallRequest} return the same decision and
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -253,6 +253,7 @@ export function constraintChainDigest(chain) {
|
|
|
253
253
|
return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
|
|
254
254
|
}
|
|
255
255
|
const ALLOW = Object.freeze({ action: "allow" });
|
|
256
|
+
const ALLOW_BY_RULE = Object.freeze({ action: "allow", decisionReason: "rule" });
|
|
256
257
|
const RETIRED_TEXT_FIELD = "reason";
|
|
257
258
|
export const DECISION_MEMBERS = Object.keys({
|
|
258
259
|
action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, persistedRuleShadowed: 0, probeReason: 0,
|
|
@@ -414,7 +415,7 @@ export function createAllowDenyPolicy(opts) {
|
|
|
414
415
|
if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, req)) {
|
|
415
416
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
416
417
|
}
|
|
417
|
-
return ALLOW;
|
|
418
|
+
return allow && opts.scope !== "narrowing" ? ALLOW_BY_RULE : ALLOW;
|
|
418
419
|
},
|
|
419
420
|
};
|
|
420
421
|
}
|
|
@@ -486,11 +487,11 @@ export function createApprovalPolicy(opts) {
|
|
|
486
487
|
: `approval denied for "${req.toolName}"${reading.note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reading.note, REVIEWER_NOTE_MAX_BODY)}` : ""}`,
|
|
487
488
|
}, reading.settlement);
|
|
488
489
|
}
|
|
489
|
-
return attest({ action: "allow" }, reading.settlement);
|
|
490
|
+
return attest({ action: "allow", decisionReason: "mode" }, reading.settlement);
|
|
490
491
|
}
|
|
491
492
|
const okRaw = ok;
|
|
492
493
|
if (okRaw === true)
|
|
493
|
-
return attest({ action: "allow" }, settledByDecider("person", "allowed", undefined));
|
|
494
|
+
return attest({ action: "allow", decisionReason: "mode" }, settledByDecider("person", "allowed", undefined));
|
|
494
495
|
if (okRaw !== false) {
|
|
495
496
|
return attest({
|
|
496
497
|
action: "deny",
|
|
@@ -500,7 +501,9 @@ export function createApprovalPolicy(opts) {
|
|
|
500
501
|
}
|
|
501
502
|
return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByDecider("person", "refused", undefined));
|
|
502
503
|
}
|
|
503
|
-
if (
|
|
504
|
+
if (nameListHolds(auto, req) || namespacedCoveringHit(autoCovering, req))
|
|
505
|
+
return ALLOW_BY_RULE;
|
|
506
|
+
if (opts.denyByDefault) {
|
|
504
507
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
505
508
|
}
|
|
506
509
|
return ALLOW;
|
|
@@ -540,6 +543,7 @@ export function combinePolicies(...policies) {
|
|
|
540
543
|
let asked;
|
|
541
544
|
let current = req;
|
|
542
545
|
let rewrite;
|
|
546
|
+
let stated;
|
|
543
547
|
const records = {};
|
|
544
548
|
let anyRecord = false;
|
|
545
549
|
let ruleAskText;
|
|
@@ -565,6 +569,10 @@ export function combinePolicies(...policies) {
|
|
|
565
569
|
if (d.updatedInput !== undefined) {
|
|
566
570
|
current = { ...current, args: d.updatedInput };
|
|
567
571
|
rewrite = d;
|
|
572
|
+
stated = d.action === "allow" ? d.decisionReason : undefined;
|
|
573
|
+
}
|
|
574
|
+
else if (d.action === "allow" && stated === undefined && d.decisionReason !== undefined) {
|
|
575
|
+
stated = d.decisionReason;
|
|
568
576
|
}
|
|
569
577
|
if (d.action === "allow" && hasEngineAttestation(d)) {
|
|
570
578
|
transferEngineAttestations(d, records);
|
|
@@ -605,7 +613,8 @@ export function combinePolicies(...policies) {
|
|
|
605
613
|
FOLDED_CLASSES_SEAT.set(out, Object.freeze([...folded]));
|
|
606
614
|
return out;
|
|
607
615
|
}
|
|
608
|
-
const
|
|
616
|
+
const base = rewrite ?? ALLOW;
|
|
617
|
+
const allowed = stated !== undefined && base.decisionReason !== stated ? { ...base, decisionReason: stated } : base;
|
|
609
618
|
if (!anyRecord)
|
|
610
619
|
return allowed;
|
|
611
620
|
const out = { ...allowed };
|
|
@@ -659,7 +668,7 @@ export function createCoarseCommandNamePolicy(opts) {
|
|
|
659
668
|
if (allow && !allow.has(parsed.name)) {
|
|
660
669
|
return fallback(`command "${parsed.name}" is not in the allowlist`);
|
|
661
670
|
}
|
|
662
|
-
return ALLOW;
|
|
671
|
+
return allow ? ALLOW_BY_RULE : ALLOW;
|
|
663
672
|
},
|
|
664
673
|
}, "sandbox_local");
|
|
665
674
|
}
|
package/dist/core/tool-spec.d.ts
CHANGED
|
@@ -274,6 +274,16 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
274
274
|
* prepare) — omitting `effect` is fine (the default IS `write`).
|
|
275
275
|
*/
|
|
276
276
|
egress?: true;
|
|
277
|
+
/**
|
|
278
|
+
* The tool's PERMISSION SEAT. `"passthrough"`: the tool states no permission of its own — under an asking permission mode
|
|
279
|
+
* (`default` / `acceptEdits` / `plan` / `auto` / `dontAsk`) a call to it runs only when some layer STATES an allow (an allow
|
|
280
|
+
* rule or allow-list entry naming it, an approver's yes, a PreToolUse hook's own `allow`); a policy fold in which nobody
|
|
281
|
+
* stated it ("no objection") gets the mode's question (class `unstated`, origin `policy`), which dontAsk refuses, auto's
|
|
282
|
+
* classifier may answer, and `bypassPermissions` never raises. The MCP and A2A materializers set it on a remote peer's
|
|
283
|
+
* tool. Absent: the tool answers for itself (the engine's own stations and the deployment's declarations). Tightening only —
|
|
284
|
+
* it can make a tool ask, never let one run.
|
|
285
|
+
*/
|
|
286
|
+
permissionSeat?: "passthrough";
|
|
277
287
|
/**
|
|
278
288
|
* design/77 §4 (Gate 4 — irreversibility axis): how reversible this tool's effect is. A THIRD axis,
|
|
279
289
|
* orthogonal to {@link effect} (repeat-safety) and {@link egress} (blast-radius). For office/finance
|
package/dist/core/tools.js
CHANGED
|
@@ -81,6 +81,7 @@ export function defineTool(spec, options) {
|
|
|
81
81
|
...(spec.effect ? { effect: spec.effect } : {}),
|
|
82
82
|
...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
|
|
83
83
|
...(spec.egress ? { egress: true } : {}),
|
|
84
|
+
...(spec.permissionSeat === "passthrough" ? { permissionSeat: "passthrough" } : {}),
|
|
84
85
|
...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
|
|
85
86
|
...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
|
|
86
87
|
...(spec.offload !== undefined ? { offload: spec.offload } : {}),
|
|
@@ -772,6 +772,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
|
|
|
772
772
|
* repeat-safety effect below its fail-closed `write` default (only the caller's `toolAxes` does). Absent on non-MCP tools.
|
|
773
773
|
*/
|
|
774
774
|
mcpReadOnlyHint?: true;
|
|
775
|
+
/**
|
|
776
|
+
* The tool's permission seat — see `ToolSpec.permissionSeat` (src/core/tool-spec.ts). `"passthrough"` = the tool states no
|
|
777
|
+
* permission of its own; set by the MCP and A2A materializers on a remote peer's tool and carried by `defineTool` from a
|
|
778
|
+
* spec. Read per call off the leg's live mount array (`mountedToolPermissionSeat`). Absent ⇒ the tool answers for itself.
|
|
779
|
+
*/
|
|
780
|
+
permissionSeat?: "passthrough";
|
|
775
781
|
/**
|
|
776
782
|
* design/120 P1.5 (CC `isConcurrencySafe(parsedInput)` parity): INPUT-level concurrency refinement
|
|
777
783
|
* for a tool whose safety depends on its arguments — CC's only real consumer is
|
|
@@ -118,7 +118,7 @@ export function applyWorkflowAgentType(spec, agentType, registry) {
|
|
|
118
118
|
if (out.memoryPersistenceCapable === undefined && def.memoryPersistenceCapable !== undefined)
|
|
119
119
|
out.memoryPersistenceCapable = def.memoryPersistenceCapable;
|
|
120
120
|
if (def.allowTools !== undefined || def.denyTools !== undefined) {
|
|
121
|
-
const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: def.denyTools } : {}) });
|
|
121
|
+
const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: def.denyTools } : {}), scope: "narrowing" });
|
|
122
122
|
out.toolPolicy = out.toolPolicy ? combinePolicies(out.toolPolicy, perType) : perType;
|
|
123
123
|
}
|
|
124
124
|
return out;
|
|
@@ -7,4 +7,4 @@ export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileSta
|
|
|
7
7
|
taskId?: string;
|
|
8
8
|
principal?: string;
|
|
9
9
|
toolCallId?: string;
|
|
10
|
-
}) => boolean,
|
|
10
|
+
}) => boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -5,13 +5,12 @@ import { decodeTextBytes } from "./encoding.js";
|
|
|
5
5
|
import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
|
|
6
6
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
|
|
7
7
|
import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
|
|
8
|
-
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS,
|
|
8
|
+
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
9
9
|
import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
|
|
10
10
|
import { openSystemReminder } from "../../core/reminder-mint.js";
|
|
11
11
|
import { discloseReminderShaped } from "../../core/reminder-disclosure.js";
|
|
12
12
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
13
|
-
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption,
|
|
14
|
-
const cyberReminder = readCyberReminder === false ? "" : readCyberReminderText(reminderMark);
|
|
13
|
+
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
|
|
15
14
|
const reminderDisclosureWindows = new Map();
|
|
16
15
|
const pdfReminderDisclosure = (key) => ({
|
|
17
16
|
mark: reminderMark,
|
|
@@ -244,7 +243,6 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
244
243
|
return {
|
|
245
244
|
content: [
|
|
246
245
|
...bodyBlocks,
|
|
247
|
-
...(cyberReminder ? [{ type: "text", text: cyberReminder }] : []),
|
|
248
246
|
...(nbDisclosure.trailer !== undefined ? [{ type: "text", text: nbDisclosure.trailer }] : []),
|
|
249
247
|
],
|
|
250
248
|
details: { type: "notebook", file: { filePath: path, cells: parsed.cells.map(stripNotebookImageData) } },
|
|
@@ -321,7 +319,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
321
319
|
counts: reminderDisclosureCounts,
|
|
322
320
|
});
|
|
323
321
|
return {
|
|
324
|
-
content: `${nbFallbackPrefix}${header}${body}${
|
|
322
|
+
content: `${nbFallbackPrefix}${header}${body}${disclosure.trailer !== undefined ? `\n${disclosure.trailer}` : ""}`,
|
|
325
323
|
details: {
|
|
326
324
|
type: "text",
|
|
327
325
|
file: {
|
|
@@ -145,33 +145,6 @@ export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | und
|
|
|
145
145
|
* divergence is a fidelity delta, not a correctness one (the partial-view marker always states the
|
|
146
146
|
* served window truthfully). */
|
|
147
147
|
export declare const MAX_READ_OUTPUT_CHARS = 100000;
|
|
148
|
-
/** Per-read content-safety reminder appended after a text read.
|
|
149
|
-
*
|
|
150
|
-
* Attribution, stated precisely because either half alone is wrong. The upstream 88 source DOES carry
|
|
151
|
-
* the same mechanism — `FileReadTool`'s `CYBER_RISK_MITIGATION_REMINDER`, appended after every text
|
|
152
|
-
* read, behind a PER-MODEL exemption gate. The 220 baseline does NOT: the text is gone, retired once
|
|
153
|
-
* the mitigation moved inside the serving model.
|
|
154
|
-
*
|
|
155
|
-
* This engine keeps it because a BYOM deployment cannot assume its serving model carries the
|
|
156
|
-
* mitigation, and adjacency is the whole mechanism (a reminder in a system prompt hundreds of turns
|
|
157
|
-
* back is not what makes a model treat the bytes in front of it as data). It costs ≈50 tokens per text
|
|
158
|
-
* read, which is why the exemption gate 88 had is restored rather than the append being unconditional:
|
|
159
|
-
* see `HandsToolkitOptions.readCyberReminder`. Default ON — a deployment that knows its model is
|
|
160
|
-
* covered turns it off explicitly.
|
|
161
|
-
*
|
|
162
|
-
* COVERAGE, stated because the switch is easy to over-read: the reminder rides the plain-text read and
|
|
163
|
-
* the notebook projection. PDF text extraction returns through its own result builder and has never
|
|
164
|
-
* carried it, so extracted PDF text reaches the model without this mitigation whatever the switch says.
|
|
165
|
-
* That gap predates the switch; its IMPERSONATION half is closed — the PDF text legs carry the same
|
|
166
|
-
* detect-and-disclose trailer as this lane (reminder-shaped bytes in extracted PDF text are disclosed
|
|
167
|
-
* at the outlet, fs-pdf.ts), and their bodies are fenced. The CYBER half (this malware-alertness
|
|
168
|
-
* sentence) still does not ride PDF text and stays an open item rather than closed silently —
|
|
169
|
-
* widening it to a third surface is a change to what every PDF read costs, not a wiring fix.
|
|
170
|
-
*
|
|
171
|
-
* A FUNCTION over the session mark, not a constant: the open tag is rendered by the mint home so it
|
|
172
|
-
* carries the run's provenance mark; the sentence bytes are unchanged, and a markless call renders
|
|
173
|
-
* the bare form verbatim. */
|
|
174
|
-
export declare function readCyberReminderText(mark: string | undefined): string;
|
|
175
148
|
export declare const BASH_DEFAULT_TIMEOUT_SEC = 120;
|
|
176
149
|
export declare const BASH_MAX_TIMEOUT_SEC = 600;
|
|
177
150
|
export declare const BASH_DEFAULT_TIMEOUT_MS: number;
|