@sema-agent/server 7.116.1 → 7.118.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 (131) hide show
  1. package/MIGRATION.md +195 -0
  2. package/USAGE.md +65 -41
  3. package/dist/approval-card.d.ts +82 -18
  4. package/dist/approval-card.js +46 -4
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/budget-tracing.js +2 -3
  8. package/dist/boot/config-center.d.ts +2 -2
  9. package/dist/boot/config-center.js +5 -7
  10. package/dist/boot/parked-revive-gate.d.ts +1 -7
  11. package/dist/boot/parked-revive-gate.js +2 -12
  12. package/dist/boot/resolve-spec.js +18 -16
  13. package/dist/boot/runner-deps.d.ts +5 -3
  14. package/dist/boot/runner-deps.js +6 -4
  15. package/dist/boot/stage-07-capability-layer.js +2 -2
  16. package/dist/brain.d.ts +17 -8
  17. package/dist/brain.js +10 -7
  18. package/dist/capabilities/hands-lane.d.ts +25 -0
  19. package/dist/capabilities/hands-lane.js +1 -1
  20. package/dist/capabilities/mcp-probe.d.ts +2 -2
  21. package/dist/config-catalog.js +3 -2
  22. package/dist/config-center/apply-effective.d.ts +6 -16
  23. package/dist/config-center/apply-effective.js +0 -6
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/restart-signal.d.ts +1 -1
  27. package/dist/config-types.d.ts +12 -16
  28. package/dist/config.d.ts +32 -29
  29. package/dist/config.js +20 -27
  30. package/dist/deployment-governance.d.ts +13 -19
  31. package/dist/deployment-governance.js +7 -14
  32. package/dist/governance-overrides.d.ts +20 -0
  33. package/dist/governance-overrides.js +8 -0
  34. package/dist/host-decision.d.ts +83 -25
  35. package/dist/host-decision.js +31 -1
  36. package/dist/http/admission.js +13 -7
  37. package/dist/http/approval-sweeps.js +16 -13
  38. package/dist/http/checkpoint-ctx.d.ts +83 -24
  39. package/dist/http/checkpoint-ctx.js +68 -8
  40. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  41. package/dist/http/mcp-dial-deadline.js +21 -0
  42. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  43. package/dist/http/memory-compliance-failure-reply.js +17 -8
  44. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  45. package/dist/http/memory-store-refusal-reply.js +16 -5
  46. package/dist/http/resume-legs.js +18 -18
  47. package/dist/http/retired-skill-manifest.d.ts +1 -1
  48. package/dist/http/route-ctx.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +21 -24
  50. package/dist/http/routes/capabilities.js +5 -3
  51. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  52. package/dist/http/routes/mcp-reconnect.js +6 -1
  53. package/dist/http/routes/memory-bundle.js +1 -1
  54. package/dist/http/routes/memory-compliance.js +1 -1
  55. package/dist/http/routes/memory-origin.js +1 -1
  56. package/dist/http/routes/runs.js +53 -33
  57. package/dist/http/routes/sessions.d.ts +1 -1
  58. package/dist/http/routes/sessions.js +3 -6
  59. package/dist/http/routes/workflows.js +2 -0
  60. package/dist/http/wire-types.d.ts +6 -1
  61. package/dist/memory-store-refusal.d.ts +42 -17
  62. package/dist/memory-store-refusal.js +25 -6
  63. package/dist/model-select.d.ts +16 -5
  64. package/dist/model-select.js +7 -3
  65. package/dist/model-unreachable-notice.d.ts +1 -1
  66. package/dist/namespaced-rule-name.d.ts +45 -39
  67. package/dist/namespaced-rule-name.js +36 -22
  68. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  69. package/dist/observability/corrupt-read-seat.js +1 -0
  70. package/dist/observability/fail-open.d.ts +15 -3
  71. package/dist/observability/fail-open.js +15 -3
  72. package/dist/observability/permission-rule-events.d.ts +3 -0
  73. package/dist/observability/permission-rule-events.js +4 -0
  74. package/dist/observability/run-terminal.js +21 -1
  75. package/dist/operator-ask.d.ts +4 -1
  76. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  77. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  78. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  79. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  80. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  81. package/dist/orchestration/workflow-notify-journal.js +7 -2
  82. package/dist/outbound-dispatcher.d.ts +47 -20
  83. package/dist/outbound-dispatcher.js +198 -23
  84. package/dist/parked-decide.d.ts +43 -48
  85. package/dist/parked-decide.js +54 -16
  86. package/dist/permission-rule-list.d.ts +20 -31
  87. package/dist/permission-rule-vocab.d.ts +4 -3
  88. package/dist/permission-rule-vocab.js +1 -1
  89. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  90. package/dist/plugins/approval-ask-store-sql.js +0 -3
  91. package/dist/plugins/one-time-migrations.d.ts +25 -0
  92. package/dist/plugins/one-time-migrations.js +192 -6
  93. package/dist/plugins/pg-pool.js +1 -1
  94. package/dist/plugins/tidb-pool.js +1 -1
  95. package/dist/plugins/web-search.d.ts +20 -1
  96. package/dist/plugins/web-search.js +39 -10
  97. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  98. package/dist/plugins/workflow-run-store-sql.js +5 -7
  99. package/dist/request-key-closure.js +1 -1
  100. package/dist/request-value-closure.d.ts +1 -1
  101. package/dist/router/route-orchestration.d.ts +2 -23
  102. package/dist/rule-entry-list.d.ts +88 -0
  103. package/dist/rule-entry-list.js +102 -0
  104. package/dist/rules-consent.js +3 -0
  105. package/dist/run-local.d.ts +4 -2
  106. package/dist/run-local.js +4 -3
  107. package/dist/runs.d.ts +1 -1
  108. package/dist/runs.js +1 -0
  109. package/dist/runtime-governance.d.ts +5 -21
  110. package/dist/runtime-governance.js +4 -8
  111. package/dist/session-policy-wire.d.ts +13 -12
  112. package/dist/session-policy-wire.js +11 -6
  113. package/dist/spec-fields.d.ts +13 -24
  114. package/dist/spec-fields.js +13 -17
  115. package/dist/standing-answer-grant.d.ts +14 -2
  116. package/dist/standing-answer-grant.js +8 -1
  117. package/dist/task-settings.d.ts +18 -18
  118. package/dist/task-settings.js +63 -32
  119. package/dist/tool-approval.d.ts +77 -145
  120. package/dist/tool-approval.js +80 -62
  121. package/dist/trace/core-keyset-guard.d.ts +6 -6
  122. package/dist/trace/engine-notice-wire.d.ts +4 -6
  123. package/dist/trace/engine-notice-wire.js +14 -1
  124. package/dist/trace/self-minted-wire.d.ts +16 -2
  125. package/dist/trace/self-minted-wire.js +7 -0
  126. package/dist/user-input-ledger.d.ts +24 -12
  127. package/dist/user-input-ledger.js +33 -14
  128. package/package.json +2 -2
  129. package/scripts/drain-check-local.mjs +103 -7
  130. package/scripts/drain-check.mysql.sql +59 -8
  131. package/scripts/drain-check.pg.sql +66 -10
@@ -1,8 +1,8 @@
1
- import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, wrapToolPolicy, } from "@sema-agent/core";
1
+ import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, wrapToolPolicy, } from "@sema-agent/core";
2
2
  import { GovernanceConfig } from "@sema-agent/settings-schema";
3
3
  import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
4
+ import { tightenGoverned } from "./governance-overrides.js";
4
5
  import { createDeploymentAuthoredPolicy, operatorAsk } from "./operator-ask.js";
5
- import { createRuleListPolicy } from "./permission-rule-list.js";
6
6
  import { heldRuleName } from "./tool-rule-names.js";
7
7
  const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
8
8
  const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
@@ -101,10 +101,6 @@ export function compileCommandPolicyLayers(rules) {
101
101
  }
102
102
  return policies;
103
103
  }
104
- export const APPROVAL_GRANT_SOURCE = "APPROVAL_GRANT";
105
- export function compileApprovalGrantLayers(grant) {
106
- return grant !== undefined && grant.length > 0 ? [createRuleListPolicy(grant, "allow", APPROVAL_GRANT_SOURCE, undefined)] : [];
107
- }
108
104
  export function autonomyOverrides(autonomy) {
109
105
  switch (autonomy) {
110
106
  case "read-only":
@@ -148,7 +144,7 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
148
144
  export function applyRuntimeGovernance(base, governance) {
149
145
  const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
150
146
  const overrides = { ...autonomyOverrides(governance.autonomy) };
151
- const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? []), ...compileApprovalGrantLayers(governance.approvalGrant)];
147
+ const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? [])];
152
148
  const governancePolicy = layers.length > 0 ? createGovernanceAskMarkingPolicy(combinePolicies(...layers.map(createDeploymentAuthoredPolicy)), marksOf) : undefined;
153
149
  if (governancePolicy !== undefined)
154
150
  overrides.toolPolicy = governancePolicy;
@@ -161,6 +157,6 @@ export function applyRuntimeGovernance(base, governance) {
161
157
  if (Object.keys(overrides).length === 0) {
162
158
  return base;
163
159
  }
164
- return tightenTaskSpec(base, overrides);
160
+ return tightenGoverned(base, overrides);
165
161
  }
166
162
  //# sourceMappingURL=runtime-governance.js.map
@@ -29,12 +29,13 @@
29
29
  * 里同点同调它;准备阶段的名审计与本仓配置表读的是同一只 `judgeRuleToolName`)—— 本仓手写的那一只(`legacyToolNameOf`)删除,零双读;
30
30
  * 「哪几桶装工具名」归 core(它的 `HOLDS_TOOL_NAMES`)。
31
31
  *
32
- * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名 ─────────────────────────────────────
33
- * 缺段(`mcp__srv__` / `mcp__`)与对端段铸不出(`mcp__github.com__*`)的协议名 core 的判官判 `current`(它只看退役名与无前缀 `__` 名),
34
- * 而配置表一直拒缺段名 —— 一个名字两张表两种答案,对端段那一半则两张表都照收、永不命中。判官与句子是 `namespaced-rule-name.ts` 的那一只
35
- * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不另铸新码,也不往 core 的判词闭集里塞本仓的词)。
36
- * 这一问要逐桶判,而 core 不导出 `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),
37
- * 随这一问在 core 自己给出判词那天整只退役。SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
32
+ * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名;7.118.0 收窄到引擎读不到的那一形 ─────────────────────────
33
+ * 对端段铸不出 / 对端段为空(`mcp__github.com__*`、`mcp__`)自 core 7.43.0 #1355 (b) 起是引擎判官自己的判词 `unmintable_segment` ——
34
+ * `assertSessionRuleNamesCurrent` 拒(`legacy_tool_name`,`detail.verdict`)、本仓映射 `400 rules.legacy_tool_name`,与退役名同码同句族;本仓 7.116.0 起
35
+ * 那只自比对端段的对照随之退役。引擎判官放行、却一只工具都够不着的只剩**工具段为空**(`mcp__srv__`):判据与句子是 `namespaced-rule-name.ts` 那一只
36
+ * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不往 core 的判词闭集里塞本仓的词)。这一问要逐桶判,而 core 不导出
37
+ * `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),随引擎判官给这一形判词那天整只退役。
38
+ * SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
38
39
  *
39
40
  * ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
40
41
  * 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
@@ -45,16 +46,16 @@
45
46
  */
46
47
  import { SessionPolicyError } from "@sema-agent/core";
47
48
  import type { SessionPermissionRules, SessionRuleNameRefusal, SessionRulesRecord } from "@sema-agent/core";
48
- import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
49
+ import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
49
50
  /** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
50
51
  export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
51
- /** 工具名桶里一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出);`verdict` = 判官的判词。 */
52
+ /** 工具名桶里一条引擎判官放行、却一只工具都够不着的协议名(工具段为空);`ns` = 它的协议。 */
52
53
  export interface SessionRuleUnreachableName {
53
54
  readonly field: SessionRuleNameRefusal["field"];
54
55
  readonly name: string;
55
- readonly verdict: UnreachableNamespacedRuleName;
56
+ readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
56
57
  }
57
- /** 逐桶逐条过协议规则名判官;第一条够不着的交回,全部够得着 ⇒ `undefined`。 */
58
+ /** 逐桶逐条问引擎读不到的那一形;第一条交回,全部够得着 ⇒ `undefined`。调用方先过引擎判官({@link legacyToolNameRefusalOf} / `assertSessionRuleNamesCurrent`)。 */
58
59
  export declare function unreachableSessionRuleNameOf(rules: SessionPermissionRules): SessionRuleUnreachableName | undefined;
59
60
  /**
60
61
  * SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
@@ -80,7 +81,7 @@ export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
80
81
  kind: "legacy-tool-name";
81
82
  refusal: SessionRuleNameRefusal;
82
83
  }
83
- /** 形对、core 判官也放行,但工具名桶里有一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出)。 */
84
+ /** 形对、core 判官也放行,但工具名桶里有一条一只工具都够不着的协议名(工具段为空 —— 引擎读不到的那一形)。 */
84
85
  | {
85
86
  kind: "unreachable-tool-name";
86
87
  unreachable: SessionRuleUnreachableName;
@@ -109,7 +110,7 @@ export declare function readSessionPermissionRules(raw: unknown): SessionPermiss
109
110
  * 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
110
111
  * ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
111
112
  * ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
112
- * ③ 工具名桶里的每一条过协议规则名判官({@link unreachableSessionRuleNameOf})—— 写进去永远匹配不到任何挂载工具的协议名,在写口拒。
113
+ * ③ 工具名桶里的每一条问引擎读不到的那一形({@link unreachableSessionRuleNameOf}:工具段为空的协议名)—— 写进去永远匹配不到任何挂载工具,在写口拒。
113
114
  * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
114
115
  * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
115
116
  * 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
@@ -1,5 +1,5 @@
1
1
  import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
2
- import { describeUnreachableNamespacedRuleName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
2
+ import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
3
3
  const RULE_FIELDS = {
4
4
  toolAllow: true,
5
5
  toolDeny: true,
@@ -24,9 +24,9 @@ const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
24
24
  export function unreachableSessionRuleNameOf(rules) {
25
25
  for (const field of TOOL_NAME_FIELDS) {
26
26
  for (const name of rules[field] ?? []) {
27
- const verdict = unreachableNamespacedRuleNameOf(name);
28
- if (verdict !== undefined)
29
- return { field, name, verdict };
27
+ const ns = emptyToolSegmentOf(name);
28
+ if (ns !== undefined)
29
+ return { field, name, ns };
30
30
  }
31
31
  }
32
32
  return undefined;
@@ -99,17 +99,22 @@ export function describeSessionPermissionRulesProblem(p) {
99
99
  }
100
100
  }
101
101
  function describeUnreachableSessionRuleName(u) {
102
- return `${u.field} names "${u.name}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)}), so nothing was written`;
102
+ return `${u.field} names "${u.name}", which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
103
  }
104
104
  function describeLegacyToolName(r) {
105
105
  switch (r.verdict) {
106
106
  case "retired": {
107
- const v = judgeRuleToolName(r.name, new Set());
107
+ const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
108
108
  const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
109
109
  return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
110
110
  }
111
111
  case "unprefixed_namespace":
112
112
  return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
113
+ case "unmintable_segment": {
114
+ const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
115
+ const guidance = v.kind === "unmintable_segment" ? v.guidance : "a protocol name whose peer segment the engine never mints";
116
+ return `${r.field} names "${r.name}", which can never match a tool (${guidance}), so nothing was written`;
117
+ }
113
118
  default: {
114
119
  const unhandled = r.verdict;
115
120
  throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
@@ -4,7 +4,6 @@
4
4
  * body WITHOUT the HTTP-layer 400 validation, so a malformed value must fall back to a safe default, never throw.
5
5
  */
6
6
  import { type AgentDefinition, type TaskSpec } from "@sema-agent/core";
7
- import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
8
7
  /**
9
8
  * E12 — normalize `body.suggestNextPrompts` → `TaskSpec.suggestNextPrompts`. `true` = on with core defaults
10
9
  * (count 3, cheap role); an object tunes `count` (clamped 1..10) / `role` (a free string — core validates it against
@@ -48,31 +47,22 @@ export declare const MAX_TASK_AGENTS = 32;
48
47
  * deny 规则来源,不是围栏)的**形**谓词。两道技能门(顶层 `body.skills[]` 的 `validateUserSkills`、`agents[].skills[]` 的
49
48
  * {@link SKILL_FIELD_SHAPES})**同一只**:一边判一边不判 = 同一个键两种话(S-377 baseDir 吃过的亏)。
50
49
  *
51
- * 本谓词只判「字符串数组」:引擎 `compileSkillGrant` 直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成
52
- * `B` / `a` / `s` / `h` 四条(deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法**不在受理面判:引擎在开跑时对读不出的条目
53
- * (`*` / 写路径 / 域名)发 `skills.allowed_tool_not_granted`,文法的属主是引擎(directory loader 同样原样抄、不判)。
54
- * 形对之后两道门再问一问 {@link unreachableSkillToolListEntryOf}(协议名的服务器段铸不出 / 缺段 ⇒ 400)—— 那一问不是文法,是「写得出却永远匹配不到」,
55
- * 与配置表、会话规则写口同一只判官。
56
- * 无条数 / 长度帽([ref]:技能面的受理帽整族退役,体积由 8 MiB 请求体上限兜)。
50
+ * 本谓词只判「字符串数组」(core 7.42.0 起与请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT` **同一只**:`rule-entry-list.ts` 的
51
+ * `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
52
+ * (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
53
+ * `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
54
+ * `skillDenyInertEntryOf`,判据 = 引擎的条目读法)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 + 引擎读不到的那一形 ⇒ 400)。
55
+ * 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
56
+ * 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
57
57
  */
58
- export declare const skillToolListOk: (v: unknown) => boolean;
59
- /** 技能的两只清单(`allowedTools` = allow 规则来源,`disallowedTools` = deny 规则来源;条目的工具名那一段两只同读)。 */
60
- declare const SKILL_TOOL_LISTS: readonly ["allowedTools", "disallowedTools"];
61
- /** 技能清单里一条够不着任何挂载工具的协议名条目。 */
62
- export interface SkillToolListUnreachable {
63
- readonly key: (typeof SKILL_TOOL_LISTS)[number];
64
- readonly entry: string;
65
- readonly verdict: UnreachableNamespacedRuleName;
66
- }
58
+ export declare const skillToolListOk: (v: unknown) => v is readonly string[];
67
59
  /**
68
- * 一条技能(形已过两道门的字符串数组判)的两只清单里,第一条「协议名写得出、却永远匹配不到」的条目 —— 判官是 `namespaced-rule-name.ts` 那一只
69
- * (配置表 / 请求三桶 / 会话规则写口同调),条目的工具名那一段经同一只 `ruleEntryToolName` 取(与配置表的规则编译器消费者同读)。都够得着 ⇒ `undefined`。
70
- * 两处受理口同调:顶层 `skills[]`(`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。
71
- * 续跑重放**不问**这一问(两层都是:重放腿不过受理口,存量体照旧原样交给引擎 —— 引擎把它编成一条永不命中的规则,与修前同一种命运,不另开读法)。
60
+ * 一条技能(形已过两道门的字符串数组判)的两只清单在受理口的拒因 —— 都过 ⇒ `undefined`。两处受理口同调:顶层 `skills[]`
61
+ * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。两问:条数帽;引擎读不到、却永远匹配不到的
62
+ * 条目(工具段为空的协议名,`rule-entry-list.ts` 那一份,与配置表、会话规则写口同一只)。退役名 / 对端段铸不出 / 读成空的条目是引擎的读法(见上):
63
+ * 允侧交引擎通告,拒侧由 `resolveSpec` 拒。
72
64
  */
73
- export declare function unreachableSkillToolListEntryOf(skill: Record<string, unknown>): SkillToolListUnreachable | undefined;
74
- /** 两处受理口同一句(句子的后半与配置表 / 会话规则写口同一只 `describeUnreachableNamespacedRuleName`)。 */
75
- export declare function describeUnreachableSkillToolListEntry(u: SkillToolListUnreachable): string;
65
+ export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
76
66
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
77
67
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
78
68
  * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。⚠️ 已知残账(core 半场):
@@ -188,5 +178,4 @@ export declare function normalizeApproachNotice(raw: unknown): false | {
188
178
  * 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
189
179
  */
190
180
  export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean, warn?: (event: string, fields: Record<string, unknown>) => void): TaskSpec["limits"];
191
- export {};
192
181
  //# sourceMappingURL=spec-fields.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { isThinkingLevel } from "@sema-agent/core";
2
2
  import { taskWallClockSec } from "./task-workflow.js";
3
3
  import { isValidCwd } from "./task-cwd.js";
4
- import { describeUnreachableNamespacedRuleName, ruleEntryToolName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
4
+ import { allowLayerEntryCountIssue, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
5
  export function normalizeSuggestNextPrompts(v) {
6
6
  if (v === true)
7
7
  return true;
@@ -66,26 +66,22 @@ const toolList = (v) => Array.isArray(v) &&
66
66
  v.length <= MAX_AGENT_TOOLS &&
67
67
  v.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_AGENT_TOOL_NAME_CHARS);
68
68
  const plainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
69
- export const skillToolListOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
69
+ export const skillToolListOk = ruleEntryListShapeOk;
70
70
  const SKILL_TOOL_LISTS = ["allowedTools", "disallowedTools"];
71
- export function unreachableSkillToolListEntryOf(skill) {
71
+ export function skillToolListRefusal(skill) {
72
72
  for (const key of SKILL_TOOL_LISTS) {
73
73
  const list = skill[key];
74
- if (!Array.isArray(list))
74
+ if (!ruleEntryListShapeOk(list))
75
75
  continue;
76
- for (const entry of list) {
77
- if (typeof entry !== "string")
78
- continue;
79
- const verdict = unreachableNamespacedRuleNameOf(ruleEntryToolName(entry));
80
- if (verdict !== undefined)
81
- return { key, entry, verdict };
82
- }
76
+ const over = allowLayerEntryCountIssue(`skill.${key}`, list);
77
+ if (over !== undefined)
78
+ return over.message;
79
+ const empty = emptyToolSegmentEntryOf(list);
80
+ if (empty !== undefined)
81
+ return describeEmptyToolSegmentEntry(`skill.${key}`, empty);
83
82
  }
84
83
  return undefined;
85
84
  }
86
- export function describeUnreachableSkillToolListEntry(u) {
87
- return `skill.${u.key} names "${u.entry}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)})`;
88
- }
89
85
  const skillFilesOk = (v) => Array.isArray(v) &&
90
86
  v.every((f) => plainObject(f) && typeof f.path === "string" && f.path.length > 0 && typeof f.content === "string");
91
87
  const SKILL_FIELD_SHAPES = {
@@ -197,9 +193,9 @@ export function validateTaskAgents(v) {
197
193
  const skills = v[i].skills;
198
194
  if (Array.isArray(skills)) {
199
195
  for (let j = 0; j < skills.length; j++) {
200
- const unreachable = unreachableSkillToolListEntryOf(skills[j]);
201
- if (unreachable !== undefined)
202
- return `agents[${i}]: skills[${j}]: ${describeUnreachableSkillToolListEntry(unreachable)}`;
196
+ const refusal = skillToolListRefusal(skills[j]);
197
+ if (refusal !== undefined)
198
+ return `agents[${i}]: skills[${j}]: ${refusal}`;
203
199
  }
204
200
  }
205
201
  const name = v[i].name;
@@ -1,8 +1,20 @@
1
- /** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest`、待决条目、停驻行的 `tool_approval` 臂)都结构上满足。 */
1
+ /** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest`、停驻行的 `tool_approval` 臂)都结构上满足。 */
2
2
  export interface StandingAnswerGrantCarrier {
3
3
  readonly toolName?: unknown;
4
4
  readonly boundGrantDigest?: unknown;
5
5
  }
6
- /** 这一问的应答是否授予这一次调用之外的东西(读法见文件头注)。 */
6
+ /** 一问的授予绑定(闭集):`none` = 应答只覆盖这一次调用;`bound` = 授予、绑定可读;`unreadable` = 授予、绑定读不出。 */
7
+ export type GrantBinding = {
8
+ readonly kind: "none";
9
+ } | {
10
+ readonly kind: "bound";
11
+ readonly digest: string;
12
+ } | {
13
+ readonly kind: "unreadable";
14
+ };
15
+ /** 这一问的授予绑定(读法见文件头注)。 */
16
+ export declare function grantBindingOf(carrier: StandingAnswerGrantCarrier): GrantBinding;
17
+ /** 这一问的应答是否授予这一次调用之外的东西(= 绑定不是 `none`)。**按工具名记**的读者(耐久豁免店的读写口、豁免列表)据它不碰这类问:
18
+ * 它们的键没有绑定这一维,记下 / 代答都会越过绑定。 */
7
19
  export declare function answerGrantsBeyondCall(carrier: StandingAnswerGrantCarrier): boolean;
8
20
  //# sourceMappingURL=standing-answer-grant.d.ts.map
@@ -1,5 +1,12 @@
1
1
  import { SKILL_TOOL_NAME } from "@sema-agent/core";
2
+ export function grantBindingOf(carrier) {
3
+ if (Object.hasOwn(carrier, "boundGrantDigest")) {
4
+ const d = carrier.boundGrantDigest;
5
+ return typeof d === "string" && d.length > 0 ? { kind: "bound", digest: d } : { kind: "unreadable" };
6
+ }
7
+ return carrier.toolName === SKILL_TOOL_NAME ? { kind: "unreadable" } : { kind: "none" };
8
+ }
2
9
  export function answerGrantsBeyondCall(carrier) {
3
- return Object.hasOwn(carrier, "boundGrantDigest") || carrier.toolName === SKILL_TOOL_NAME;
10
+ return grantBindingOf(carrier).kind !== "none";
4
11
  }
5
12
  //# sourceMappingURL=standing-answer-grant.js.map
@@ -12,22 +12,21 @@
12
12
  * idempotent (the same tighten), so no special resume handling is needed (mirrors runtime-governance).
13
13
  *
14
14
  * 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3) —— **scope: the `settings.permissions` channel this module projects, and only it**:
15
- * - permissions are folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any override that would
16
- * LOOSEN a stricter base safety field. Through THIS channel the client can ADD denies/asks/handsReadOnly on top of the
17
- * deployment + operator baseline, never widen it. Its `allow` rules (S-654 (c), 7.115.0) STATE an allow for the tools they name —
18
- * the answer core 7.40.0 wants before it runs an MCP / A2A call nobody stated — and never override a deny or an ask
19
- * from any layer (deny > ask > allow); a miss has no opinion. They release no ask a layer raises itself: on built-in
20
- * tools the engine's own write gate and shell checks still ask (unlike the reference implementation's `permissions.allow`).
21
- * - ⚠️ This is NOT a claim about the request as a whole. Since server 7.116.0 (core 7.41.0) a request-carried skill's
22
- * `allowedTools` — activated by the model's approved `Skill` card, or named in `userInvokedSkills` — IS an accepted
23
- * loosening on a different channel (`skills[]` → `TaskSpec.skills`, never through this module): for the rest of that run
24
- * it releases the `unstated` / `ordinary` asks the engine itself raises (including the built-in write gate and the shell's
25
- * ordinary asks; same predicate as a persisted allow rule), on hosted / multi-tenant deployments too — no deployment
26
- * switch, by ruling. It still clears no deny / ask rule (request or operator), no governance / shell-floor /
27
- * write-protection / plan-mode / probe / mandated ask, and opens no write containment. An operator who needs a person
28
- * asked for a given tool regardless uses the per-tool knobs: `APPROVAL_REQUIRE` (an ask rule), `APPROVAL_NEVER_AUTO`
29
- * (a person, mandated), `MANUAL_MODE_SHELL_GATE=always` (shell). Pinned on the real assembly:
30
- * `test/skill-grant-operator-knobs.test.ts`.
15
+ * - `deny` / `ask` are folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any override that would
16
+ * LOOSEN a stricter base safety field. Through those two buckets the client can only ADD denies / asks / handsReadOnly on
17
+ * top of the deployment + operator baseline.
18
+ * - `allow` (server 7.117.0, core 7.42.0) is NOT a policy: the list rides WHOLE and verbatim into `TaskSpec.allowedTools`,
19
+ * the request member of the engine's ONE allow layer (beside a deployment's `RunnerDeps.allowedTools`, a person's stored
20
+ * rows, an active skill's `allowed-tools` and a PreToolUse hook's `allow`), read under one predicate. It IS an accepted
21
+ * loosening for this run: an entry clears the `unstated` / `ordinary` questions the engine raises for the calls it covers
22
+ * (the built-in write gate and the shell's ordinary asks included), on hosted / multi-tenant deployments too — no
23
+ * deployment switch, by ruling (the execution lane is the sandbox; the operator's tools are the per-tool knobs). It clears
24
+ * no deny / ask rule (request or operator), no governance / shell-floor / write-protection / plan-mode / probe / mandated
25
+ * ask and no PreToolUse hook's ask, and opens no write containment; an entry that matches and cannot clear is disclosed on
26
+ * the card (`allowRuleShadowed`). An operator who needs a person asked for a given tool regardless uses the per-tool knobs:
27
+ * `APPROVAL_REQUIRE` (an ask rule), `APPROVAL_NEVER_AUTO` (a person, mandated), `MANUAL_MODE_SHELL_GATE=always` (shell).
28
+ * A request-carried skill's `allowedTools` is the same member's twin (same grammar, same predicate, same admission judge
29
+ * `rule-entry-list.ts`). Pinned on the real assembly: `test/allow-layer-operator-knobs.test.ts`.
31
30
  * - permissionMode(轴B #2 注释保鲜,1.254——[ref]/[ref] 后的真实现):`defaultMode` 收**五**模式
32
31
  * (default | acceptEdits | plan | bypassPermissions | auto),bypass/auto 被 HONOR 为 gate-shape 选择
33
32
  * ——但深层不变量未变:bypass=「不加 ask 门」,恒不越部署基线(tightenTaskSpec deny-wins,基线 deny/
@@ -265,8 +264,9 @@ export interface SettingsPermissionBucketIssue {
265
264
  }
266
265
  /**
267
266
  * 三桶的**唯一**判据(纯函数)。`perms` 不是对象 ⇒ 不判(那一形归「settings.permissions must be an object」那道门);桶缺席 / `null`
268
- * ⇒ 不判(与 `permissions: null` 的「容器缺席」同一约定)。逐桶按 {@link SETTINGS_PERMISSION_BUCKETS} 的序:不是数组 ⇒ `bad_entry`(无
269
- * `index`);超帽 ⇒ `over_cap`(先判,拒体有界);首个非串 / 空串项 ⇒ `bad_entry` + 它的下标。全合法 ⇒ `null`。
267
+ * ⇒ 不判(与 `permissions: null` 的「容器缺席」同一约定)。逐桶按 {@link SETTINGS_PERMISSION_BUCKETS} 的序、按该桶的条目规则:不是数组 ⇒ `bad_entry`
268
+ * (无 `index`);`rule_policy` 桶超帽 ⇒ `over_cap`(先判,拒体有界)、首个非串 / 空串项 ⇒ `bad_entry` + 它的下标;`allow_layer` 桶首个非串项 ⇒
269
+ * `bad_entry` + 它的下标(整表判据 = `ruleEntryListShapeOk`),超帽同样 `over_cap`(帽的理由不同,见段头)。全合法 ⇒ `null`。
270
270
  */
271
271
  export declare function settingsPermissionBucketIssue(perms: unknown): SettingsPermissionBucketIssue | null;
272
272
  /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
@@ -1,9 +1,12 @@
1
- import { combinePolicies, createFsWriteGatePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, ruleToolGrammarOf, } from "@sema-agent/core";
1
+ import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { unmatchableToolNamesRefusal } from "./config.js";
3
3
  import { MAX_PERMISSION_RULES_PER_LIST, createRuleListPolicy } from "./permission-rule-list.js";
4
+ import { allowLayerEntryCountIssue, describeAllowLayerOverCap, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
+ import { tightenGoverned } from "./governance-overrides.js";
4
6
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
5
7
  import { parseHooksConfig } from "./hooks/hook-runner.js";
6
8
  import { heldRuleName } from "./tool-rule-names.js";
9
+ import { policyAskStandingAnswerFacts, standingAnswerDurablyRecordable } from "./tool-approval.js";
7
10
  export const PERMISSION_MODE_REQUEST_FACE = {
8
11
  default: "request",
9
12
  acceptEdits: "request",
@@ -83,15 +86,28 @@ function cleanEnvMap(v) {
83
86
  }
84
87
  export const SETTINGS_PERMISSION_BUCKETS = ["allow", "deny", "ask"];
85
88
  export const SETTINGS_PERMISSION_BUCKET_REFUSALS = ["over_cap", "bad_entry"];
86
- function bucketIssueMessage(f) {
89
+ const BUCKET_ENTRY_RULE = { allow: "allow_layer", deny: "rule_policy", ask: "rule_policy" };
90
+ function bucketIssueMessage(f, rule) {
87
91
  switch (f.reason) {
88
92
  case "over_cap":
89
- return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
90
- `and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
93
+ switch (rule) {
94
+ case "rule_policy":
95
+ return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
96
+ `and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
97
+ case "allow_layer":
98
+ return describeAllowLayerOverCap(f.field, f.count);
99
+ }
91
100
  case "bad_entry":
92
- return f.index === undefined
93
- ? `${f.field} must be an array of non-empty rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
94
- : `${f.field}[${f.index}] is not a non-empty rule string — every entry must be one. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
101
+ switch (rule) {
102
+ case "rule_policy":
103
+ return f.index === undefined
104
+ ? `${f.field} must be an array of non-empty rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
105
+ : `${f.field}[${f.index}] is not a non-empty rule string — every entry must be one. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
106
+ case "allow_layer":
107
+ return f.index === undefined
108
+ ? `${f.field} must be an array of permission-rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
109
+ : `${f.field}[${f.index}] is not a string — every entry must be a permission-rule string. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
110
+ }
95
111
  }
96
112
  }
97
113
  export function settingsPermissionBucketIssue(perms) {
@@ -102,18 +118,34 @@ export function settingsPermissionBucketIssue(perms) {
102
118
  if (v === undefined || v === null)
103
119
  continue;
104
120
  const field = `settings.permissions.${bucket}`;
121
+ const rule = BUCKET_ENTRY_RULE[bucket];
105
122
  let fields;
106
123
  if (!Array.isArray(v))
107
124
  fields = { field, reason: "bad_entry" };
108
- else if (v.length > MAX_PERMISSION_RULES_PER_LIST)
109
- fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
110
125
  else {
111
- const index = v.findIndex((x) => typeof x !== "string" || x.length === 0);
112
- if (index >= 0)
113
- fields = { field, reason: "bad_entry", index };
126
+ switch (rule) {
127
+ case "rule_policy": {
128
+ if (v.length > MAX_PERMISSION_RULES_PER_LIST)
129
+ fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
130
+ else {
131
+ const index = v.findIndex((x) => typeof x !== "string" || x.length === 0);
132
+ if (index >= 0)
133
+ fields = { field, reason: "bad_entry", index };
134
+ }
135
+ break;
136
+ }
137
+ case "allow_layer": {
138
+ const over = allowLayerEntryCountIssue(field, v);
139
+ if (over !== undefined)
140
+ fields = { field, reason: "over_cap", count: over.count, cap: over.cap };
141
+ else if (!ruleEntryListShapeOk(v))
142
+ fields = { field, reason: "bad_entry", index: v.findIndex((x) => typeof x !== "string") };
143
+ break;
144
+ }
145
+ }
114
146
  }
115
147
  if (fields !== undefined)
116
- return { message: bucketIssueMessage(fields), fields };
148
+ return { message: bucketIssueMessage(fields, rule), fields };
117
149
  }
118
150
  return null;
119
151
  }
@@ -265,7 +297,7 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
265
297
  }
266
298
  return v;
267
299
  }
268
- const RULE_SOURCE_BY_BEHAVIOR = { allow: "settings.permissions.allow", ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
300
+ const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
269
301
  const WORKFLOW_ASK_NAMES = [RUN_WORKFLOW_TOOL_NAME];
270
302
  const isWorkflowName = (name) => name === RUN_WORKFLOW_TOOL_NAME;
271
303
  function createWorkflowAskPolicy(isExempt) {
@@ -273,7 +305,8 @@ function createWorkflowAskPolicy(isExempt) {
273
305
  async check(req) {
274
306
  if (heldRuleName(req, isWorkflowName) === undefined)
275
307
  return { action: "allow" };
276
- if (isExempt) {
308
+ const ask = { action: "ask", message: `dynamic workflow "${req.toolName}" requires approval in this permission mode`, decisionReason: "rule" };
309
+ if (isExempt && standingAnswerDurablyRecordable(policyAskStandingAnswerFacts(req, ask))) {
277
310
  try {
278
311
  if (await isExempt(req.toolName))
279
312
  return { action: "allow", decisionReason: "rule" };
@@ -281,7 +314,7 @@ function createWorkflowAskPolicy(isExempt) {
281
314
  catch {
282
315
  }
283
316
  }
284
- return { action: "ask", message: `dynamic workflow "${req.toolName}" requires approval in this permission mode`, decisionReason: "rule" };
317
+ return ask;
285
318
  },
286
319
  nameSets: [{ ask: WORKFLOW_ASK_NAMES }],
287
320
  };
@@ -301,7 +334,6 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
301
334
  for (const [field, names, behavior] of [
302
335
  ["permissions.ask", perms.ask, "ask"],
303
336
  ["permissions.deny", perms.deny, "deny"],
304
- ["permissions.allow", perms.allow, "allow"],
305
337
  ]) {
306
338
  for (const name of names ?? []) {
307
339
  const parsed = parseRuleText(name, behavior);
@@ -313,30 +345,31 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
313
345
  }
314
346
  }
315
347
  }
316
- for (const [field, names, behavior] of [
317
- ["permissions.ask", perms.ask, "ask"],
318
- ["permissions.deny", perms.deny, "deny"],
319
- ["permissions.allow", perms.allow, "allow"],
320
- ]) {
348
+ const judgments = [
349
+ ["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
350
+ ["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
351
+ ];
352
+ for (const [field, names, judgment] of judgments) {
321
353
  if (!names)
322
354
  continue;
323
- const unmatchable = unmatchableToolNamesRefusal(field, names, { consumer: "rule-policy", behavior, ...(pathBases !== undefined ? { pathBases } : {}) });
355
+ const unmatchable = unmatchableToolNamesRefusal(field, names, judgment);
324
356
  if (unmatchable !== undefined)
325
357
  throw new Error(unmatchable.message);
326
358
  }
359
+ const emptyAllow = perms.allow !== undefined ? emptyToolSegmentEntryOf(perms.allow) : undefined;
360
+ if (emptyAllow !== undefined)
361
+ throw new Error(describeEmptyToolSegmentEntry("permissions.allow", emptyAllow));
327
362
  }
328
363
  export function deriveSettingsPolicy(settings, gate, workflowGate) {
329
364
  const perms = settings.permissions;
330
365
  if (!perms)
331
366
  return {};
332
- const { allow, deny, ask, defaultMode } = perms;
367
+ const { deny, ask, defaultMode } = perms;
333
368
  const pathBases = gate?.taskRoot !== undefined || gate?.taskHome !== undefined
334
369
  ? { ...(gate.taskRoot !== undefined ? { root: gate.taskRoot } : {}), ...(gate.taskHome !== undefined ? { home: gate.taskHome } : {}) }
335
370
  : undefined;
336
371
  rejectUnmatchableSettingsNames(perms, pathBases);
337
372
  const policies = [];
338
- if (allow)
339
- policies.push(createRuleListPolicy(allow, "allow", RULE_SOURCE_BY_BEHAVIOR.allow, pathBases));
340
373
  if (deny)
341
374
  policies.push(createRuleListPolicy(deny, "deny", RULE_SOURCE_BY_BEHAVIOR.deny, pathBases));
342
375
  if (ask)
@@ -380,15 +413,13 @@ export function applyTaskSettings(base, settings, gate, workflowGate) {
380
413
  const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
381
414
  let next = base;
382
415
  if (toolPolicy !== undefined || handsReadOnly !== undefined) {
383
- const overrides = {};
384
- if (toolPolicy !== undefined)
385
- overrides.toolPolicy = toolPolicy;
386
- if (handsReadOnly !== undefined)
387
- overrides.handsReadOnly = handsReadOnly;
388
- next = tightenTaskSpec(next, overrides);
416
+ next = tightenGoverned(next, { ...(toolPolicy !== undefined ? { toolPolicy } : {}), ...(handsReadOnly !== undefined ? { handsReadOnly } : {}) });
389
417
  }
390
418
  if (enablePlanMode)
391
419
  next = { ...next, enablePlanMode: true };
420
+ const allow = settings.permissions?.allow;
421
+ if (allow !== undefined)
422
+ next = { ...next, allowedTools: allow };
392
423
  if (settings.outputStyle) {
393
424
  const appended = next.appendSystemPrompt ? `${next.appendSystemPrompt}\n\n${settings.outputStyle}` : settings.outputStyle;
394
425
  next = { ...next, appendSystemPrompt: appended };