@sema-agent/core 7.19.0 → 7.20.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 +183 -2
- package/dist/agents/subagent.js +12 -20
- package/dist/agents/teacher.js +2 -5
- package/dist/agents/verify.js +21 -10
- package/dist/brain/anthropic.js +3 -1
- package/dist/brain/open-responses.js +4 -2
- package/dist/brain/openai.js +3 -1
- package/dist/brain/stream-engine.d.ts +4 -0
- package/dist/brain/stream-engine.js +4 -0
- package/dist/core/auto-mode.d.ts +37 -18
- package/dist/core/auto-mode.js +1 -1
- package/dist/core/child-clamp-seats.d.ts +79 -0
- package/dist/core/child-clamp-seats.js +29 -0
- package/dist/core/engine-notice.d.ts +67 -49
- package/dist/core/fs-write-gate-policy.d.ts +17 -9
- package/dist/core/fs-write-gate-policy.js +51 -4
- package/dist/core/gate-bounded-await.d.ts +38 -0
- package/dist/core/gate-bounded-await.js +53 -0
- package/dist/core/gate-fold.js +16 -48
- package/dist/core/gate-lanes.js +11 -3
- package/dist/core/gate-outcome.d.ts +9 -6
- package/dist/core/governance-codes.d.ts +16 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +8 -8
- package/dist/core/memory-engine/engine.d.ts +14 -9
- package/dist/core/memory-engine/engine.js +21 -8
- package/dist/core/memory-engine/layout.d.ts +31 -0
- package/dist/core/memory-engine/layout.js +68 -3
- package/dist/core/memory-engine/types.d.ts +24 -0
- package/dist/core/memory.d.ts +19 -0
- package/dist/core/memory.js +12 -0
- package/dist/core/remote-env.d.ts +32 -0
- package/dist/core/remote-env.js +4 -1
- package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
- package/dist/core/runner/advertised-writable-dirs.js +51 -3
- package/dist/core/runner/announce-once-ledger.d.ts +15 -0
- package/dist/core/runner/announce-once-ledger.js +8 -0
- package/dist/core/runner/classifier-ladder.d.ts +5 -0
- package/dist/core/runner/classifier-ladder.js +13 -2
- package/dist/core/runner/contracts.d.ts +44 -1
- package/dist/core/runner/denial-limit-arms.d.ts +14 -1
- package/dist/core/runner/denial-limit-arms.js +5 -4
- package/dist/core/runner/env-capability-census.d.ts +30 -0
- package/dist/core/runner/env-capability-census.js +38 -0
- package/dist/core/runner/forward-sink.d.ts +32 -0
- package/dist/core/runner/forward-sink.js +41 -0
- package/dist/core/runner/gate-exit.d.ts +4 -0
- package/dist/core/runner/gate-exit.js +4 -1
- package/dist/core/runner/git-status-frame.d.ts +0 -9
- package/dist/core/runner/listing-fold.d.ts +57 -0
- package/dist/core/runner/listing-fold.js +75 -0
- package/dist/core/runner/prepare-execution-env.d.ts +1 -1
- package/dist/core/runner/prepare-execution-env.js +2 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
- package/dist/core/runner/prepare-gate-stations.js +6 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +3 -5
- package/dist/core/runner/prepare-listings.d.ts +3 -0
- package/dist/core/runner/prepare-listings.js +4 -2
- package/dist/core/runner/prepare-memory.js +7 -1
- package/dist/core/runner/prepare-policy-chain.js +5 -5
- package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
- package/dist/core/runner/prepare-run-refs.d.ts +4 -2
- package/dist/core/runner/prepare-run-refs.js +5 -4
- package/dist/core/runner/prepare-task.js +7 -5
- package/dist/core/runner/run-clock-and-content.js +3 -7
- package/dist/core/runner/run-harness-handlers.js +10 -10
- package/dist/core/runner/run-leg.js +28 -19
- package/dist/core/runner/run-settle-and-teardown.js +2 -5
- package/dist/core/runner/run-turn-boundary.js +44 -33
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/synthetic-tools.d.ts +13 -39
- package/dist/core/runner/synthetic-tools.js +15 -59
- package/dist/core/runner/task-root-canonical.d.ts +45 -0
- package/dist/core/runner/task-root-canonical.js +18 -0
- package/dist/core/runner/turn-attachments.d.ts +125 -49
- package/dist/core/runner/turn-attachments.js +124 -66
- package/dist/core/safe-notify.d.ts +5 -0
- package/dist/core/safe-notify.js +24 -2
- package/dist/core/sensitive-path-policy.d.ts +53 -27
- package/dist/core/sensitive-path-policy.js +16 -26
- package/dist/core/skill-listing-budget.d.ts +34 -40
- package/dist/core/skill-listing-budget.js +25 -26
- package/dist/core/skills-directory.js +17 -10
- package/dist/core/task-registry.js +2 -1
- package/dist/core/task-spec.d.ts +8 -7
- package/dist/core/tool-policy.d.ts +12 -1
- package/dist/core/tool-policy.js +39 -25
- package/dist/core/write-protect.d.ts +98 -18
- package/dist/core/write-protect.js +88 -34
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/orchestration/workflow-primitives.js +10 -20
- package/dist/stores/file/memory-store.d.ts +5 -2
- package/dist/stores/file/memory-store.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
- package/dist/tools/fs/bash-readonly-classifier.js +317 -35
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +12 -25
- package/dist/tools/fs/fs-shared.d.ts +22 -0
- package/dist/tools/fs/fs-shared.js +30 -0
- package/dist/tools/fs/index.d.ts +1 -1
- package/dist/tools/fs/index.js +1 -1
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.20.0.json +568 -0
|
@@ -2,7 +2,8 @@ import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
|
2
2
|
import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
3
|
import { protocolOf } from "../protocol-table.js";
|
|
4
4
|
import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SKILL_LISTING_DEFAULT_BUDGET } from "../skill-listing-budget.js";
|
|
6
|
+
import { foldListingWithin, foldRemovalArmWithin, removalArmFloor } from "./listing-fold.js";
|
|
6
7
|
import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
|
|
7
8
|
export const TODO_REMINDER_CONFIG = {
|
|
8
9
|
TURNS_SINCE_WRITE: 10,
|
|
@@ -24,6 +25,33 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
|
|
|
24
25
|
export const ATTACHMENT_BYTE_CAP = 8 * 1024;
|
|
25
26
|
const PROJECTION_ITEMS_MAX = 50;
|
|
26
27
|
const PROJECTION_CONTENT_MAX = 80;
|
|
28
|
+
const LISTING_FRAME_FLOOR_BYTES = 768;
|
|
29
|
+
const DATE_CHANGE_MAX_BYTES = 160;
|
|
30
|
+
const WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES = 256;
|
|
31
|
+
export const ATTACHMENT_DELIVERY_CLASS = {
|
|
32
|
+
todo_reminder: { class: "clippable" },
|
|
33
|
+
task_reminder: { class: "clippable" },
|
|
34
|
+
tool_search_usage_reminder: { class: "clippable" },
|
|
35
|
+
changed_files: { class: "clippable" },
|
|
36
|
+
plan_mode: { class: "clippable" },
|
|
37
|
+
budget_usd: { class: "clippable" },
|
|
38
|
+
total_tokens_reminder: { class: "clippable" },
|
|
39
|
+
background_tasks: { class: "clippable" },
|
|
40
|
+
tools_delta: { class: "intact", bound: (inp) => toolsDeltaBound(inp) },
|
|
41
|
+
agent_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) + (inp.agentModels !== undefined ? Buffer.byteLength(modelsAvailableLine(inp.agentModels), "utf8") : 0) },
|
|
42
|
+
skills_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) },
|
|
43
|
+
mcp_instructions: { class: "intact", bound: (inp) => mcpInstructionsBound(inp) },
|
|
44
|
+
mcp_dropped_tools: { class: "intact", bound: (inp) => mcpDroppedToolsBound(inp) },
|
|
45
|
+
date_change: { class: "intact", bound: () => DATE_CHANGE_MAX_BYTES },
|
|
46
|
+
instructions_change: { class: "intact", bound: () => INSTRUCTIONS_CHANGE_BYTE_CAP },
|
|
47
|
+
workflow_size_guideline_change: { class: "intact", bound: () => WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES },
|
|
48
|
+
};
|
|
49
|
+
export function isIntactSource(source) {
|
|
50
|
+
return ATTACHMENT_DELIVERY_CLASS[source].class === "intact";
|
|
51
|
+
}
|
|
52
|
+
function listingFrameBound(budget) {
|
|
53
|
+
return Math.max((budget ?? SKILL_LISTING_DEFAULT_BUDGET).blockMaxBytes, LISTING_FRAME_FLOOR_BYTES);
|
|
54
|
+
}
|
|
27
55
|
const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
|
|
28
56
|
const ATTACHMENT_TAGS_OWNED = {
|
|
29
57
|
skills_listing: SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills"),
|
|
@@ -208,23 +236,15 @@ export function collectDueAttachments(state, inp) {
|
|
|
208
236
|
(out ??= []).push({ source: "tools_delta", body });
|
|
209
237
|
}
|
|
210
238
|
const agentListingBody = inp.config.agentListing && inp.agentListing !== undefined
|
|
211
|
-
? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels)
|
|
239
|
+
? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels, inp.listingBudget)
|
|
212
240
|
: undefined;
|
|
241
|
+
if (agentListingBody !== undefined)
|
|
242
|
+
(out ??= []).push({ source: "agent_listing", body: agentListingBody });
|
|
213
243
|
const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
|
|
214
|
-
? renderSkillsListingDelta(state, inp.skillsListing, inp.
|
|
244
|
+
? renderSkillsListingDelta(state, inp.skillsListing, inp.listingBudget)
|
|
215
245
|
: undefined;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
listingPair.push({ source: "agent_listing", body: agentListingBody });
|
|
219
|
-
if (skillsListingBody !== undefined) {
|
|
220
|
-
const att = { source: "skills_listing", body: skillsListingBody };
|
|
221
|
-
if (state.skillsListingStarved === true)
|
|
222
|
-
listingPair.unshift(att);
|
|
223
|
-
else
|
|
224
|
-
listingPair.push(att);
|
|
225
|
-
}
|
|
226
|
-
for (const att of listingPair)
|
|
227
|
-
(out ??= []).push(att);
|
|
246
|
+
if (skillsListingBody !== undefined)
|
|
247
|
+
(out ??= []).push({ source: "skills_listing", body: skillsListingBody });
|
|
228
248
|
if (inp.config.mcpInstructions && inp.mcpInstructions !== undefined) {
|
|
229
249
|
const body = renderMcpInstructionsDelta(inp.mcpInstructions.added, inp.mcpInstructions.removed);
|
|
230
250
|
if (body !== undefined)
|
|
@@ -244,9 +264,6 @@ export function collectDueAttachments(state, inp) {
|
|
|
244
264
|
if (out === undefined)
|
|
245
265
|
return EMPTY;
|
|
246
266
|
const capped = capAttachments(out);
|
|
247
|
-
if (skillsListingBody !== undefined) {
|
|
248
|
-
state.skillsListingStarved = !capped.some((a) => a.source === "skills_listing" && a.body === skillsListingBody);
|
|
249
|
-
}
|
|
250
267
|
if (pendingSurfaced !== undefined && capped.some((a) => a.source === "changed_files")) {
|
|
251
268
|
for (const c of pendingSurfaced)
|
|
252
269
|
state.surfacedMtime.set(c.path, c.mtimeMs);
|
|
@@ -434,15 +451,19 @@ export function renderToolsDelta(input) {
|
|
|
434
451
|
const added = input.added ?? [];
|
|
435
452
|
if (added.length > 0) {
|
|
436
453
|
const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
: "")
|
|
454
|
+
const grouped = added.length > TOOLS_DELTA_LIST_MAX;
|
|
455
|
+
const exception = swapped.length > 0
|
|
456
|
+
? grouped
|
|
457
|
+
? `\nException: ${swapped.length} of them were too large to inline, so their full declarations are in the tools list instead.`
|
|
458
|
+
: `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
|
|
459
|
+
: "";
|
|
460
|
+
blocks.push((grouped ? `${added.length} deferred tools are now ${input.staticFace === true ? "active" : "available"}: ${groupByMcpServer(added)}. ` : "") +
|
|
461
|
+
(input.staticFace === true
|
|
462
|
+
? (grouped ? "Call them directly. " : "The following deferred tools are now active — call them directly. ") +
|
|
463
|
+
`The tools list keeps compact placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema${grouped ? "." : ":\n"}`
|
|
464
|
+
: (grouped ? "" : "The following deferred tools are now available. ") + `Their full schemas are loaded — call them directly like any other tool${grouped ? "." : ":\n"}`) +
|
|
465
|
+
(grouped ? "" : added.map((n) => `- ${n}`).join("\n")) +
|
|
466
|
+
exception);
|
|
446
467
|
}
|
|
447
468
|
const readded = input.readded ?? [];
|
|
448
469
|
if (readded.length > 0) {
|
|
@@ -530,22 +551,35 @@ function listingIdentity(name) {
|
|
|
530
551
|
export function agentListingDeltaHeader(toolName) {
|
|
531
552
|
return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
532
553
|
}
|
|
533
|
-
export function
|
|
554
|
+
export function renderAgentListingFrame(state, entries, toolName, models, budget = SKILL_LISTING_DEFAULT_BUDGET) {
|
|
534
555
|
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
535
556
|
const line = (e) => {
|
|
536
557
|
const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
|
|
537
558
|
return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
|
|
538
559
|
};
|
|
560
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
561
|
+
const joiner = bytes(AGENT_DELTA_BLOCK_JOINER);
|
|
562
|
+
const fold = (items, header, maxBytes) => foldListingWithin(items, {
|
|
563
|
+
blockMaxBytes: maxBytes,
|
|
564
|
+
full: line,
|
|
565
|
+
nameOnly: (e) => `- ${safe(e.name)}`,
|
|
566
|
+
nameOf: (e) => e.name,
|
|
567
|
+
capMarker: AGENT_LISTING_CAP_MARKER,
|
|
568
|
+
overflowLine: (n) => `(… +${n} more agent types not listed)`,
|
|
569
|
+
frame: (lines) => [header, ...lines].join("\n"),
|
|
570
|
+
});
|
|
539
571
|
const announced = state.announcedAgentTypes;
|
|
540
572
|
if (announced === undefined) {
|
|
541
573
|
if (entries.length === 0)
|
|
542
574
|
return undefined;
|
|
543
|
-
const
|
|
575
|
+
const tail = [];
|
|
544
576
|
if (entries.length > 1)
|
|
545
|
-
|
|
577
|
+
tail.push(AGENT_CONCURRENCY_NOTE);
|
|
546
578
|
if (models !== undefined && models.length > 0)
|
|
547
|
-
|
|
548
|
-
|
|
579
|
+
tail.push(modelsAvailableLine(models));
|
|
580
|
+
const tailCost = tail.reduce((acc, t) => acc + joiner + bytes(t), 0);
|
|
581
|
+
const roster = fold(entries, agentListingInitialHeader(toolName), budget.blockMaxBytes - tailCost);
|
|
582
|
+
return { text: [roster.text, ...tail].join(AGENT_DELTA_BLOCK_JOINER), nameOnly: roster.nameOnly, dropped: roster.dropped, listed: entries.length };
|
|
549
583
|
}
|
|
550
584
|
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
551
585
|
const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
|
|
@@ -557,17 +591,26 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
557
591
|
state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
|
|
558
592
|
if (added.length === 0 && removed.length === 0 && !modelsDrifted)
|
|
559
593
|
return undefined;
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
594
|
+
const modelsLine = modelsDrifted ? modelsAvailableLine(models) : undefined;
|
|
595
|
+
const modelsCost = modelsLine !== undefined ? joiner + bytes(modelsLine) : 0;
|
|
596
|
+
const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
|
|
597
|
+
const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(AGENT_LISTING_REMOVED_HEADER, removed.length)) : 0;
|
|
598
|
+
const addArm = added.length > 0 ? fold(added, agentListingDeltaHeader(toolName), budget.blockMaxBytes - modelsCost - ambientCost - removalReserve) : undefined;
|
|
599
|
+
const addCost = addArm === undefined ? 0 : bytes(addArm.text) + joiner;
|
|
600
|
+
const blocks = addArm === undefined ? [] : [addArm.text];
|
|
563
601
|
if (removed.length > 0) {
|
|
564
|
-
blocks.push(
|
|
602
|
+
blocks.push(foldRemovalArmWithin(removed, AGENT_LISTING_REMOVED_HEADER, budget.blockMaxBytes - modelsCost - ambientCost - addCost));
|
|
565
603
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
566
604
|
}
|
|
567
|
-
if (
|
|
568
|
-
blocks.push(
|
|
569
|
-
return blocks.join(
|
|
605
|
+
if (modelsLine !== undefined)
|
|
606
|
+
blocks.push(modelsLine);
|
|
607
|
+
return { text: blocks.join(AGENT_DELTA_BLOCK_JOINER), nameOnly: addArm?.nameOnly ?? [], dropped: addArm?.dropped ?? [], listed: added.length };
|
|
608
|
+
}
|
|
609
|
+
export function renderAgentListingDelta(state, entries, toolName, models, budget) {
|
|
610
|
+
return renderAgentListingFrame(state, entries, toolName, models, budget)?.text;
|
|
570
611
|
}
|
|
612
|
+
const AGENT_LISTING_CAP_MARKER = "(listing budget reached — lower-priority agent types are listed name-only)";
|
|
613
|
+
const AGENT_DELTA_BLOCK_JOINER = "\n\n";
|
|
571
614
|
export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
|
|
572
615
|
function modelsAvailableLine(models) {
|
|
573
616
|
const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
|
|
@@ -597,28 +640,7 @@ export function commitAgentListing(state, entries, models) {
|
|
|
597
640
|
export const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
|
|
598
641
|
export const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
|
|
599
642
|
const SKILLS_DELTA_BLOCK_JOINER = "\n\n";
|
|
600
|
-
function
|
|
601
|
-
return [SKILLS_LISTING_REMOVED_HEADER, `(… +${count} more are no longer available)`].join("\n");
|
|
602
|
-
}
|
|
603
|
-
function renderSkillsRemovalArm(removed, maxBytes) {
|
|
604
|
-
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
605
|
-
const lines = removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`);
|
|
606
|
-
const head = bytes(SKILLS_LISTING_REMOVED_HEADER);
|
|
607
|
-
const lineCost = (l) => bytes(l) + 1;
|
|
608
|
-
const prefix = new Array(lines.length + 1);
|
|
609
|
-
prefix[0] = 0;
|
|
610
|
-
for (let i = 0; i < lines.length; i++)
|
|
611
|
-
prefix[i + 1] = prefix[i] + lineCost(lines[i]);
|
|
612
|
-
if (head + prefix[lines.length] <= maxBytes)
|
|
613
|
-
return [SKILLS_LISTING_REMOVED_HEADER, ...lines].join("\n");
|
|
614
|
-
for (let m = lines.length - 1; m >= 0; m--) {
|
|
615
|
-
const tail = `(… +${lines.length - m} more are no longer available)`;
|
|
616
|
-
if (head + prefix[m] + lineCost(tail) <= maxBytes)
|
|
617
|
-
return [SKILLS_LISTING_REMOVED_HEADER, ...lines.slice(0, m), tail].join("\n");
|
|
618
|
-
}
|
|
619
|
-
return skillsRemovalArmFloor(lines.length);
|
|
620
|
-
}
|
|
621
|
-
export function renderSkillsListingFrame(state, entries, budget) {
|
|
643
|
+
export function renderSkillsListingFrame(state, entries, budget = SKILL_LISTING_DEFAULT_BUDGET) {
|
|
622
644
|
const announced = state.announcedSkills;
|
|
623
645
|
if (announced === undefined) {
|
|
624
646
|
if (entries.length === 0)
|
|
@@ -634,20 +656,20 @@ export function renderSkillsListingFrame(state, entries, budget) {
|
|
|
634
656
|
return undefined;
|
|
635
657
|
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
636
658
|
const joiner = bytes(SKILLS_DELTA_BLOCK_JOINER);
|
|
637
|
-
const frameMaxBytes =
|
|
659
|
+
const frameMaxBytes = budget.blockMaxBytes;
|
|
638
660
|
const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
|
|
639
|
-
const removalReserve = removed.length > 0 ? joiner + bytes(
|
|
661
|
+
const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(SKILLS_LISTING_REMOVED_HEADER, removed.length)) : 0;
|
|
640
662
|
const addArm = added.length > 0
|
|
641
663
|
? renderSkillLinesWithin(added, {
|
|
642
664
|
blockMaxBytes: frameMaxBytes - ambientCost - removalReserve,
|
|
643
|
-
descMaxChars: budget
|
|
665
|
+
descMaxChars: budget.descMaxChars,
|
|
644
666
|
frame: (lines) => [SKILLS_LISTING_DELTA_HEADER, ...lines].join("\n"),
|
|
645
667
|
})
|
|
646
668
|
: undefined;
|
|
647
669
|
const addCost = addArm === undefined ? 0 : bytes(addArm.text) + (removed.length > 0 ? joiner : 0);
|
|
648
670
|
const blocks = addArm === undefined ? [] : [addArm.text];
|
|
649
671
|
if (removed.length > 0) {
|
|
650
|
-
blocks.push(
|
|
672
|
+
blocks.push(foldRemovalArmWithin(removed, SKILLS_LISTING_REMOVED_HEADER, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
|
|
651
673
|
}
|
|
652
674
|
return {
|
|
653
675
|
text: blocks.join(SKILLS_DELTA_BLOCK_JOINER),
|
|
@@ -703,6 +725,37 @@ export function selectMcpDroppedBatch(entries) {
|
|
|
703
725
|
}
|
|
704
726
|
return out;
|
|
705
727
|
}
|
|
728
|
+
const TOOLS_DELTA_FIXED_MAX_BYTES = 2048;
|
|
729
|
+
function toolsDeltaBound(inp) {
|
|
730
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
731
|
+
const list = (names) => (names.length > TOOLS_DELTA_LIST_MAX ? bytes(groupByMcpServer(names)) : names.reduce((acc, n) => acc + bytes(n) + 3, 0));
|
|
732
|
+
const swapped = (names) => names.reduce((acc, n) => acc + bytes(n) + 2, 0);
|
|
733
|
+
const mcp = inp.mcpToolsDelta;
|
|
734
|
+
return (TOOLS_DELTA_FIXED_MAX_BYTES +
|
|
735
|
+
list(inp.newTools ?? []) +
|
|
736
|
+
swapped(inp.newToolsSwappedUnderStatic ?? []) +
|
|
737
|
+
bytes(groupByMcpServer(mcp?.readded ?? [])) +
|
|
738
|
+
swapped(inp.readdedToolsSwappedUnderStatic ?? []) +
|
|
739
|
+
list(mcp?.removed ?? []) +
|
|
740
|
+
(mcp?.failedServers ?? []).slice(0, TOOLS_DELTA_LIST_MAX).reduce((acc, f) => acc + bytes(f.name) + bytes(f.error ?? "") + 8, 0));
|
|
741
|
+
}
|
|
742
|
+
const MCP_INSTRUCTIONS_FENCE_MAX_BYTES = 512;
|
|
743
|
+
function mcpInstructionsBound(inp) {
|
|
744
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
745
|
+
const m = inp.mcpInstructions;
|
|
746
|
+
if (m === undefined)
|
|
747
|
+
return 0;
|
|
748
|
+
return (512 +
|
|
749
|
+
m.added.reduce((acc, a) => acc + MCP_INSTRUCTIONS_MAX_CHARS * 4 + bytes(a.server) + MCP_INSTRUCTIONS_FENCE_MAX_BYTES, 0) +
|
|
750
|
+
m.removed.reduce((acc, n) => acc + bytes(n) + 1, 0));
|
|
751
|
+
}
|
|
752
|
+
const MCP_DROPPED_FIXED_MAX_BYTES = 1024;
|
|
753
|
+
function mcpDroppedToolsBound(inp) {
|
|
754
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
755
|
+
const first = inp.mcpDroppedTools?.[0];
|
|
756
|
+
const single = first === undefined ? 0 : MCP_DROPPED_FIXED_MAX_BYTES + bytes(first.tool) + bytes(first.server) + bytes(first.reason) + 32;
|
|
757
|
+
return Math.max(MCP_DROPPED_BATCH_MAX_BYTES, single);
|
|
758
|
+
}
|
|
706
759
|
const PLAN_MODE_FULL_BODY = `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. That tool inherently requests plan approval — never ask for approval via plain text.`;
|
|
707
760
|
const PLAN_MODE_SPARSE_BODY = `Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. Never ask about plan approval via plain text.`;
|
|
708
761
|
const TRUNCATION_MARKER = "\n…[truncated]";
|
|
@@ -710,12 +763,17 @@ const MIN_KEEP_BYTES = 160;
|
|
|
710
763
|
function capAttachments(atts) {
|
|
711
764
|
let total = 0;
|
|
712
765
|
for (const a of atts)
|
|
713
|
-
|
|
766
|
+
if (!isIntactSource(a.source))
|
|
767
|
+
total += Buffer.byteLength(a.body, "utf8");
|
|
714
768
|
if (total <= ATTACHMENT_BYTE_CAP)
|
|
715
769
|
return atts;
|
|
716
770
|
let budget = ATTACHMENT_BYTE_CAP;
|
|
717
771
|
const out = [];
|
|
718
772
|
for (const a of atts) {
|
|
773
|
+
if (isIntactSource(a.source)) {
|
|
774
|
+
out.push(a);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
719
777
|
const len = Buffer.byteLength(a.body, "utf8");
|
|
720
778
|
if (len <= budget) {
|
|
721
779
|
out.push(a);
|
|
@@ -56,6 +56,11 @@ export interface SafeNotifierOptions {
|
|
|
56
56
|
*/
|
|
57
57
|
readonly onError?: (failure: SafeNotifyFailure) => void;
|
|
58
58
|
}
|
|
59
|
+
/** The sentence of a contained failure, for a consumer that puts it in front of a person. Total by the
|
|
60
|
+
* same rule as {@link containedError}: an error object handed in from elsewhere (a `lastErrorAt`
|
|
61
|
+
* reading, a rejection an adapter wrapped) may still carry a hostile `message`, and the disclosure a
|
|
62
|
+
* site composes from it must not be the thing that faults. */
|
|
63
|
+
export declare function safeErrorMessage(error: unknown): string;
|
|
59
64
|
export interface SafeNotifier {
|
|
60
65
|
/**
|
|
61
66
|
* Run one host-callback invocation, containing any throw. Returns `true` when it completed, `false` when it
|
package/dist/core/safe-notify.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
function containedError(thrown) {
|
|
2
|
+
try {
|
|
3
|
+
if (thrown instanceof Error && typeof thrown.message === "string")
|
|
4
|
+
return thrown;
|
|
5
|
+
return new Error(String(thrown));
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return new Error(`a thrown value of type ${typeof thrown} that cannot be inspected or converted to text`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function safeErrorMessage(error) {
|
|
12
|
+
try {
|
|
13
|
+
const m = error?.message;
|
|
14
|
+
if (typeof m === "string")
|
|
15
|
+
return m;
|
|
16
|
+
return String(error);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "a failure whose message could not be read";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
1
22
|
export function createSafeNotifier(opts) {
|
|
2
23
|
const onError = opts?.onError;
|
|
3
24
|
let sites;
|
|
4
25
|
let total = 0;
|
|
5
26
|
const record = (site, thrown) => {
|
|
6
|
-
const error =
|
|
27
|
+
const error = containedError(thrown);
|
|
7
28
|
total += 1;
|
|
8
29
|
if (sites === undefined)
|
|
9
30
|
sites = new Map();
|
|
@@ -60,8 +81,9 @@ export function createSafeNotifier(opts) {
|
|
|
60
81
|
export function observeThenableRejection(r, notifier, site) {
|
|
61
82
|
if (typeof r?.then === "function") {
|
|
62
83
|
r.then(undefined, (err) => {
|
|
84
|
+
const error = containedError(err);
|
|
63
85
|
notifier.notify(() => {
|
|
64
|
-
throw
|
|
86
|
+
throw error;
|
|
65
87
|
}, site);
|
|
66
88
|
});
|
|
67
89
|
}
|
|
@@ -54,20 +54,13 @@ export declare function agentConfigDenyPatterns(): readonly string[];
|
|
|
54
54
|
* hard refusal for every deployment that wires this list.
|
|
55
55
|
*
|
|
56
56
|
* SEVERITY LAYERING for the agent-configuration class: the law is {@link agentConfigDenyPatterns} —
|
|
57
|
-
* configuration FILES here, the DIRECTORY one step down on
|
|
58
|
-
* (
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* configuration FILES here, the DIRECTORY one step down on the always-on write-protection table
|
|
58
|
+
* (`WRITE_PROTECTED_DEFAULT_TABLE`'s `.claude` / `.sema` rows — the same reach, since that judge sees the
|
|
59
|
+
* real target too, {@link import("./write-protect.js").WriteProtectionMatcher.judge}); a directory also
|
|
60
|
+
* holds the person's own material, and a directory-wide DENY entry turned an explicit instruction to edit
|
|
61
|
+
* `.claude/projects/…` into a refusal nobody could lift (a deny outranks an ask under the fold).
|
|
61
62
|
*/
|
|
62
63
|
export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
|
|
63
|
-
/**
|
|
64
|
-
* The ASK-severity half of the same guard: the agent-configuration DIRECTORIES, on the SAME judge
|
|
65
|
-
* ({@link createSensitivePathPolicy}'s `askPatterns` seat, whose ABSENCE reads as this list) — only the
|
|
66
|
-
* directories, since the configuration FILES inside them are deny material above. It lives HERE and not
|
|
67
|
-
* only on `WRITE_PROTECTED_DEFAULT_TABLE`, which names the same segments but judges LEXICALLY: a
|
|
68
|
-
* symlink with an innocent name walks past that one, so demoting to it alone trades severity for REACH.
|
|
69
|
-
*/
|
|
70
|
-
export declare const RECOMMENDED_SENSITIVE_ASK_PATTERNS: readonly string[];
|
|
71
64
|
/**
|
|
72
65
|
* HRD-PRM-10 — the engine data root, read from the same knob its two consumers read
|
|
73
66
|
* (`$AGENT_DATA_DIR ?? ~/.ai-agent`, off the HOST at wiring time, NFC-normalized) and returned in
|
|
@@ -85,16 +78,53 @@ export declare const RECOMMENDED_SENSITIVE_ASK_PATTERNS: readonly string[];
|
|
|
85
78
|
*/
|
|
86
79
|
export declare function dataRootSpellings(explicit: string | undefined): string[];
|
|
87
80
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
81
|
+
* The data root's identity in the SAME namespace a judged key lives in: every declared spelling
|
|
82
|
+
* ({@link dataRootSpellings}) run through the SAME `canonicalizeTarget` that mints the target key. Not a
|
|
83
|
+
* second canonicalization — the same one, on the other operand, which is what lets
|
|
84
|
+
* {@link relativeUnderRoot}'s comparison be exact.
|
|
85
|
+
*
|
|
86
|
+
* Resolved PER CALL, deliberately not cached. A cached canonical root is a snapshot of the symlink
|
|
87
|
+
* topology at first use: with a data root reached through a symlink, retargeting it left the policy
|
|
88
|
+
* still exempting the OLD target for the life of the run. The module header already accepts paying
|
|
89
|
+
* `canonicalizeTarget` twice for a guarded write as the price of a decoupled gate; this is the same
|
|
90
|
+
* bargain on the other operand, and a request-scoped cache is the same note's answer if profiling
|
|
91
|
+
* ever asks for one. A spelling that cannot be resolved contributes NO exemption (the guard's side wins).
|
|
92
|
+
*
|
|
93
|
+
* ONE resolver for both write judges (the deny set here, the always-on table's target view) — an
|
|
94
|
+
* engine-internal seam, like the two neighbours above.
|
|
95
|
+
*/
|
|
96
|
+
export declare function canonicalDataRootKeys(env: ExecutionEnv, spellings: readonly string[], signal?: AbortSignal): Promise<string[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Is `targetKey` inside the data root `rootKey`, and if so what is its path relative to it? Returns
|
|
99
|
+
* the relative sub-path (`""` when the target IS the root), or `null` for everything else.
|
|
100
|
+
*
|
|
101
|
+
* 🔴 EXACT, and deliberately nothing else. Both operands are minted by the SAME canonicalizer, so
|
|
102
|
+
* every transformation this comparison could apply — a case fold, an NFC pass, splitting a foreign
|
|
103
|
+
* separator family, unioning several readings of the root — can only ADD matches. On the DENY side of
|
|
104
|
+
* this file that is the safe direction and several of those transforms are applied on purpose (a
|
|
105
|
+
* spelling filter is not a path filter). Here the direction inverts: this test grants an EXEMPTION, so
|
|
106
|
+
* an added match is not a wider guard, it is a bypass of one — each of case folding, an NFC pass and a
|
|
107
|
+
* foreign-family read was measured leaking an unrelated sibling into the exemption (their pins name the
|
|
108
|
+
* shapes). The rule: compare what the canonicalizer produced, byte for byte, every uncertainty falling
|
|
109
|
+
* to the deny side.
|
|
110
|
+
*
|
|
111
|
+
* The separator family comes from the ROOT key (a backslash is an ordinary filename character on
|
|
112
|
+
* POSIX, so a root `/x/.sema/data` must not swallow the sibling FILE `/x/.sema/data\evil`), and a root
|
|
113
|
+
* that reduces to nothing exempts nothing rather than exempting the filesystem.
|
|
92
114
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
* Exported as an ENGINE-INTERNAL seam (not re-exported from `src/index.ts`), like {@link dataRootSpellings}:
|
|
116
|
+
* the always-on write-protection judge applies the same exemption on its target view, and a second
|
|
117
|
+
* containment test would be a second place for the two judges to disagree about what "inside" means.
|
|
118
|
+
*/
|
|
119
|
+
export declare function relativeUnderRoot(targetKey: string, rootKey: string): string | null;
|
|
120
|
+
/**
|
|
121
|
+
* Build a composable policy that DENIES writes (via the guarded fs tools) whose CANONICAL target lands on
|
|
122
|
+
* a guarded pattern. `patterns` is deployment-supplied — no default list is shipped; empty = a no-op allow.
|
|
123
|
+
* A deny is a refusal no approver and no persisted allow rule can lift. The ASK severity of the same class
|
|
124
|
+
* (the agent-configuration DIRECTORIES) is not a tier here: it is the always-on write-protection table's,
|
|
125
|
+
* which judges the same real target ({@link import("./write-protect.js").WriteProtectionMatcher.judge} —
|
|
126
|
+
* its contract sentence `write_protection.judges_the_real_target` says the two differ in vocabulary and
|
|
127
|
+
* severity, never in reach).
|
|
98
128
|
*
|
|
99
129
|
* `env` is the same ExecutionEnv the hands run against (inject the one you pass to the Runner), so
|
|
100
130
|
* canonicalization sees the task's real filesystem (a remote/E2B task resolves in its container).
|
|
@@ -104,17 +134,13 @@ export declare function dataRootSpellings(explicit: string | undefined): string[
|
|
|
104
134
|
*
|
|
105
135
|
* Fail-open by design: a tool with no string `path` returns `allow` here — this guard adds friction on
|
|
106
136
|
* top of the other gates, it is not the only line of defense, and it must not turn an unrelated tool
|
|
107
|
-
* call into a spurious verdict. The
|
|
108
|
-
* guarded path → deny
|
|
137
|
+
* call into a spurious verdict. The verdict it owns is narrow and certain: "the real target IS a
|
|
138
|
+
* guarded path → deny". (A target it could not RESOLVE is the one fail-CLOSED arm, below.)
|
|
109
139
|
*/
|
|
110
140
|
export declare function createSensitivePathPolicy(opts: {
|
|
111
141
|
env: ExecutionEnv;
|
|
112
142
|
/** `readonly` accepted so {@link RECOMMENDED_SENSITIVE_PATTERNS} can be passed as-is (widening, non-breaking). */
|
|
113
143
|
patterns: readonly string[];
|
|
114
|
-
/** The ASK-severity set, same syntax and same judge as `patterns`, consulted only where the deny set
|
|
115
|
-
* misses. ABSENT ⇒ {@link RECOMMENDED_SENSITIVE_ASK_PATTERNS}, so a deployment already wiring this
|
|
116
|
-
* guard keeps its reach over those directories without a code change; `[]` = no ask tier. */
|
|
117
|
-
askPatterns?: readonly string[];
|
|
118
144
|
rootPath?: string;
|
|
119
145
|
/**
|
|
120
146
|
* HRD-PRM-10 — the engine data root this deployment runs on. Default
|
|
@@ -50,7 +50,6 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
|
50
50
|
".bash_history",
|
|
51
51
|
".zsh_history",
|
|
52
52
|
];
|
|
53
|
-
export const RECOMMENDED_SENSITIVE_ASK_PATTERNS = [".claude", ".sema"];
|
|
54
53
|
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
|
|
55
54
|
export function dataRootSpellings(explicit) {
|
|
56
55
|
const raw = explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
@@ -65,7 +64,20 @@ export function dataRootSpellings(explicit) {
|
|
|
65
64
|
}
|
|
66
65
|
return out;
|
|
67
66
|
}
|
|
68
|
-
function
|
|
67
|
+
export async function canonicalDataRootKeys(env, spellings, signal) {
|
|
68
|
+
const keys = [];
|
|
69
|
+
for (const spelling of spellings) {
|
|
70
|
+
try {
|
|
71
|
+
const r = await canonicalizeTarget(env, spelling, signal, pathFamilyOf({ root: spelling }));
|
|
72
|
+
if (r.ok && !keys.includes(r.key))
|
|
73
|
+
keys.push(r.key);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
export function relativeUnderRoot(targetKey, rootKey) {
|
|
69
81
|
const seps = isWinFormPath(rootKey) ? ["\\", "/"] : ["/"];
|
|
70
82
|
let root = rootKey;
|
|
71
83
|
while (root.length > 0 && seps.includes(root[root.length - 1]))
|
|
@@ -96,25 +108,11 @@ function matchSensitive(canonicalKey, compiled, aliasResolved) {
|
|
|
96
108
|
}
|
|
97
109
|
export function createSensitivePathPolicy(opts) {
|
|
98
110
|
const compiled = compilePatterns(opts.patterns);
|
|
99
|
-
const compiledAsk = compilePatterns(opts.askPatterns ?? RECOMMENDED_SENSITIVE_ASK_PATTERNS);
|
|
100
111
|
const dataRootPaths = dataRootSpellings(opts.dataRoot);
|
|
101
|
-
const dataRootKeys = async (signal) => {
|
|
102
|
-
const keys = [];
|
|
103
|
-
for (const spelling of dataRootPaths) {
|
|
104
|
-
try {
|
|
105
|
-
const r = await canonicalizeTarget(opts.env, spelling, signal, pathFamilyOf({ root: spelling }));
|
|
106
|
-
if (r.ok && !keys.includes(r.key))
|
|
107
|
-
keys.push(r.key);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return keys;
|
|
113
|
-
};
|
|
114
112
|
const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
|
|
115
113
|
return {
|
|
116
114
|
async check(req, signal) {
|
|
117
|
-
if (compiled.length === 0
|
|
115
|
+
if (compiled.length === 0)
|
|
118
116
|
return { action: "allow" };
|
|
119
117
|
if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
|
|
120
118
|
return { action: "allow" };
|
|
@@ -133,7 +131,7 @@ export function createSensitivePathPolicy(opts) {
|
|
|
133
131
|
};
|
|
134
132
|
}
|
|
135
133
|
let relative = null;
|
|
136
|
-
for (const rootKey of await
|
|
134
|
+
for (const rootKey of await canonicalDataRootKeys(opts.env, dataRootPaths, signal)) {
|
|
137
135
|
relative = relativeUnderRoot(canon.key, rootKey);
|
|
138
136
|
if (relative !== null)
|
|
139
137
|
break;
|
|
@@ -149,14 +147,6 @@ export function createSensitivePathPolicy(opts) {
|
|
|
149
147
|
decisionReason: "safety",
|
|
150
148
|
};
|
|
151
149
|
}
|
|
152
|
-
const askHit = matchSensitive(judged, compiledAsk, canon.aliasResolved === true);
|
|
153
|
-
if (askHit) {
|
|
154
|
-
return {
|
|
155
|
-
action: "ask",
|
|
156
|
-
message: `this write's real target resolves inside the guarded directory "${askHit}" — explicit approval required`,
|
|
157
|
-
decisionReason: "safety",
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
150
|
return { action: "allow" };
|
|
161
151
|
},
|
|
162
152
|
};
|