@sema-agent/core 5.49.0 → 5.50.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 +49 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.js +4 -1
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +15 -1
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/package.json +1 -1
package/dist/brain/reasoning.js
CHANGED
|
@@ -10,28 +10,52 @@ const RANK = {
|
|
|
10
10
|
export function isThinkingLevel(v) {
|
|
11
11
|
return typeof v === "string" && Object.prototype.hasOwnProperty.call(RANK, v);
|
|
12
12
|
}
|
|
13
|
+
export function reasoningRequestCarried(model, reasoning) {
|
|
14
|
+
return !!model.reasoning && isThinkingLevel(reasoning) && reasoning !== "off";
|
|
15
|
+
}
|
|
13
16
|
export function rankOf(level) {
|
|
14
17
|
return RANK[level];
|
|
15
18
|
}
|
|
16
19
|
export const DEFAULT_EFFORT_LEVELS = ["minimal", "low", "medium", "high"];
|
|
17
20
|
const BINARY_FORMATS = new Set(["qwen", "zai", "qwen-chat-template"]);
|
|
18
21
|
export const RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-chatgpt-responses"]);
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
export const MIN_THINKING_TOKENS = 1024;
|
|
23
|
+
export function budgetCapSkipsThinking(outputCapTokens, hardCap) {
|
|
24
|
+
return hardCap && outputCapTokens < MIN_THINKING_TOKENS * 2;
|
|
25
|
+
}
|
|
26
|
+
export function declaredEffortLevels(v) {
|
|
27
|
+
return Array.isArray(v) && v.length > 0 ? v : undefined;
|
|
28
|
+
}
|
|
29
|
+
export function mintEffortWireValue(requested, model, allowed) {
|
|
30
|
+
const resolution = resolveEffort(requested, allowed ?? DEFAULT_EFFORT_LEVELS);
|
|
31
|
+
let wireValue = resolution.effective;
|
|
32
|
+
const mapped = model.thinkingLevelMap?.[resolution.effective];
|
|
33
|
+
if (mapped === null)
|
|
34
|
+
wireValue = undefined;
|
|
35
|
+
else if (mapped !== undefined)
|
|
36
|
+
wireValue = mapped;
|
|
37
|
+
return { resolution, wireValue };
|
|
38
|
+
}
|
|
39
|
+
export function resolveReasoning(requested, model, facts) {
|
|
40
|
+
const resolved = dispatchReasoning(requested, model, facts);
|
|
41
|
+
if (requested !== "off" && !reasoningRequestCarried(model, requested)) {
|
|
22
42
|
return { requested, effective: "off", graded: false, clamped: true, format: resolved.format, endpoint: resolved.endpoint, dropped: true };
|
|
23
43
|
}
|
|
24
44
|
return resolved;
|
|
25
45
|
}
|
|
26
|
-
function
|
|
27
|
-
return requested !== "off" && model.thinkingLevelMap?.[effective] === null;
|
|
28
|
-
}
|
|
29
|
-
function dispatchReasoning(requested, model) {
|
|
46
|
+
function dispatchReasoning(requested, model, facts) {
|
|
30
47
|
const endpoint = model.api ?? "unknown";
|
|
31
48
|
const compat = (model.compat ?? {});
|
|
32
49
|
if (model.api === "anthropic-messages") {
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
const declaredAnthropic = declaredEffortLevels(compat.effortLevels);
|
|
51
|
+
if (declaredAnthropic !== undefined) {
|
|
52
|
+
return { ...resolveEffort(requested, declaredAnthropic), format: "effort", endpoint };
|
|
53
|
+
}
|
|
54
|
+
if (requested !== "off" &&
|
|
55
|
+
compat.thinkingMode !== "adaptive" &&
|
|
56
|
+
facts !== undefined &&
|
|
57
|
+
budgetCapSkipsThinking(facts.outputCapTokens, facts.hardOutputCap)) {
|
|
58
|
+
return { requested, effective: "off", graded: false, clamped: true, format: "budget", endpoint, dropped: true };
|
|
35
59
|
}
|
|
36
60
|
return { requested, effective: requested, graded: true, clamped: false, format: "budget", endpoint };
|
|
37
61
|
}
|
|
@@ -39,11 +63,11 @@ function dispatchReasoning(requested, model) {
|
|
|
39
63
|
if (compat.supportsReasoningEffort === false) {
|
|
40
64
|
return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
|
|
41
65
|
}
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
66
|
+
const responsesMint = mintEffortWireValue(requested, model, compat.reasoningEffortLevels);
|
|
67
|
+
if (requested !== "off" && responsesMint.wireValue === undefined) {
|
|
44
68
|
return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
|
|
45
69
|
}
|
|
46
|
-
return { ...
|
|
70
|
+
return { ...responsesMint.resolution, format: "responses", endpoint };
|
|
47
71
|
}
|
|
48
72
|
const format = compat.thinkingFormat ?? "openai";
|
|
49
73
|
if (BINARY_FORMATS.has(format)) {
|
|
@@ -53,11 +77,11 @@ function dispatchReasoning(requested, model) {
|
|
|
53
77
|
if (!supportsEffort && format !== "openrouter") {
|
|
54
78
|
return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
|
|
55
79
|
}
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
80
|
+
const mint = mintEffortWireValue(requested, model, compat.reasoningEffortLevels);
|
|
81
|
+
if (requested !== "off" && mint.wireValue === undefined) {
|
|
58
82
|
return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
|
|
59
83
|
}
|
|
60
|
-
return { ...
|
|
84
|
+
return { ...mint.resolution, format, endpoint };
|
|
61
85
|
}
|
|
62
86
|
export function resolveEffort(requested, allowed = DEFAULT_EFFORT_LEVELS) {
|
|
63
87
|
const declared = Array.isArray(allowed) ? allowed.filter((lvl) => isThinkingLevel(lvl) && lvl !== "off") : [];
|
|
@@ -23,7 +23,7 @@ export declare function reservedFor(api: string): ReadonlySet<string>;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): Record<string, unknown>;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Per-call auth REPLACES construction-time auth: drop every auth-bearing header
|
|
27
27
|
* (case-insensitive `authorization` / `x-api-key`) from an already-merged header bag. Called by a
|
|
28
28
|
* brain's buildRequest ONLY when a per-call `options.apiKey` is present — the brain then re-emits
|
|
29
29
|
* the credential in its own wire posture (anthropic `x-api-key`, openai `Bearer`), making the
|
|
@@ -33,6 +33,42 @@ export declare function applyExtraBody(body: Record<string, unknown>, extraBody:
|
|
|
33
33
|
* folds duplicates into one comma-joined value — broken auth both ways).
|
|
34
34
|
*/
|
|
35
35
|
export declare function stripAuthHeaders(headers: Record<string, string>): void;
|
|
36
|
+
/**
|
|
37
|
+
* #343 — the shared USER-HEADER merge layer (`model.headers` → construction `config.headers` →
|
|
38
|
+
* per-call `options.headers`, later bag wins), CASE-FOLD deduplicated: HTTP header field names are
|
|
39
|
+
* case-insensitive (RFC 9110), but the plain-object spread the three brains used
|
|
40
|
+
* (`{...model.headers, ...config.headers, ...options.headers}`) keyed by exact spelling — a
|
|
41
|
+
* `X-Tenant` in one bag and `x-tenant` in another BOTH survived and both went on the wire, where
|
|
42
|
+
* fetch's Headers folds them into one comma-joined value ("a, b"): neither writer's value, and the
|
|
43
|
+
* later layer's documented override silently defeated. Now a later bag's entry replaces an earlier
|
|
44
|
+
* case-variant; the WINNER'S spelling and value survive (a single-spelling config — every existing
|
|
45
|
+
* deployment — is byte-identical on the wire).
|
|
46
|
+
*
|
|
47
|
+
* EXEMPT: the auth carriers (`authorization` / `x-api-key`, any case) pass through with the exact
|
|
48
|
+
* legacy spread semantics (same-spelling override only, no case-fold dedup) — their case handling is
|
|
49
|
+
* {@link stripAuthHeaders}' pinned jurisdiction (the per-call-replaces flow and the
|
|
50
|
+
* header-only ANTHROPIC_AUTH_TOKEN shape, which must survive under its own capital-A spelling), and
|
|
51
|
+
* this layer must not become a second, subtly different auth authority.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* #343 (review r4) — assign a STRUCTURAL locked header under its canonical lowercase name, deleting
|
|
55
|
+
* every case-variant spelling first. The brains hard-lock `content-type` / `anthropic-version` AFTER
|
|
56
|
+
* the user-bag merge precisely so they "can NEVER be overridden" (council design/40) — but a valid
|
|
57
|
+
* user bag carrying `Content-Type: text/plain` survived BESIDE the lowercase lock, and the platform
|
|
58
|
+
* `Headers` fold turns the pair into `text/plain, application/json` on the wire: the lock decided
|
|
59
|
+
* nothing. Auth carriers are deliberately NOT routed through here (per-call replacement + the
|
|
60
|
+
* header-only boot flow are {@link stripAuthHeaders}' pinned jurisdiction).
|
|
61
|
+
*/
|
|
62
|
+
export declare function lockHeader(headers: Record<string, string>, lowerName: string, value: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* #343 (review r4) — case-fold READ-AND-CLAIM for an AUGMENTABLE structural header (`anthropic-beta`):
|
|
65
|
+
* returns the current value under whatever spelling the user bag carried and deletes that spelling,
|
|
66
|
+
* so the caller's canonical lowercase write REPLACES it instead of duplicating beside it (the
|
|
67
|
+
* read-modify-write used to key the read by the exact lowercase name and miss `Anthropic-Beta`,
|
|
68
|
+
* losing the user's betas from the merge AND double-sending the header).
|
|
69
|
+
*/
|
|
70
|
+
export declare function takeHeaderCasefold(headers: Record<string, string>, lowerName: string): string | undefined;
|
|
71
|
+
export declare function mergeHeaders(...bags: Array<Record<string, string> | undefined>): Record<string, string>;
|
|
36
72
|
/**
|
|
37
73
|
* The output-cap key(s) each lane's wire form uses. A lane's set is exactly the keys THAT lane's
|
|
38
74
|
* endpoint reads — a stray cap key belonging to another wire form is inert there and must not be
|
|
@@ -54,13 +54,51 @@ export function applyExtraBody(body, extraBody, reserved) {
|
|
|
54
54
|
}
|
|
55
55
|
return { ...passthrough, ...body };
|
|
56
56
|
}
|
|
57
|
+
const AUTH_CARRIER_NAMES = new Set(["authorization", "x-api-key"]);
|
|
57
58
|
export function stripAuthHeaders(headers) {
|
|
58
59
|
for (const k of Object.keys(headers)) {
|
|
59
|
-
|
|
60
|
-
if (lower === "authorization" || lower === "x-api-key")
|
|
60
|
+
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
|
|
61
61
|
delete headers[k];
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
export function lockHeader(headers, lowerName, value) {
|
|
65
|
+
for (const k of Object.keys(headers)) {
|
|
66
|
+
if (k !== lowerName && k.toLowerCase() === lowerName)
|
|
67
|
+
delete headers[k];
|
|
68
|
+
}
|
|
69
|
+
headers[lowerName] = value;
|
|
70
|
+
}
|
|
71
|
+
export function takeHeaderCasefold(headers, lowerName) {
|
|
72
|
+
for (const k of Object.keys(headers)) {
|
|
73
|
+
if (k.toLowerCase() === lowerName) {
|
|
74
|
+
const v = headers[k];
|
|
75
|
+
delete headers[k];
|
|
76
|
+
return v;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export function mergeHeaders(...bags) {
|
|
82
|
+
const out = {};
|
|
83
|
+
const spellingByFold = new Map();
|
|
84
|
+
for (const bag of bags) {
|
|
85
|
+
if (!bag)
|
|
86
|
+
continue;
|
|
87
|
+
for (const [name, value] of Object.entries(bag)) {
|
|
88
|
+
const fold = name.toLowerCase();
|
|
89
|
+
if (AUTH_CARRIER_NAMES.has(fold)) {
|
|
90
|
+
out[name] = value;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const prior = spellingByFold.get(fold);
|
|
94
|
+
if (prior !== undefined && prior !== name)
|
|
95
|
+
delete out[prior];
|
|
96
|
+
spellingByFold.set(fold, name);
|
|
97
|
+
out[name] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
64
102
|
export const OUTPUT_CAP_KEYS = {
|
|
65
103
|
openai: ["max_tokens", "max_completion_tokens"],
|
|
66
104
|
anthropic: ["max_tokens"],
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -193,7 +193,10 @@ export interface McpRefreshResult {
|
|
|
193
193
|
* DOMAIN for the swap (review F4: a prefix domain is self-healing and decoupled from the diff
|
|
194
194
|
* baseline, which advances even when a consumer skips a swap). Present on every entry. */
|
|
195
195
|
prefix: string;
|
|
196
|
-
|
|
196
|
+
/** `"revoked"` (design/338): the host ledger marks this server revoked — the refresh did NOT
|
|
197
|
+
* contact it (no tools/list round trip; the engine must not hand a severed server a request
|
|
198
|
+
* channel or splice its post-revocation text into the model catalog). */
|
|
199
|
+
status: "refreshed" | "not_connected" | "failed" | "revoked";
|
|
197
200
|
toolCount: number;
|
|
198
201
|
added: string[];
|
|
199
202
|
removed: string[];
|
|
@@ -467,6 +470,9 @@ export declare function materializeMcpTools(specs: McpServerSpec[], principal?:
|
|
|
467
470
|
reminderDisclosure?: {
|
|
468
471
|
reminderMark?: string;
|
|
469
472
|
counts?: ReminderDisclosureCounts;
|
|
473
|
+
}, mcpRevocations?: {
|
|
474
|
+
isRevoked(serverName: string): boolean;
|
|
475
|
+
onProbeFailure?: (error: unknown) => void;
|
|
470
476
|
}): Promise<MaterializedMcp>;
|
|
471
477
|
/**
|
|
472
478
|
* Fold the caller's AUTHORITATIVE per-tool override (design F: caller = trust root) over the server-hint axis.
|
package/dist/core/mcp.js
CHANGED
|
@@ -650,7 +650,30 @@ export function mcpToolSchemaProblem(schema) {
|
|
|
650
650
|
}
|
|
651
651
|
return undefined;
|
|
652
652
|
}
|
|
653
|
-
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
653
|
+
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
|
|
654
|
+
let revocationProbeFailed = false;
|
|
655
|
+
const isServerRevoked = (serverName) => {
|
|
656
|
+
if (mcpRevocations === undefined)
|
|
657
|
+
return false;
|
|
658
|
+
try {
|
|
659
|
+
const r = mcpRevocations.isRevoked(serverName);
|
|
660
|
+
if (typeof r !== "boolean") {
|
|
661
|
+
throw new Error(`isRevoked returned ${r instanceof Promise ? "a Promise — the ledger seat is SYNCHRONOUS by contract" : `a non-boolean (${typeof r})`}`);
|
|
662
|
+
}
|
|
663
|
+
return r;
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
if (!revocationProbeFailed) {
|
|
667
|
+
revocationProbeFailed = true;
|
|
668
|
+
try {
|
|
669
|
+
mcpRevocations.onProbeFailure?.(e);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
654
677
|
const mcpDisclosure = reminderDisclosure?.reminderMark !== undefined
|
|
655
678
|
? { mark: reminderDisclosure.reminderMark, windows: new Map(), ...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}) }
|
|
656
679
|
: undefined;
|
|
@@ -665,7 +688,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
665
688
|
const droppedTools = [];
|
|
666
689
|
let disposing = false;
|
|
667
690
|
const serverHandles = [];
|
|
668
|
-
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure)));
|
|
691
|
+
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
|
|
669
692
|
try {
|
|
670
693
|
for (let i = 0; i < specs.length; i++) {
|
|
671
694
|
const r = settled[i];
|
|
@@ -701,7 +724,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
701
724
|
statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
|
|
702
725
|
}
|
|
703
726
|
}
|
|
704
|
-
const resourceTools = buildResourceTools(resourceServers);
|
|
727
|
+
const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
|
|
705
728
|
tools.push(...resourceTools.tools);
|
|
706
729
|
toolAxes.push(...resourceTools.axes);
|
|
707
730
|
}
|
|
@@ -729,10 +752,14 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
729
752
|
results.push({ server: h.name, prefix: prefixOf(h.name), status: "not_connected", toolCount: 0, added: [], removed: [] });
|
|
730
753
|
continue;
|
|
731
754
|
}
|
|
755
|
+
if (isServerRevoked(h.name)) {
|
|
756
|
+
results.push({ server: h.name, prefix: prefixOf(h.name), status: "revoked", toolCount: 0, added: [], removed: [], error: "server revoked by the operator — the refresh did not contact it" });
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
732
759
|
try {
|
|
733
760
|
const listed = await listToolsLenient(h.client);
|
|
734
761
|
cacheMcpToolMetadata(h.client, listed.tools);
|
|
735
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure);
|
|
762
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
|
|
736
763
|
const newNames = serverTools.map((t) => t.name);
|
|
737
764
|
const added = newNames.filter((n) => !h.toolNames.includes(n));
|
|
738
765
|
const removed = h.toolNames.filter((n) => !newNames.includes(n));
|
|
@@ -882,7 +909,7 @@ function renderDirChildren(server, uri, children, flags) {
|
|
|
882
909
|
terminate: false,
|
|
883
910
|
};
|
|
884
911
|
}
|
|
885
|
-
function buildResourceTools(resourceServers) {
|
|
912
|
+
function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
886
913
|
const listable = resourceServers.filter((rs) => rs.listAllowed);
|
|
887
914
|
const readable = resourceServers.filter((rs) => rs.readAllowed);
|
|
888
915
|
if (listable.length === 0 && readable.length === 0)
|
|
@@ -910,6 +937,11 @@ function buildResourceTools(resourceServers) {
|
|
|
910
937
|
const all = [];
|
|
911
938
|
const errors = [];
|
|
912
939
|
for (const rs of targets) {
|
|
940
|
+
if (isServerRevoked(rs.server)) {
|
|
941
|
+
errors.push({ server: rs.server, error: "server revoked by the operator mid-session (request not sent)" });
|
|
942
|
+
sections.push(`[${rs.server}] Error: server revoked by the operator — its resources are unavailable this turn.`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
913
945
|
if (rs.health.dead) {
|
|
914
946
|
errors.push({ server: rs.server, error: "server disconnected (transport closed earlier in this task)" });
|
|
915
947
|
sections.push(`[${rs.server}] Error: server disconnected — its resources are unavailable.`);
|
|
@@ -970,6 +1002,14 @@ function buildResourceTools(resourceServers) {
|
|
|
970
1002
|
if (!rs)
|
|
971
1003
|
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with readable resources. Available: ${serverNames(readable)}.` }], details: undefined, terminate: false };
|
|
972
1004
|
const what = `The read of resource ${inlineUntrusted(uri)}`;
|
|
1005
|
+
if (isServerRevoked(server)) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
|
|
1008
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1009
|
+
terminate: false,
|
|
1010
|
+
isError: true,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
973
1013
|
if (rs.health.dead)
|
|
974
1014
|
throwDeadServer(server, what);
|
|
975
1015
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -1021,6 +1061,14 @@ function buildResourceTools(resourceServers) {
|
|
|
1021
1061
|
if (!rs)
|
|
1022
1062
|
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(listable)}.` }], details: undefined, terminate: false };
|
|
1023
1063
|
const what = `The directory listing of ${inlineUntrusted(uri)}`;
|
|
1064
|
+
if (isServerRevoked(server)) {
|
|
1065
|
+
return {
|
|
1066
|
+
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
|
|
1067
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1068
|
+
terminate: false,
|
|
1069
|
+
isError: true,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1024
1072
|
if (rs.health.dead)
|
|
1025
1073
|
throwDeadServer(server, what);
|
|
1026
1074
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -1123,7 +1171,7 @@ const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient
|
|
|
1123
1171
|
async function listToolsLenient(client, options) {
|
|
1124
1172
|
return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
|
|
1125
1173
|
}
|
|
1126
|
-
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
1174
|
+
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1127
1175
|
const elicitOn = spec.elicitation === true && onElicit !== undefined;
|
|
1128
1176
|
const health = { dead: false, pendingElicitations: 0, lastElicitationClosedAt: 0 };
|
|
1129
1177
|
const client = new Client({ name: `sema-core/${spec.name}`, version: "0.1.0" }, { capabilities: elicitOn ? { elicitation: { form: {} } } : {} });
|
|
@@ -1159,7 +1207,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1159
1207
|
};
|
|
1160
1208
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1161
1209
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1162
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure);
|
|
1210
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1163
1211
|
const caps = client.getServerCapabilities();
|
|
1164
1212
|
const resourceInfo = caps?.resources
|
|
1165
1213
|
? {
|
|
@@ -1192,7 +1240,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1192
1240
|
throw err;
|
|
1193
1241
|
}
|
|
1194
1242
|
}
|
|
1195
|
-
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure) {
|
|
1243
|
+
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1196
1244
|
const serverTools = [];
|
|
1197
1245
|
const serverAxes = [];
|
|
1198
1246
|
const dropped = [];
|
|
@@ -1235,6 +1283,14 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1235
1283
|
...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
|
|
1236
1284
|
execute: async (_toolCallId, params, signal) => {
|
|
1237
1285
|
const what = `The call to tool ${inlineUntrusted(remoteName)}`;
|
|
1286
|
+
if (isServerRevoked?.(spec.name) === true) {
|
|
1287
|
+
return {
|
|
1288
|
+
content: [{ type: "text", text: `The call to MCP server "${spec.name}" was refused: the server was revoked by the operator mid-session. The call was NOT sent, so the server did not execute it. The tool list updates at the next turn.` }],
|
|
1289
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server: spec.name },
|
|
1290
|
+
terminate: false,
|
|
1291
|
+
isError: true,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1238
1294
|
if (health.dead)
|
|
1239
1295
|
throwDeadServer(spec.name, what);
|
|
1240
1296
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -849,6 +849,11 @@ export declare class MemoryEngine {
|
|
|
849
849
|
* the deleted disk file WAS the backend's storage). Zero-copy skips getByIds: its read-side scan
|
|
850
850
|
* cannot see a deleted file, and calling it mid-harvest would sync-adopt in-session edits. */
|
|
851
851
|
private committedContentFor;
|
|
852
|
+
/** {@link committedContentFor} with the FAULT axis preserved: `fault: true` means the backend
|
|
853
|
+
* read THREW and no shadow answered — "could not read" — which consumers that make destructive
|
|
854
|
+
* decisions on absence (the projection-debt settlement clears a row on "entry gone") must
|
|
855
|
+
* distinguish from a clean miss. The 8 non-destructive read sites keep the folded face. */
|
|
856
|
+
private committedContentOrFault;
|
|
852
857
|
/**
|
|
853
858
|
* design/336 §2.2 (r4-9) — the COMMITTED frontmatter an origin carry-forward is computed against.
|
|
854
859
|
* Deliberately NOT {@link committedContentFor}'s non-zero-copy leg: that one calls the backend's
|
|
@@ -910,8 +915,32 @@ export declare class MemoryEngine {
|
|
|
910
915
|
/** Persist the canonical projection back to the session file (id minting / frontmatter completion).
|
|
911
916
|
* C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
|
|
912
917
|
* commit point. Skips the write when the disk already holds the canonical bytes (zero-copy: the
|
|
913
|
-
* journal's own execute step wrote them, making this an idempotent no-op).
|
|
918
|
+
* journal's own execute step wrote them, making this an idempotent no-op). Returns whether the
|
|
919
|
+
* disk now holds the canonical bytes — the ordinary completion family stays best-effort on a
|
|
920
|
+
* `false` (the next materialize re-projects), but the id-COMPLETION family's caller reads it to
|
|
921
|
+
* keep the #366 projection-debt row standing (an id-less plane file must re-bind to its
|
|
922
|
+
* committed id at the next harvest, never re-admit as a duplicate). */
|
|
914
923
|
private writeBackProjection;
|
|
924
|
+
/**
|
|
925
|
+
* #366 — repair an id-less-but-committed seat IN PLACE (the failed write-back's retry): write the
|
|
926
|
+
* committed canonical bytes over the seat and settle the debt row on success. Callers have
|
|
927
|
+
* already proven the seat carries no unadmitted edit (rev equality against its baseline), so the
|
|
928
|
+
* overwrite is content-preserving — it restores the id line, the completed frontmatter and any
|
|
929
|
+
* carried marker, nothing else. A refused read/write leaves the row standing (retried next pass;
|
|
930
|
+
* the next materialize's projection loop repairs and settles it too).
|
|
931
|
+
*/
|
|
932
|
+
private repairProjectionSeat;
|
|
933
|
+
/**
|
|
934
|
+
* #366 — validate a projection-debt row against the COMMITTED state (side-effect-free read: the
|
|
935
|
+
* retrievalView face in copy-out, never the File backend's adopting `getByIds`). Three-valued on
|
|
936
|
+
* purpose: `valid` binds, `stale` clears the row (the seat claim dissolved — entry gone, moved or
|
|
937
|
+
* renamed), and `unknown` (committed state unreadable) makes the caller DEFER the file fail-closed
|
|
938
|
+
* — treating a transient read fault as "stale" would clear the row and mint the very duplicate
|
|
939
|
+
* the account exists to close. Zero-copy answers `stale` by construction: there the backend's own
|
|
940
|
+
* commit writes the id into the plane file, so any standing row is a leftover, and the plain
|
|
941
|
+
* `getByIds` there would sync-adopt mid-harvest.
|
|
942
|
+
*/
|
|
943
|
+
private debtCommittedProjection;
|
|
915
944
|
/** Sibling scope subdir names under `dir` (excluded from a scope-tree walk when `dir` is the root —
|
|
916
945
|
* a root-owning layer's chmod/restore must never touch another scope's home). */
|
|
917
946
|
private siblingScopeDirNames;
|