@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. 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(spec) {
174
- return spec.transport.kind === "stdio" ? "stdio" : "http";
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 collapseMcpErrorStampInPlace(err) {
224
- if (!(err instanceof McpError))
224
+ function sanitizeMcpErrorTextInPlace(err) {
225
+ if (!(err instanceof Error))
225
226
  return;
226
- const collapsed = collapseMcpErrorPrefix(err.message);
227
- if (collapsed !== err.message)
228
- err.message = collapsed;
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 instanceof Error ? collapseMcpErrorPrefix(err.message) : String(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 = err instanceof Error ? err.message : String(err);
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 = err instanceof Error ? err.message : String(err);
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 (t.kind === "stdio") {
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 settled = await Promise.allSettled(entries.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
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: err instanceof Error ? err.message : String(err) } };
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 = collapseMcpErrorPrefix(err.message);
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: err instanceof Error ? err.message : String(err) };
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 instanceof Error
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(spec), ...resourceInfo } } : {}),
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(spec));
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 { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
32
- import type { SessionRuleOverlay } from "./permission-rule-session.js";
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 { errText, isValidConsentScope, normalizePersistedRule, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
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 listed = await storeForOwner(opts.deps.provider, owner).list();
300
- let table = listed.rules;
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.list()).rev;
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
- if (candidate.scope.kind === "session") {
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.list();
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
  }