@sema-agent/server 7.114.0 → 7.115.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 (68) hide show
  1. package/MIGRATION.md +73 -1
  2. package/USAGE.md +35 -2
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-card.d.ts +21 -24
  10. package/dist/approval-card.js +12 -0
  11. package/dist/approval.js +5 -2
  12. package/dist/boot/exit-boundary.d.ts +3 -2
  13. package/dist/boot/exit-boundary.js +3 -0
  14. package/dist/boot/resolve-spec.js +4 -1
  15. package/dist/boot/shutdown.d.ts +19 -0
  16. package/dist/boot/shutdown.js +12 -1
  17. package/dist/capabilities/composite-members.d.ts +1 -0
  18. package/dist/capabilities/composite-members.js +1 -0
  19. package/dist/config-catalog.js +2 -1
  20. package/dist/config-center/apply-effective.js +11 -11
  21. package/dist/config-provider.d.ts +16 -0
  22. package/dist/config-provider.js +11 -2
  23. package/dist/config-types.d.ts +20 -1
  24. package/dist/config.d.ts +41 -18
  25. package/dist/config.js +36 -13
  26. package/dist/deployment-governance.d.ts +15 -4
  27. package/dist/deployment-governance.js +20 -2
  28. package/dist/fleet/fleet-bus.d.ts +1 -22
  29. package/dist/hooks/hook-runner.d.ts +29 -3
  30. package/dist/hooks/hook-runner.js +30 -17
  31. package/dist/http/admission.js +7 -2
  32. package/dist/http/approval-sweeps.js +20 -0
  33. package/dist/http/checkpoint-ctx.d.ts +68 -0
  34. package/dist/http/checkpoint-ctx.js +36 -0
  35. package/dist/http/durable-run-leg.js +2 -1
  36. package/dist/http/resume-legs.js +9 -0
  37. package/dist/http/route-ctx.d.ts +7 -0
  38. package/dist/http/routes/a2a-serve.d.ts +1 -3
  39. package/dist/http/routes/a2a-serve.js +0 -2
  40. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  41. package/dist/http/routes/admin-memory-optout.js +0 -1
  42. package/dist/http/routes/capabilities.js +1 -0
  43. package/dist/http/routes/diagnostics.d.ts +5 -0
  44. package/dist/http/routes/diagnostics.js +2 -0
  45. package/dist/http/routes/sessions.js +1 -1
  46. package/dist/http/routes/tasks.js +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/leader/wire.js +2 -1
  49. package/dist/local-data-root.d.ts +1 -1
  50. package/dist/mcp-content-origin.d.ts +0 -8
  51. package/dist/model-entry-refusal.d.ts +10 -2
  52. package/dist/model-entry-refusal.js +19 -3
  53. package/dist/observability/fail-open.d.ts +5 -8
  54. package/dist/permission-rule-list.d.ts +81 -0
  55. package/dist/permission-rule-list.js +17 -0
  56. package/dist/run-local.js +4 -1
  57. package/dist/runtime-governance.d.ts +21 -1
  58. package/dist/runtime-governance.js +6 -1
  59. package/dist/security.js +2 -2
  60. package/dist/session-policy-wire.d.ts +5 -4
  61. package/dist/task-settings.d.ts +6 -5
  62. package/dist/task-settings.js +16 -30
  63. package/dist/tool-approval.d.ts +16 -4
  64. package/dist/tool-approval.js +2 -1
  65. package/dist/trace/core-keyset-guard.d.ts +9 -4
  66. package/dist/trace/project.js +9 -0
  67. package/package.json +4 -4
  68. package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
@@ -1,4 +1,4 @@
1
- import { HooksConfig } from "@sema-agent/settings-schema/hooks";
1
+ import { HooksConfig, type HookEventName } from "@sema-agent/settings-schema/hooks";
2
2
  import type { Hooks } from "@sema-agent/core";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { HookNoticeObservation } from "../fleet/fleet-bus.js";
@@ -80,6 +80,10 @@ export interface ParseHooksConfigResult {
80
80
  * 拒放集合相对 7.113.0 的一处放宽(披露在 CHANGELOG / MIGRATION):超过上限的字面别名 matcher 7.113.0 拒(当时字面判不设上限),自 7.114.0 两档都判未判、受理。
81
81
  *
82
82
  * 错误句只回显**目录里的**别名与正名(闭集字符串),不回显调用方的原字节(S-716 姿势)。
83
+ *
84
+ * 🔴 **判决是数据,句子与体的机读位同出一只铸点**(S-1025;client-core 请托,additive):{@link deadToolMatcherIssue} 回 {@link DeadToolMatcherIssue}(event / alias / canonical)
85
+ * 而不是句子;{@link buildDeadToolMatcherRefusal} 是**唯一**把它铸成拒句 + `field` + `detail` 的函数,受理门只取它的返回、**不解析句子**。体上多出的两键让三端不必解析英文就能把这只拒
86
+ * 与普通提交失败分流、提示用户改 matcher;句子逐字同 7.114.0、零新码。
83
87
  */
84
88
  /** 超此长度的 matcher 不在受理面读(两档同一个上限;未判,reason = oversize)。与 {@link MAX_HOOK_COMMAND_CHARS} 同量纲的服务侧防线:真实 matcher(精确名 / 交替 / `mcp__.*`)远在其内;目录全部工具名交替 ~0.5 KiB。 */
85
89
  export declare const MAX_MATCHER_EVAL_CHARS = 8192;
@@ -95,13 +99,35 @@ export interface MatcherEvalBudget {
95
99
  readonly budgetMs?: number;
96
100
  readonly now?: () => number;
97
101
  }
102
+ /** 一条受理面判决(纯数据):`event` 事件下的某只 matcher 选中了退役别名 `alias`、却选不中它的正名 `canonical`。`alias` / `canonical` 恒取自 core 静态目录(闭集字符串,不含调用方原字节)。 */
103
+ export interface DeadToolMatcherIssue {
104
+ readonly event: HookEventName;
105
+ readonly alias: string;
106
+ readonly canonical: string;
107
+ }
108
+ /** {@link buildDeadToolMatcherRefusal} 的返回(纯数据):拒句 + 400 体上的两个机读位。`field` 恒为字面 `"settings.hooks"`;`detail` 与 {@link DeadToolMatcherIssue} 同三键(逐个挑出,不 spread)。 */
109
+ export interface DeadToolMatcherRefusal {
110
+ readonly message: string;
111
+ readonly field: "settings.hooks";
112
+ readonly detail: {
113
+ readonly event: HookEventName;
114
+ readonly alias: string;
115
+ readonly canonical: string;
116
+ };
117
+ }
118
+ /**
119
+ * 判决 → 拒句 + 机读位。**拒句的唯一铸点**:句子逐字 = 7.114.0 的那一句(`test/hooks-alias-matcher-s995.test.ts` R7 冻结);受理门 `admission.ts` 用返回值填 400 体,
120
+ * 不从句子里反解字段。
121
+ */
122
+ export declare function buildDeadToolMatcherRefusal(issue: DeadToolMatcherIssue): DeadToolMatcherRefusal;
98
123
  /**
99
- * fresh 提交面专用判据(S-924 / S-995):配置里第一条「永远打不中」的工具事件 matcher 的拒句;没有 ⇒ undefined。
124
+ * fresh 提交面专用判据(S-924 / S-995):配置里第一条「永远打不中」的工具事件 matcher 的判决数据({@link DeadToolMatcherIssue});没有 ⇒ undefined。
125
+ * 拒句与 400 体的机读位由 {@link buildDeadToolMatcherRefusal} 从这份数据铸(S-1025)。
100
126
  * 入参是 {@link parseHooksConfig} 已校验过的配置;`depth` 由调用点按车道定(见头注:`taskHooksHonored(config) ? "regex" : "literal"`)。
101
127
  * `budget` **仅供测试注入**({@link MatcherEvalBudget});生产调用点只传前两个实参。
102
128
  * **只有 `admission.ts` 的 fresh 提交门调它** —— resume 重放不调(理由见上面的头注)。未判的 matcher 过 `recordFailOpen`(tag `MATCHER_UNJUDGED_TAG`)。
103
129
  */
104
- export declare function deadToolMatcherIssue(config: HooksConfig, depth: MatcherEvalDepth, budget?: MatcherEvalBudget): string | undefined;
130
+ export declare function deadToolMatcherIssue(config: HooksConfig, depth: MatcherEvalDepth, budget?: MatcherEvalBudget): DeadToolMatcherIssue | undefined;
105
131
  /**
106
132
  * 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。
107
133
  * 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。
@@ -142,6 +142,13 @@ function deadAliasInMatcher(matcher, depth, budget) {
142
142
  }
143
143
  return undefined;
144
144
  }
145
+ export function buildDeadToolMatcherRefusal(issue) {
146
+ return {
147
+ message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
148
+ field: "settings.hooks",
149
+ detail: { event: issue.event, alias: issue.alias, canonical: issue.canonical },
150
+ };
151
+ }
145
152
  export function deadToolMatcherIssue(config, depth, budget = {}) {
146
153
  const now = budget.now ?? (() => performance.now());
147
154
  const deadline = { deadlineMs: now() + (budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS), now };
@@ -158,7 +165,7 @@ export function deadToolMatcherIssue(config, depth, budget = {}) {
158
165
  recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
159
166
  continue;
160
167
  }
161
- return `settings.hooks.${event}: matcher "${verdict.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${verdict.canonical}"`;
168
+ return { event, alias: verdict.alias, canonical: verdict.canonical };
162
169
  }
163
170
  }
164
171
  return undefined;
@@ -855,6 +862,7 @@ export function createTaskHooks(config, ctx) {
855
862
  let currentInput = input;
856
863
  let rewritten = false;
857
864
  let asked;
865
+ let allowedFor;
858
866
  let unknownDecision = false;
859
867
  const contexts = [];
860
868
  const deadlineAt = Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000;
@@ -868,11 +876,12 @@ export function createTaskHooks(config, ctx) {
868
876
  }
869
877
  if (!m)
870
878
  continue;
879
+ const groupInput = currentInput;
871
880
  const payload = {
872
881
  ...basePayload(ctx, tctx.permissionMode),
873
882
  hook_event_name: "PreToolUse",
874
883
  tool_name: toolName,
875
- tool_input: currentInput,
884
+ tool_input: groupInput,
876
885
  tool_use_id: tctx.toolCallId,
877
886
  };
878
887
  const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
@@ -913,10 +922,13 @@ export function createTaskHooks(config, ctx) {
913
922
  asked = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
914
923
  }
915
924
  if (decision === "allow" || decision === undefined) {
916
- if (h.updatedInput !== null && typeof h.updatedInput === "object") {
925
+ const rewrites = h.updatedInput !== null && typeof h.updatedInput === "object";
926
+ if (rewrites) {
917
927
  currentInput = h.updatedInput;
918
928
  rewritten = true;
919
929
  }
930
+ if (decision === "allow")
931
+ allowedFor = { input: rewrites ? h.updatedInput : groupInput };
920
932
  }
921
933
  else if (decision !== "deny" && decision !== "ask") {
922
934
  unknownDecision = true;
@@ -946,13 +958,11 @@ export function createTaskHooks(config, ctx) {
946
958
  ...buildHookContextField(contexts),
947
959
  };
948
960
  }
949
- if (rewritten || contexts.length > 0) {
950
- return {
951
- action: "allow",
952
- ...(rewritten ? { updatedInput: currentInput } : {}),
953
- ...buildHookContextField(contexts),
954
- };
955
- }
961
+ const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...buildHookContextField(contexts) };
962
+ if (allowedFor !== undefined && allowedFor.input === currentInput)
963
+ return { action: "allow", ...carried };
964
+ if (rewritten || contexts.length > 0)
965
+ return carried;
956
966
  return undefined;
957
967
  };
958
968
  hooks.preToolUse = preToolUseHook;
@@ -1324,6 +1334,7 @@ export function composeHooks(deployment, task) {
1324
1334
  let current = input;
1325
1335
  const contexts = [];
1326
1336
  let asked;
1337
+ let allowedFor;
1327
1338
  for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
1328
1339
  if (!slot)
1329
1340
  continue;
@@ -1343,6 +1354,10 @@ export function composeHooks(deployment, task) {
1343
1354
  asked = r;
1344
1355
  break;
1345
1356
  case "allow":
1357
+ if (updatedInput !== undefined)
1358
+ current = updatedInput;
1359
+ allowedFor = { input: current };
1360
+ break;
1346
1361
  case undefined:
1347
1362
  if (updatedInput !== undefined)
1348
1363
  current = updatedInput;
@@ -1362,13 +1377,11 @@ export function composeHooks(deployment, task) {
1362
1377
  ...buildHookContextField(contexts),
1363
1378
  };
1364
1379
  }
1365
- if (current !== input || contexts.length > 0) {
1366
- return {
1367
- action: "allow",
1368
- ...(current !== input ? { updatedInput: current } : {}),
1369
- ...buildHookContextField(contexts),
1370
- };
1371
- }
1380
+ const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
1381
+ if (allowedFor !== undefined && allowedFor.input === current)
1382
+ return { action: "allow", ...carried };
1383
+ if (current !== input || contexts.length > 0)
1384
+ return carried;
1372
1385
  return undefined;
1373
1386
  };
1374
1387
  }
@@ -7,7 +7,7 @@ import { taskBodyKeyIssue } from "../request-key-closure.js";
7
7
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
8
8
  import { selfOrchestrationDenial } from "../task-workflow.js";
9
9
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
10
- import { deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
10
+ import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
11
11
  import { judgeRequestWebSearch } from "../plugins/web-search.js";
12
12
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
13
13
  import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
@@ -202,7 +202,8 @@ export function createAdmission(ctx) {
202
202
  }
203
203
  const deadMatcher = deadToolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
204
204
  if (deadMatcher !== undefined) {
205
- sendError(res, 400, "request.field_invalid", deadMatcher);
205
+ const refusal = buildDeadToolMatcherRefusal(deadMatcher);
206
+ sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
206
207
  return null;
207
208
  }
208
209
  }
@@ -420,6 +421,10 @@ export function createAdmission(ctx) {
420
421
  sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
421
422
  return null;
422
423
  }
424
+ if (l.brainCallGuardrailMs !== undefined) {
425
+ sendError(res, 400, "request.field_invalid", "limits.brainCallGuardrailMs is not caller-settable here — the engine's default wall on a single brain call applies (one hour); remove it");
426
+ return null;
427
+ }
423
428
  for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
424
429
  const v = l[k];
425
430
  if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
@@ -7,6 +7,8 @@ import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
7
7
  import { hasServiceAuth } from "./dispatch.js";
8
8
  import { gatedPrincipal } from "./principal-gate.js";
9
9
  import { readinessOf, billableRefusalOf } from "./readiness.js";
10
+ import { readResumeCtx } from "./resume-ctx-read.js";
11
+ import { allowListReadingUnstated } from "./checkpoint-ctx.js";
10
12
  const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
11
13
  function blockedScalars(body) {
12
14
  const out = {};
@@ -64,17 +66,35 @@ export function createApprovalSweeps(ctx) {
64
66
  return false;
65
67
  }
66
68
  };
69
+ const allowListUnservable = async (sessionId) => {
70
+ const read = await readResumeCtx(cs.getCtx(sessionId));
71
+ if (read.kind === "corrupt")
72
+ return false;
73
+ return read.kind === "ok" && allowListReadingUnstated(read.ctx);
74
+ };
67
75
  let parkedSkipped = 0;
76
+ const allowListSkipped = [];
68
77
  for (const { sessionId } of expired) {
69
78
  if (await isParkedOwned(sessionId)) {
70
79
  parkedSkipped += 1;
71
80
  continue;
72
81
  }
82
+ if (await allowListUnservable(sessionId)) {
83
+ allowListSkipped.push(sessionId);
84
+ continue;
85
+ }
73
86
  await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
74
87
  deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, ...blockedScalars(r.body) }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
75
88
  }
76
89
  if (parkedSkipped > 0)
77
90
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
91
+ if (allowListSkipped.length > 0) {
92
+ deps.logger?.warn?.("deny_sweep_allow_list_reading_skipped", {
93
+ count: allowListSkipped.length,
94
+ sessionIds: allowListSkipped,
95
+ note: "these expired approvals belong to tasks suspended by a server version that read settings.permissions.allow as a whitelist; this version does not replay them, so the SLA deny cannot be delivered. They stay suspended (session lock held) until the reaper collects them at their terminal time — cancel them (POST /v1/runs/:id/cancel) to release the sessions now",
96
+ });
97
+ }
78
98
  }
79
99
  async function redeemParkedAsk(req) {
80
100
  const cs = deps.checkpointStore;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 续跑重建输入(`checkpoint_ctx` 的行体,{@link CheckpointCtx})的**写形**,以及持久化体里 `settings.permissions.allow` 的**读法记号** —— S-1034(7.115.0)。
3
+ *
4
+ * ## 病
5
+ * 7.115.0 把 `settings.permissions.allow` 从「名单」(名单外的工具一律拒)改成「allow 规则」(命中 = 带理由的放行,未命中 = 不表态、照常判)。
6
+ * 续跑家族重建 spec 时把**持久化的提交体**原样重放进 `resolveSpec`(`resume-legs.ts` 的 `resolveResumeSpec`)——
7
+ * 一条在 ≤7.114.0 上停驻、体里带非空 `allow` 的行,升级后续跑就被按新读法读:名单外的工具不再被拒。持久面上的静默语义翻转(放宽方向)。
8
+ * 坏形 `allow`(`Task` / `mcp__figma` / `Bash(npm:*)` …)的旧行则每次续跑都 409、SLA 拒扫腿逐拍重试直到收割 —— 一张本来放不开任何门的停驻被砖化。
9
+ *
10
+ * ## 规则(一条;持久数据的「响亮拒 + 一次性迁移」,零双读)
11
+ * 续跑重放持久化体之前问一句:**体里的 `allow` 在生效(非空数组)而本行没有「按表态读」的记号 ⇒ 拒**。
12
+ * 行留停驻、字节不动(拒发生在认领之前,run 行、卡、ctx 一个字节都不写);出路 = 作为新任务重提,这条任务若还停驻着先取消(`task_done` 唤醒停驻与 workflow 宿主那条腿没有可取消的 run)。
13
+ * **没有**「按旧名单语义继续读」的臂:那是第二种读法,正是本规则要消灭的东西。
14
+ * · 记号 = ≥7.115.0 写下的行在体里带非空 `allow` 时,ctx 上的 `permissionsAllowReading: "statement"`(闭集一词)。
15
+ * 写点 = 每一次**新的**持久化写({@link buildCheckpointCtx} 是它们的唯一构造口);续跑腿的写回传的是读进来的那只 ctx(或其展开),记号随行,不重新判。
16
+ * · 体里没有 `allow`(或空数组 —— 7.114.0 的名单判据 `acceptedRuleList` 对空数组同样读成「没有名单」)的行不写记号、不受本规则影响。
17
+ *
18
+ * ## 判据与 7.114.0 的关系(如实)
19
+ * 7.114.0 只在三桶整体合法时才读名单;判据这里只看「`allow` 是非空数组」,比 7.114.0 的「名单生效」宽一格(坏桶的行 7.114.0 也不读名单)——
20
+ * 方向 = 多拒,不会漏拒;而坏桶的体当年就被 fresh 受理门拒在门外,盘上本不该有。
21
+ *
22
+ * ## 何时删(一次性迁移,不是长期双读)
23
+ * 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件 = 所有 ≤7.114.0 停驻的行都已结束或被收割
24
+ * (停驻行的终局时刻 = 落盘起 `TERMINAL_BACKSTOP_MS`,见 `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删:
25
+ * 届时盘上不会再有没记号却带 `allow` 的行,而有记号的行读不读它都一样。
26
+ */
27
+ import type { CheckpointCtx } from "./route-ctx.js";
28
+ import type { TaskRequestBody } from "./wire-types.js";
29
+ /** 读法记号的闭集(一词):这一行体里的 `settings.permissions.allow` 是按「allow 规则(表态)」写下、也该按它读的。 */
30
+ export declare const PERMISSIONS_ALLOW_READINGS: readonly ["statement"];
31
+ export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
32
+ /**
33
+ * 体里的 `settings.permissions.allow` 是否**在生效**:非空数组(与 7.114.0 名单判据 `acceptedRuleList` 同一个「在」——空数组 = 没有)。
34
+ * 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
35
+ */
36
+ export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
37
+ /**
38
+ * 一次**新的**持久化写的 ctx(提交腿 / `POST /v1/runs` 族 / leader worker)—— 写记号的唯一口。
39
+ *
40
+ * 调用方是「本进程刚按表态读过这份体」的写点:fresh 提交(受理门按 7.115.0 读过 `allow`),或已经过了续跑拒臂的重放腿
41
+ * (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
42
+ * 续跑腿的**写回**不经本函数:它传读进来的 ctx(或其展开),记号随行;在写回口重新判会把一条没记号的旧行「洗」成新行。
43
+ */
44
+ export declare function buildCheckpointCtx(fields: {
45
+ body: TaskRequestBody;
46
+ memoryScope?: string | undefined;
47
+ unattendedLeg?: true;
48
+ }): CheckpointCtx;
49
+ /**
50
+ * 本行能否被按 7.115.0 的读法重放:体里 `allow` 在生效而行上没有记号 ⇒ **不能**(`true` = 拒)。
51
+ * 记号的值不在闭集里(手改 / 将来的写者)⇒ 同样拒:认不得的记号不是证据。
52
+ */
53
+ export declare function allowListReadingUnstated(ctx: CheckpointCtx): boolean;
54
+ /**
55
+ * 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending;**不是**「原样重试会成功」)。
56
+ * 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子说原因(这条任务停在 `allow` 还是名单的版本上)与出路(作为新任务重提,仍停驻的先取消;要「只许这些工具」用会话规则 `toolAllow` / 每请求 `excludeTools`)。
57
+ *
58
+ * 🔴 形是给冻结门看的(`api-error-text-freeze` 的 resume 家族收集器):`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
59
+ */
60
+ export declare function allowListReadingRefusal(): {
61
+ status: number;
62
+ body: {
63
+ error: string;
64
+ errorCode: string;
65
+ retriable: true;
66
+ };
67
+ };
68
+ //# sourceMappingURL=checkpoint-ctx.d.ts.map
@@ -0,0 +1,36 @@
1
+ export const PERMISSIONS_ALLOW_READINGS = ["statement"];
2
+ export function bodyCarriesAllowList(body) {
3
+ const settings = body?.settings;
4
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings))
5
+ return false;
6
+ const permissions = settings["permissions"];
7
+ if (permissions === null || typeof permissions !== "object" || Array.isArray(permissions))
8
+ return false;
9
+ const allow = permissions["allow"];
10
+ return Array.isArray(allow) && allow.length > 0;
11
+ }
12
+ export function buildCheckpointCtx(fields) {
13
+ return {
14
+ body: fields.body,
15
+ ...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
16
+ ...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
17
+ ...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "statement" } : {}),
18
+ };
19
+ }
20
+ export function allowListReadingUnstated(ctx) {
21
+ if (!bodyCarriesAllowList(ctx.body))
22
+ return false;
23
+ const reading = ctx.permissionsAllowReading;
24
+ return !(typeof reading === "string" && PERMISSIONS_ALLOW_READINGS.includes(reading));
25
+ }
26
+ export function allowListReadingRefusal() {
27
+ return {
28
+ status: 409,
29
+ body: {
30
+ error: "this task was suspended by an earlier server version, which read settings.permissions.allow as a whitelist (every tool not on the list was refused); this version reads that list as a statement that the named tools are allowed, not as a whitelist, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
31
+ errorCode: "checkpoint.unsupported_version",
32
+ retriable: true,
33
+ },
34
+ };
35
+ }
36
+ //# sourceMappingURL=checkpoint-ctx.js.map
@@ -8,6 +8,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
8
8
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
9
9
  import { clearTurnActivity, readTurnActivityMs } from "../turn-activity.js";
10
10
  import { recordResourceWindow } from "../resource-window.js";
11
+ import { buildCheckpointCtx } from "./checkpoint-ctx.js";
11
12
  export async function mintAndDriveDurableRun(ctx, runStore, args) {
12
13
  const { deps } = ctx;
13
14
  const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
@@ -31,7 +32,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
31
32
  return { status: placementReject.status, body: devicePlacementErrorBody(placementReject) };
32
33
  commitSessionInheritEnv(deps, args.prepared.spec.sessionId, args.prepared.body);
33
34
  if (deps.checkpointStore) {
34
- await deps.checkpointStore.putCtx(sessionId, { body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope });
35
+ await deps.checkpointStore.putCtx(sessionId, buildCheckpointCtx({ body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope }));
35
36
  }
36
37
  const anchorOwner = args.prepared.auth?.principal ?? null;
37
38
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
@@ -42,6 +42,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
42
42
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
43
43
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
44
44
  import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
45
+ import { allowListReadingRefusal, allowListReadingUnstated } from "./checkpoint-ctx.js";
45
46
  import { httpErrorCode } from "./send.js";
46
47
  function isCheckpointReopenedFailure(r) {
47
48
  const code = failureCodeOf(r.terminal);
@@ -398,7 +399,13 @@ export function createResumeLegs(ctx) {
398
399
  });
399
400
  return { spec, engineNotices };
400
401
  };
402
+ const refuseAllowListReading = (sessionId) => {
403
+ deps.logger?.warn?.("resume_refused_allow_list_reading", { sessionId, note: "this session's stored request carries settings.permissions.allow written by a server that read it as a whitelist; it is not replayed under the current reading — the task stays suspended until someone cancels it" });
404
+ return allowListReadingRefusal();
405
+ };
401
406
  const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
407
+ if (allowListReadingUnstated(ctx))
408
+ return { ok: false, answer: refuseAllowListReading(auth.sessionId) };
402
409
  try {
403
410
  const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth);
404
411
  return { ok: true, spec, engineNotices };
@@ -1332,6 +1339,8 @@ export function createResumeLegs(ctx) {
1332
1339
  return refuse(resumeCtxCorruptAnswer(ctxRead.where));
1333
1340
  if (ctxRead.kind === "absent")
1334
1341
  return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
1342
+ if (allowListReadingUnstated(ctxRead.ctx))
1343
+ return refuse(refuseAllowListReading(sessionId));
1335
1344
  return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1336
1345
  }
1337
1346
  async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
@@ -161,6 +161,13 @@ export interface CheckpointCtx {
161
161
  * 传原 ctx / 其展开 ⇒ 标随行。缺席 = 不是无人值守腿(HTTP 提交腿;7.106.0 前落盘的行 —— 那时这些腿不铸模式,checkpoint 里也没有)。
162
162
  */
163
163
  unattendedLeg?: true;
164
+ /**
165
+ * 🔴 S-1034(7.115.0)—— 体里 `settings.permissions.allow` 的**读法记号**:在场 = 这一行是 ≥7.115.0 写下的、体里带非空 `allow`,
166
+ * 它按「allow 规则(表态)」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
167
+ * 缺席而体里带非空 `allow` = ≤7.114.0 停驻的行(那时 `allow` 是名单)⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
168
+ * 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
169
+ */
170
+ permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
164
171
  }
165
172
  /** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
166
173
  export interface DriveResumeArgs {
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { type A2ATaskState } from "@sema-agent/core";
2
+ import type { A2ATaskState } from "@sema-agent/core";
3
3
  import type { A2aServeConfig } from "../../config-types.js";
4
4
  import { type RunRecord } from "../../plugins/store-contracts.js";
5
5
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
@@ -69,8 +69,6 @@ export declare function a2aStateForRun(run: Pick<RunRecord, "status" | "errorCod
69
69
  * `capabilities.a2aServe` 的子串,好让 `route-shape-roster` 层E 如实认出它们。
70
70
  */
71
71
  export declare function handleA2a(req: IncomingMessage, res: ServerResponse, match: RouteMatch<A2aRpcRouteId>, ctx: RouteCtx): Promise<void>;
72
- /** 九词闭集的对外可见锚(测试用它证明我们只发闭集词;也让「加一个自造词」在这里就被看见)。 */
73
- export declare const A2A_SERVE_TASK_STATES: readonly A2ATaskState[];
74
72
  export declare const A2A_SERVE_ROUTES: readonly [{
75
73
  readonly id: "a2a-agent-card";
76
74
  readonly path: "/.well-known/agent-card.json";
@@ -1,7 +1,6 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
4
- import { A2A_TASK_STATES } from "@sema-agent/core";
5
4
  import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
6
5
  import { HttpError } from "../../security.js";
7
6
  import { scopedIdempotencyKey } from "../idempotency.js";
@@ -396,7 +395,6 @@ export async function handleA2a(req, res, match, ctx) {
396
395
  function sendRpc(res, body) {
397
396
  sendJson(res, 200, body);
398
397
  }
399
- export const A2A_SERVE_TASK_STATES = A2A_TASK_STATES;
400
398
  export const A2A_SERVE_ROUTES = [
401
399
  { id: "a2a-agent-card", path: "/.well-known/agent-card.json", methods: ["GET"] },
402
400
  ];
@@ -42,8 +42,6 @@
42
42
  import type { IncomingMessage, ServerResponse } from "node:http";
43
43
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
44
44
  export declare const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
45
- /** 带参口(`:principal`)⇒ 路由声明表(`http/route-table.ts`)里走 `pattern` 行,不是 `path` 行。 */
46
- export declare const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE: RegExp;
47
45
  export declare function handleMemoryOptOut(req: IncomingMessage, res: ServerResponse, match: RouteMatch<MemoryOptOutRouteId>, ctx: RouteCtx): Promise<void>;
48
46
  export declare const MEMORY_OPTOUT_ROUTES: readonly [{
49
47
  readonly id: "memory-optout-grants-list";
@@ -4,7 +4,6 @@ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../pri
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
6
6
  export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
7
- export const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE = /^\/v1\/admin\/memory-optout\/([^/]+)$/;
8
7
  const PutBodySchema = z.object({ allow: z.boolean(), note: z.string().min(1).max(500).optional() }).strict();
9
8
  export async function handleMemoryOptOut(req, res, match, ctx) {
10
9
  const { deps } = ctx;
@@ -122,6 +122,7 @@ export async function handleCapabilities(req, res, match, ctx) {
122
122
  subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
123
123
  subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
124
124
  taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config) },
125
+ settings: { permissions: { allowGrant: true } },
125
126
  permissionModeAuto: {
126
127
  accepted: true,
127
128
  classifierSeat: deps.autoModeFace?.seatMounted === true,
@@ -32,6 +32,9 @@ export interface ServerWiringGates {
32
32
  streamAskWindowMs: PostureKnobReading<number>;
33
33
  sessionAutoTitle: PostureKnobReading<boolean>;
34
34
  checkpointStore: boolean;
35
+ /** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT`)原样 —— 空表 = 旋钮未设。规则名是工具名不是秘密,所以本面给全名单(条数 = 长度);
36
+ * 本位**只**在运维面(operator-only,门在路由上),不进 `/v1/capabilities` 的客户端能力位:它说的是部署替谁表了态,不是调用方能用什么。 */
37
+ approvalGrant: readonly string[];
35
38
  }
36
39
  /** `buildServerWiringGates` 的入参 = 三个谓词各自的**真输入**(不接受已算好的布尔:那样这里读到的就
37
40
  * 不再是装配现场的事实,而是别处算完可能已漂的复述)。 */
@@ -49,6 +52,8 @@ export interface ServerWiringGateInput {
49
52
  streamAskWindowMsSource: PostureSource;
50
53
  sessionAutoTitle: boolean;
51
54
  sessionAutoTitleSource: PostureSource;
55
+ /** 7.115.0:`config.approvalGrant` 本身(读面不重算)。 */
56
+ approvalGrant: readonly string[];
52
57
  }
53
58
  /** 纯数据产物(`build*`):三个门的当前读数。`streamApproval` 走**单一谓词**
54
59
  * `resolveStreamApprovalGate` —— 与 `/v1/capabilities` 的 `streamApproval` 位、与回决口的 501 门同一个
@@ -35,6 +35,7 @@ export function buildServerWiringGates(input) {
35
35
  postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
36
36
  }),
37
37
  checkpointStore: input.checkpointStore !== undefined,
38
+ approvalGrant: [...input.approvalGrant],
38
39
  };
39
40
  }
40
41
  export function buildStaticWiringAudit(facts) {
@@ -121,6 +122,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
121
122
  streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
122
123
  sessionAutoTitle: deps.config.sessionAutoTitle === true,
123
124
  sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
125
+ approvalGrant: deps.config.approvalGrant,
124
126
  }),
125
127
  });
126
128
  return;
@@ -5,7 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
8
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
8
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
9
9
  import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
10
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
11
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
27
27
  import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
28
28
  import { buildSyncSubmitBody } from "../sync-submit-body.js";
29
29
  import { assertNever } from "../route-ctx.js";
30
+ import { buildCheckpointCtx } from "../checkpoint-ctx.js";
30
31
  import { withModelReachScope } from "../../model-reach-scope.js";
31
32
  import { createInstructionSelectionLeg } from "../../instruction-selection.js";
32
33
  const DISCONNECT_PARK_GRACE_MS = 10_000;
@@ -252,7 +253,7 @@ export async function handleTasks(req, res, match, ctx) {
252
253
  let ledgerSink;
253
254
  let ledgerTerminal = false;
254
255
  if (deps.checkpointStore && prepared.spec.sessionId) {
255
- await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
256
+ await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
256
257
  }
257
258
  if (durableTaskId && deps.runStore) {
258
259
  const rs = deps.runStore;
@@ -804,7 +805,7 @@ export async function handleTasks(req, res, match, ctx) {
804
805
  }
805
806
  commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
806
807
  if (deps.checkpointStore && prepared.spec.sessionId) {
807
- await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
808
+ await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
808
809
  }
809
810
  const syncCancelCtrl = createRunAbort();
810
811
  if (prepared.spec.sessionId)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { loadConfig, type ServiceConfig } from "./config.js";
2
- export { composeExecutionLane, assertExecutionLaneInvariants } from "./boot/execution-lane.js";
2
+ export { composeExecutionLane, assertExecutionLaneInvariants, type ExecutionLaneComposeCtx } from "./boot/execution-lane.js";
3
3
  export { createBrain } from "./brain.js";
4
4
  export { createSessionStore } from "./plugins/session-store.js";
5
5
  export { createStoreBackend, type StoreBackend, type SessionCaptureRecordSource } from "./plugins/store-backend.js";
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
8
  import { withUnattendedLeg } from "../unattended-permission-mode.js";
9
+ import { buildCheckpointCtx } from "../http/checkpoint-ctx.js";
9
10
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
10
11
  import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
11
12
  import { createDurableAskPolicy } from "../approval.js";
@@ -267,7 +268,7 @@ export function createLeaderRunner(cfg) {
267
268
  const provisionWorker = async (sub) => {
268
269
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
269
270
  const durable = cfg.durable?.();
270
- const resumeCtx = { body: { objective: sub.spec.objective }, unattendedLeg: true };
271
+ const resumeCtx = buildCheckpointCtx({ body: { objective: sub.spec.objective }, unattendedLeg: true });
271
272
  let durableSpec = {};
272
273
  if (durable) {
273
274
  const d = durable;
@@ -4,7 +4,7 @@
4
4
  * 链(第一个非空者胜,空串视作缺席):`LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `<home>/.ai-agent`。
5
5
  *
6
6
  * 为什么是一只**纯函数、env 与 home 都由调用方递入**:本函数有两个读者 —— 服务自己的 `loadConfig`(递 `process.env` / `os.homedir()`)与随包发的运维脚本
7
- * `scripts/retired-mode-seed-drain-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
7
+ * `scripts/drain-check-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
8
8
  * 设了 `AGENT_DATA_DIR` 的部署不带参数跑脚本会读到另一个目录并报「干净」)。纯叶、零反向依赖 ⇒ 脚本引它不会把整个服务装载起来。
9
9
  *
10
10
  * ⚠️ 与**配置根**(`configLocalRoot`:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,不吃 `LOCAL_DATA_ROOT`)不是同一条链 —— 两根分家的那一形由
@@ -19,15 +19,8 @@
19
19
  * 单向钉挡不住第二种(手滑把 `"local"` 打成 `"locale"` 只会让一个合法声明被静默拒),所以两条都要。
20
20
  */
21
21
  import type { ToolContentOrigin } from "@sema-agent/core";
22
- /** 编译期断言:`T` 必须收敛到 `never`(有残余 ⇒ tsc 红)。 */
23
- type AssertNever<T extends never> = T;
24
22
  /** core `ToolContentOrigin` 的执法副本 —— **唯一**一份(见模块头注)。 */
25
23
  export declare const CONTENT_ORIGIN_WORDS: readonly ["local", "execution", "external"];
26
- type ContentOriginWord = (typeof CONTENT_ORIGIN_WORDS)[number];
27
- type _ContentOriginCovers = AssertNever<Exclude<ToolContentOrigin, ContentOriginWord>>;
28
- type _ContentOriginNoStray = AssertNever<Exclude<ContentOriginWord, ToolContentOrigin>>;
29
- /** (占位使用,防 noUnusedLocals;类型别名零运行时。) */
30
- export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOriginNoStray];
31
24
  /**
32
25
  * 词表内?—— 两个平面共用的**那一只**谓词。
33
26
  *
@@ -38,5 +31,4 @@ export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOrigin
38
31
  export declare function isContentOriginWord(v: unknown): v is ToolContentOrigin;
39
32
  /** 坏值的**可打印**摘要(进 400 文案 / warn 元数据)——中和 + 截长,绝不把任意长度的调用方输入原样回显。 */
40
33
  export declare function describeBadContentOrigin(v: unknown): string;
41
- export {};
42
34
  //# sourceMappingURL=mcp-content-origin.d.ts.map