@sema-agent/core 5.54.0 → 5.56.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 +160 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +159 -12
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +46 -0
- package/dist/core/mcp.js +132 -6
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rules.d.ts +30 -0
- package/dist/core/permission-rules.js +71 -8
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +39 -1
- package/dist/core/runner/prepare-task.js +63 -35
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +170 -31
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +23 -0
- package/dist/core/tool-policy.js +29 -13
- package/dist/core/types.d.ts +126 -17
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +424 -6
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +14 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +22 -6
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
2
|
-
import {
|
|
2
|
+
import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
3
|
import { protocolOf } from "../protocol-table.js";
|
|
4
4
|
import { buildSkillsBlock, skillListingLine } from "./synthetic-tools.js";
|
|
5
5
|
import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
|
|
@@ -20,6 +20,11 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
|
|
|
20
20
|
export const ATTACHMENT_BYTE_CAP = 8 * 1024;
|
|
21
21
|
const PROJECTION_ITEMS_MAX = 50;
|
|
22
22
|
const PROJECTION_CONTENT_MAX = 80;
|
|
23
|
+
const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
|
|
24
|
+
const ATTACHMENT_TAGS_SKILLS_OWNER = SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills");
|
|
25
|
+
export function attachmentEnvelopeTags(source) {
|
|
26
|
+
return source === "skills_listing" ? ATTACHMENT_TAGS_SKILLS_OWNER : ATTACHMENT_TAGS_DEFAULT;
|
|
27
|
+
}
|
|
23
28
|
export const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
|
|
24
29
|
export function createAttachmentState() {
|
|
25
30
|
return {
|
|
@@ -358,8 +363,9 @@ export function renderBudgetUsd(used, total) {
|
|
|
358
363
|
return `USD budget: $${used}/$${total}; $${total - used} remaining`;
|
|
359
364
|
}
|
|
360
365
|
export function renderOrphanedBackgroundTasks(tasks) {
|
|
366
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
361
367
|
return (`The container was restarted. The following background tasks were running and are now stopped:\n` +
|
|
362
|
-
tasks.map((t) => `- ${t.description || "(no description)"} (task ${t.id})`).join("\n") +
|
|
368
|
+
tasks.map((t) => `- ${safe(t.description || "(no description)")} (task ${safe(t.id)})`).join("\n") +
|
|
363
369
|
`\nRe-create them if still needed.`);
|
|
364
370
|
}
|
|
365
371
|
function renderChangedFiles(paths) {
|
|
@@ -449,7 +455,7 @@ export const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
|
|
|
449
455
|
export const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
|
|
450
456
|
export const AMBIENT_CONTEXT_NOTE = "This is ambient context — do not narrate it to the user unless they ask or it is directly relevant to their request.";
|
|
451
457
|
export function agentListingInitialHeader(toolName) {
|
|
452
|
-
return `Available agent types for the ${toolName} tool:`;
|
|
458
|
+
return `Available agent types for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
453
459
|
}
|
|
454
460
|
export function replayAnnouncedListing(texts, headers) {
|
|
455
461
|
let announced;
|
|
@@ -492,13 +498,17 @@ export function replayAnnouncedListing(texts, headers) {
|
|
|
492
498
|
return announced;
|
|
493
499
|
}
|
|
494
500
|
export const AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
|
|
501
|
+
function listingIdentity(name) {
|
|
502
|
+
return sanitizeUntrustedText(name, SHELLED_BODY_ENVELOPE_TAGS);
|
|
503
|
+
}
|
|
495
504
|
export function agentListingDeltaHeader(toolName) {
|
|
496
|
-
return `New agent types are now available for the ${toolName} tool:`;
|
|
505
|
+
return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
497
506
|
}
|
|
498
507
|
export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
508
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
499
509
|
const line = (e) => {
|
|
500
|
-
const base = e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`;
|
|
501
|
-
return `${base} (Tools: ${e.tools ?? AGENT_TOOLS_NOTE_DEFAULT})`;
|
|
510
|
+
const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
|
|
511
|
+
return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
|
|
502
512
|
};
|
|
503
513
|
const announced = state.announcedAgentTypes;
|
|
504
514
|
if (announced === undefined) {
|
|
@@ -511,18 +521,22 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
511
521
|
blocks.push(modelsAvailableLine(models));
|
|
512
522
|
return blocks.join("\n\n");
|
|
513
523
|
}
|
|
514
|
-
const
|
|
515
|
-
const
|
|
524
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
525
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name))).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
526
|
+
const removed = [...announced.keys()]
|
|
527
|
+
.filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)))
|
|
528
|
+
.sort();
|
|
516
529
|
const modelsDrifted = state.announcedModels !== undefined &&
|
|
517
530
|
models !== undefined &&
|
|
518
|
-
(state.announcedModels.length !== models.length ||
|
|
531
|
+
(state.announcedModels.length !== models.length ||
|
|
532
|
+
state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
|
|
519
533
|
if (added.length === 0 && removed.length === 0 && !modelsDrifted)
|
|
520
534
|
return undefined;
|
|
521
535
|
const blocks = [];
|
|
522
536
|
if (added.length > 0)
|
|
523
537
|
blocks.push(`${agentListingDeltaHeader(toolName)}\n${added.map(line).join("\n")}`);
|
|
524
538
|
if (removed.length > 0) {
|
|
525
|
-
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
539
|
+
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${safe(n)}`).join("\n")}`);
|
|
526
540
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
527
541
|
}
|
|
528
542
|
if (modelsDrifted)
|
|
@@ -531,7 +545,8 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
531
545
|
}
|
|
532
546
|
export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
|
|
533
547
|
function modelsAvailableLine(models) {
|
|
534
|
-
|
|
548
|
+
const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
|
|
549
|
+
return `${MODELS_AVAILABLE_PREFIX}${named.length > 0 ? named.join(", ") : "(none)"}`;
|
|
535
550
|
}
|
|
536
551
|
export function replayAnnouncedModels(texts) {
|
|
537
552
|
let last;
|
|
@@ -563,15 +578,16 @@ export function renderSkillsListingDelta(state, entries) {
|
|
|
563
578
|
return undefined;
|
|
564
579
|
return buildSkillsBlock(entries);
|
|
565
580
|
}
|
|
566
|
-
const
|
|
567
|
-
const
|
|
581
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
582
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name)));
|
|
583
|
+
const removed = [...announced.keys()].filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)));
|
|
568
584
|
if (added.length === 0 && removed.length === 0)
|
|
569
585
|
return undefined;
|
|
570
586
|
const blocks = [];
|
|
571
587
|
if (added.length > 0)
|
|
572
588
|
blocks.push(`${SKILLS_LISTING_DELTA_HEADER}\n${added.map((e) => skillListingLine(e)).join("\n")}`);
|
|
573
589
|
if (removed.length > 0) {
|
|
574
|
-
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
590
|
+
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`).join("\n")}`);
|
|
575
591
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
576
592
|
}
|
|
577
593
|
return blocks.join("\n\n");
|
|
@@ -581,12 +597,10 @@ export function commitSkillsListing(state, entries) {
|
|
|
581
597
|
}
|
|
582
598
|
export const MCP_INSTRUCTIONS_MAX_CHARS = 8 * 1024;
|
|
583
599
|
export function fenceMcpServerInstructions(server, text) {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
? `${fenced}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
589
|
-
: fenced;
|
|
600
|
+
const fence = delimitUntrustedWithClip(`MCP server "${server}" instructions`, text, MCP_INSTRUCTIONS_MAX_CHARS);
|
|
601
|
+
return fence.clipped
|
|
602
|
+
? `${fence.text}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
603
|
+
: fence.text;
|
|
590
604
|
}
|
|
591
605
|
export function renderMcpInstructionsDelta(added, removed) {
|
|
592
606
|
const blocks = [];
|
|
@@ -84,6 +84,38 @@ export interface ReconcileReport {
|
|
|
84
84
|
* `runner.resume()` uses a checkpoint-aware entry that bypasses this reconcile entirely for those calls.
|
|
85
85
|
*/
|
|
86
86
|
export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
|
|
87
|
+
/**
|
|
88
|
+
* backlog #389 伴生 (D-2) — CC 2.1.223's interruption markers, VERBATIM (`$U`/`CR` @ `CC:151120-151121`,
|
|
89
|
+
* minted as a USER message by `Jce` @ `CC:640141-640154`). CC mints one on every abort whose reason is
|
|
90
|
+
* outside `{"interrupt","refusal-fallback-edit"}` — and the interactive Esc / remote cancel, which is
|
|
91
|
+
* what `TaskStream.interrupt()` corresponds to, is precisely on the minting side (`CC:1033698-1033733`).
|
|
92
|
+
* The suppressed reason is the one case where the user's own replacement message is already the context.
|
|
93
|
+
*
|
|
94
|
+
* Taken verbatim rather than reworded: this string is an INPUT to later reasoning in CC (its own
|
|
95
|
+
* "interrupted then immediately retried the same action" rule reads it back), and the constitution's
|
|
96
|
+
* standing rule is that a question CC has answered is answered in CC's form.
|
|
97
|
+
*/
|
|
98
|
+
export declare const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
99
|
+
/** backlog #389 伴生 — the tool-use variant (`CR`): the run was cut while a tool batch was in flight. */
|
|
100
|
+
export declare const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
101
|
+
/**
|
|
102
|
+
* backlog #389 伴生 (D-2) — append the interruption marker so the SESSION records that a person stopped
|
|
103
|
+
* this run.
|
|
104
|
+
*
|
|
105
|
+
* Without it, an interrupt that lands on the model stream (no tool call in flight) leaves literally no
|
|
106
|
+
* trace: the orphan reconcile has nothing to close, and the empty aborted assistant is deliberately not
|
|
107
|
+
* persisted (`isEmptyFailureAssistant`). The next run on that session then reads a transcript in which
|
|
108
|
+
* the half-finished work simply stops, and continues as if it had ended by itself.
|
|
109
|
+
*
|
|
110
|
+
* MUST be called AFTER {@link reconcileInterruptedSession} on the same interruption: a user message
|
|
111
|
+
* placed between an assistant's tool calls and their results is exactly the invalid sequence the
|
|
112
|
+
* reconcile exists to prevent.
|
|
113
|
+
*
|
|
114
|
+
* Returns the persisted entry id so the caller can mint its `message_committed` frame.
|
|
115
|
+
*/
|
|
116
|
+
export declare function appendInterruptionMarker(session: Session, opts: {
|
|
117
|
+
toolUseInFlight: boolean;
|
|
118
|
+
}): Promise<string>;
|
|
87
119
|
/**
|
|
88
120
|
* Reconcile a resumed session's active branch: close any orphan tool calls with a synthetic
|
|
89
121
|
* interrupted `toolResult` (never re-running the tool). Returns what was recovered.
|
|
@@ -81,6 +81,21 @@ export function findOrphanToolCalls(messages, suspendedBatch) {
|
|
|
81
81
|
});
|
|
82
82
|
return orphans;
|
|
83
83
|
}
|
|
84
|
+
export const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
85
|
+
export const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
86
|
+
export async function appendInterruptionMarker(session, opts) {
|
|
87
|
+
return await session.appendMessage({
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: opts.toolUseInFlight ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
provenance: "engine-note",
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
|
|
85
100
|
const { messages } = await session.buildContext();
|
|
86
101
|
const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* lines from a still-running watch). Everything else is a terminal completion notification. */
|
|
3
3
|
export type TaskNotificationStatus = "completed" | "failed" | "killed" | "cancelled" | "event";
|
|
4
4
|
export type SystemInjectionPriority = "now" | "next" | "later";
|
|
5
|
+
/** The closed value set of {@link SystemInjectionPriority}, as data — the wire-facing entries validate
|
|
6
|
+
* against THIS rather than against three inline string literals, so widening the type widens the guard.
|
|
7
|
+
* (Names taken verbatim from CC 2.1.223's `UYo` @ `CC:644918`; see the semantics note on
|
|
8
|
+
* {@link SystemInjection.priority} for which of CC's behaviors this engine implements.) */
|
|
9
|
+
export declare const SYSTEM_INJECTION_PRIORITIES: readonly SystemInjectionPriority[];
|
|
10
|
+
/** backlog #389 伴生 (D-3, bad-value loudness) — is this a priority this engine knows at all? A value
|
|
11
|
+
* outside the set is a caller bug that must be refused, never silently folded into a default: the
|
|
12
|
+
* no-subscriber retain path indexes `queues[priority]` and the park path stores it verbatim, so an
|
|
13
|
+
* unknown value used to travel until something downstream failed on it (or didn't, and lied). */
|
|
14
|
+
export declare function isSystemInjectionPriority(value: unknown): value is SystemInjectionPriority;
|
|
5
15
|
export interface TaskNotificationPayload {
|
|
6
16
|
task_id: string;
|
|
7
17
|
/** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
|
|
@@ -137,13 +147,30 @@ export interface ExternalNotificationInput {
|
|
|
137
147
|
}
|
|
138
148
|
export interface SystemInjection<TPayload = unknown> {
|
|
139
149
|
kind: "task_notification";
|
|
140
|
-
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
150
|
+
/**
|
|
151
|
+
* design/116 §7 — in THIS engine every priority delivers at the NEXT turn boundary via
|
|
152
|
+
* `harness.steer()`, mid-work included, in ARRIVAL order (consecutive frames batch); `priority`
|
|
153
|
+
* affects only the park/uplink path. A delivery that races the agent going idle parks on
|
|
154
|
+
* PendingSessionNotifications for the session's next run; `drain()` serves that parked lane.
|
|
155
|
+
*
|
|
156
|
+
* **Anchor correction (backlog #389 伴生 / hallucination audit H-1).** The 2026-08-05 re-anchor
|
|
157
|
+
* justified flattening the ladder with "CC's queued task-notification inputs are UNCONDITIONALLY
|
|
158
|
+
* deliverable at the boundary (CC 2.1.221)". That sentence is FALSE as a statement about CC, on
|
|
159
|
+
* 221 and 223 alike: the mid-turn fold is gated at `getCommandsByMaxPriority("next")`
|
|
160
|
+
* (`pretty221.js:449195` / `pretty223.js:415586`), which admits `now`+`next` and EXCLUDES `later`
|
|
161
|
+
* — and `enqueuePendingNotification` defaults to `later`. CC's background-completion notices fold
|
|
162
|
+
* mid-turn because they explicitly say `priority:"next"`; its ultraplan/artifact notices take the
|
|
163
|
+
* default and deliberately do NOT. So all three of CC's values carry live delivery semantics
|
|
164
|
+
* (`now` = abort the running turn, `next` = fold into it, `later` = wait for the next one).
|
|
165
|
+
*
|
|
166
|
+
* The engineering conclusion the re-anchor reached — a background completion must reach a busy
|
|
167
|
+
* model at the boundary rather than starve behind a "deliver only when it would otherwise stop"
|
|
168
|
+
* rule — stands on its own. What does not stand is the claim that CC has no ladder. Restoring the
|
|
169
|
+
* `later` = "do not fold into the running turn" arm is a behavior-face change and `now` = "abort
|
|
170
|
+
* the running turn" is a new capability; both are ruled changes, not silent ones. Until then the
|
|
171
|
+
* gap is DISCLOSED at the injection funnel rather than left as a silently inert knob (`now` is
|
|
172
|
+
* announced, an unknown value is refused) — the bad-value loudness rule.
|
|
173
|
+
*/
|
|
147
174
|
priority: SystemInjectionPriority;
|
|
148
175
|
dedupKey: string;
|
|
149
176
|
payload: TPayload;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
2
|
+
export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
|
|
3
|
+
export function isSystemInjectionPriority(value) {
|
|
4
|
+
return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
|
|
5
|
+
}
|
|
2
6
|
const TASK_NOTIFICATION_TAGS = [
|
|
3
7
|
"task-notification",
|
|
4
8
|
"task-id",
|
|
@@ -214,6 +218,9 @@ export class SystemInjectionQueue {
|
|
|
214
218
|
listeners = new Set();
|
|
215
219
|
droppedOldest = 0;
|
|
216
220
|
enqueue(item) {
|
|
221
|
+
if (!isSystemInjectionPriority(item.priority)) {
|
|
222
|
+
throw new Error(`SystemInjectionQueue: unknown priority ${JSON.stringify(item.priority)} — expected one of ${SYSTEM_INJECTION_PRIORITIES.join("/")}`);
|
|
223
|
+
}
|
|
217
224
|
if (this.dedup.has(item.dedupKey))
|
|
218
225
|
return false;
|
|
219
226
|
this.dedup.add(item.dedupKey);
|
|
@@ -239,7 +246,10 @@ export class SystemInjectionQueue {
|
|
|
239
246
|
return true;
|
|
240
247
|
}
|
|
241
248
|
drain(priority) {
|
|
242
|
-
|
|
249
|
+
if (priority !== undefined && !isSystemInjectionPriority(priority)) {
|
|
250
|
+
throw new Error(`SystemInjectionQueue: unknown priority ${JSON.stringify(priority)} — expected one of ${SYSTEM_INJECTION_PRIORITIES.join("/")}`);
|
|
251
|
+
}
|
|
252
|
+
const priorities = priority ? [priority] : [...SYSTEM_INJECTION_PRIORITIES];
|
|
243
253
|
const out = [];
|
|
244
254
|
for (const p of priorities) {
|
|
245
255
|
const q = this.queues[p];
|
|
@@ -166,11 +166,23 @@ export declare function parkBackgroundAgentLane(core: DurableAgentCore, id: stri
|
|
|
166
166
|
* design/153 §7.1 (件1 codex HIGH split-brain fence) — the registry-aware reconciliation entry an
|
|
167
167
|
* IN-PROCESS deployment calls on its retention cadence. Two halves, one call:
|
|
168
168
|
* 1. LIVE half: this instance's own parked handles are probed against their checkpoint and, on
|
|
169
|
-
* expired/missing, settled failed THROUGH the handle (poll/stop/durable row
|
|
170
|
-
* quiescence poked, arbitration closure dropped, durable clear rides the settle
|
|
169
|
+
* expired/missing/DECIDED-ELSEWHERE, settled failed THROUGH the handle (poll/stop/durable row
|
|
170
|
+
* stay coherent — quiescence poked, arbitration closure dropped, durable clear rides the settle
|
|
171
|
+
* write).
|
|
171
172
|
* 2. STORE half: delegates to the store-level {@link reconcileParkedAgents} with
|
|
172
173
|
* `excludeWriterId` = this instance's writerId, so foreign (dead-process) rows are cleaned
|
|
173
174
|
* without ever touching a row this process is live-managing.
|
|
175
|
+
*
|
|
176
|
+
* backlog #400 — the live half used to skip EVERY non-expired/missing state, `resolved` included, and
|
|
177
|
+
* that was the whole zombie: a parked row whose checkpoint was decided by the task-level
|
|
178
|
+
* `resumeCheckpoint` leg (a serving layer's documented fallback when its parked-row discriminator
|
|
179
|
+
* misses) is decided but still says `parked`, its side effects already executed by a path that never
|
|
180
|
+
* touched this registry. Nothing else could reach it either — a re-claim answers
|
|
181
|
+
* `checkpoint_not_pending`, and the store half excludes exactly the rows this process live-manages —
|
|
182
|
+
* so it stayed `parked` for the life of the process. It now converges here. The one `resolved` case
|
|
183
|
+
* that must still be LET THROUGH is the registry's own claim window (reservation → consume flip): that
|
|
184
|
+
* skip is load-bearing, and it is now expressed as what it actually is — a claim in flight — instead
|
|
185
|
+
* of as a blanket state filter that also swallowed the case with no claimant at all.
|
|
174
186
|
*/
|
|
175
187
|
export declare function reconcileParkedAgentsLane(core: DurableAgentCore, stores: {
|
|
176
188
|
agentStore: import("./background-agent-store.js").BackgroundAgentStore;
|
|
@@ -275,7 +287,12 @@ export declare function abortBackgroundAgentsForOwnerLane(core: DurableAgentCore
|
|
|
275
287
|
* SendMessage consume, so the two verbs can never drift. Ladder:
|
|
276
288
|
* 0a. verbatim `handle.name` match — LATEST-WINS (CC SendMessage.md: "a newer agent took the name
|
|
277
289
|
* (latest wins)"; older rows stay task_id-addressable), any status (names keep working after
|
|
278
|
-
* completion — the resume face is the CC "resumes it from its transcript" counterpart)
|
|
290
|
+
* completion — the resume face is the CC "resumes it from its transcript" counterpart).
|
|
291
|
+
* **Latest-wins is RESOLUTION, not permission to redirect a conversation**: CC pairs the same
|
|
292
|
+
* resolver with a per-conversation pin guard (`send_message_pin_guard`), and SendMessage REFUSES
|
|
293
|
+
* a send whose name has changed hands since this caller last used it. That guard lives at the
|
|
294
|
+
* SendMessage face (`createSendMessageTool`), exactly as it does upstream — this resolver stays
|
|
295
|
+
* a pure resolver, and TaskStop keeps its own (preferRunning) posture untouched;
|
|
279
296
|
* 0b. normalized `handle.name` match — same latest-wins (the spawn regex bans whitespace, so
|
|
280
297
|
* normalization here is effectively NFKC+lowercase: "Builder"/"builder" share the name pool);
|
|
281
298
|
* 1+. the pre-[c209] description layers, UNCHANGED (exact → normalized, running-preferred +
|
|
@@ -484,12 +484,41 @@ export async function reconcileParkedAgentsLane(core, stores, scope, now, opts)
|
|
|
484
484
|
state = "unreadable";
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
-
|
|
487
|
+
let error;
|
|
488
|
+
if (state === "expired" || state === "missing") {
|
|
489
|
+
error = `approval checkpoint ${state} while parked (reconciled)`;
|
|
490
|
+
}
|
|
491
|
+
else if (state === "resolved") {
|
|
492
|
+
let row;
|
|
493
|
+
try {
|
|
494
|
+
row = await stores.agentStore.get(handle.id, scope);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (row === null || row.status !== "parked" || row.parkClaimId !== undefined)
|
|
500
|
+
continue;
|
|
501
|
+
try {
|
|
502
|
+
const reread = (await stores.checkpointStore.get(token));
|
|
503
|
+
if (reread === null || reread.status !== "resolved")
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
error =
|
|
510
|
+
"approval checkpoint resolved outside the registry's claim path while parked — the decision was " +
|
|
511
|
+
"redeemed by another leg and this row's outcome is unknown (reconciled)";
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
488
514
|
continue;
|
|
515
|
+
}
|
|
489
516
|
if (handle.status !== "parked")
|
|
490
517
|
continue;
|
|
518
|
+
if (handle.parkedCheckpointToken !== token)
|
|
519
|
+
continue;
|
|
491
520
|
handle.status = "failed";
|
|
492
|
-
handle.error =
|
|
521
|
+
handle.error = error;
|
|
493
522
|
handle.parkedCheckpointToken = undefined;
|
|
494
523
|
handle.resolveParkedStop = undefined;
|
|
495
524
|
handle.updatedAt = now;
|
|
@@ -383,6 +383,29 @@ export interface ToolPolicyProjection {
|
|
|
383
383
|
*/
|
|
384
384
|
readonly requiresLiveRemainder: boolean;
|
|
385
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the covering
|
|
388
|
+
* spellings (`<ns>__<peer>`, `<ns>__<peer>__<glob>`) of ANY protocol namespace, which CC's rule
|
|
389
|
+
* matcher resolves and an exact-membership test silently cannot. Every name-keyed lane in this file
|
|
390
|
+
* consults these ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*`
|
|
391
|
+
* is outside the minted charset and a minted name always carries a tool segment), so leaving it in the
|
|
392
|
+
* exact set too costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
|
|
393
|
+
*
|
|
394
|
+
* Namespace-generic on purpose: the covering grammar is a property of the shared `<prefix><peer>__<tool>`
|
|
395
|
+
* name shape, not of MCP, and keying it to one prefix made every sibling-namespace covering rule
|
|
396
|
+
* (`a2a__<peer>__*`) compile clean and match nothing — inert in the deny half, and in an ALLOW list the
|
|
397
|
+
* removal of that peer's whole tool set.
|
|
398
|
+
*
|
|
399
|
+
* The list is usually empty, which is why every call site tests it before scanning.
|
|
400
|
+
*
|
|
401
|
+
* Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
|
|
402
|
+
* `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
|
|
403
|
+
* list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
|
|
404
|
+
* way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
|
|
405
|
+
*/
|
|
406
|
+
export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
|
|
407
|
+
/** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */
|
|
408
|
+
export declare function namespacedCoveringHit(covering: readonly string[], toolName: string): boolean;
|
|
386
409
|
/**
|
|
387
410
|
* Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
|
|
388
411
|
* component's deny, or `undefined` when the projection has no opinion (it is deny-only by
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -5,7 +5,8 @@ import { join, normalize as normalizePath, posix as posixPath, sep, win32 as win
|
|
|
5
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
6
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
7
7
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
8
|
-
import { parsePermissionRule } from "./permission-rules.js";
|
|
8
|
+
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
9
|
+
import { protocolOf } from "./protocol-table.js";
|
|
9
10
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
11
|
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
11
12
|
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
@@ -36,16 +37,22 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
36
37
|
export function decisionText(d) {
|
|
37
38
|
return d.message;
|
|
38
39
|
}
|
|
40
|
+
export function namespacedCoveringEntries(entries) {
|
|
41
|
+
return (entries ?? []).filter(isNamespacedCoveringRuleName);
|
|
42
|
+
}
|
|
43
|
+
export function namespacedCoveringHit(covering, toolName) {
|
|
44
|
+
return covering.length > 0 && covering.some((e) => namespacedRuleNameCovers(e, toolName));
|
|
45
|
+
}
|
|
39
46
|
export function checkToolPolicyProjection(projection, req) {
|
|
40
47
|
for (const c of projection.components) {
|
|
41
48
|
if (c.kind === "tool_deny") {
|
|
42
|
-
if (c.names.includes(req.toolName)) {
|
|
49
|
+
if (c.names.includes(req.toolName) || namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
|
|
43
50
|
return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
|
|
44
51
|
}
|
|
45
52
|
continue;
|
|
46
53
|
}
|
|
47
54
|
if (c.kind === "tool_allowlist") {
|
|
48
|
-
if (!c.names.includes(req.toolName)) {
|
|
55
|
+
if (!c.names.includes(req.toolName) && !namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
|
|
49
56
|
return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
|
|
50
57
|
}
|
|
51
58
|
continue;
|
|
@@ -173,11 +180,15 @@ export function createAllowDenyPolicy(opts) {
|
|
|
173
180
|
return undefined;
|
|
174
181
|
const kept = [];
|
|
175
182
|
for (const entry of entries) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
const ns = protocolOf(entry);
|
|
184
|
+
if (ns !== undefined) {
|
|
185
|
+
const suffix = entry.slice(ns.prefix.length);
|
|
186
|
+
const sep = suffix.indexOf("__");
|
|
187
|
+
const malformed = suffix.length === 0 || sep === 0 || (sep > 0 && suffix.length === sep + 2);
|
|
188
|
+
if (malformed) {
|
|
189
|
+
const family = ns.id.toUpperCase();
|
|
190
|
+
const lesson = `a malformed ${family} tool name (empty peer or tool segment) — it can never match any mounted tool. ` +
|
|
191
|
+
`Use \`${ns.prefix}<peer>\` for every tool of a peer, or \`${ns.prefix}<peer>__<tool>\` for one tool.`;
|
|
181
192
|
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
182
193
|
continue;
|
|
183
194
|
}
|
|
@@ -218,6 +229,8 @@ export function createAllowDenyPolicy(opts) {
|
|
|
218
229
|
opts = { ...opts, ...(screenedAllow ? { allow: screenedAllow } : {}), ...(screenedDeny ? { deny: screenedDeny } : {}) };
|
|
219
230
|
const allow = opts.allow ? new Set(opts.allow) : undefined;
|
|
220
231
|
const deny = new Set(opts.deny ?? []);
|
|
232
|
+
const denyCovering = namespacedCoveringEntries(opts.deny);
|
|
233
|
+
const allowCovering = namespacedCoveringEntries(opts.allow);
|
|
221
234
|
return {
|
|
222
235
|
projection: {
|
|
223
236
|
components: [
|
|
@@ -229,10 +242,10 @@ export function createAllowDenyPolicy(opts) {
|
|
|
229
242
|
nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
|
|
230
243
|
check(req) {
|
|
231
244
|
const toolName = req.toolName;
|
|
232
|
-
if (deny.has(toolName)) {
|
|
245
|
+
if (deny.has(toolName) || namespacedCoveringHit(denyCovering, toolName)) {
|
|
233
246
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
234
247
|
}
|
|
235
|
-
if (allow && !allow.has(toolName)) {
|
|
248
|
+
if (allow && !allow.has(toolName) && !namespacedCoveringHit(allowCovering, toolName)) {
|
|
236
249
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
237
250
|
}
|
|
238
251
|
return ALLOW;
|
|
@@ -249,6 +262,9 @@ export function createApprovalPolicy(opts) {
|
|
|
249
262
|
const need = new Set(opts.requireApproval);
|
|
250
263
|
const deny = new Set(opts.deny ?? []);
|
|
251
264
|
const auto = new Set(opts.autoAllow ?? []);
|
|
265
|
+
const needCovering = namespacedCoveringEntries(opts.requireApproval);
|
|
266
|
+
const denyCovering = namespacedCoveringEntries(opts.deny);
|
|
267
|
+
const autoCovering = namespacedCoveringEntries(opts.autoAllow);
|
|
252
268
|
return {
|
|
253
269
|
projection: {
|
|
254
270
|
components: [
|
|
@@ -266,10 +282,10 @@ export function createApprovalPolicy(opts) {
|
|
|
266
282
|
],
|
|
267
283
|
async check(req, signal) {
|
|
268
284
|
const toolName = req.toolName;
|
|
269
|
-
if (deny.has(toolName)) {
|
|
285
|
+
if (deny.has(toolName) || namespacedCoveringHit(denyCovering, toolName)) {
|
|
270
286
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
271
287
|
}
|
|
272
|
-
if (need.has(toolName)) {
|
|
288
|
+
if (need.has(toolName) || namespacedCoveringHit(needCovering, toolName)) {
|
|
273
289
|
if (signal?.aborted) {
|
|
274
290
|
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" }, "task_aborted", req);
|
|
275
291
|
}
|
|
@@ -303,7 +319,7 @@ export function createApprovalPolicy(opts) {
|
|
|
303
319
|
}
|
|
304
320
|
return withCoreMintedResolution({ action: "deny", message: `approval denied for "${req.toolName}"`, settledBy: "human" }, "human_refused", req);
|
|
305
321
|
}
|
|
306
|
-
if (opts.denyByDefault && !auto.has(toolName)) {
|
|
322
|
+
if (opts.denyByDefault && !auto.has(toolName) && !namespacedCoveringHit(autoCovering, toolName)) {
|
|
307
323
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
308
324
|
}
|
|
309
325
|
return ALLOW;
|