@sema-agent/server 7.78.0 → 7.78.2

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 (46) hide show
  1. package/README.md +6 -0
  2. package/USAGE.md +4 -0
  3. package/dist/approval-card.d.ts +37 -4
  4. package/dist/approval-card.js +22 -2
  5. package/dist/boot/resolve-spec.js +6 -3
  6. package/dist/config-catalog.d.ts +18 -1
  7. package/dist/config-catalog.js +3 -3
  8. package/dist/governance-http-status.d.ts +27 -0
  9. package/dist/governance-http-status.js +11 -0
  10. package/dist/http/routes/approvals-assistant.d.ts +35 -0
  11. package/dist/http/routes/approvals-assistant.js +39 -1
  12. package/dist/http/routes/tasks.js +4 -9
  13. package/dist/http/server.d.ts +28 -4
  14. package/dist/http/server.js +33 -13
  15. package/dist/observability/fail-open.d.ts +4 -0
  16. package/dist/observability/fail-open.js +4 -0
  17. package/dist/parked-decide.d.ts +7 -2
  18. package/dist/plugins/host-workspace-registry.d.ts +27 -0
  19. package/dist/plugins/host-workspace-registry.js +27 -0
  20. package/dist/plugins/one-time-migrations.d.ts +3 -0
  21. package/dist/plugins/one-time-migrations.js +18 -11
  22. package/dist/plugins/remote-env-host.d.ts +15 -0
  23. package/dist/plugins/remote-env-host.js +41 -1
  24. package/dist/request-key-closure.d.ts +30 -0
  25. package/dist/request-key-closure.js +56 -0
  26. package/dist/runs.d.ts +51 -0
  27. package/dist/runs.js +4 -11
  28. package/dist/runtime-governance.d.ts +18 -4
  29. package/dist/runtime-governance.js +2 -2
  30. package/dist/spec-fields.d.ts +4 -2
  31. package/dist/spec-fields.js +4 -5
  32. package/dist/task-a2a.js +8 -6
  33. package/dist/task-mcp.js +5 -3
  34. package/dist/task-settings.d.ts +49 -17
  35. package/dist/task-settings.js +37 -38
  36. package/dist/tool-approval.d.ts +35 -3
  37. package/dist/tool-approval.js +41 -19
  38. package/dist/tool-axis-words.d.ts +30 -0
  39. package/dist/tool-axis-words.js +9 -0
  40. package/dist/trace/core-keyset-guard.d.ts +3 -3
  41. package/dist/trace/ledger-sink.js +12 -5
  42. package/dist/trace/model-segment.d.ts +32 -2
  43. package/dist/trace/model-segment.js +15 -5
  44. package/dist/trace/project.d.ts +37 -3
  45. package/dist/trace/project.js +14 -10
  46. package/package.json +3 -3
package/dist/task-a2a.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
3
  import { HttpError } from "./security.js";
4
+ import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
5
+ import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
6
  export const MAX_REQUEST_A2A_PEERS = 32;
5
7
  export function a2aInjectionHonored(config) {
6
8
  if (config.lockedConfigKeys?.includes("a2a") === true)
@@ -49,16 +51,16 @@ function normalizeA2aPeer(e) {
49
51
  if (!ax || typeof ax !== "object" || Array.isArray(ax))
50
52
  return null;
51
53
  const a = ax;
52
- if (a.effect !== undefined && a.effect !== "read" && a.effect !== "write" && a.effect !== "idempotent")
54
+ if (a.effect !== undefined && !isToolEffect(a.effect))
53
55
  return null;
54
56
  if (a.egress !== undefined && typeof a.egress !== "boolean")
55
57
  return null;
56
- if (a.irreversibility !== undefined && a.irreversibility !== "always" && a.irreversibility !== "never")
58
+ if (a.irreversibility !== undefined && !isToolAxisIrreversibility(a.irreversibility))
57
59
  return null;
58
60
  const entry = {
59
- ...(a.effect !== undefined ? { effect: a.effect } : {}),
60
- ...(a.egress !== undefined ? { egress: a.egress } : {}),
61
- ...(a.irreversibility !== undefined ? { irreversibility: a.irreversibility } : {}),
61
+ ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
62
+ ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
63
+ ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
62
64
  };
63
65
  if (Object.keys(entry).length > 0)
64
66
  axes[skill] = entry;
@@ -91,7 +93,7 @@ export function assertRequestA2aUnlocked(bodyA2a, lockedKeys) {
91
93
  return;
92
94
  if (bodyA2a === undefined || bodyA2a === null)
93
95
  return;
94
- throw new HttpError(400, "`a2aPeers` is administratively locked by this deployment (locked key \"a2a\") — task-supplied A2A peers are refused; locks are config-time and there is no request-time unlock", {
96
+ throw new HttpError(GOVERNANCE_HTTP_STATUS["config.locked_key"], "`a2aPeers` is administratively locked by this deployment (locked key \"a2a\") — task-supplied A2A peers are refused; locks are config-time and there is no request-time unlock", {
95
97
  code: "config.locked_key",
96
98
  });
97
99
  }
package/dist/task-mcp.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
3
  import { HttpError } from "./security.js";
4
+ import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
5
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
5
6
  import { readMcpSourceLabel } from "./mcp-source-label.js";
6
7
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
8
+ import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
7
9
  export const MAX_REQUEST_MCP_SERVERS = 32;
8
10
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
9
11
  export function mcpInjectionHonored(config) {
@@ -72,9 +74,9 @@ function normalizeServer(e, noteBadOrigin) {
72
74
  continue;
73
75
  const a = ax;
74
76
  const entry = {
75
- ...(a.effect === "read" || a.effect === "write" || a.effect === "idempotent" ? { effect: a.effect } : {}),
77
+ ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
76
78
  ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
77
- ...(a.irreversibility === "always" || a.irreversibility === "never" ? { irreversibility: a.irreversibility } : {}),
79
+ ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
78
80
  };
79
81
  if (Object.keys(entry).length > 0)
80
82
  axes[tool] = entry;
@@ -123,7 +125,7 @@ export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
123
125
  return;
124
126
  if (bodyMcp === undefined || bodyMcp === null)
125
127
  return;
126
- throw new HttpError(400, "`mcpServers` is administratively locked by this deployment (locked key \"mcp\") — task-supplied MCP servers are refused; locks are config-time and there is no request-time unlock", {
128
+ throw new HttpError(GOVERNANCE_HTTP_STATUS["config.locked_key"], "`mcpServers` is administratively locked by this deployment (locked key \"mcp\") — task-supplied MCP servers are refused; locks are config-time and there is no request-time unlock", {
127
129
  code: "config.locked_key",
128
130
  });
129
131
  }
@@ -37,6 +37,7 @@
37
37
  */
38
38
  import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core";
39
39
  import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
40
+ import { type KeyClosureIssue } from "./request-key-closure.js";
40
41
  /** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [ref]/[ref] five-word
41
42
  * table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set ([ref] P2-1 corpus check): CC's own
42
43
  * full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`,
@@ -118,10 +119,40 @@ export declare function effectivePermissionMode(body: {
118
119
  * 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的
119
120
  * 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。
120
121
  *
121
- * 🔴 **闭集 exhaustive switch,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
122
+ * 🔴 **闭集穷举表,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
122
123
  * 是**编译错误**([ref] 的安全轴纪律:词表的未知项不许有静默兜底臂)。
123
124
  */
124
125
  export declare function shellGateForMode(effMode: SettingsPermissionMode): "off" | "classify";
126
+ /**
127
+ * [ref] —— 生效模式 → `TaskSpec.writeFace`(**写**面的 roots 容纳判断)。`"open"` = 工作区根之外也能写;
128
+ * `undefined` = **不写这个键**(core 缺省 `"roots"`,与 7.77.0 及以前逐字节相同)。表逐字在
129
+ * {@link MODE_ENGINE_FACES},裁定与硬边界写在 `resolve-spec.ts` 的写点上。
130
+ *
131
+ * 🔴 与 {@link shellGateForMode} 同律:入参非可选 —— 「表态缺席」不是这张表的一行(那是调用点判的
132
+ * 「整个键都不写」),把它折进来就会逼出第二个 `undefined` 语义。
133
+ */
134
+ export declare function writeFaceForMode(effMode: SettingsPermissionMode): "open" | undefined;
135
+ /**
136
+ * [ref] ① —— 生效模式 → `TaskSpec.autoModeRequested`(auto **意图**座,CC 250「用户开」半场)。
137
+ * `true` 只有 auto 一行;`undefined` = 不写键。表逐字在 {@link MODE_ENGINE_FACES}。
138
+ */
139
+ export declare function autoModeRequestedForMode(effMode: SettingsPermissionMode): true | undefined;
140
+ /**
141
+ * S-346 —— [ref] 技能清单预算两旋钮的**值域谓词**(一对具名单铸点)。
142
+ *
143
+ * 两层同问这一只:FRESH 提交面的 400 门(`http/server.ts`)问「合不合法」,RESUME 宽容层
144
+ * ({@link parseTaskSettings})问「收不收下」—— 这两句话在这两个键上是**同一个问题的正反面**
145
+ * (400 的条件恰是受理条件的否定)。修前它们各写一遍字面比较,而承重的两件事
146
+ * (`Number.isFinite` 挡 `NaN`/`Infinity`、`Number.isSafeInteger` 与引擎受理域**同字**)
147
+ * 靠两份注释各自记着 —— 任一处漏一个判据,同一个坏值就会在一条腿上 400、在另一条腿上被收下。
148
+ *
149
+ * 值域**抄 CC settings schema**,不自创;server 侧零加工(不夹紧、不换算、不复刻 core 的默认值),
150
+ * 判据全文见两个调用点的顶注。
151
+ */
152
+ export declare function skillListingBudgetFractionOk(v: unknown): v is number;
153
+ /** {@link skillListingBudgetFractionOk} 的孪生(`int().positive()`;`isSafeInteger` 而不是 `isInteger` 的
154
+ * 理由见那里)。 */
155
+ export declare function skillListingMaxDescOk(v: unknown): v is number;
125
156
  /** The validated, service-trusted subset of `SemaSettings` we project onto the spec. `env` is parsed only to REPORT
126
157
  * it as received-but-deferred when off the host lane (never silently dropped); a MALFORMED `hooks` likewise reports
127
158
  * deferred (submit 路径另有 400 fail-loud),valid `hooks` 进 applied shape(hook-runner 阶段一)。 */
@@ -160,6 +191,13 @@ export interface ParsedTaskSettings {
160
191
  * 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
161
192
  * core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
162
193
  hooks?: HooksConfig;
194
+ /** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
195
+ * 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
196
+ * 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。
197
+ * server 零加工透传到 `TaskSpec`,默认值属主是引擎(缺席 ⇒ 整键不写)。 */
198
+ skillListingBudgetFraction?: number;
199
+ /** 同族第二键:每条技能描述在清单里的字符帽(正整数;CC 默认 1536)。超出由引擎**截断**,不是拒。 */
200
+ skillListingMaxDescChars?: number;
163
201
  }
164
202
  /** The result of parsing an untrusted `body.settings`: the applied subset + the names of present-but-deferred
165
203
  * fields (so the caller can log/advertise them — no silent drop). `null` settings ⇒ `{ settings: undefined }`. */
@@ -180,10 +218,15 @@ export declare const MAX_SETTINGS_ENV_VARS = 256;
180
218
  export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768;
181
219
  export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
182
220
  /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
183
- * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 在 {@link parseTaskSettings};
184
- * `webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
185
- * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。 */
186
- export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
221
+ * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在
222
+ * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
223
+ * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。
224
+ *
225
+ * 🔴 两个 `skillListing*`([ref],7.77.0)= **CC settings 同名键**,值域抄 CC 的 schema 声明
226
+ * (`skillListingBudgetFraction` 是 `gt(0).lte(1)`、`skillListingMaxDescChars` 是 `int().positive()`);
227
+ * 消费点 = 本文件的 parse + `boot/resolve-spec.ts` 的 `TaskSpec` 透传。语义(预算怎么花、描述怎么裁、
228
+ * 溢出怎么退成 name-only)整个归引擎,本仓一个字节都不渲染,也**不复刻默认值**——缺席即整键不写。 */
229
+ export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
187
230
  /** 本 service 消费的 `settings.permissions` 子键 —— 闭集(值形校验另有既有门,本门只判键名)。 */
188
231
  export declare const TASK_SETTINGS_PERMISSION_KEYS: readonly ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
189
232
  /** CC/SDK 合约里合法、本 service **不接线**的顶层键。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响
@@ -202,17 +245,6 @@ export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "
202
245
  /** 同上,`settings.permissions` 子层(出处 = SDK `SettingsPermissions` 的三个声明键)。
203
246
  * `additionalDirectories` 有**同请求替代面**:顶层 `body.additionalDirectories`([ref],resolve-spec 真消费)。 */
204
247
  export declare const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS: readonly ["additionalDirectories", "disableBypassPermissionsMode"];
205
- /** 拒体里逐字回显的键名条数上限。键名原样回显(不猜语义),但**条数有界** —— 一个塞满键的体不许把 400 响应
206
- * 放大成流量面。超出部分只报数。 */
207
- export declare const MAX_SETTINGS_KEY_ECHO = 20;
208
- /** {@link taskSettingsKeyIssue} 的拒体:一句 wire 文案 + 两张分类键名表(消费端可机读,不必解析文案)。 */
209
- export interface TaskSettingsKeyIssue {
210
- message: string;
211
- /** 词表外的键(拼错/未来键),`settings.x` / `settings.permissions.x` 全路径形,已清洗+截断+排序+封顶。 */
212
- unknownKeys: string[];
213
- /** CC/SDK 合约合法但本 service 不接线的键,同形。 */
214
- unsupportedKeys: string[];
215
- }
216
248
  /**
217
249
  * FRESH 提交面的 `settings` 键闭集裁决:顶层或 `permissions` 子层出现受理集之外的键 ⇒ 返回拒体(调用方 400
218
250
  * `request.body_shape`);全部受理 ⇒ `null`。
@@ -224,7 +256,7 @@ export interface TaskSettingsKeyIssue {
224
256
  * 「settings must be an object」/「settings.permissions must be an object」两道门,免得同一个体被两句话抢答。
225
257
  * · `permissions: null` 是既有钉住的「容器缺席」形,照旧不咬。
226
258
  */
227
- export declare function taskSettingsKeyIssue(raw: unknown): TaskSettingsKeyIssue | null;
259
+ export declare function taskSettingsKeyIssue(raw: unknown): KeyClosureIssue | null;
228
260
  /**
229
261
  * Validate an untrusted `body.settings` (raw `unknown` off the wire) into the service-trusted {@link
230
262
  * ParsedTaskSettings}. NEVER trusts the wire type — every field is shape-checked. `defaultMode` accepts the FIVE
@@ -1,6 +1,7 @@
1
1
  import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
+ import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
4
5
  import { parseHooksConfig } from "./hooks/hook-runner.js";
5
6
  export function coercePermissionMode(raw) {
6
7
  const strict = coercePermissionModeStrict(raw);
@@ -34,15 +35,26 @@ export function effectivePermissionMode(body, settings) {
34
35
  return requested;
35
36
  }
36
37
  export function shellGateForMode(effMode) {
37
- switch (effMode) {
38
- case "bypassPermissions":
39
- return "off";
40
- case "auto":
41
- case "default":
42
- case "acceptEdits":
43
- case "plan":
44
- return "classify";
45
- }
38
+ return MODE_ENGINE_FACES[effMode].shellGate;
39
+ }
40
+ const MODE_ENGINE_FACES = {
41
+ bypassPermissions: { shellGate: "off", writeFace: "open", autoModeRequested: undefined },
42
+ auto: { shellGate: "classify", writeFace: undefined, autoModeRequested: true },
43
+ default: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
44
+ acceptEdits: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
45
+ plan: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
46
+ };
47
+ export function writeFaceForMode(effMode) {
48
+ return MODE_ENGINE_FACES[effMode].writeFace;
49
+ }
50
+ export function autoModeRequestedForMode(effMode) {
51
+ return MODE_ENGINE_FACES[effMode].autoModeRequested;
52
+ }
53
+ export function skillListingBudgetFractionOk(v) {
54
+ return typeof v === "number" && Number.isFinite(v) && v > 0 && v <= 1;
55
+ }
56
+ export function skillListingMaxDescOk(v) {
57
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
46
58
  }
47
59
  export const MAX_SETTINGS_OUTPUT_STYLE_CHARS = 16_384;
48
60
  export const MAX_SETTINGS_PERMISSION_RULES = 1_000;
@@ -72,49 +84,31 @@ function cleanStringArray(v) {
72
84
  const out = v.filter((x) => typeof x === "string" && x.length > 0).slice(0, MAX_SETTINGS_PERMISSION_RULES);
73
85
  return out.length > 0 ? out : undefined;
74
86
  }
75
- export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
87
+ export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
76
88
  export const TASK_SETTINGS_PERMISSION_KEYS = ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
77
89
  export const TASK_SETTINGS_UNSUPPORTED_KEYS = ["apiKeyHelper", "crossSessionInbound", "dialogExpiry", "sandbox", "statusLine"];
78
90
  export const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS = ["additionalDirectories", "disableBypassPermissionsMode"];
79
- export const MAX_SETTINGS_KEY_ECHO = 20;
80
- const MAX_SETTINGS_KEY_ECHO_CHARS = 64;
81
91
  const ACCEPTED_TOP = new Set(TASK_SETTINGS_KEYS);
82
92
  const ACCEPTED_PERM = new Set(TASK_SETTINGS_PERMISSION_KEYS);
83
93
  const UNSUPPORTED_TOP = new Set(TASK_SETTINGS_UNSUPPORTED_KEYS);
84
94
  const UNSUPPORTED_PERM = new Set(TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS);
85
- function echoSettingsKey(prefix, key) {
86
- const clean = key.replace(CONTROL_AND_BIDI_STRIP_RE, "");
87
- return `${prefix}${clean.length > MAX_SETTINGS_KEY_ECHO_CHARS ? `${clean.slice(0, MAX_SETTINGS_KEY_ECHO_CHARS)}…` : clean}`;
88
- }
89
95
  export function taskSettingsKeyIssue(raw) {
90
96
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
91
97
  return null;
92
- const unknown = [];
93
- const unsupported = [];
94
- const scan = (bag, prefix, accepted, tolerated) => {
95
- for (const k of Object.keys(bag)) {
96
- if (accepted.has(k))
97
- continue;
98
- (tolerated.has(k) ? unsupported : unknown).push(echoSettingsKey(prefix, k));
99
- }
100
- };
101
- scan(raw, "settings.", ACCEPTED_TOP, UNSUPPORTED_TOP);
98
+ const buckets = { unknown: [], unsupported: [] };
99
+ scanKeyClosure(raw, "settings.", ACCEPTED_TOP, UNSUPPORTED_TOP, buckets);
102
100
  const perms = raw.permissions;
103
101
  if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
104
- scan(perms, "settings.permissions.", ACCEPTED_PERM, UNSUPPORTED_PERM);
102
+ scanKeyClosure(perms, "settings.permissions.", ACCEPTED_PERM, UNSUPPORTED_PERM, buckets);
105
103
  }
104
+ const { unknown, unsupported } = buckets;
106
105
  if (unknown.length === 0 && unsupported.length === 0)
107
106
  return null;
108
107
  unknown.sort();
109
108
  unsupported.sort();
110
- const clause = (names, label) => {
111
- const shown = names.slice(0, MAX_SETTINGS_KEY_ECHO);
112
- const more = names.length - shown.length;
113
- return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
114
- };
115
109
  const parts = [
116
- ...(unknown.length > 0 ? [clause(unknown, "unknown (not a settings key here — check the spelling)")] : []),
117
- ...(unsupported.length > 0 ? [clause(unsupported, "unsupported (a CC settings key this service does not wire)")] : []),
110
+ ...(unknown.length > 0 ? [keyClosureClause(unknown, "unknown (not a settings key here — check the spelling)")] : []),
111
+ ...(unsupported.length > 0 ? [keyClosureClause(unsupported, "unsupported (a CC settings key this service does not wire)")] : []),
118
112
  ];
119
113
  const dirHint = unsupported.some((n) => n.endsWith(".additionalDirectories"))
120
114
  ? " Extra directories ride the top-level body.additionalDirectories instead."
@@ -123,8 +117,8 @@ export function taskSettingsKeyIssue(raw) {
123
117
  message: `settings: unrecognized key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
124
118
  `${parts.join("; ")}.${dirHint} ` +
125
119
  `Accepted: settings.{${[...TASK_SETTINGS_KEYS].join(",")}} and settings.permissions.{${[...TASK_SETTINGS_PERMISSION_KEYS].join(",")}}`,
126
- unknownKeys: unknown.slice(0, MAX_SETTINGS_KEY_ECHO),
127
- unsupportedKeys: unsupported.slice(0, MAX_SETTINGS_KEY_ECHO),
120
+ unknownKeys: unknown.slice(0, MAX_KEY_ECHO),
121
+ unsupportedKeys: unsupported.slice(0, MAX_KEY_ECHO),
128
122
  };
129
123
  }
130
124
  export function parseTaskSettings(raw) {
@@ -178,8 +172,13 @@ export function parseTaskSettings(raw) {
178
172
  out.ultracode = true;
179
173
  if (hooksConfig)
180
174
  out.hooks = hooksConfig;
181
- const hasApplied = out.permissions !== undefined || out.model !== undefined || out.outputStyle !== undefined || out.shellEnv !== undefined || out.ultracode !== undefined || out.hooks !== undefined;
182
- return { settings: hasApplied ? out : undefined, deferred };
175
+ const fraction = s.skillListingBudgetFraction;
176
+ if (skillListingBudgetFractionOk(fraction))
177
+ out.skillListingBudgetFraction = fraction;
178
+ const maxDesc = s.skillListingMaxDescChars;
179
+ if (skillListingMaxDescOk(maxDesc))
180
+ out.skillListingMaxDescChars = maxDesc;
181
+ return { settings: Object.keys(out).length > 0 ? out : undefined, deferred };
183
182
  }
184
183
  export function hasConstitutionAnchors(text) {
185
184
  return typeof text === "string" && text.includes(CYBER_RISK) && text.includes(URL_SAFETY) && text.includes(HARNESS_SECTION_ANCHOR);
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage } from "node:http";
2
- import { type AskRequest, type AskOutcome, type RuleOffer } from "@sema-agent/core";
2
+ import { type AskRequest, type AskOutcome, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
3
3
  import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
4
4
  import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
5
5
  import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
@@ -169,6 +169,27 @@ export interface ToolApprovalFrame {
169
169
  * `GateDisposition.denied.cause`(`tool_end.gate` / `permissionDenied.gate`),ask 面自此没有这一位。
170
170
  */
171
171
  ruleStoreUnreadable?: string;
172
+ /**
173
+ * **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
174
+ * 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
175
+ *
176
+ * 🔴 **在场才读,永远不读缺席**。缺席不是「无计可施」:它同时覆盖敏感路径 deny 行(按模式判,加任何
177
+ * 根都不动它)、读边界压根读不懂的命令(heredoc / 换行 / 命令替换 / 参数当程序)、没展开的 glob、
178
+ * 递归遍历、以及每一只**不是**读边界提的 ask —— 7.18.0 及以前的**每**一只 ask 都是这一形。
179
+ *
180
+ * 🔴 **原样回传**(core 契约 `shell.read_boundary.grant_candidate_is_the_grant`):`dir` 是绝对、已做词法
181
+ * 折叠、用读边界自己的拼法写的串;**显示的串就是要加的串**。壳可以为了好看渲一个缩短形,回传必须是
182
+ * 本键里的原串。⇒ 本仓的姿势是「整只逐字带,或整只丢」,**绝不截**(见 `MAX_READ_ROOT_CANDIDATE_DIR`)。
183
+ *
184
+ * 🔴 **echo-only,零判读**:server 不从命令文本重推目录,也不复核它真不真能清掉这只 ask —— core 已经
185
+ * 把提议的根放回读边界又走了一遍才递出来,下游再判一遍就是同一事实的第二个判官(与 `origin` /
186
+ * `ruleStoreUnreadable` / `denialLimitFallback` 三条顶注的同一条禁令)。
187
+ *
188
+ * 🔴 **只此一面**:core 的负控逐字写着「the durable park row has no twin of this seat in this version」——
189
+ * 一只 PARKED 的根外读 ask 渲的仍是单独那张确认卡。所以 `card_json` / 耐久面**没有**这一座,本仓也不
190
+ * 自铸一个(自铸 = 同一事实的第二个源,而且那个源会在 park 兑现时说谎)。
191
+ */
192
+ readRootCandidate?: ReadRootGrantCandidate;
172
193
  /**
173
194
  * **E-14**([ref]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**工具输入**
174
195
  * (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类属主 `text-bidi.ts`)。
@@ -858,9 +879,20 @@ export declare class ToolApprovalCoordinator {
858
879
  * 残余 = 那类行的归因会记成 `routing_failure`(其实是政策 deny),归因精化登记为候件。
859
880
  */
860
881
  private unattendedOutcome;
882
+ /** 政策**读**口(不是终局铸口)—— 回答「这台部署把无人可答翻成 park 吗」。
883
+ *
884
+ * 🔴 与 {@link unattendedOutcome} 分家是 S-187 的一半:此前两件事共用一只方法,于是墓碑腿那次
885
+ * 纯粹的**政策查询**(`unattendedOutcome() !== false`)会和同一只 ask 的真终局各记一条遥测 ——
886
+ * 「一只 ask 一条线」当场不成立。读口不落日志,铸口落;谁也不兼职。 */
887
+ private get unattendedRoutesToPark();
861
888
  /** {@link unattendedOutcome} 的**公开**口:`windowZero` 的 immediate-unavailable 闭包长在装配层
862
- * (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。 */
863
- unattendedAskOutcome(): "unavailable" | false;
889
+ * (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。
890
+ * 出处随调用方给(那条腿知道自己是哪一只 ask)。 */
891
+ unattendedAskOutcome(site: {
892
+ taskId?: string;
893
+ owner?: string | null;
894
+ toolName?: string;
895
+ }): "unavailable" | false;
864
896
  /**
865
897
  * [ref] 车3 刀 3b —— [ref] §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
866
898
  *
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
8
8
  import { createLogger } from "./observability/logger.js";
9
9
  import { recordFailOpen } from "./observability/fail-open.js";
10
10
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
11
- import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readRuleEvidence, readRuleOffersAbsence, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
11
+ import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
12
12
  import { projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
13
13
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
14
14
  const defaultLogger = createLogger();
@@ -300,11 +300,29 @@ export class ToolApprovalCoordinator {
300
300
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
301
301
  this.askAudit = opts?.askAudit;
302
302
  }
303
- unattendedOutcome() {
304
- return this.unattendedPolicy === "deny" ? false : "unavailable";
303
+ unattendedOutcome(site) {
304
+ const outcome = this.unattendedPolicy === "deny" ? false : "unavailable";
305
+ defaultLogger.warn("approver_unavailable", {
306
+ arm: site.arm,
307
+ policy: this.unattendedPolicy,
308
+ outcome: outcome === false ? "deny" : "unavailable",
309
+ owner: site.owner ?? null,
310
+ taskId: site.taskId ?? null,
311
+ toolName: site.toolName ?? null,
312
+ reachability: site.reachability ?? null,
313
+ });
314
+ return outcome;
315
+ }
316
+ get unattendedRoutesToPark() {
317
+ return this.unattendedPolicy !== "deny";
305
318
  }
306
- unattendedAskOutcome() {
307
- return this.unattendedOutcome();
319
+ unattendedAskOutcome(site) {
320
+ return this.unattendedOutcome({
321
+ arm: "window_zero",
322
+ ...(site.taskId !== undefined ? { taskId: site.taskId } : {}),
323
+ ...(site.owner !== undefined && site.owner !== null ? { owner: site.owner } : {}),
324
+ ...(site.toolName !== undefined ? { toolName: site.toolName } : {}),
325
+ });
308
326
  }
309
327
  admit(taskId, owner) {
310
328
  if (!this.askStore)
@@ -758,7 +776,7 @@ export class ToolApprovalCoordinator {
758
776
  ask = async (req, signal) => {
759
777
  const ctx = this.als.getStore();
760
778
  if (!ctx)
761
- return this.unattendedOutcome();
779
+ return this.unattendedOutcome({ arm: "no_run_context", toolName: req.toolName });
762
780
  return this.askBroadcast({
763
781
  taskId: ctx.taskId,
764
782
  owner: ctx.owner,
@@ -786,11 +804,9 @@ export class ToolApprovalCoordinator {
786
804
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
787
805
  const reachability = aliveCtxs.length === 0 ? "unreached" : "reachable";
788
806
  if (reachability === "unreached") {
789
- const unattended = this.unattendedOutcome();
790
- if (unattended === false)
791
- return unattended;
792
- if (!this.askStore)
793
- return unattended;
807
+ if (!this.unattendedRoutesToPark || !this.askStore) {
808
+ return this.unattendedOutcome({ arm: "no_live_target", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
809
+ }
794
810
  }
795
811
  const primary = aliveCtxs[0];
796
812
  const governanceForced = (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
@@ -809,7 +825,7 @@ export class ToolApprovalCoordinator {
809
825
  admitMaxPerTask: this.admitMaxPerTask,
810
826
  admitMaxPerOwner: this.admitMaxPerOwner,
811
827
  });
812
- return this.unattendedOutcome();
828
+ return this.unattendedOutcome({ arm: "admission_exhausted", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
813
829
  }
814
830
  const id = uuidv7();
815
831
  const child = isChildAsk(req);
@@ -848,6 +864,7 @@ export class ToolApprovalCoordinator {
848
864
  ...((a) => (a !== undefined ? { ruleOffersAbsence: a } : {}))(readRuleOffersAbsence(req)),
849
865
  ...((f) => (f !== undefined ? { denialLimitFallback: f } : {}))(readDenialLimitFallback(req)),
850
866
  ...(typeof req.ruleStoreUnreadable === "string" && req.ruleStoreUnreadable.length > 0 ? { ruleStoreUnreadable: req.ruleStoreUnreadable } : {}),
867
+ ...((g) => (g !== undefined ? { readRootCandidate: g } : {}))(readReadRootCandidate(req)),
851
868
  ...((c) => (c !== undefined ? { probeCause: c } : {}))(readProbeCause(req)),
852
869
  ...((c) => (c !== undefined ? { ruleEvidence: c } : {}))(readRuleEvidence(req)),
853
870
  ...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
@@ -904,7 +921,9 @@ export class ToolApprovalCoordinator {
904
921
  if (existingOrCreated.row.state !== "STREAM_PENDING") {
905
922
  releaseAdmission();
906
923
  const replayed = replayTerminalAskRow(existingOrCreated.row);
907
- return replayed === "unavailable" ? this.unattendedOutcome() : replayed;
924
+ return replayed === "unavailable"
925
+ ? this.unattendedOutcome({ arm: "replayed_parked", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability })
926
+ : replayed;
908
927
  }
909
928
  const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.row.cardJson);
910
929
  const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
@@ -985,7 +1004,7 @@ export class ToolApprovalCoordinator {
985
1004
  voidAbandonedRow(lateEnsuredAskId);
986
1005
  if (reachability === "unreached") {
987
1006
  releaseAdmission();
988
- return this.unattendedOutcome();
1007
+ return this.unattendedOutcome({ arm: "unreached_no_row", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
989
1008
  }
990
1009
  }
991
1010
  }
@@ -1014,8 +1033,11 @@ export class ToolApprovalCoordinator {
1014
1033
  return false;
1015
1034
  };
1016
1035
  const shapeOutcome = (settled) => {
1017
- if (windowRouteUnavailable)
1018
- return this.unattendedOutcome() === false ? shapeDeny() : "unavailable";
1036
+ if (windowRouteUnavailable) {
1037
+ return this.unattendedOutcome({ arm: "window_expired", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability }) === false
1038
+ ? shapeDeny()
1039
+ : "unavailable";
1040
+ }
1019
1041
  if (settled.allowed) {
1020
1042
  return settled.updatedInput !== undefined ? { allow: true, updatedInput: settled.updatedInput } : true;
1021
1043
  }
@@ -1032,7 +1054,7 @@ export class ToolApprovalCoordinator {
1032
1054
  for (const [sig, fn] of abortListeners)
1033
1055
  sig.removeEventListener("abort", fn);
1034
1056
  this.pending.delete(id);
1035
- if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedOutcome() !== false) {
1057
+ if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedRoutesToPark) {
1036
1058
  this.recordParkTombstone(wireApprovalId, { kind: "park", askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
1037
1059
  parkRouted = true;
1038
1060
  }
@@ -1456,11 +1478,11 @@ export class ToolApprovalCoordinator {
1456
1478
  .catch(() => undefined);
1457
1479
  }
1458
1480
  if (emitFailed)
1459
- return this.unattendedOutcome();
1481
+ return this.unattendedOutcome({ arm: "emit_failed", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1460
1482
  if (windowRouteUnavailable)
1461
1483
  return shapeOutcome(settled);
1462
1484
  if (!emitSettled && lastOutcome === "expired")
1463
- return this.unattendedOutcome();
1485
+ return this.unattendedOutcome({ arm: "emit_unsettled", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1464
1486
  return shapeOutcome(settled);
1465
1487
  }
1466
1488
  respond(id, principal, body, httpReq) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词,**两面共用一份**。
3
+ *
4
+ * ## 病(修前)
5
+ * `task-mcp.ts` 与 `task-a2a.ts` 各自在请求腿上逐键校验同一个三成员结构
6
+ * (`effect` / `egress` / `irreversibility`),而两个**闭集词表**是**手抄的字面串**:四份字面量
7
+ * (两文件 × 两个词轴),没有一份钉在 core 的型上。后果不是「今天判错了」,而是**判据比声明窄**这件事
8
+ * 没有任何门看得见 —— core 哪天给 `ToolEffect` 加一个词(或把 `irreversibility` 的两词改成三词,它在
9
+ * `AskOrigin` 那一面**已经**是 `never|maybe|always` 三词了),两条请求腿会把那个**合法**的新词
10
+ * **静默剔除**(MCP 腿)或**整条 server 判假**(A2A 腿),而 tsc 一声不吭。
11
+ *
12
+ * ## 修法
13
+ * 词表留在本仓(运行期要它),但**两向钉死在 core 的型上**:core 加词 ⇒ 正向门红;本表留了一个 core
14
+ * 已经没有的词 ⇒ 反向门红。两个面共用**同一只**谓词,于是「什么算一个合法的轴值」在本仓只有一个答案。
15
+ *
16
+ * 🔴 **只管值域,不管处置**:两条腿对坏值的处置**刻意不同**(MCP = 丢这一键、留 server;A2A = 整条拒),
17
+ * 判据与理由各写在各自的调用点。合并处置会抹掉那条真实的分歧(MCP 的 `toolAxes` 有 server-hint 腿可折,
18
+ * A2A 收下的就是最终值),而分歧本身是裁定过的。
19
+ */
20
+ import type { McpServerSpec, ToolEffect } from "@sema-agent/core";
21
+ /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明两面逐字同声明。 */
22
+ export type ToolAxisEntry = NonNullable<McpServerSpec["toolAxes"]>[string];
23
+ /** 轴覆写上那个**两词**的不可逆档(⚠️ 与 `AskOrigin` 面的**三词** `never|maybe|always` 不是同一个词表:
24
+ * 覆写面只收两极,`maybe` 是引擎按工具自己的声明定的,调用方说不出口)。 */
25
+ export type ToolAxisIrreversibility = NonNullable<ToolAxisEntry["irreversibility"]>;
26
+ /** 值是不是一个合法的 `effect` 词(两条请求腿共用)。 */
27
+ export declare function isToolEffect(v: unknown): v is ToolEffect;
28
+ /** 值是不是一个合法的**覆写面** `irreversibility` 词(两条请求腿共用)。 */
29
+ export declare function isToolAxisIrreversibility(v: unknown): v is ToolAxisIrreversibility;
30
+ //# sourceMappingURL=tool-axis-words.d.ts.map
@@ -0,0 +1,9 @@
1
+ const TOOL_EFFECT_WORDS = ["read", "write", "idempotent"];
2
+ const TOOL_AXIS_IRREVERSIBILITY_WORDS = ["always", "never"];
3
+ export function isToolEffect(v) {
4
+ return typeof v === "string" && TOOL_EFFECT_WORDS.includes(v);
5
+ }
6
+ export function isToolAxisIrreversibility(v) {
7
+ return typeof v === "string" && TOOL_AXIS_IRREVERSIBILITY_WORDS.includes(v);
8
+ }
9
+ //# sourceMappingURL=tool-axis-words.js.map
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
43
43
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
44
44
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
45
45
  type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
46
- type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable";
46
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate";
47
47
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
48
48
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
49
49
  type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
@@ -121,8 +121,8 @@ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "seve
121
121
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
122
122
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
123
123
  type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
124
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested";
125
- type TaskSpecUnwired = "excludeAllTools" | "writeFace" | "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
124
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars";
125
+ type TaskSpecUnwired = "excludeAllTools" | "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
126
126
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
127
127
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
128
128
  type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles";