@sema-agent/core 7.50.1 → 7.51.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.
Files changed (189) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/dist/agents/child-failure.d.ts +5 -16
  3. package/dist/agents/child-failure.js +4 -24
  4. package/dist/agents/subagent-steps.js +2 -2
  5. package/dist/agents/subagent.d.ts +4 -13
  6. package/dist/agents/subagent.js +94 -204
  7. package/dist/agents/tool-filter.d.ts +2 -2
  8. package/dist/brain/anthropic.d.ts +1 -1
  9. package/dist/brain/anthropic.js +7 -5
  10. package/dist/brain/open-responses.d.ts +1 -1
  11. package/dist/brain/open-responses.js +3 -2
  12. package/dist/brain/openai.d.ts +1 -1
  13. package/dist/brain/openai.js +3 -2
  14. package/dist/brain/request-route.d.ts +2 -2
  15. package/dist/brain/route-adjudicator.d.ts +16 -7
  16. package/dist/brain/route-adjudicator.js +23 -7
  17. package/dist/brain/route-conformance.js +23 -5
  18. package/dist/brain/stream-engine.js +9 -3
  19. package/dist/core/a2a.js +6 -8
  20. package/dist/core/allow-entry.d.ts +45 -26
  21. package/dist/core/allow-entry.js +48 -24
  22. package/dist/core/arg-summary.d.ts +8 -85
  23. package/dist/core/arg-summary.js +4 -386
  24. package/dist/core/ask-origin.d.ts +35 -33
  25. package/dist/core/ask-origin.js +9 -11
  26. package/dist/core/checkpoint-requirements.d.ts +6 -0
  27. package/dist/core/checkpoint-requirements.js +1 -0
  28. package/dist/core/checkpoint-store.d.ts +15 -34
  29. package/dist/core/checkpoint-store.js +5 -4
  30. package/dist/core/classifier-unavailable-arm.d.ts +12 -8
  31. package/dist/core/classifier-unavailable-arm.js +7 -2
  32. package/dist/core/credential-vocabulary.d.ts +3 -2
  33. package/dist/core/credential-vocabulary.js +27 -18
  34. package/dist/core/delegation-frames.d.ts +40 -15
  35. package/dist/core/delegation-frames.js +34 -0
  36. package/dist/core/engine-notice.d.ts +35 -11
  37. package/dist/core/gate-fold.js +9 -7
  38. package/dist/core/gate-lanes.js +6 -4
  39. package/dist/core/gate-outcome.d.ts +87 -11
  40. package/dist/core/gate-outcome.js +171 -61
  41. package/dist/core/governance-codes.d.ts +7 -1
  42. package/dist/core/governance-codes.js +9 -0
  43. package/dist/core/hooks.d.ts +4 -4
  44. package/dist/core/hooks.js +4 -3
  45. package/dist/core/mcp-connection-pool.d.ts +2 -0
  46. package/dist/core/mcp-connection-pool.js +2 -1
  47. package/dist/core/mcp-failure.js +2 -1
  48. package/dist/core/mcp-server-spec.d.ts +27 -5
  49. package/dist/core/mcp.d.ts +13 -38
  50. package/dist/core/mcp.js +28 -23
  51. package/dist/core/media-byte-cap.d.ts +25 -0
  52. package/dist/core/media-byte-cap.js +18 -2
  53. package/dist/core/memory-engine/consolidation-driver.js +1 -1
  54. package/dist/core/mode-policy.d.ts +25 -1
  55. package/dist/core/mode-policy.js +28 -13
  56. package/dist/core/own-property-receipt.d.ts +18 -0
  57. package/dist/core/own-property-receipt.js +39 -0
  58. package/dist/core/permission-rule-model.d.ts +32 -11
  59. package/dist/core/permission-rule-model.js +12 -7
  60. package/dist/core/persisted-rule-arms.d.ts +3 -3
  61. package/dist/core/prompt-section-seat.d.ts +54 -0
  62. package/dist/core/prompt-section-seat.js +49 -0
  63. package/dist/core/protocol-http-auth.d.ts +44 -0
  64. package/dist/core/protocol-http-auth.js +45 -0
  65. package/dist/core/read-face-seats.d.ts +30 -0
  66. package/dist/core/read-face-seats.js +38 -0
  67. package/dist/core/redaction-passes.d.ts +82 -0
  68. package/dist/core/redaction-passes.js +378 -0
  69. package/dist/core/remote-env.d.ts +4 -3
  70. package/dist/core/resume-startup-cause.d.ts +56 -0
  71. package/dist/core/resume-startup-cause.js +43 -0
  72. package/dist/core/rule-denial-sentence.d.ts +2 -2
  73. package/dist/core/runner/announce-once-ledger.js +2 -2
  74. package/dist/core/runner/classifier-ladder.d.ts +17 -13
  75. package/dist/core/runner/classifier-ladder.js +12 -10
  76. package/dist/core/runner/construction-doors.js +9 -20
  77. package/dist/core/runner/contracts.d.ts +4 -4
  78. package/dist/core/runner/gate-exit.js +1 -1
  79. package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
  80. package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
  81. package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
  82. package/dist/core/runner/person-path-rows.d.ts +4 -3
  83. package/dist/core/runner/policy-throw.js +4 -4
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  85. package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
  86. package/dist/core/runner/prepare-gate-stations.js +3 -3
  87. package/dist/core/runner/prepare-hands-readface.js +1 -19
  88. package/dist/core/runner/prepare-inherited-gate.js +10 -9
  89. package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
  90. package/dist/core/runner/prepare-policy-chain.js +10 -13
  91. package/dist/core/runner/prepare-project-context.d.ts +4 -4
  92. package/dist/core/runner/prepare-project-context.js +2 -1
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -0
  94. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
  95. package/dist/core/runner/prepare-prompt-inputs.js +4 -4
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
  97. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  98. package/dist/core/runner/prepare-task.js +6 -4
  99. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
  100. package/dist/core/runner/prepare-turn-wiring.js +8 -7
  101. package/dist/core/runner/run-harness-handlers.js +2 -2
  102. package/dist/core/runner/run-leg.js +10 -9
  103. package/dist/core/runner/run-settle-and-teardown.js +5 -5
  104. package/dist/core/runner/run-turn-boundary.js +3 -2
  105. package/dist/core/runner/runtask.d.ts +1 -2
  106. package/dist/core/runner/runtask.js +8 -12
  107. package/dist/core/runner/session-rule-policy.d.ts +15 -11
  108. package/dist/core/runner/session-rule-policy.js +43 -33
  109. package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
  110. package/dist/core/runner/stream-settle-backstop.js +2 -1
  111. package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
  112. package/dist/core/runner/sub-agent-deny-lists.js +23 -12
  113. package/dist/core/runner/terminal-account.js +3 -3
  114. package/dist/core/runner/tool-output-projection.js +3 -8
  115. package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
  116. package/dist/core/runner/tool-result-media-seat.js +13 -0
  117. package/dist/core/runner/turn-attachments.d.ts +13 -12
  118. package/dist/core/runner/turn-attachments.js +11 -7
  119. package/dist/core/runner-deps.d.ts +4 -3
  120. package/dist/core/safety-merge-corpus.js +21 -21
  121. package/dist/core/secret-env.js +1 -1
  122. package/dist/core/session-policy-store.d.ts +89 -72
  123. package/dist/core/session-policy-store.js +100 -93
  124. package/dist/core/shell-read-silence.d.ts +38 -4
  125. package/dist/core/shell-read-silence.js +102 -0
  126. package/dist/core/side-query.d.ts +7 -8
  127. package/dist/core/skill-grant-binding.d.ts +32 -11
  128. package/dist/core/skill-grant-binding.js +11 -3
  129. package/dist/core/skill-grant.d.ts +4 -3
  130. package/dist/core/skill-grant.js +5 -4
  131. package/dist/core/skills-directory.d.ts +12 -1
  132. package/dist/core/skills-directory.js +17 -8
  133. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  134. package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
  135. package/dist/core/swappable-deps.d.ts +15 -1
  136. package/dist/core/swappable-deps.js +25 -1
  137. package/dist/core/synthetic-read-verdict.d.ts +16 -14
  138. package/dist/core/synthetic-read-verdict.js +28 -8
  139. package/dist/core/task-notification.d.ts +13 -3
  140. package/dist/core/task-notification.js +12 -1
  141. package/dist/core/task-spec.d.ts +14 -15
  142. package/dist/core/tool-name-readings.d.ts +0 -4
  143. package/dist/core/tool-name-readings.js +0 -1
  144. package/dist/core/tool-policy.d.ts +40 -9
  145. package/dist/core/tool-policy.js +25 -7
  146. package/dist/core/types.d.ts +3 -3
  147. package/dist/core/types.js +1 -1
  148. package/dist/core/untrusted-egress.d.ts +60 -11
  149. package/dist/core/untrusted-egress.js +120 -9
  150. package/dist/core/untrusted-text.js +16 -0
  151. package/dist/core/wiring-manifest.d.ts +22 -8
  152. package/dist/core/wiring-manifest.js +12 -4
  153. package/dist/engine/harness/agent-harness.d.ts +2 -1
  154. package/dist/engine/harness/agent-harness.js +4 -1
  155. package/dist/engine/harness/types.d.ts +3 -0
  156. package/dist/engine/llm/types.d.ts +7 -9
  157. package/dist/engine/loop/agent-loop.d.ts +8 -3
  158. package/dist/engine/loop/agent-loop.js +26 -9
  159. package/dist/engine/loop/types.d.ts +10 -0
  160. package/dist/index.d.ts +17 -5
  161. package/dist/index.js +14 -5
  162. package/dist/orchestration/run-workflow-tool.js +8 -8
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompt-assembly/assemble.d.ts +3 -1
  165. package/dist/prompt-assembly/assemble.js +6 -0
  166. package/dist/prompts/cowork.d.ts +42 -0
  167. package/dist/prompts/cowork.js +529 -0
  168. package/dist/prompts/default.d.ts +14 -1
  169. package/dist/prompts/default.js +3 -0
  170. package/dist/stores/file/session-policy-store.d.ts +6 -3
  171. package/dist/stores/file/session-policy-store.js +5 -16
  172. package/dist/tools/artifact/artifact-tool.js +2 -0
  173. package/dist/tools/fs/image-admission.d.ts +57 -4
  174. package/dist/tools/fs/image-admission.js +85 -0
  175. package/dist/tools/fs/path-containment.d.ts +2 -0
  176. package/dist/tools/fs/path-containment.js +5 -0
  177. package/dist/tools/fs/shell-read-check.d.ts +31 -6
  178. package/dist/tools/fs/shell-read-check.js +2 -4
  179. package/dist/tools/fs/shell-read-environment.d.ts +10 -25
  180. package/dist/tools/fs/shell-read-environment.js +6 -18
  181. package/dist/tools/fs/shell-read-establish.d.ts +3 -2
  182. package/dist/tools/fs/shell-read-establish.js +5 -5
  183. package/dist/tools/fs/shell-read-judge.d.ts +38 -8
  184. package/dist/tools/fs/shell-read-judge.js +12 -10
  185. package/dist/tools/web.js +2 -2
  186. package/package.json +1 -1
  187. package/test/export-surface.snapshot.json +94 -10
  188. package/wire-changes/core@7.50.2.json +42 -0
  189. package/wire-changes/core@7.51.0.json +2421 -0
@@ -1,12 +1,13 @@
1
1
  import { canonicalizeTarget, isWinFormPath, pathFamilyOf, writeTargetPath } from "../../tools/fs/safety.js";
2
- import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
3
- import { continuesLineage } from "../session-policy-store.js";
4
- import { thrownText } from "../thrown-value.js";
5
- import { heldBy, wireOf } from "../tool-name-readings.js";
2
+ import { continuesLineage, readSessionRulesShape } from "../session-policy-store.js";
3
+ import { compileAllowList, compiledEntryName, judgeAllowEntries, uncoveredCommandSegment } from "../allow-entry.js";
4
+ import { inlineUntrusted } from "../untrusted-text.js";
5
+ import { callTargetReadingOf, pathTargetBaseOf, readCallTarget } from "../effective-path-target.js";
6
+ import { egressThrownText } from "../untrusted-egress.js";
7
+ import { wireOf } from "../tool-name-readings.js";
6
8
  import { ruleDenialSentence } from "../rule-denial-sentence.js";
7
9
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
8
10
  import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames } from "../tool-registry.js";
9
- import { pathTargetBaseOf } from "../effective-path-target.js";
10
11
  import { judgePathRows } from "../path-guard-rows.js";
11
12
  import { domainRow } from "../fs-write-gate-policy.js";
12
13
  export const PATH_CONFINABLE_WRITE_TOOLS = pathConfinableWriteToolNames();
@@ -53,7 +54,7 @@ export function createLiveSessionRules(store, floor, hooks) {
53
54
  const wait = gateWaitFailureOf(err);
54
55
  announce(wait !== undefined
55
56
  ? `the session-policy store read for session ${JSON.stringify(floor.sessionId)} ended without an answer (${wait})`
56
- : `the session-policy store threw while re-reading session ${JSON.stringify(floor.sessionId)}: ${thrownText(err)}`);
57
+ : `the session-policy store threw while re-reading session ${JSON.stringify(floor.sessionId)}: ${egressThrownText("same-principal", err)}`);
57
58
  return enforced;
58
59
  }
59
60
  if (live === null) {
@@ -104,23 +105,30 @@ export function createLiveSessionRules(store, floor, hooks) {
104
105
  };
105
106
  }
106
107
  function compileSessionRules(rules) {
108
+ const shaped = readSessionRulesShape(rules);
109
+ if ("fault" in shaped) {
110
+ const { field, got } = shaped.fault;
111
+ const what = got === "not a rule bucket" ? "a key that is no rule bucket" : field === "" ? `a value that is ${got}, not an object of rule buckets` : `${field} holding ${got}`;
112
+ return { deny: [], allow: undefined, allowDirs: undefined, pathRows: false, names: { deny: [], allow: [] }, unreadable: `the session rules cannot be read as rules (they hold ${what}) — every call is refused until they are rewritten` };
113
+ }
114
+ const { toolDeny, toolAllow, allowDirs } = shaped.rules;
115
+ const denyList = compileAllowList(toolDeny ?? [], "deny");
116
+ const allowList = toolAllow === undefined ? undefined : compileAllowList(toolAllow, "allow");
117
+ const rowsOf = (behavior, list) => list.rows.map((r) => ({ behavior, entry: r.entry, compiled: r.compiled }));
118
+ const deny = rowsOf("deny", denyList);
119
+ const allow = allowList === undefined ? undefined : rowsOf("allow", allowList);
120
+ const namesOf = (rows, list) => [...rows.map((r) => compiledEntryName(r.compiled)), ...(list?.inert ?? []).map((i) => i.entry)];
107
121
  return {
108
- toolDeny: new Set(rules.toolDeny ?? []),
109
- toolAllow: rules.toolAllow ? new Set(rules.toolAllow) : undefined,
110
- toolDenyCovering: namespacedCoveringEntries(rules.toolDeny),
111
- toolAllowCovering: namespacedCoveringEntries(rules.toolAllow),
112
- cmdPolicy: rules.commandAllow || rules.commandDeny
113
- ? createCoarseCommandNamePolicy({
114
- ...(rules.commandAllow ? { allow: rules.commandAllow } : {}),
115
- ...(rules.commandDeny ? { deny: rules.commandDeny } : {}),
116
- defaultAction: "deny",
117
- })
118
- : undefined,
119
- allowDirs: rules.allowDirs && rules.allowDirs.length > 0 ? rules.allowDirs : undefined,
122
+ deny,
123
+ allow,
124
+ allowDirs: allowDirs !== undefined && allowDirs.length > 0 ? allowDirs : undefined,
125
+ pathRows: [...deny, ...(allow ?? [])].some((r) => r.compiled.kind === "path"),
126
+ names: { deny: namesOf(deny, denyList), allow: namesOf(allow ?? [], allowList) },
120
127
  };
121
128
  }
122
129
  export function createSessionRulePolicy(rules, opts) {
123
130
  const { env, rootPath, toolEffects } = opts;
131
+ const anchors = opts.anchors ?? (rootPath !== undefined ? { root: rootPath } : {});
124
132
  const source = "seed" in rules ? rules : { seed: rules };
125
133
  const seed = source.seed;
126
134
  const readRules = source.read ?? (() => Promise.resolve(seed));
@@ -133,22 +141,24 @@ export function createSessionRulePolicy(rules, opts) {
133
141
  }
134
142
  return compiled;
135
143
  };
144
+ const seedNames = compile(seed).names;
136
145
  return {
137
- nameSets: [{ ...(seed.toolDeny?.length ? { deny: [...seed.toolDeny] } : {}), ...(seed.toolAllow?.length ? { allow: [...seed.toolAllow] } : {}) }],
146
+ nameSets: [{ ...(seedNames.deny.length > 0 ? { deny: seedNames.deny } : {}), ...(seedNames.allow.length > 0 ? { allow: seedNames.allow } : {}) }],
138
147
  async check(req, signal) {
139
- const { toolDeny, toolAllow, toolDenyCovering, toolAllowCovering, cmdPolicy, allowDirs } = compile(await readRules(req.toolCallId, signal));
140
- const listed = (set) => heldBy("policy.sessionRules", set, req);
141
- if (listed(toolDeny) || namespacedCoveringHit(toolDenyCovering, req))
142
- return deny(req.toolName);
143
- if (toolAllow && !listed(toolAllow) && !namespacedCoveringHit(toolAllowCovering, req)) {
144
- return deny(req.toolName, "it is not in the session-rule allowlist");
145
- }
146
- if (cmdPolicy) {
147
- const d = await cmdPolicy.check(req, signal);
148
- if (d.action === "deny")
149
- return { ...d, message: ruleDenialSentence({ source: "session", tool: req.toolName, ...(d.message !== undefined ? { detail: d.message } : {}) }) };
150
- if (d.action !== "allow")
151
- return d;
148
+ const { deny: denyRows, allow: allowRows, allowDirs, unreadable, pathRows } = compile(await readRules(req.toolCallId, signal));
149
+ if (unreadable !== undefined)
150
+ return deny(req.toolName, unreadable);
151
+ if (denyRows.length > 0 || allowRows !== undefined) {
152
+ const located = req.root === undefined && rootPath !== undefined ? { ...req, root: rootPath } : req;
153
+ const call = pathRows && callTargetReadingOf(req) === undefined ? { ...req, targetReading: await readCallTarget(env, located, signal) } : req;
154
+ const judged = { root: rootPath, sessionId: undefined, liveCwd: () => req.cwd, anchors };
155
+ const refusing = denyRows.length > 0 ? judgeAllowEntries(denyRows, call, "deny", judged) : undefined;
156
+ if (refusing !== undefined)
157
+ return deny(req.toolName);
158
+ if (allowRows !== undefined && judgeAllowEntries(allowRows, call, "allow", judged, "narrowing") === undefined) {
159
+ const segment = uncoveredCommandSegment(allowRows, call, judged, "narrowing");
160
+ return deny(req.toolName, segment === undefined ? "it is not in the session-rule allowlist" : `it is not in the session-rule allowlist (no entry covers ${JSON.stringify(inlineUntrusted(segment, 200))})`);
161
+ }
152
162
  }
153
163
  if (allowDirs) {
154
164
  if (!isProtectedWrite(req)) {
@@ -175,7 +185,7 @@ export function createSessionRulePolicy(rules, opts) {
175
185
  }
176
186
  const views = canon.ok ? { views: [{ path: canon.key, kind: "canonical" }] } : { views: [], unreadable: canon.message };
177
187
  if (judgePathRows([domainRow("session", roots)], views, "allow") === undefined) {
178
- return deny(req.toolName, canon.ok ? `the write to ${JSON.stringify(path)} is outside the session-rule allowDirs (${allowDirs.join(", ")})` : `the real target of the write to ${JSON.stringify(path)} could not be resolved against the session-rule allowDirs`);
188
+ return deny(req.toolName, canon.ok ? `the write to ${JSON.stringify(path)} is outside the session-rule allowDirs` : `the real target of the write to ${JSON.stringify(path)} could not be resolved against the session-rule allowDirs`);
179
189
  }
180
190
  }
181
191
  }
@@ -1,4 +1,4 @@
1
- import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
1
+ import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES, TASK_NOTIFICATION_TERMINAL_STATUSES } from "../task-notification.js";
2
2
  export function streamLifecycleVerbs(input) {
3
3
  const { live, ready, orTimeout, steeringError, notifyRef, captureOptOutRef, mcpRedialRef, manualCompactRef, detachHub } = input;
4
4
  const notify = async (input, opts) => {
@@ -10,7 +10,7 @@ export function streamLifecycleVerbs(input) {
10
10
  if (typeof input?.task_id !== "string" || input.task_id.trim() === "") {
11
11
  throw notifyError("input.task_id must be a non-empty string", "notify.invalid_payload");
12
12
  }
13
- const VALID_STATUSES = new Set(["completed", "failed", "killed", "cancelled", "event"]);
13
+ const VALID_STATUSES = new Set([...TASK_NOTIFICATION_TERMINAL_STATUSES, "event"]);
14
14
  if (!VALID_STATUSES.has(input.status)) {
15
15
  throw notifyError(`input.status must be one of ${[...VALID_STATUSES].join("/")}`, "notify.invalid_payload");
16
16
  }
@@ -1,4 +1,5 @@
1
1
  import { errorCodeOf, thrownFailureOf } from "./terminal-account.js";
2
+ import { reopenReasonOf, resumeStartupCauseOf } from "../resume-startup-cause.js";
2
3
  import { resumeDecisionWasNegative } from "./decide-continuation.js";
3
4
  import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
4
5
  import { hasDestroy } from "../remote-env.js";
@@ -11,7 +12,7 @@ export function streamSettleBackstop(input) {
11
12
  const { spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner, sessions, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
12
13
  const onRunRejected = async (err) => {
13
14
  let { code, message, detail } = thrownFailureOf(err, thrownText(err));
14
- const reopenReason = code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch" ? "tool_unavailable" : "env_failed";
15
+ const reopenReason = reopenReasonOf(resumeStartupCauseOf(code));
15
16
  const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
16
17
  let reopenCommitted = false;
17
18
  let reopenStateUnknown = false;
@@ -3,15 +3,18 @@
3
3
  * `TaskSpec.excludeTools`, unless the delegation lane spawned it — its spawning run judged them where they were mounted), the definitions it
4
4
  * carries (`TaskSpec.agents`), and the definitions and tool-level lists a mounted delegation tool carries (the `authored` faces
5
5
  * — the engine's built-ins are not judged; only a tool a deployment mounted (depth 0) publishes them). One rule for both sides:
6
- * a value that is not a list, an entry that is not a string, a spelling that can name no tool, a stale name and a name the
7
- * universe does not carry are REFUSED; a list whose universe the engine cannot list (a delegation tool that injects tools per
8
- * spawn with no declared roster) turns the refusals that need a universe into host notices. And one contradiction, judged on
9
- * every leg: a denylist (the run's own or one of its ceiling) that reaches a tool the leg's kind relies on (the catalog's
10
- * protocol column) is refused — neither a silently ignored deny nor a silently crippled leg.
6
+ * a value that is not a list, an entry that is not a string and a spelling that can name no tool are REFUSED; an entry whose
7
+ * name no tool of the universe answers to (a server this deployment does not declare, a stale or misspelt name) is KEPT as
8
+ * written — the selector reads every entry by name, so a denied one hides a tool of that name whenever one is mounted and an
9
+ * allowed one admits it — and told to the host, one account per entry. And one contradiction, judged on every leg: a
10
+ * denylist (the run's own or one of its ceiling) that reaches a tool the leg's kind relies on (the catalog's protocol column)
11
+ * is refused — neither a silently ignored deny nor a silently crippled leg.
11
12
  *
12
13
  * Universe = the task's assembled roster before the valve, every tool the catalog describes, every mounted delegation tool's
13
14
  * pool and the caller's own tools (names and aliases), and the protocol peers the task declares (a namespaced spelling under a
14
- * declared peer names a tool of it — the peer's tool list is runtime data, so the peer is what is checked).
15
+ * declared peer names a tool of it — the peer's tool list is runtime data, so the peer is what is checked). A delegation tool
16
+ * whose factory injects tools per spawn with no declared roster (`extraTools`) lists none of them here; a kept entry reaches
17
+ * its products by name like any other later-mounted tool.
15
18
  *
16
19
  * Throws `config.tool_face_control` for the run's own lists and the protocol contradiction, `config.agent.unknown_tool` for a
17
20
  * definition's or a delegation tool's lists — synchronous, like the phase that calls it.
@@ -24,8 +27,9 @@ import type { AnnounceOnceSink } from "./contracts.js";
24
27
  /**
25
28
  * The names a definition's denied-tools entries stand for, for a lane that has neither a roster nor a notice sink at the point
26
29
  * it builds its own policy (a workflow leg's agentType): each entry is read by the one reading, so `Tool(content)` denies the
27
- * whole tool there too, and an entry that names no tool is refused by the same refusal. The stale-name and told dispositions
28
- * need the leg's roster and a sink; the leg's own policy-name audit is what reads them.
30
+ * whole tool there too, a spelling no tool name can have is refused by the same refusal, and a name no tool answers to is
31
+ * returned as written (the policy denies a call of that name whenever one is made). The account of such a name needs the
32
+ * leg's roster and a sink; the leg's own policy-name audit is what reads them.
29
33
  */
30
34
  export declare function deniedToolNamesOf(where: string, value: unknown): string[];
31
35
  /**
@@ -23,6 +23,14 @@ function refused(message, code, reason) {
23
23
  return e;
24
24
  }
25
25
  const LIST_WORD = { allowed: "allowed tools", denied: "denied tools" };
26
+ function grammarRefusal(where, side, flaw, code) {
27
+ return refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its ${LIST_WORD[side]}, a spelling no tool name can have — fix the entry. ${DENY_LIST_NAMES_ONLY}`, code, "unknown_name");
28
+ }
29
+ const KEPT_ENTRY = {
30
+ allowed: "it grants nothing today and stays in force for a tool of that name mounted later",
31
+ denied: "it denies nothing today and stays in force for a tool of that name mounted later",
32
+ };
33
+ const PEER_WORD = { mcp: "MCP server", a2a: "A2A peer" };
26
34
  function judgeToolNameList(where, side, value, universe, advise, code) {
27
35
  if (value === undefined || value === null)
28
36
  return;
@@ -40,7 +48,8 @@ function judgeToolNameList(where, side, value, universe, advise, code) {
40
48
  throw refused(`${where} declares "*" in its ${LIST_WORD[side]}: a denied list has no wildcard — the entry would hide nothing. To give the agent no tools, write an empty allowed list.`, code, "unsupported_pattern");
41
49
  }
42
50
  const read = readDeniedEntry(entry);
43
- const name = side === "denied" && read.kind === "name" ? read.name : entry;
51
+ const toolName = read.kind === "name" ? read.name : entry;
52
+ const name = side === "denied" ? toolName : entry;
44
53
  const content = side === "denied" && read.kind === "name" && read.content;
45
54
  const reach = side === "denied" ? readDeniedToolList([entry]) : (readAllowedToolList([entry]) ?? readDeniedToolList([]));
46
55
  const split = splitNamespacedName(name);
@@ -49,11 +58,13 @@ function judgeToolNameList(where, side, value, universe, advise, code) {
49
58
  continue;
50
59
  throw refused(`${where} declares ${renderListEntry(entry)} in its ${LIST_WORD[side]}: an allowed list has no protocol-wide form — name a peer (\`${split.ns.prefix}<peer>\`) or its tools.`, code, "unsupported_pattern");
51
60
  }
52
- const { inert } = compileAllowList([name], "deny", universe.known);
61
+ const { inert } = compileAllowList([toolName], "deny", universe.known);
53
62
  const flaw = inert[0] === undefined ? undefined : { ...inert[0], entry };
54
- const stale = flaw !== undefined && (flaw.reason === "tool_name" || flaw.reason === "near_miss");
55
- if (flaw !== undefined && (flaw.reason === "grammar" || (stale && universe.complete))) {
56
- throw refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its ${LIST_WORD[side]}, but no tool of that spelling exists in this deployment — fix the entry or mount the tool.${flaw.reason === "grammar" ? ` ${DENY_LIST_NAMES_ONLY}` : ""}`, code, "unknown_name");
63
+ if (flaw !== undefined && flaw.reason === "grammar")
64
+ throw grammarRefusal(where, side, flaw, code);
65
+ if (toolName !== name) {
66
+ advise(new Error(`${where} declares ${renderListEntry(entry)} in its allowed tools: an allowed list names tools, not content, so this entry grants nothing — name the tool "${inlineUntrusted(toolName, 80)}" to grant it, or move the content rule to a denied list or a permission rule.`));
67
+ continue;
57
68
  }
58
69
  if (flaw === undefined && universeCarries(reach, name, universe)) {
59
70
  if (content)
@@ -61,9 +72,9 @@ function judgeToolNameList(where, side, value, universe, advise, code) {
61
72
  continue;
62
73
  }
63
74
  const phrase = flaw !== undefined ? inertEntryPhrase(flaw) : renderListEntry(entry);
64
- if (universe.complete)
65
- throw refused(`${where} declares tool ${phrase} in its ${LIST_WORD[side]}, a name no tool of this deployment answers to — fix the entry or mount the tool.`, code, "unknown_name");
66
- advise(new Error(`${where} declares tool ${phrase} in its ${LIST_WORD[side]}, a name no listed tool answers to — a delegation tool this task mounts injects tools the engine cannot enumerate (extraTools), so the name cannot be checked here.`));
75
+ const undeclared = split !== undefined && split.peer !== "" && !split.peer.includes("*") && !universe.peers.has(mintNamespacePrefix(split.ns, split.peer));
76
+ const why = undeclared ? `no ${PEER_WORD[split.ns.id]} named "${inlineUntrusted(split.peer, 80)}" is declared for this task` : "no tool of this task answers to that name";
77
+ advise(new Error(`${where} declares tool ${phrase} in its ${LIST_WORD[side]}: ${why}, so ${KEPT_ENTRY[side]}. If the name is misspelt, fix the entry.`));
67
78
  }
68
79
  }
69
80
  export function deniedToolNamesOf(where, value) {
@@ -78,7 +89,7 @@ export function deniedToolNamesOf(where, value) {
78
89
  throw refused(notAStringSentence(where, "denied", index, read.type), "config.agent.unknown_tool", "not_a_string");
79
90
  const flaw = compileAllowList([read.name], "deny").inert[0];
80
91
  if (flaw !== undefined && flaw.reason === "grammar")
81
- throw refused(`${where} declares tool ${inertEntryPhrase({ ...flaw, entry: read.name })} in its denied tools, but no tool of that spelling exists in this deployment — fix the entry or mount the tool. ${DENY_LIST_NAMES_ONLY}`, "config.agent.unknown_tool", "unknown_name");
92
+ throw grammarRefusal(where, "denied", { ...flaw, entry: read.name }, "config.agent.unknown_tool");
82
93
  return read.name;
83
94
  });
84
95
  }
@@ -128,10 +139,10 @@ export function judgeRunToolLists(input) {
128
139
  if (split !== undefined && split.peer !== "")
129
140
  peers.add(mintNamespacePrefix(split.ns, split.peer));
130
141
  }
131
- const universe = { known, peers, complete: delegationTools.every((t) => t.agentToolPool !== undefined) };
142
+ const universe = { known, peers };
132
143
  if (!input.judgedUpstream) {
133
- judgeToolNameList(input.ownWhere ?? "TaskSpec.allowTools", "allowed", input.own.allow, { ...universe, complete: true }, advise, "config.tool_face_control");
134
- judgeToolNameList(input.ownWhere ?? "TaskSpec.excludeTools", "denied", input.own.exclude, { ...universe, complete: true }, advise, "config.tool_face_control");
144
+ judgeToolNameList(input.ownWhere ?? "TaskSpec.allowTools", "allowed", input.own.allow, universe, advise, "config.tool_face_control");
145
+ judgeToolNameList(input.ownWhere ?? "TaskSpec.excludeTools", "denied", input.own.exclude, universe, advise, "config.tool_face_control");
135
146
  }
136
147
  for (const def of spec.agents ?? []) {
137
148
  judgeToolNameList(`TaskSpec.agents: agent "${def.name}"`, "allowed", def.allowTools, universe, advise, "config.agent.unknown_tool");
@@ -3,7 +3,7 @@ import { isProviderEchoShape } from "../../brain/errors.js";
3
3
  import { isProviderCondition, isProviderConditionSource } from "../../brain/provider-dialect.js";
4
4
  import { admittedRequestRoute } from "../../brain/request-route.js";
5
5
  import { readMember } from "../thrown-value.js";
6
- import { redactSecrets } from "../untrusted-egress.js";
6
+ import { egressText } from "../untrusted-egress.js";
7
7
  import { isSensitiveName, looksLikeCredentialValue } from "../secret-env.js";
8
8
  import { isSensitiveRecordKey } from "../credential-vocabulary.js";
9
9
  export function providerVerdictOf(final) {
@@ -41,7 +41,7 @@ const CODED_DETAIL_CHARS = 16_384;
41
41
  const REDACTED = "[redacted]";
42
42
  const INDEX_KEY_RE = /^(?:0|[1-9][0-9]*)$/;
43
43
  function carriesCredential(text) {
44
- return looksLikeCredentialValue(text) || redactSecrets(text) !== text;
44
+ return looksLikeCredentialValue(text) || egressText("same-principal", text) !== text;
45
45
  }
46
46
  const NAME_CHARS = 128;
47
47
  function namesCredential(name) {
@@ -171,7 +171,7 @@ function codedLinkOf(err) {
171
171
  return fallback;
172
172
  }
173
173
  export function thrownFailureOf(err, text) {
174
- const message = redactSecrets(text);
174
+ const message = egressText("same-principal", text);
175
175
  const coded = codedLinkOf(err);
176
176
  if (coded === undefined)
177
177
  return { message };
@@ -1,13 +1,9 @@
1
1
  import { isTextBlock, renderContentBlock } from "../../internal/llm.js";
2
- import { imageBytes, mediaBudgetBytes } from "../media-byte-cap.js";
2
+ import { isMediaBlock, mediaBlockDescriptor, mediaBudgetBytes, omittedMediaDescriptor } from "../media-byte-cap.js";
3
3
  import { engineCardTypes } from "../tool-registry.js";
4
4
  import { readMember } from "../thrown-value.js";
5
5
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
6
6
  const marker = (n) => `…[+${n} chars truncated]`;
7
- const isMediaBlock = (b) => {
8
- const block = b;
9
- return (block?.type === "image" || block?.type === "document") && typeof block.data === "string";
10
- };
11
7
  const ownChars = (b) => isTextBlock(b) ? b.text.length + (JSON.stringify({ ...b, type: undefined, text: undefined })?.length ?? 2) - 2 : isMediaBlock(b) ? b.data.length : (JSON.stringify(b)?.length ?? 0);
12
8
  const boundTextLane = (blocks, lane) => {
13
9
  const sized = lane.map((i) => ({ i, size: ownChars(blocks[i]) }));
@@ -37,12 +33,11 @@ const fitMedia = (media, budget) => {
37
33
  for (const [i, m] of media.entries()) {
38
34
  const cost = used + m.data.length >= budget ? Infinity : wireBytes(m);
39
35
  const kept = used + cost <= budget;
40
- const out = kept ? m : { type: m.type, ...(typeof m.mimeType === "string" && m.mimeType.length <= 255 ? { mimeType: m.mimeType } : {}), bytes: imageBytes(m), truncated: true };
36
+ const out = kept ? m : mediaBlockDescriptor(m);
41
37
  const outCost = kept ? cost : wireBytes(out);
42
38
  degraded ||= !kept;
43
39
  if (used + outCost > budget) {
44
- const rest = media.slice(i);
45
- blocks.push({ type: "omitted", blocks: rest.length, bytes: rest.reduce((n, r) => n + imageBytes(r), 0), truncated: true });
40
+ blocks.push(omittedMediaDescriptor(media.slice(i)));
46
41
  break;
47
42
  }
48
43
  used += outCost;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The run's account of the tool-result seat's media refusals (`tool_result.media_refused`, operator). The loop's
3
+ * post-execution half admits every media block an executed call's result carries (`admitToolResultMedia`, the last step
4
+ * of `finalizeToolOutcome`) and hands each call's refusals here once; the placeholders already stand in the result, so
5
+ * this seat only words the account and delivers it on the deployment's notice sink. The detail carries the seat's
6
+ * closed-set words, counts, the call's identity and a media type only when it can be one — never the bytes.
7
+ */
8
+ import { type EngineNotice } from "../engine-notice.js";
9
+ import type { ToolResultMediaRefusal } from "../../tools/fs/image-admission.js";
10
+ /** The run's attribution pair, as every per-run notice carries it. */
11
+ export interface ToolResultMediaSeatIds {
12
+ sessionId: string;
13
+ runId: string;
14
+ }
15
+ export declare function toolResultMediaRefusedSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolResultMediaSeatIds): (info: {
16
+ toolName: string;
17
+ toolCallId: string;
18
+ refused: readonly ToolResultMediaRefusal[];
19
+ }) => void;
@@ -0,0 +1,13 @@
1
+ import { deliverEngineNotice } from "../engine-notice.js";
2
+ export function toolResultMediaRefusedSeat(onNotice, ids) {
3
+ return ({ toolName, toolCallId, refused }) => {
4
+ const reasons = [...new Set(refused.map((r) => r.reason))].join(", ");
5
+ deliverEngineNotice(onNotice, {
6
+ code: "tool_result.media_refused",
7
+ message: `The ${JSON.stringify(toolName)} tool's result carried ${refused.length} media block(s) whose bytes the media admission refused (${reasons}). ` +
8
+ `Each was replaced in the result by a text placeholder naming the refusal, so neither the model nor the event stream received those bytes; ` +
9
+ `the tool, or the server behind it, returned bytes that are not what their block claims.`,
10
+ detail: { toolName, toolCallId, refused: refused.map((r) => ({ ...r })), sessionId: ids.sessionId, runId: ids.runId },
11
+ });
12
+ };
13
+ }
@@ -1,4 +1,5 @@
1
1
  import type { McpDroppedTool } from "../mcp.js";
2
+ import type { McpInstructionsEntry } from "../prompt-section-seat.js";
2
3
  import type { TaskSpec } from "../types.js";
3
4
  import { type SkillListingEntry, type SkillsBlockRender } from "./synthetic-tools.js";
4
5
  import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
@@ -599,10 +600,7 @@ export interface AttachmentInputs {
599
600
  /** G1 续批 mcp_instructions: pending deltas snapshotted from `MaterializedMcp.instructionsDelta`.
600
601
  * CALLER-owned consumption on intact cap survival (same `newTools` discipline). */
601
602
  mcpInstructions?: {
602
- added: ReadonlyArray<{
603
- server: string;
604
- text: string;
605
- }>;
603
+ added: ReadonlyArray<McpInstructionsEntry>;
606
604
  removed: readonly string[];
607
605
  };
608
606
  /** [cc216] mcp_dropped_tools: intake-dropped MCP tools snapshotted from
@@ -989,26 +987,29 @@ export declare const MCP_INSTRUCTIONS_MAX_CHARS: number;
989
987
  * drift.
990
988
  */
991
989
  export declare function fenceMcpServerInstructions(server: string, text: string): string;
990
+ /**
991
+ * The ONE renderer of an MCP instructions entry, read by both lanes. A server's text rides the fence above;
992
+ * a host's replacement is host text and takes the reference's per-server block, `## <server>` then the
993
+ * text (cli296.js B[17396126,17396429), `Yqe`), unfenced. Both are bounded by the same per-entry cap and
994
+ * disclose the same cut, so the frame's size bound holds for the entry that actually rides.
995
+ */
996
+ export declare function renderMcpInstructionsEntry(entry: McpInstructionsEntry): string;
992
997
  /**
993
998
  * G1 续批 — MCP server-instruction delta (CC 2.1.198 `mcp_instructions_delta`, bundle
994
999
  * pretty.js:599225-599248, headers VERBATIM). Deliberate CC deviations (recorded):
995
1000
  * - added instruction text is UNTRUSTED server-controlled text → each block rides inside a
996
1001
  * `delimitUntrusted` fence (CC injects the raw block; sema's external-inlet posture is fencing —
997
1002
  * the text is additionally already `sanitizeUntrustedText`-neutralized at the mcp.ts producer).
998
- * The fence is now applied through {@link fenceMcpServerInstructions}, shared with the
999
- * STABLE system-prompt lane, so the two lanes can never again carry the same text with different
1000
- * trust postures — and it carries a per-server {@link MCP_INSTRUCTIONS_MAX_CHARS} bound (the
1001
- * frame is intact-class: its size is this producer's promise, servers × that cap).
1003
+ * Each entry renders through {@link renderMcpInstructionsEntry}, shared with the STABLE system-prompt
1004
+ * lane, so one entry never carries two postures; every entry, server's or host's, carries the per-entry
1005
+ * {@link MCP_INSTRUCTIONS_MAX_CHARS} bound (the frame is intact-class: its size is entries × that cap).
1002
1006
  * The disconnect list is followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (CC `Emr`,
1003
1007
  * pretty.js:599240/599992 — resolved by the 2026-07-10 parity spot audit).
1004
1008
  * Removal names are `\n`-joined WITHOUT a `- ` prefix (CC-exact; the agent-listing removal list DOES
1005
1009
  * carry the prefix — the asymmetry is CC's, kept verbatim). Exported for the run loop's
1006
1010
  * intact-survival consumption check (tools_delta discipline).
1007
1011
  */
1008
- export declare function renderMcpInstructionsDelta(added: ReadonlyArray<{
1009
- server: string;
1010
- text: string;
1011
- }>, removed: readonly string[]): string | undefined;
1012
+ export declare function renderMcpInstructionsDelta(added: ReadonlyArray<McpInstructionsEntry>, removed: readonly string[]): string | undefined;
1012
1013
  /**
1013
1014
  * [cc216] mcp_dropped_tools — announce intake-dropped MCP tools ONCE (CC's
1014
1015
  * `mcp_dropped_tools_delta` system reminder). Per-entry line shape follows CC's `qny`
@@ -1,5 +1,5 @@
1
1
  import { readMember } from "../thrown-value.js";
2
- import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
2
+ import { delimitUntrustedWithClip, inlineUntrusted, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
3
3
  import { protocolOf } from "../protocol-table.js";
4
4
  import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
5
5
  import { SKILL_LISTING_DEFAULT_BUDGET } from "../skill-listing-budget.js";
@@ -677,16 +677,20 @@ export function commitSkillsListing(state, entries) {
677
677
  export const MCP_INSTRUCTIONS_MAX_CHARS = 8 * 1024;
678
678
  export function fenceMcpServerInstructions(server, text) {
679
679
  const fence = delimitUntrustedWithClip(`MCP server "${server}" instructions`, text, MCP_INSTRUCTIONS_MAX_CHARS);
680
- return fence.clipped
681
- ? `${fence.text}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
682
- : fence.text;
680
+ return fence.clipped ? `${fence.text}\n${MCP_INSTRUCTIONS_TRUNCATION_NOTE}` : fence.text;
681
+ }
682
+ const MCP_INSTRUCTIONS_TRUNCATION_NOTE = `(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`;
683
+ export function renderMcpInstructionsEntry(entry) {
684
+ if (entry.author !== "host")
685
+ return fenceMcpServerInstructions(entry.server, entry.text);
686
+ const cps = [...entry.text];
687
+ const body = cps.length > MCP_INSTRUCTIONS_MAX_CHARS ? `${cps.slice(0, MCP_INSTRUCTIONS_MAX_CHARS - 1).join("")}…\n${MCP_INSTRUCTIONS_TRUNCATION_NOTE}` : entry.text;
688
+ return `## ${inlineUntrusted(entry.server, 160)}\n${body}`;
683
689
  }
684
690
  export function renderMcpInstructionsDelta(added, removed) {
685
691
  const blocks = [];
686
692
  if (added.length > 0) {
687
- blocks.push(`# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n${added
688
- .map((a) => fenceMcpServerInstructions(a.server, a.text))
689
- .join("\n\n")}`);
693
+ blocks.push(`# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n${added.map(renderMcpInstructionsEntry).join("\n\n")}`);
690
694
  }
691
695
  if (removed.length > 0) {
692
696
  blocks.push(`The following MCP servers have disconnected. Their instructions above no longer apply:\n${removed.join("\n")}`);
@@ -145,10 +145,9 @@ export interface RunnerDeps {
145
145
  * row (#433 made the registry total over the catalog — no engine-minted code is
146
146
  * audience-defaulted any more) — a
147
147
  * wire projector forwards it operator-tier and needs no per-session de-duplication of its own.
148
+ * The probe's second argument, `{ fingerprint }`, is passed exactly when the server's declaration carries one.
148
149
  */
149
- mcpRevocations?: {
150
- isRevoked(serverName: string): boolean;
151
- };
150
+ mcpRevocations?: import("./mcp-server-spec.js").McpRevocationLedger;
152
151
  /**
153
152
  * design/147 S1c (ruled 2026-07-18) — the DURABLE name→agent roster behind explicit-name
154
153
  * addressing, a storage-tier seam like the checkpoint store: core bundles `MemoryRosterStore`
@@ -1144,6 +1143,8 @@ export interface RunnerDeps {
1144
1143
  * prepares. Always filled by the Runner; optional so hand-built ctx objects stay valid.
1145
1144
  */
1146
1145
  sessionId?: string;
1146
+ /** The run this prepare belongs to — the id the `task.start` trace frame, `noteTaskRun` and `TaskResult.runId` carry. Always filled by the Runner. */
1147
+ runId?: string;
1147
1148
  /**
1148
1149
  * design-A C-1/C-2 ([A2]) — where this prepare sits in the snapshot lifecycle; the deployment's
1149
1150
  * cache-invalidation signal (core only derives and forwards it, and ignores the answer):
@@ -9,11 +9,11 @@ export const SAFETY_MERGE_CONFORMANCE_CORPUS = [
9
9
  },
10
10
  {
11
11
  name: "command-deny-removed",
12
- intent: "removing a commandDeny loosens",
13
- prior: { commandDeny: ["rm -rf"] },
14
- next: { commandDeny: [] },
12
+ intent: "removing a shell command rule from toolDeny loosens (the command runs again)",
13
+ prior: { toolDeny: ["Bash(rm:*)"] },
14
+ next: { toolDeny: [] },
15
15
  loosens: true,
16
- reasons: ['removed commandDeny "rm -rf"'],
16
+ reasons: ['removed toolDeny "Bash(rm:*)"'],
17
17
  },
18
18
  {
19
19
  name: "toolAllow-removed-all-allowed",
@@ -32,12 +32,12 @@ export const SAFETY_MERGE_CONFORMANCE_CORPUS = [
32
32
  reasons: ['widened toolAllow with "Bash"'],
33
33
  },
34
34
  {
35
- name: "commandAllow-widened",
36
- intent: "adding a command outside the org command allowlist loosens",
37
- prior: { commandAllow: ["ls"] },
38
- next: { commandAllow: ["ls", "curl"] },
35
+ name: "command-allow-widened",
36
+ intent: "adding a shell command rule outside the org allowlist loosens",
37
+ prior: { toolAllow: ["Read", "Bash(ls:*)"] },
38
+ next: { toolAllow: ["Read", "Bash(ls:*)", "Bash(curl:*)"] },
39
39
  loosens: true,
40
- reasons: ['widened commandAllow with "curl"'],
40
+ reasons: ['widened toolAllow with "Bash(curl:*)"'],
41
41
  },
42
42
  {
43
43
  name: "allowDirs-confinement-removed",
@@ -63,14 +63,6 @@ export const SAFETY_MERGE_CONFORMANCE_CORPUS = [
63
63
  loosens: true,
64
64
  reasons: ['removed toolDeny "Bash"', 'widened allowDirs with "/x"'],
65
65
  },
66
- {
67
- name: "commandAllow-removed-all-allowed",
68
- intent: "dropping the COMMAND allowlist (distinct field from toolAllow) re-permits every command — the max loosen on the command axis (review: this branch was unpinned)",
69
- prior: { commandAllow: ["ls"] },
70
- next: {},
71
- loosens: true,
72
- reasons: ["removed commandAllow allowlist (all allowed)"],
73
- },
74
66
  {
75
67
  name: "allowDirs-confinement-removed-empty-array",
76
68
  intent: "an EXPLICIT empty allowDirs [] removes confinement exactly like undefined (loosenReasons branches on .length===0) — center must treat [] and absent identically",
@@ -184,10 +176,18 @@ export const SAFETY_MERGE_CONFORMANCE_CORPUS = [
184
176
  reasons: [],
185
177
  },
186
178
  {
187
- name: "commandAllow-added-from-all",
188
- intent: "introducing a command allowlist where there was none narrows from all-allowed (per-field symmetry with toolAllow)",
189
- prior: {},
190
- next: { commandAllow: ["ls"] },
179
+ name: "inert-deny-entry-removed-safe",
180
+ intent: "removing a deny entry that reads as nothing (an empty entry, a wildcard peer, a `__` name with no protocol prefix) relaxes nothing — the comparison reads the EFFECTIVE entries on both sides",
181
+ prior: { toolDeny: ["Bash", "", "mcp__*", "github__x"] },
182
+ next: { toolDeny: ["Bash"] },
183
+ loosens: false,
184
+ reasons: [],
185
+ },
186
+ {
187
+ name: "inert-allow-entry-added-safe",
188
+ intent: "adding an allowlist entry that reads as nothing admits nothing, so it widens nothing",
189
+ prior: { toolAllow: ["Read"] },
190
+ next: { toolAllow: ["Read", "mcp__*"] },
191
191
  loosens: false,
192
192
  reasons: [],
193
193
  },
@@ -7,7 +7,7 @@ function isExemptEnvName(name) {
7
7
  }
8
8
  const ENV_NAMES = envNameView(CREDENTIAL_VOCABULARY);
9
9
  const OTEL_ENV_RE = /^(?:INPUT_)?OTEL_/i;
10
- const SEMA_NAMESPACE_SECRET_RE = new RegExp(`^SEMA_(?:.*_)?(?:KEY|KEYS|TOKEN|${ENV_NAMES.countedPlural}|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY)(?:_|$)`, "is");
10
+ const SEMA_NAMESPACE_SECRET_RE = new RegExp(`^SEMA_(?:.*_)?${ENV_NAMES.segmentWord}(?:_|$)`, "is");
11
11
  const BUNDLER_SETTINGS = ["BUILD", "LOCAL", "MIRROR", "PATH", "WITH", "WITHOUT", "CACHE", "DISABLE", "IGNORE", "ONLY"];
12
12
  const BUNDLER_HOST = "(?:[A-Za-z0-9]+(?:___[A-Za-z0-9]+)*__)+[A-Za-z]{2,}";
13
13
  const BUNDLER_CREDENTIAL_RE = new RegExp(`^(?:INPUT_)?BUNDLE_(?!(?:${BUNDLER_SETTINGS.join("|")})__(?!${BUNDLER_HOST}$))\\w*__`, "i");