@sema-agent/core 5.59.0 → 5.60.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 +40 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/errors.d.ts +18 -1
- package/dist/brain/errors.js +7 -1
- package/dist/brain/input-too-long.d.ts +57 -0
- package/dist/brain/input-too-long.js +35 -0
- package/dist/brain/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +116 -19
- package/dist/core/checkpoint-store.js +15 -8
- package/dist/core/context-edit.d.ts +243 -41
- package/dist/core/context-edit.js +247 -32
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +5 -0
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.js +10 -6
- package/dist/core/memory-engine/content-origin.d.ts +24 -2
- package/dist/core/memory-engine/content-origin.js +6 -1
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/permission-rule-consent.js +9 -5
- package/dist/core/runner/prepare-config-doors.d.ts +22 -1
- package/dist/core/runner/prepare-config-doors.js +36 -0
- package/dist/core/runner/prepare-task.d.ts +28 -1
- package/dist/core/runner/prepare-task.js +106 -8
- package/dist/core/runner/runtask.js +41 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +24 -8
- package/dist/core/tool-policy.js +3 -3
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +127 -2
- package/dist/core/untrusted-text.d.ts +11 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/llm/types.d.ts +21 -2
- package/dist/engine/loop/agent-loop.js +7 -1
- package/dist/engine/loop/types.d.ts +4 -1
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
|
@@ -93,7 +93,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
93
93
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
94
94
|
* no longer minted.
|
|
95
95
|
*/
|
|
96
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
|
|
96
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
|
|
97
97
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
98
98
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
99
99
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -17,6 +17,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
17
17
|
"memory.session_polluted",
|
|
18
18
|
"memory.harvest_quarantined",
|
|
19
19
|
"memory.delegation_static_mark_waived",
|
|
20
|
+
"memory.content_class_declared",
|
|
20
21
|
"memory.consolidation_driver_superseded",
|
|
21
22
|
"memory.challenge_sweep_failed",
|
|
22
23
|
"memory.lineage_settle_failed",
|
|
@@ -85,6 +86,7 @@ export const RULE_SYNC_DROP_CODES = {
|
|
|
85
86
|
server_rejected: "local-quarantined",
|
|
86
87
|
};
|
|
87
88
|
export const ENGINE_NOTICE_CODES = [
|
|
89
|
+
"config.autocompact_window_clamped",
|
|
88
90
|
"config.env_timeout_discarded",
|
|
89
91
|
"config.materialize_env_discarded",
|
|
90
92
|
"config.models_swapped",
|
|
@@ -98,6 +100,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
98
100
|
"memory.session_polluted",
|
|
99
101
|
"memory.harvest_quarantined",
|
|
100
102
|
"memory.delegation_static_mark_waived",
|
|
103
|
+
"memory.content_class_declared",
|
|
101
104
|
"memory.hold_opened",
|
|
102
105
|
"memory.hold_released",
|
|
103
106
|
"memory.hold_disposed",
|
|
@@ -122,6 +125,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
122
125
|
"memory.hold_disposed": "user",
|
|
123
126
|
"task.user_steer_undrained": "user",
|
|
124
127
|
"task.user_followup_undrained": "user",
|
|
128
|
+
"config.autocompact_window_clamped": "operator",
|
|
125
129
|
"config.env_timeout_discarded": "operator",
|
|
126
130
|
"config.materialize_env_discarded": "operator",
|
|
127
131
|
"config.models_swapped": "operator",
|
|
@@ -132,6 +136,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
132
136
|
"delegation.transcript_integrity": "operator",
|
|
133
137
|
"mcp.revocation_probe_failed": "operator",
|
|
134
138
|
"workflow.governance_key_stripped": "operator",
|
|
139
|
+
"memory.content_class_declared": "operator",
|
|
135
140
|
"memory.consolidation_recommended": "operator",
|
|
136
141
|
"memory.consolidation_committed": "operator",
|
|
137
142
|
"memory.consolidation_conflict": "operator",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunnerDeps, TaskSpec } from "./types.js";
|
|
1
|
+
import type { McpServerSpec, RunnerDeps, TaskSpec } from "./types.js";
|
|
2
2
|
import type { ToolPolicy } from "./tool-policy.js";
|
|
3
3
|
/** The closed set of administratively lockable configuration keys. Adding a member is a deliberate
|
|
4
4
|
* edit HERE (tsc forces the registry row), never a free-form string. */
|
|
@@ -38,9 +38,12 @@ export interface LockedConfig {
|
|
|
38
38
|
export interface LockedPreflight {
|
|
39
39
|
/** The validated lock set (empty when the deployment declares none). */
|
|
40
40
|
lockedKeys: ReadonlySet<LockedKey>;
|
|
41
|
-
/** Effective MCP server list for this task: `spec.mcp` when the
|
|
42
|
-
*
|
|
43
|
-
* no
|
|
41
|
+
/** Effective MCP server list for this task: a validated, frozen SNAPSHOT of `spec.mcp` when the
|
|
42
|
+
* key is unlocked (see {@link snapshotAndValidateMcpEntries} — the content-class declaration is read once
|
|
43
|
+
* and judged here, so no mint site downstream can be handed a different value than the door
|
|
44
|
+
* approved); with `mcp` locked a spec-supplied value has already been refused, so this is always
|
|
45
|
+
* `undefined` (core has no deployment-level MCP seat — a locked deployment mounts no
|
|
46
|
+
* task-supplied servers). */
|
|
44
47
|
mcp: TaskSpec["mcp"];
|
|
45
48
|
/** Effective caller tool policy: the same `spec.toolPolicy ?? deps.toolPolicy` slot the gate has
|
|
46
49
|
* always enforced, resolved ONCE here. With `toolPolicy` locked, the deployment's own
|
|
@@ -61,6 +64,35 @@ export interface LockedPreflight {
|
|
|
61
64
|
* then fails the first prepare instead of silently guarding nothing).
|
|
62
65
|
*/
|
|
63
66
|
export declare function resolveLockedKeys(config: LockedConfig | undefined): ReadonlySet<LockedKey>;
|
|
67
|
+
/**
|
|
68
|
+
* design/378 — turn a caller-owned MCP entry list into FROZEN PLAIN DATA, reading every own property
|
|
69
|
+
* exactly once.
|
|
70
|
+
*
|
|
71
|
+
* WHY A COPY. `McpServerSpec.contentOrigin` decides whether a server's tools mark the session's
|
|
72
|
+
* memory, and the entry objects travel a long way: the preparation door validates them, then
|
|
73
|
+
* `materializeMcpTools` connects them, then every mid-task refresh re-reads the same object. A value
|
|
74
|
+
* read off a LIVE object is only true for as long as the object holds still, and the fail-open
|
|
75
|
+
* direction is the reachable one — so both boundaries consume data instead.
|
|
76
|
+
*
|
|
77
|
+
* The read discipline is the load-bearing part, and it is why this is a spread rather than
|
|
78
|
+
* "read the property, then copy the object": a getter answering ALTERNATING LEGAL values would
|
|
79
|
+
* satisfy any number of re-reads (`"external"` to a validator, `"local"` to a mint site), and no
|
|
80
|
+
* vocabulary guard anywhere has purchase on a legal value. One spread evaluates every own property
|
|
81
|
+
* EXACTLY ONCE and materializes the result as plain data; every later reader — validator, connect,
|
|
82
|
+
* refresh — then sees the same byte. Frozen so nothing downstream rewrites it.
|
|
83
|
+
*
|
|
84
|
+
* Boundary, stated: the copy is one level deep — nested option objects (transport, headers, env,
|
|
85
|
+
* toolAxes) stay shared with the caller's entry exactly as before, because the axis this owns is the
|
|
86
|
+
* content class; readers of the nested shapes normalize their own values (the transport DISCRIMINATOR
|
|
87
|
+
* is derived, never echoed raw). Own ENUMERABLE properties only: an entry whose fields live on a
|
|
88
|
+
* prototype was never the plain configuration record this type describes, and its absent class reads
|
|
89
|
+
* fail-closed like any other absence.
|
|
90
|
+
*
|
|
91
|
+
* Exported because the SECOND boundary needs the identical guarantee: `materializeMcpTools` is public
|
|
92
|
+
* and a library caller reaches it without passing any door, so it snapshots its own input through
|
|
93
|
+
* this same function (idempotent over an already-frozen entry). One spelling, both entrances.
|
|
94
|
+
*/
|
|
95
|
+
export declare function snapshotMcpEntries(entries: readonly McpServerSpec[]): McpServerSpec[];
|
|
64
96
|
/**
|
|
65
97
|
* The single preflight resolver: validate the lock declaration, refuse a spec that tries to occupy
|
|
66
98
|
* a locked slot (`config.locked_key`, two-state — the whole prepare is rejected, nothing is
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isToolContentOrigin, TOOL_CONTENT_ORIGINS } from "./memory-engine/content-origin.js";
|
|
2
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
1
3
|
export const LOCKED_KEY_REGISTRY = {
|
|
2
4
|
mcp: { specFields: ["mcp"] },
|
|
3
5
|
toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] },
|
|
@@ -23,6 +25,37 @@ export function resolveLockedKeys(config) {
|
|
|
23
25
|
}
|
|
24
26
|
return keys;
|
|
25
27
|
}
|
|
28
|
+
export function snapshotMcpEntries(entries) {
|
|
29
|
+
return entries.map((entry) => Object.freeze({ ...entry }));
|
|
30
|
+
}
|
|
31
|
+
function describeDeclared(value) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value) ?? String(value);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
try {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return `[unrepresentable ${typeof value}]`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function snapshotAndValidateMcpEntries(entries) {
|
|
45
|
+
const copies = snapshotMcpEntries(entries);
|
|
46
|
+
for (const copy of copies) {
|
|
47
|
+
const declared = copy.contentOrigin;
|
|
48
|
+
if (declared !== undefined && !isToolContentOrigin(declared)) {
|
|
49
|
+
throw codedError("config.mcp_content_class", `MCP server ${inlineUntrusted(typeof copy.name === "string" ? JSON.stringify(copy.name) : describeDeclared(copy.name), 80)} declares contentOrigin ` +
|
|
50
|
+
`${inlineUntrusted(describeDeclared(declared), 80)}, which is not one of ` +
|
|
51
|
+
`${TOOL_CONTENT_ORIGINS.map((v) => `"${v}"`).join(" | ")}. The declaration decides whether this server's tools ` +
|
|
52
|
+
`mark the session's memory externally exposed — a value this engine cannot read is refused at the door rather ` +
|
|
53
|
+
`than folded to a class, because folding it would let a deployment believe a declaration is in force while every ` +
|
|
54
|
+
`call keeps writing a one-way durable mark.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return copies;
|
|
58
|
+
}
|
|
26
59
|
export function preflightLockedConfig(spec, deps) {
|
|
27
60
|
const lockedKeys = resolveLockedKeys(deps.lockedConfig);
|
|
28
61
|
for (const key of lockedKeys) {
|
|
@@ -36,7 +69,7 @@ export function preflightLockedConfig(spec, deps) {
|
|
|
36
69
|
}
|
|
37
70
|
return {
|
|
38
71
|
lockedKeys,
|
|
39
|
-
mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
|
|
72
|
+
mcp: lockedKeys.has("mcp") || spec.mcp === undefined ? undefined : snapshotAndValidateMcpEntries(spec.mcp),
|
|
40
73
|
toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
|
|
41
74
|
basePolicyForResumeEdit: lockedKeys.has("toolPolicy") ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit),
|
|
42
75
|
};
|
package/dist/core/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ import { truncateError } from "./tool-errors.js";
|
|
|
14
14
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
15
15
|
import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
|
|
16
16
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
17
|
+
import { snapshotMcpEntries } from "./locked-config.js";
|
|
17
18
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
18
19
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
19
20
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
@@ -753,8 +754,9 @@ export function mcpToolSchemaProblem(schema) {
|
|
|
753
754
|
return undefined;
|
|
754
755
|
}
|
|
755
756
|
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
|
|
757
|
+
const entries = snapshotMcpEntries(specs);
|
|
756
758
|
{
|
|
757
|
-
const collision = findNamespacePrefixCollision(MCP_NAMESPACE,
|
|
759
|
+
const collision = findNamespacePrefixCollision(MCP_NAMESPACE, entries.map((s) => s.name));
|
|
758
760
|
if (collision) {
|
|
759
761
|
const [a, b] = collision.peers;
|
|
760
762
|
const e = new Error(a === b
|
|
@@ -801,11 +803,11 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
801
803
|
const droppedTools = [];
|
|
802
804
|
let disposing = false;
|
|
803
805
|
const serverHandles = [];
|
|
804
|
-
const settled = await Promise.allSettled(
|
|
806
|
+
const settled = await Promise.allSettled(entries.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
|
|
805
807
|
try {
|
|
806
|
-
for (let i = 0; i <
|
|
808
|
+
for (let i = 0; i < entries.length; i++) {
|
|
807
809
|
const r = settled[i];
|
|
808
|
-
const spec =
|
|
810
|
+
const spec = entries[i];
|
|
809
811
|
if (r.status === "fulfilled") {
|
|
810
812
|
const s = r.value;
|
|
811
813
|
clients.push(s.client);
|
|
@@ -1513,6 +1515,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1513
1515
|
}
|
|
1514
1516
|
}
|
|
1515
1517
|
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1518
|
+
const declaredContentOrigin = spec.contentOrigin;
|
|
1516
1519
|
const serverTools = [];
|
|
1517
1520
|
const serverAxes = [];
|
|
1518
1521
|
const dropped = [];
|
|
@@ -1576,7 +1579,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1576
1579
|
const mcpToolMeta = t._meta;
|
|
1577
1580
|
const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
|
|
1578
1581
|
const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
|
|
1579
|
-
|
|
1582
|
+
const mounted = {
|
|
1580
1583
|
name: namespacedName,
|
|
1581
1584
|
description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
|
|
1582
1585
|
label: `${spec.name}:${remoteName}`,
|
|
@@ -1678,7 +1681,8 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1678
1681
|
return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
|
|
1679
1682
|
return { content, details: res, terminate: false };
|
|
1680
1683
|
},
|
|
1681
|
-
}
|
|
1684
|
+
};
|
|
1685
|
+
serverTools.push(declaredContentOrigin !== undefined ? withContentOrigin(mounted, declaredContentOrigin) : mounted);
|
|
1682
1686
|
}
|
|
1683
1687
|
return { serverTools, serverAxes, dropped, advisories };
|
|
1684
1688
|
}
|
|
@@ -22,13 +22,22 @@
|
|
|
22
22
|
* does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
|
|
23
23
|
* explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
|
|
24
24
|
* defaults local: core owns those definitions, and the fail-closed default exists for tools core
|
|
25
|
-
* CANNOT vouch for, not for its own.
|
|
25
|
+
* CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
|
|
26
|
+
* closed vocabulary — an unreadable one is not a declaration.
|
|
27
|
+
*
|
|
28
|
+
* The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
|
|
29
|
+
* not bring content from outside the deployment's trust boundary", which a unix socket to another
|
|
30
|
+
* process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
|
|
31
|
+
* does not. Distance is not the axis; whose content it is, is.
|
|
26
32
|
*
|
|
27
33
|
* There is deliberately no "gate inactive" state: every mounted tool gets a class.
|
|
28
34
|
*/
|
|
29
35
|
import type { ToolContentOrigin } from "../types.js";
|
|
30
36
|
export interface ClassifyToolContentOriginInput {
|
|
31
|
-
/** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present
|
|
37
|
+
/** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
|
|
38
|
+
* a member of the closed vocabulary ({@link isToolContentOrigin}); a present non-member is the
|
|
39
|
+
* unreadable-declaration case and classifies EXTERNAL. The annotation is intent, not a guarantee:
|
|
40
|
+
* the property rides tool objects untyped. */
|
|
32
41
|
declared?: ToolContentOrigin;
|
|
33
42
|
/** True ⇔ the name belongs to a protocol namespace (mcp__/a2a__ …) — an external channel. */
|
|
34
43
|
isProtocolTool: boolean;
|
|
@@ -37,6 +46,10 @@ export interface ClassifyToolContentOriginInput {
|
|
|
37
46
|
/** True ⇔ `TaskSpec.memory.trustedTools` names this tool (explicit host exemption). */
|
|
38
47
|
trusted: boolean;
|
|
39
48
|
}
|
|
49
|
+
/** The vocabulary in wire/message order — the one spelling list a refusal quotes back. */
|
|
50
|
+
export declare const TOOL_CONTENT_ORIGINS: readonly ToolContentOrigin[];
|
|
51
|
+
/** Runtime membership in the closed vocabulary. */
|
|
52
|
+
export declare function isToolContentOrigin(value: unknown): value is ToolContentOrigin;
|
|
40
53
|
export declare function classifyToolContentOrigin(input: ClassifyToolContentOriginInput): ToolContentOrigin;
|
|
41
54
|
/**
|
|
42
55
|
* Declare `origin` on a tool object built OUTSIDE `defineTool` — a raw AgentTool literal, which is
|
|
@@ -60,6 +73,15 @@ export interface AgentToolFace {
|
|
|
60
73
|
export interface AgentPoolTool {
|
|
61
74
|
name: string;
|
|
62
75
|
aliases?: readonly string[];
|
|
76
|
+
/** The pool row's own declaration — a STATIC surface the deployment writes when it composes the
|
|
77
|
+
* delegation tool, and the only class information this judgment has about the child's roster.
|
|
78
|
+
*
|
|
79
|
+
* A per-entry MCP declaration (design/378, `McpServerSpec.contentOrigin`) does NOT reach here: the
|
|
80
|
+
* pool is a declaration, not a live roster, and this judgment never sees the server entries. A
|
|
81
|
+
* deployment that hands a declared server's `mcp__` tools to children MIRRORS the value onto the
|
|
82
|
+
* matching pool rows; not mirroring it leaves them on the protocol arm, so the delegation reads
|
|
83
|
+
* external and the parent over-marks — the safe direction, and the reason this is a documented
|
|
84
|
+
* duty rather than a refusal. */
|
|
63
85
|
contentOrigin?: ToolContentOrigin;
|
|
64
86
|
}
|
|
65
87
|
/**
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
|
|
2
|
+
export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
|
|
3
|
+
export function isToolContentOrigin(value) {
|
|
4
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(TOOL_CONTENT_ORIGIN_SET, value);
|
|
5
|
+
}
|
|
1
6
|
export function classifyToolContentOrigin(input) {
|
|
2
7
|
if (input.declared !== undefined)
|
|
3
|
-
return input.declared;
|
|
8
|
+
return isToolContentOrigin(input.declared) ? input.declared : "external";
|
|
4
9
|
if (input.trusted)
|
|
5
10
|
return "local";
|
|
6
11
|
if (input.isProtocolTool)
|
package/dist/core/memory.d.ts
CHANGED
|
@@ -477,6 +477,16 @@ export interface NormalizedMemorySpec {
|
|
|
477
477
|
* of the fail-closed `"external"` default. The explicit-config channel for a deployment whose custom
|
|
478
478
|
* tools predate the {@link import("./types.js").ToolSpec.contentOrigin} declaration; it never
|
|
479
479
|
* overrides a tool's OWN declaration. Normalized: trimmed, deduped, empties dropped.
|
|
480
|
+
*
|
|
481
|
+
* NOT the channel for MCP servers (design/378). A per-name entry here keys on the MINTED name — the
|
|
482
|
+
* host has to predict the namespacing and charset normalization — covers nothing a mid-task refresh
|
|
483
|
+
* adds, and says "exempt this undeclared tool" where the fact is "this whole server is mine".
|
|
484
|
+
* {@link import("./types.js").McpServerSpec.contentOrigin} states that fact at the entry, and being
|
|
485
|
+
* a DECLARATION it wins over this allowlist in both directions: a declared server's tools leave this
|
|
486
|
+
* list's reach entirely, an explicit `"external"` declaration included (that is the one way a
|
|
487
|
+
* deployment can pin a server's tools BEYOND this exemption). This list is not retired — it remains
|
|
488
|
+
* the in-register channel for undeclared HOST tools and for a deployment not ready to touch its
|
|
489
|
+
* server entries.
|
|
480
490
|
*/
|
|
481
491
|
trustedTools?: string[];
|
|
482
492
|
/**
|
|
@@ -70,6 +70,7 @@ function syntheticCheckpoint(scope) {
|
|
|
70
70
|
toolName: "SelfCheck",
|
|
71
71
|
args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
|
|
72
72
|
boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
|
|
73
|
+
hasBidiControls: true,
|
|
73
74
|
batchToolCallIds: [`${scope}:call`],
|
|
74
75
|
completedCallIds: [],
|
|
75
76
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
@@ -115,7 +115,7 @@ function approvalRecordDamageOf(rec) {
|
|
|
115
115
|
const seen = new Set();
|
|
116
116
|
for (const member of members) {
|
|
117
117
|
if (!Number.isInteger(member) || member < 0 || rec.candidates[member] === undefined) {
|
|
118
|
-
return `offer ${at} references candidate ${member}, which the record does not carry`;
|
|
118
|
+
return `offer ${at} references candidate ${escapeForDisclosure(member)}, which the record does not carry`;
|
|
119
119
|
}
|
|
120
120
|
const cand = rec.candidates[member];
|
|
121
121
|
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
|
|
@@ -138,10 +138,14 @@ function approvalRecordDamageOf(rec) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined) {
|
|
141
|
-
return `${rec.kind} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
141
|
+
return `${escapeForDisclosure(rec.kind)} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
for (const [at] of rec.candidates.entries()) {
|
|
146
|
+
if (!claimed.has(at))
|
|
147
|
+
return `candidate ${at} belongs to no offer — every candidate row is claimed by exactly one offer`;
|
|
148
|
+
}
|
|
145
149
|
return undefined;
|
|
146
150
|
}
|
|
147
151
|
export function ruleOffersOfRecord(rec) {
|
|
@@ -325,7 +329,7 @@ function checkEditedRuleText(text, command) {
|
|
|
325
329
|
if (!ruleAdmitsCommand(parsed.rule, command)) {
|
|
326
330
|
return {
|
|
327
331
|
ok: false,
|
|
328
|
-
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${command}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
332
|
+
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${escapeForDisclosure(command)}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
329
333
|
};
|
|
330
334
|
}
|
|
331
335
|
return { ok: true, canonicalRule: parsed.rule.rule };
|
|
@@ -342,7 +346,7 @@ export function precheckEditedRuleText(text, command) {
|
|
|
342
346
|
throw e;
|
|
343
347
|
}
|
|
344
348
|
if (hasUnrenderableCharacters(command)) {
|
|
345
|
-
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is
|
|
349
|
+
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is refused at this preflight face (likelier a caller wiring fault than a card's command; the in-engine confirmation arm still judges such a card, with the command escaped on its refusal line)");
|
|
346
350
|
e.code = "config.invalid_argument";
|
|
347
351
|
throw e;
|
|
348
352
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
23
23
|
import type { Model } from "../../internal/llm.js";
|
|
24
24
|
import type { ThinkingLevel } from "../../internal/harness.js";
|
|
25
|
+
import type { ContextEditMachine } from "../context-edit.js";
|
|
25
26
|
import { type LockedPreflight } from "../locked-config.js";
|
|
26
27
|
import { type ResolvedRole } from "../roles.js";
|
|
27
28
|
import type { SessionStore } from "../session.js";
|
|
@@ -119,6 +120,13 @@ export interface PrepareConfigDoorsResult {
|
|
|
119
120
|
/** owned — the resolved interaction posture (spec > spawning run > deployment); the
|
|
120
121
|
* AskUserQuestion mount and the child-ctx injection read this same value. */
|
|
121
122
|
resolvedInteractionPosture: "interactive" | "headless" | undefined;
|
|
123
|
+
/** owned — the validated/resolved microCompact knob (design/374; {@link resolveMicroCompactKnob}
|
|
124
|
+
* for the door + placement reasoning). The per-run state builder consumes exactly these values —
|
|
125
|
+
* the deps bag is never re-read after this door. */
|
|
126
|
+
microCompactKnob: {
|
|
127
|
+
machine: ContextEditMachine;
|
|
128
|
+
clearOnRejection: boolean;
|
|
129
|
+
};
|
|
122
130
|
/** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
|
|
123
131
|
resolvedRole: ResolvedRole;
|
|
124
132
|
/** owned — the resolved main model (`resolvedRole.model`, re-exposed as the name every later
|
|
@@ -163,5 +171,18 @@ export interface PrepareConfigDoorsResult {
|
|
|
163
171
|
* the resolver's off states). */
|
|
164
172
|
brainCallGuardrailMs: number | undefined;
|
|
165
173
|
}
|
|
166
|
-
/**
|
|
174
|
+
/** design/374 — the microCompact knob door (坏值响亮度默认律, the sibling knob doors' form:
|
|
175
|
+
* interactionPosture / toolModelGate / staleToolResultOffload). Placement is load-bearing (review
|
|
176
|
+
* r3): it runs in THIS pre-first-await config-doors phase so a malformed declaration refuses
|
|
177
|
+
* BEFORE any resource-bearing preparation — session acquisition, execution-env creation, the MCP
|
|
178
|
+
* dial — whose external effects a late refusal's teardown cannot undo. SINGLE READ per seat, and
|
|
179
|
+
* the CONTAINER is judged first (reviews r1/r2): an accessor-backed bag answering a legal value at
|
|
180
|
+
* one read and junk at another must be caught by whichever read this door takes, and a
|
|
181
|
+
* `microCompact: "cc"` / null / number / array bag makes both property reads answer undefined —
|
|
182
|
+
* it must refuse exactly like a malformed field, never fold to the defaults. The RESOLVED values
|
|
183
|
+
* returned here are immutable and are the state builder's ONLY source — nothing re-reads the bag. */
|
|
184
|
+
export declare function resolveMicroCompactKnob(bag: RunnerDeps["microCompact"]): {
|
|
185
|
+
machine: ContextEditMachine;
|
|
186
|
+
clearOnRejection: boolean;
|
|
187
|
+
};
|
|
167
188
|
export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;
|
|
@@ -83,10 +83,45 @@ export function resolveModelPromptTraits(model, spec, internals) {
|
|
|
83
83
|
fableMitigations: isFableFamilyModelId(model.id),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
function microCompactConfigError(field, value, legal) {
|
|
87
|
+
let shown;
|
|
88
|
+
try {
|
|
89
|
+
shown = JSON.stringify(value) ?? String(value);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
try {
|
|
93
|
+
shown = String(value);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
shown = `[unrepresentable ${typeof value}]`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const seat = field === undefined ? "microCompact" : `microCompact.${field}`;
|
|
100
|
+
const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` +
|
|
101
|
+
`never folded to the default: a silently-ignored opt-in would run the pre-374 machine while the ` +
|
|
102
|
+
`deployment believes it opted in.`);
|
|
103
|
+
e.code = "config.microcompact_invalid";
|
|
104
|
+
return e;
|
|
105
|
+
}
|
|
106
|
+
export function resolveMicroCompactKnob(bag) {
|
|
107
|
+
if (bag !== undefined && (typeof bag !== "object" || bag === null || Array.isArray(bag))) {
|
|
108
|
+
throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys");
|
|
109
|
+
}
|
|
110
|
+
const declaredMachine = bag?.machine;
|
|
111
|
+
if (declaredMachine !== undefined && declaredMachine !== "legacy" && declaredMachine !== "cc") {
|
|
112
|
+
throw microCompactConfigError("machine", declaredMachine, `"legacy" | "cc"`);
|
|
113
|
+
}
|
|
114
|
+
const declaredClearOnRejection = bag?.clearOnRejection;
|
|
115
|
+
if (declaredClearOnRejection !== undefined && typeof declaredClearOnRejection !== "boolean") {
|
|
116
|
+
throw microCompactConfigError("clearOnRejection", declaredClearOnRejection, "a boolean");
|
|
117
|
+
}
|
|
118
|
+
return { machine: declaredMachine ?? "legacy", clearOnRejection: declaredClearOnRejection === true };
|
|
119
|
+
}
|
|
86
120
|
export function prepareConfigDoors(input) {
|
|
87
121
|
const { deps, sessions, resume, internals } = input;
|
|
88
122
|
let spec = input.spec;
|
|
89
123
|
assertRestoreGatedToolsValue(spec.restoreGatedTools);
|
|
124
|
+
const microCompactKnob = resolveMicroCompactKnob(deps.microCompact);
|
|
90
125
|
const assertToolNameListValue = (value, seat) => {
|
|
91
126
|
if (value === undefined)
|
|
92
127
|
return;
|
|
@@ -318,6 +353,7 @@ export function prepareConfigDoors(input) {
|
|
|
318
353
|
promptProfile,
|
|
319
354
|
lockedPreflight,
|
|
320
355
|
resolvedInteractionPosture,
|
|
356
|
+
microCompactKnob,
|
|
321
357
|
resolvedRole,
|
|
322
358
|
model,
|
|
323
359
|
thinking,
|
|
@@ -20,6 +20,7 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
|
|
|
20
20
|
import { type GitStatusLaneRef } from "./git-status-frame.js";
|
|
21
21
|
import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
|
|
22
22
|
import type { ToolDisclosureManifest } from "../trace.js";
|
|
23
|
+
import { type ClearedProjectionLedger, type ContextEditMachine, type OccurrenceIndex } from "../context-edit.js";
|
|
23
24
|
import type { TaskNotificationPayload } from "../task-notification.js";
|
|
24
25
|
import { type CwdRef, type ReadFace } from "../../tools/fs/index.js";
|
|
25
26
|
import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
|
|
@@ -134,7 +135,6 @@ export declare function checkpointScopeOf(spec: {
|
|
|
134
135
|
export { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
135
136
|
export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
136
137
|
export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
|
|
137
|
-
/** Everything the run loop needs, built once by {@link prepareTask} (task setup, isolated from the loop). */
|
|
138
138
|
export interface Prepared {
|
|
139
139
|
harness: AgentHarness;
|
|
140
140
|
/** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
|
|
@@ -876,6 +876,33 @@ export interface Prepared {
|
|
|
876
876
|
trimPressureRef: {
|
|
877
877
|
droppedMessages: boolean;
|
|
878
878
|
};
|
|
879
|
+
/** design/374 slices 1b/2 — the microCompact machine state this run: the selected clearing
|
|
880
|
+
* machine, the cleared-projection ledger (request-view application, durable decisions — see
|
|
881
|
+
* `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
|
|
882
|
+
* start EMPTY by construction), the last request's projection seat (what the provider actually
|
|
883
|
+
* saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
|
|
884
|
+
* raw session rebuild), and the MC-R knob. Default machine "legacy" + MC-R off ⇒ the ledger
|
|
885
|
+
* never gains an entry and every replay is a same-reference no-op (default bytes unchanged). */
|
|
886
|
+
microCompact: PreparedMicroCompact;
|
|
887
|
+
}
|
|
888
|
+
/** See {@link Prepared.microCompact}. */
|
|
889
|
+
export interface PreparedMicroCompact {
|
|
890
|
+
machine: ContextEditMachine;
|
|
891
|
+
/** MC-R (design/374 §3.2): one-shot clear-and-retry on a provider input-too-long rejection.
|
|
892
|
+
* Default false (X2: lands with the machinery, flips with slice 3). */
|
|
893
|
+
clearOnRejection: boolean;
|
|
894
|
+
ledger: ClearedProjectionLedger;
|
|
895
|
+
projectionRef: {
|
|
896
|
+
current?: {
|
|
897
|
+
/** The FINAL projected view of the last provider request (post trim/sweep). */
|
|
898
|
+
messages: AgentMessage[];
|
|
899
|
+
/** Occurrence coordinates of that view (object-identity first, unambiguous-group fallback). */
|
|
900
|
+
keyOf: OccurrenceIndex["keyOf"];
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
/** The same offload persist seat the frontier machine uses (write-once, idempotent), so MC-R
|
|
904
|
+
* clears compose identical markers. Absent when no offload store is configured. */
|
|
905
|
+
offloadPersist?: (toolCallId: string, fullText: string) => string;
|
|
879
906
|
}
|
|
880
907
|
/**
|
|
881
908
|
* WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
|