@sema-agent/core 7.4.0 → 7.5.1
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 +69 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/peer-notices.d.ts +11 -1
- package/dist/agents/peer-session-drain.js +2 -0
- package/dist/agents/verify.d.ts +2 -1
- package/dist/core/ask-origin.d.ts +130 -0
- package/dist/core/ask-origin.js +35 -0
- package/dist/core/auto-mode-arming.d.ts +40 -1
- package/dist/core/auto-mode-arming.js +51 -3
- package/dist/core/auto-mode.d.ts +87 -10
- package/dist/core/auto-mode.js +34 -6
- package/dist/core/checkpoint-store.d.ts +41 -6
- package/dist/core/checkpoint-store.js +8 -0
- package/dist/core/hooks.d.ts +63 -19
- package/dist/core/hooks.js +37 -16
- package/dist/core/mcp.d.ts +47 -55
- package/dist/core/mcp.js +89 -31
- package/dist/core/park-selfcheck.js +3 -0
- package/dist/core/permission-rule-consent.d.ts +2 -11
- package/dist/core/permission-rule-consent.js +13 -62
- package/dist/core/permission-rule-org.d.ts +70 -54
- package/dist/core/permission-rule-org.js +47 -61
- package/dist/core/permission-rule-provider.d.ts +162 -0
- package/dist/core/permission-rule-provider.js +148 -0
- package/dist/core/permission-rule-session.d.ts +23 -19
- package/dist/core/permission-rule-session.js +5 -5
- package/dist/core/permission-rule-store.d.ts +46 -21
- package/dist/core/permission-rule-store.js +13 -6
- package/dist/core/permission-rule-sync.d.ts +2 -1
- package/dist/core/permission-rule-sync.js +11 -0
- package/dist/core/runner/assemble-result.d.ts +3 -2
- package/dist/core/runner/checkpoint-scope.d.ts +32 -0
- package/dist/core/runner/checkpoint-scope.js +4 -0
- package/dist/core/runner/contracts.d.ts +1878 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +57 -31
- package/dist/core/runner/denial-limit-arms.js +42 -17
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-announce-once.d.ts +83 -0
- package/dist/core/runner/prepare-announce-once.js +105 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
- package/dist/core/runner/prepare-hands-readface.js +3 -3
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
- package/dist/core/runner/prepare-permission-rules.js +140 -0
- package/dist/core/runner/prepare-project-context.d.ts +131 -0
- package/dist/core/runner/prepare-project-context.js +150 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +182 -0
- package/dist/core/runner/prepare-question-face.d.ts +119 -0
- package/dist/core/runner/prepare-question-face.js +83 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
- package/dist/core/runner/prepare-task.d.ts +11 -1815
- package/dist/core/runner/prepare-task.js +138 -2542
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
- package/dist/core/runner/prepare-wiring-manifest.js +240 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
- package/dist/core/runner/prepare-workspace-restore.js +1 -22
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.d.ts +11 -2
- package/dist/core/runner/runtask.js +27 -9
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/sensitive-path-policy.d.ts +16 -0
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +57 -9
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/types.d.ts +63 -51
- package/dist/core/wiring-manifest.d.ts +40 -3
- package/dist/core/wiring-manifest.js +4 -3
- package/dist/core/write-protect.d.ts +13 -2
- package/dist/core/write-protect.js +58 -29
- package/dist/engine/harness/types.d.ts +38 -16
- package/dist/engine/harness/types.js +25 -1
- package/dist/engine/session/session.d.ts +3 -11
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -0
- package/dist/stores/file/adoption/adopt.d.ts +1 -1
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/permission-rule-adopt.js +4 -3
- package/dist/stores/file/permission-rule-store.d.ts +65 -25
- package/dist/stores/file/permission-rule-store.js +215 -37
- package/dist/stores/file/task-list-store.d.ts +1 -1
- package/dist/tools/fs/read-face.d.ts +1 -1
- package/package.json +8 -2
- package/test/export-surface.snapshot.json +76 -28
package/dist/core/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactSecrets } from "./untrusted-egress.js";
|
|
1
2
|
import { MCP_NAMESPACE } from "./protocol-table.js";
|
|
2
3
|
import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
|
|
3
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -170,8 +171,8 @@ export function mcpIdleTimeoutMs(kind) {
|
|
|
170
171
|
? (parseEnvMs("MCP_IDLE_TIMEOUT_STDIO") ?? MCP_IDLE_TIMEOUT_STDIO_DEFAULT_MS)
|
|
171
172
|
: (parseEnvMs("MCP_IDLE_TIMEOUT_HTTP") ?? MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS);
|
|
172
173
|
}
|
|
173
|
-
function idleKindOf(
|
|
174
|
-
return
|
|
174
|
+
function idleKindOf(kind) {
|
|
175
|
+
return kind === "stdio" ? "stdio" : "http";
|
|
175
176
|
}
|
|
176
177
|
const IDLE_WATCHDOG_ABORT_REASON = Symbol("mcp-idle-watchdog-abort");
|
|
177
178
|
function armMcpIdleWatchdog(health, idleMs, outerSignal) {
|
|
@@ -220,18 +221,22 @@ export function collapseMcpErrorPrefix(message) {
|
|
|
220
221
|
out = out.slice(m[0].length);
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
|
-
function
|
|
224
|
-
if (!(err instanceof
|
|
224
|
+
function sanitizeMcpErrorTextInPlace(err) {
|
|
225
|
+
if (!(err instanceof Error))
|
|
225
226
|
return;
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
err.message =
|
|
227
|
+
const sanitized = redactSecrets(err instanceof McpError ? collapseMcpErrorPrefix(err.message) : err.message);
|
|
228
|
+
if (sanitized !== err.message)
|
|
229
|
+
err.message = sanitized;
|
|
229
230
|
}
|
|
230
231
|
function namedMcpFailureText(err) {
|
|
231
|
-
const detail = err
|
|
232
|
+
const detail = remoteErrorText(err, { collapse: true });
|
|
232
233
|
const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
|
|
233
234
|
return condition !== undefined ? `${condition}: ${detail}` : detail;
|
|
234
235
|
}
|
|
236
|
+
function remoteErrorText(err, opts) {
|
|
237
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
238
|
+
return redactSecrets(opts?.collapse === true ? collapseMcpErrorPrefix(raw) : raw);
|
|
239
|
+
}
|
|
235
240
|
function isTransportLost(err) {
|
|
236
241
|
if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
|
|
237
242
|
return true;
|
|
@@ -284,6 +289,38 @@ export function describeHttpTransportFailure(err) {
|
|
|
284
289
|
}
|
|
285
290
|
return undefined;
|
|
286
291
|
}
|
|
292
|
+
export const MCP_FAILURE_CODES = ["connect_refused", "connection_failed", "network", "not_mcp_response", "spawn_failed", "connection_closed", "timeout", "protocol", "invalid_config", "unknown"];
|
|
293
|
+
export function mcpFailureCodeOf(err, transport) {
|
|
294
|
+
if (err instanceof McpError) {
|
|
295
|
+
if (err.code === ErrorCode.ConnectionClosed)
|
|
296
|
+
return "connection_closed";
|
|
297
|
+
if (err.code === ErrorCode.RequestTimeout)
|
|
298
|
+
return "timeout";
|
|
299
|
+
return "protocol";
|
|
300
|
+
}
|
|
301
|
+
const http = describeHttpTransportFailure(err);
|
|
302
|
+
if (http !== undefined) {
|
|
303
|
+
if (http.httpStatus !== undefined)
|
|
304
|
+
return `http_${http.httpStatus}`;
|
|
305
|
+
if (err instanceof StreamableHTTPError)
|
|
306
|
+
return "not_mcp_response";
|
|
307
|
+
const code = networkErrorCode(err);
|
|
308
|
+
if (code === undefined)
|
|
309
|
+
return "network";
|
|
310
|
+
if (code === "ERR_INVALID_URL")
|
|
311
|
+
return "invalid_config";
|
|
312
|
+
if (code.startsWith("ERR_"))
|
|
313
|
+
return "unknown";
|
|
314
|
+
if (transport === "stdio")
|
|
315
|
+
return "spawn_failed";
|
|
316
|
+
if (transport === undefined)
|
|
317
|
+
return "connection_failed";
|
|
318
|
+
return http.delivered === "no" ? "connect_refused" : "connection_failed";
|
|
319
|
+
}
|
|
320
|
+
if (isTransportLost(err))
|
|
321
|
+
return "connection_closed";
|
|
322
|
+
return "unknown";
|
|
323
|
+
}
|
|
287
324
|
function writeEffectWarning(writeEffect) {
|
|
288
325
|
return writeEffect
|
|
289
326
|
? " This tool is write-capable: treat its side effects as POSSIBLY APPLIED and verify the actual state before retrying."
|
|
@@ -301,6 +338,7 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
301
338
|
}
|
|
302
339
|
throw e;
|
|
303
340
|
};
|
|
341
|
+
sanitizeMcpErrorTextInPlace(err);
|
|
304
342
|
if (ctx.idle?.signal.reason === IDLE_WATCHDOG_ABORT_REASON) {
|
|
305
343
|
const serverLabel = inlineUntrusted(ctx.server);
|
|
306
344
|
const e = new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
|
|
@@ -313,7 +351,6 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
313
351
|
}
|
|
314
352
|
if (ctx.signal?.aborted)
|
|
315
353
|
finish(err);
|
|
316
|
-
collapseMcpErrorStampInPlace(err);
|
|
317
354
|
const serverLabel = inlineUntrusted(ctx.server);
|
|
318
355
|
const fenceServerText = (label, raw) => delimitUntrusted(label, truncateMcpErrorText(raw));
|
|
319
356
|
if (err instanceof McpError && err.code === ErrorCode.RequestTimeout) {
|
|
@@ -337,7 +374,7 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
337
374
|
}
|
|
338
375
|
const httpFailure = describeHttpTransportFailure(err);
|
|
339
376
|
if (httpFailure !== undefined) {
|
|
340
|
-
const detail =
|
|
377
|
+
const detail = remoteErrorText(err);
|
|
341
378
|
const fenced = `\nThe transport error follows as external/untrusted data:\n${fenceServerText(`${ctx.server} transport error`, detail)}`;
|
|
342
379
|
const e = new Error(httpFailure.delivered === "no"
|
|
343
380
|
? `${ctx.what} could not reach MCP server "${serverLabel}": ${httpFailure.condition}. The request was not delivered, so the server did not execute it. This server's tools and resources will keep failing until its endpoint is reachable again — do not retry them; use an alternative if one exists.${fenced}`
|
|
@@ -360,7 +397,7 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
360
397
|
}
|
|
361
398
|
}
|
|
362
399
|
if (ctx.attributeServer) {
|
|
363
|
-
const msg =
|
|
400
|
+
const msg = remoteErrorText(err);
|
|
364
401
|
finish(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, msg)}`, { cause: err }));
|
|
365
402
|
}
|
|
366
403
|
finish(err);
|
|
@@ -479,15 +516,19 @@ export function resolveProtocolHttpHeaders(protocol, t, principal) {
|
|
|
479
516
|
}
|
|
480
517
|
return t.headers;
|
|
481
518
|
}
|
|
482
|
-
function buildTransport(spec, principal) {
|
|
519
|
+
function buildTransport(spec, principal, kind) {
|
|
483
520
|
const t = spec.transport;
|
|
484
|
-
if (
|
|
521
|
+
if (kind === "stdio") {
|
|
522
|
+
if (!("command" in t))
|
|
523
|
+
throw new Error(`MCP server "${spec.name}": transport kind "stdio" carries no command`);
|
|
485
524
|
return new StdioClientTransport({
|
|
486
525
|
command: t.command,
|
|
487
526
|
args: t.args,
|
|
488
527
|
env: t.env,
|
|
489
528
|
});
|
|
490
529
|
}
|
|
530
|
+
if (!("url" in t))
|
|
531
|
+
throw new Error(`MCP server "${spec.name}": transport kind "${String(kind)}" carries no url`);
|
|
491
532
|
const headers = resolveProtocolHttpHeaders(MCP_NAMESPACE.id, t, principal);
|
|
492
533
|
return new StreamableHTTPClientTransport(new URL(t.url), {
|
|
493
534
|
requestInit: headers ? { headers } : undefined,
|
|
@@ -803,7 +844,16 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
803
844
|
const droppedTools = [];
|
|
804
845
|
let disposing = false;
|
|
805
846
|
const serverHandles = [];
|
|
806
|
-
const
|
|
847
|
+
const dialKinds = entries.map((spec) => {
|
|
848
|
+
try {
|
|
849
|
+
const kind = spec.transport.kind;
|
|
850
|
+
return kind === "stdio" || kind === "http" ? kind : undefined;
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
const settled = await Promise.allSettled(entries.map((spec, i) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked, dialKinds[i])));
|
|
807
857
|
try {
|
|
808
858
|
for (let i = 0; i < entries.length; i++) {
|
|
809
859
|
const r = settled[i];
|
|
@@ -811,7 +861,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
811
861
|
if (r.status === "fulfilled") {
|
|
812
862
|
const s = r.value;
|
|
813
863
|
clients.push(s.client);
|
|
814
|
-
serverHandles.push({ name: spec.name, spec, client: s.client, health: s.health, tools: s.tools, listedRaw: s.listedTools });
|
|
864
|
+
serverHandles.push({ name: spec.name, spec, client: s.client, health: s.health, tools: s.tools, listedRaw: s.listedTools, transportKind: dialKinds[i] });
|
|
815
865
|
tools.push(...s.tools);
|
|
816
866
|
toolAxes.push(...s.axes);
|
|
817
867
|
if (s.instructions) {
|
|
@@ -843,7 +893,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
843
893
|
}
|
|
844
894
|
else {
|
|
845
895
|
warnings.push(asServerWarning(spec, r.reason));
|
|
846
|
-
statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
|
|
896
|
+
statuses.push({ name: spec.name, status: "failed", errorCode: mcpFailureCodeOf(r.reason, dialKinds[i]), error: namedMcpFailureText(r.reason) });
|
|
847
897
|
}
|
|
848
898
|
}
|
|
849
899
|
const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
|
|
@@ -884,7 +934,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
884
934
|
const retainedRaw = listed.incomplete !== undefined ? h.listedRaw.filter((t) => !advertised.has(t.name)) : [];
|
|
885
935
|
const mergedRaw = retainedRaw.length > 0 ? [...listed.tools, ...retainedRaw] : listed.tools;
|
|
886
936
|
cacheMcpToolMetadata(h.client, retainedRaw.length > 0 ? [...retainedRaw, ...listed.tools] : listed.tools);
|
|
887
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
|
|
937
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked, h.transportKind);
|
|
888
938
|
const priorNames = h.tools.map((t) => t.name);
|
|
889
939
|
const newNames = serverTools.map((t) => t.name);
|
|
890
940
|
const added = newNames.filter((n) => !priorNames.includes(n));
|
|
@@ -1016,7 +1066,7 @@ export async function walkMcpListPages(fetchPage, opts) {
|
|
|
1016
1066
|
catch (err) {
|
|
1017
1067
|
if (pages === 0 || opts.signal?.aborted)
|
|
1018
1068
|
throw err;
|
|
1019
|
-
return { items, incomplete: { reason: "page_error", pages, error:
|
|
1069
|
+
return { items, incomplete: { reason: "page_error", pages, error: remoteErrorText(err) } };
|
|
1020
1070
|
}
|
|
1021
1071
|
opts.onPage?.();
|
|
1022
1072
|
const pageStart = items.length;
|
|
@@ -1099,13 +1149,13 @@ async function readDirViaExtension(rs, uri, signal, timeoutMs, watchdog) {
|
|
|
1099
1149
|
if (err instanceof McpError && err.code === ErrorCode.InvalidParams) {
|
|
1100
1150
|
if (pages > 0)
|
|
1101
1151
|
return { kind: "ok", resources, cursorInvalid: true };
|
|
1102
|
-
const detail =
|
|
1152
|
+
const detail = remoteErrorText(err, { collapse: true });
|
|
1103
1153
|
return { kind: classifyDirReadInvalidParams(detail), detail };
|
|
1104
1154
|
}
|
|
1105
1155
|
if (err instanceof McpError && err.code === ErrorCode.MethodNotFound)
|
|
1106
1156
|
return { kind: "unsupported" };
|
|
1107
1157
|
if (pages > 0 && !signal?.aborted) {
|
|
1108
|
-
return { kind: "ok", resources, incomplete:
|
|
1158
|
+
return { kind: "ok", resources, incomplete: remoteErrorText(err) };
|
|
1109
1159
|
}
|
|
1110
1160
|
throw err;
|
|
1111
1161
|
}
|
|
@@ -1212,9 +1262,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
|
1212
1262
|
throw err;
|
|
1213
1263
|
const msg = watchdog.idleSignal.reason === IDLE_WATCHDOG_ABORT_REASON
|
|
1214
1264
|
? idleWatchdogTripText(idleMs)
|
|
1215
|
-
: err
|
|
1216
|
-
? err.message
|
|
1217
|
-
: String(err);
|
|
1265
|
+
: remoteErrorText(err);
|
|
1218
1266
|
errors.push({ server: rs.server, error: msg });
|
|
1219
1267
|
sections.push(`[${rs.server}] Error: could not list resources: ${inlineUntrusted(msg)}`);
|
|
1220
1268
|
}
|
|
@@ -1441,7 +1489,7 @@ async function listToolsLenient(client, options) {
|
|
|
1441
1489
|
});
|
|
1442
1490
|
return { tools: walk.items, ...(walk.incomplete !== undefined ? { incomplete: walk.incomplete } : {}) };
|
|
1443
1491
|
}
|
|
1444
|
-
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1492
|
+
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked, transportKind) {
|
|
1445
1493
|
const elicitOn = spec.elicitation === true && onElicit !== undefined;
|
|
1446
1494
|
const health = { dead: false, pendingElicitations: 0, lastElicitationClosedAt: 0 };
|
|
1447
1495
|
const client = new Client({ name: `sema-core/${spec.name}`, version: "0.1.0" }, { capabilities: elicitOn ? { elicitation: { form: {} } } : {} });
|
|
@@ -1465,7 +1513,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1465
1513
|
}
|
|
1466
1514
|
});
|
|
1467
1515
|
}
|
|
1468
|
-
const transport = buildTransport(spec, principal);
|
|
1516
|
+
const transport = buildTransport(spec, principal, transportKind);
|
|
1469
1517
|
const announce = {};
|
|
1470
1518
|
const startupMs = mcpStartupTimeoutMs();
|
|
1471
1519
|
const startupOpts = startupMs !== undefined ? { timeout: startupMs } : undefined;
|
|
@@ -1477,7 +1525,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1477
1525
|
};
|
|
1478
1526
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1479
1527
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1480
|
-
const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1528
|
+
const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind);
|
|
1481
1529
|
const caps = client.getServerCapabilities();
|
|
1482
1530
|
const resourceInfo = caps?.resources
|
|
1483
1531
|
? {
|
|
@@ -1506,7 +1554,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1506
1554
|
listedTools: listed.tools,
|
|
1507
1555
|
...(instructions ? { instructions } : {}),
|
|
1508
1556
|
...(listed.incomplete !== undefined ? { listingIncomplete: listed.incomplete } : {}),
|
|
1509
|
-
...(resourceInfo && (resourceInfo.listAllowed || resourceInfo.readAllowed) ? { resourceServer: { server: spec.name, client, health, transportKind: idleKindOf(
|
|
1557
|
+
...(resourceInfo && (resourceInfo.listAllowed || resourceInfo.readAllowed) ? { resourceServer: { server: spec.name, client, health, transportKind: idleKindOf(transportKind), ...resourceInfo } } : {}),
|
|
1510
1558
|
};
|
|
1511
1559
|
}
|
|
1512
1560
|
catch (err) {
|
|
@@ -1514,7 +1562,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1514
1562
|
throw err;
|
|
1515
1563
|
}
|
|
1516
1564
|
}
|
|
1517
|
-
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1565
|
+
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind) {
|
|
1518
1566
|
const declaredContentOrigin = spec.contentOrigin;
|
|
1519
1567
|
const serverTools = [];
|
|
1520
1568
|
const serverAxes = [];
|
|
@@ -1599,7 +1647,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1599
1647
|
if (health.dead)
|
|
1600
1648
|
throwDeadServer(spec.name, what);
|
|
1601
1649
|
const timeoutMs = mcpToolTimeoutMs();
|
|
1602
|
-
const idleMs = mcpIdleTimeoutMs(idleKindOf(
|
|
1650
|
+
const idleMs = mcpIdleTimeoutMs(idleKindOf(transportKind));
|
|
1603
1651
|
const watchdog = armMcpIdleWatchdog(health, idleMs, signal);
|
|
1604
1652
|
let res;
|
|
1605
1653
|
try {
|
|
@@ -1638,7 +1686,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1638
1686
|
const errScLine = structuredContentErrorLine(res.structuredContent, parts.join("\n"));
|
|
1639
1687
|
if (errScLine !== undefined)
|
|
1640
1688
|
parts.push(errScLine);
|
|
1641
|
-
const body = truncateMcpErrorText(parts.join("\n").trim());
|
|
1689
|
+
const body = truncateMcpErrorText(redactSecrets(parts.join("\n").trim()));
|
|
1642
1690
|
const msg = body
|
|
1643
1691
|
? `MCP tool ${inlineUntrusted(remoteName)} reported an error. The server's error content follows as external/untrusted data:\n${delimitUntrusted(`${spec.name} tool error`, body)}`
|
|
1644
1692
|
: `MCP tool ${inlineUntrusted(remoteName)} reported an error`;
|
|
@@ -1687,10 +1735,20 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1687
1735
|
return { serverTools, serverAxes, dropped, advisories };
|
|
1688
1736
|
}
|
|
1689
1737
|
function asServerWarning(spec, err) {
|
|
1690
|
-
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
|
|
1738
|
+
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: sanitizedCause(err) });
|
|
1691
1739
|
warning.code = "mcp.server_unavailable";
|
|
1692
1740
|
return warning;
|
|
1693
1741
|
}
|
|
1742
|
+
function sanitizedCause(err) {
|
|
1743
|
+
const copy = new Error(namedMcpFailureText(err));
|
|
1744
|
+
if (err instanceof Error) {
|
|
1745
|
+
copy.name = err.name;
|
|
1746
|
+
const code = err.code;
|
|
1747
|
+
if (typeof code === "string" || typeof code === "number")
|
|
1748
|
+
copy.code = code;
|
|
1749
|
+
}
|
|
1750
|
+
return copy;
|
|
1751
|
+
}
|
|
1694
1752
|
function schemaAdvisoryWarning(server, tool, reason) {
|
|
1695
1753
|
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" tool "${tool}" is mounted, but ${reason}.`);
|
|
1696
1754
|
warning.code = "mcp.tool_schema_advisory";
|
|
@@ -73,6 +73,9 @@ function syntheticCheckpoint(scope) {
|
|
|
73
73
|
hasBidiControls: true,
|
|
74
74
|
preview: { probe: true, note: "park wiring self-check display stub" },
|
|
75
75
|
previewWithheld: "oversize",
|
|
76
|
+
requiresRealApproval: true,
|
|
77
|
+
denialLimitFallback: { consecutive: 3, total: 3, limit: "consecutive", autoDenyAfterMs: 0 },
|
|
78
|
+
origin: "denial_limit_fallback",
|
|
76
79
|
batchToolCallIds: [`${scope}:call`],
|
|
77
80
|
completedCallIds: [],
|
|
78
81
|
},
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* defended against: for a file backend, host = user, no more and no less.
|
|
29
29
|
*/
|
|
30
30
|
import { type EditedRuleBreadthWarning, type UncoveredSegmentDetail, type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
31
|
-
import type {
|
|
32
|
-
import type
|
|
31
|
+
import type { RuleOwner } from "./permission-rule-store.js";
|
|
32
|
+
import { type PermissionRuleStoreProvider } from "./permission-rule-provider.js";
|
|
33
33
|
/** One candidate rule inside an approval record: the exact text and where it would apply. */
|
|
34
34
|
export interface RuleCandidate {
|
|
35
35
|
rule: string;
|
|
@@ -219,15 +219,6 @@ export interface RuleConsentDeps {
|
|
|
219
219
|
* happened, and withholding the receipt helps no one.
|
|
220
220
|
*/
|
|
221
221
|
cardEdits?: boolean;
|
|
222
|
-
/**
|
|
223
|
-
* design/382 §4.3 — the SESSION-RULE OVERLAY: where a `{kind:"session"}` scoped candidate lands at
|
|
224
|
-
* redemption (the store write leg is never taken for one), and where the prepare-time coverage read
|
|
225
|
-
* merges a session's standing rows from. Host-provided, session-lifetime, never synced — see
|
|
226
|
-
* `permission-rule-session.ts` for the contract and the reference implementation. Absent ⇒ a
|
|
227
|
-
* session-scope candidate's redemption refuses loudly (there is nowhere for it to land), and
|
|
228
|
-
* coverage reads see no session rows — both fail toward asking.
|
|
229
|
-
*/
|
|
230
|
-
sessionRules?: SessionRuleOverlay;
|
|
231
222
|
/**
|
|
232
223
|
* design/382 §2.4-6 — the deployment's sensitive-read negative control for the directoryRead MINT
|
|
233
224
|
* floor, same-source with its compiled read-deny judge (`(d) => readDeny.matchPath(d) !== null` is
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
|
-
import {
|
|
3
|
+
import { effectiveOrThrow } from "./permission-rule-provider.js";
|
|
4
|
+
import { errText, isValidConsentScope, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
5
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
6
|
rows = new Map();
|
|
6
7
|
async get(id) {
|
|
@@ -296,18 +297,8 @@ export async function prepareCardApproval(opts) {
|
|
|
296
297
|
})();
|
|
297
298
|
let coverage;
|
|
298
299
|
try {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
const overlay = opts.deps.sessionRules;
|
|
302
|
-
if (overlay !== undefined && opts.sessionId !== undefined) {
|
|
303
|
-
const served = structuredClone(await overlay.read(opts.sessionId));
|
|
304
|
-
const sessionRows = served.filter((r) => {
|
|
305
|
-
const scope = r?.scope;
|
|
306
|
-
return scope?.kind === "session" && scope.sessionId === opts.sessionId && !("reject" in normalizePersistedRule(r));
|
|
307
|
-
});
|
|
308
|
-
if (sessionRows.length > 0)
|
|
309
|
-
table = [...sessionRows, ...listed.rules];
|
|
310
|
-
}
|
|
300
|
+
const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
|
|
301
|
+
const table = view.rules;
|
|
311
302
|
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
|
|
312
303
|
}
|
|
313
304
|
catch {
|
|
@@ -614,45 +605,10 @@ async function applyRedemption(args) {
|
|
|
614
605
|
if (writer === undefined)
|
|
615
606
|
return { status: "refused", reason: "the resolved permission-rule store has no write face" };
|
|
616
607
|
const origin = originOfRecordKind(args.kind);
|
|
617
|
-
if (args.candidate.scope.kind === "session") {
|
|
618
|
-
const overlay = args.deps.sessionRules;
|
|
619
|
-
if (overlay === undefined) {
|
|
620
|
-
return {
|
|
621
|
-
status: "refused",
|
|
622
|
-
reason: "this deployment wired no session-rule overlay (RuleConsentDeps.sessionRules) — a session-scope authorization has nowhere to land",
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
let outcome;
|
|
626
|
-
try {
|
|
627
|
-
outcome = await overlay.apply(args.candidate.scope.sessionId, {
|
|
628
|
-
rule: args.parsedRule.rule,
|
|
629
|
-
tool: args.parsedRule.tool,
|
|
630
|
-
match: args.parsedRule.match,
|
|
631
|
-
command: args.parsedRule.command,
|
|
632
|
-
add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
catch (err) {
|
|
636
|
-
return { status: "refused", reason: `the session-rule overlay refused the write: ${errText(err)}` };
|
|
637
|
-
}
|
|
638
|
-
if ("refused" in outcome) {
|
|
639
|
-
return {
|
|
640
|
-
status: "refused",
|
|
641
|
-
reason: "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)",
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
let rev = 0;
|
|
645
|
-
try {
|
|
646
|
-
rev = (await store.list()).rev;
|
|
647
|
-
}
|
|
648
|
-
catch {
|
|
649
|
-
}
|
|
650
|
-
return { status: "redeemed", rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev, alreadyRedeemed: args.replay };
|
|
651
|
-
}
|
|
652
608
|
for (let attempt = 0; attempt < REDEEM_MAX_ATTEMPTS; attempt++) {
|
|
653
609
|
let rev;
|
|
654
610
|
try {
|
|
655
|
-
rev = (await store
|
|
611
|
+
rev = (await effectiveOrThrow(store)).rev;
|
|
656
612
|
}
|
|
657
613
|
catch (err) {
|
|
658
614
|
return { status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` };
|
|
@@ -676,6 +632,12 @@ async function applyRedemption(args) {
|
|
|
676
632
|
catch (err) {
|
|
677
633
|
return { status: "refused", reason: `the permission-rule store refused the write: ${errText(err)}` };
|
|
678
634
|
}
|
|
635
|
+
if ("refused" in res) {
|
|
636
|
+
return {
|
|
637
|
+
status: "refused",
|
|
638
|
+
reason: "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)",
|
|
639
|
+
};
|
|
640
|
+
}
|
|
679
641
|
if (!("conflict" in res)) {
|
|
680
642
|
return { status: "redeemed", rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: res.rev, alreadyRedeemed: args.replay };
|
|
681
643
|
}
|
|
@@ -875,20 +837,9 @@ export async function redeemRuleBatch(opts) {
|
|
|
875
837
|
const candidate = rec.candidates[candidateIndex];
|
|
876
838
|
const row = { candidateIndex, rule: candidate.rule, scope: candidate.scope };
|
|
877
839
|
let alreadyThere = false;
|
|
878
|
-
|
|
879
|
-
const sessionScope = candidate.scope;
|
|
880
|
-
try {
|
|
881
|
-
const overlayRows = opts.deps.sessionRules === undefined ? [] : await opts.deps.sessionRules.read(sessionScope.sessionId);
|
|
882
|
-
alreadyThere = overlayRows.some((r) => r.rule === candidate.rule && sameScope(r.scope, sessionScope));
|
|
883
|
-
}
|
|
884
|
-
catch (err) {
|
|
885
|
-
members.push({ ...row, status: "refused", reason: `could not read the session-rule overlay: ${errText(err)}` });
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
840
|
+
{
|
|
890
841
|
try {
|
|
891
|
-
const snap = await store.
|
|
842
|
+
const snap = await effectiveOrThrow(store, candidate.scope.kind === "session" ? { sessionId: candidate.scope.sessionId } : undefined);
|
|
892
843
|
rev = snap.rev;
|
|
893
844
|
alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
894
845
|
}
|