@sema-agent/server 7.130.0 → 7.131.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 (103) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +36 -8
  5. package/dist/audit.js +4 -1
  6. package/dist/bench/s1/live-deps.js +2 -0
  7. package/dist/boot/resolve-spec.js +38 -15
  8. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  9. package/dist/boot/stage-10-http-server.js +6 -3
  10. package/dist/capabilities/builtin-tools.js +2 -1
  11. package/dist/capabilities/composite-members.d.ts +1 -0
  12. package/dist/capabilities/composite-members.js +1 -0
  13. package/dist/capabilities/hands-lane.js +2 -2
  14. package/dist/capabilities/host-tool-names.d.ts +17 -0
  15. package/dist/capabilities/host-tool-names.js +15 -0
  16. package/dist/capabilities/mcp-probe.js +3 -3
  17. package/dist/capabilities/repo-tools.js +4 -3
  18. package/dist/capabilities/scenarios.d.ts +8 -7
  19. package/dist/capabilities/scenarios.js +5 -4
  20. package/dist/capabilities/select-environment-tool.js +2 -1
  21. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  22. package/dist/capabilities/send-user-file-tool.js +2 -2
  23. package/dist/capabilities/tool-defer.js +2 -1
  24. package/dist/config-catalog.js +16 -5
  25. package/dist/config-invariants.js +2 -2
  26. package/dist/config-provider.js +2 -2
  27. package/dist/config-types.d.ts +6 -0
  28. package/dist/config.d.ts +2 -2
  29. package/dist/config.js +18 -17
  30. package/dist/elicitation.d.ts +7 -0
  31. package/dist/elicitation.js +20 -5
  32. package/dist/hooks/hook-llm.js +2 -2
  33. package/dist/hooks/hook-runner.d.ts +54 -8
  34. package/dist/hooks/hook-runner.js +86 -16
  35. package/dist/http/admission.js +2 -2
  36. package/dist/http/approval-sweeps.js +2 -2
  37. package/dist/http/checkpoint-ctx.d.ts +12 -11
  38. package/dist/http/checkpoint-ctx.js +4 -4
  39. package/dist/http/dispatch.js +1 -1
  40. package/dist/http/resume-legs.js +6 -5
  41. package/dist/http/routes/approvals-assistant.js +1 -1
  42. package/dist/http/routes/capabilities.js +6 -16
  43. package/dist/http/routes/runs.js +6 -10
  44. package/dist/http/routes/sessions.js +22 -10
  45. package/dist/http/routes/tasks.js +16 -11
  46. package/dist/http/scenario-read-face.d.ts +14 -0
  47. package/dist/http/scenario-read-face.js +5 -0
  48. package/dist/http/send.d.ts +10 -0
  49. package/dist/http/send.js +6 -0
  50. package/dist/http/server.d.ts +16 -12
  51. package/dist/http/server.js +2 -2
  52. package/dist/http/stored-body-replay.d.ts +10 -3
  53. package/dist/http/stored-body-replay.js +27 -6
  54. package/dist/http/wire-types.d.ts +18 -5
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +2 -1
  57. package/dist/mcp-assertion-header.d.ts +2 -2
  58. package/dist/mcp-assertion-header.js +3 -3
  59. package/dist/model-entry-refusal.d.ts +1 -7
  60. package/dist/model-route-endpoint.d.ts +51 -11
  61. package/dist/model-route-endpoint.js +45 -23
  62. package/dist/model-route-gap.d.ts +5 -4
  63. package/dist/model-route-gap.js +1 -1
  64. package/dist/model-select.d.ts +58 -0
  65. package/dist/model-select.js +31 -0
  66. package/dist/namespaced-rule-name.d.ts +7 -4
  67. package/dist/namespaced-rule-name.js +4 -3
  68. package/dist/observability/fail-open.d.ts +11 -6
  69. package/dist/observability/fail-open.js +5 -1
  70. package/dist/plugins/local-session-store.js +2 -2
  71. package/dist/plugins/one-time-migrations.d.ts +10 -1
  72. package/dist/plugins/one-time-migrations.js +16 -1
  73. package/dist/question.d.ts +18 -9
  74. package/dist/question.js +21 -13
  75. package/dist/request-key-closure.d.ts +2 -2
  76. package/dist/request-key-closure.js +16 -9
  77. package/dist/rule-entry-list.d.ts +124 -17
  78. package/dist/rule-entry-list.js +140 -24
  79. package/dist/run-local.d.ts +3 -3
  80. package/dist/run-local.js +8 -7
  81. package/dist/runs.d.ts +42 -2
  82. package/dist/runs.js +45 -2
  83. package/dist/spec-fields.d.ts +8 -4
  84. package/dist/stored-request-hooks-m20.d.ts +23 -0
  85. package/dist/stored-request-hooks-m20.js +124 -0
  86. package/dist/stored-request-tool-axes-m19.d.ts +1 -1
  87. package/dist/task-mcp.d.ts +41 -77
  88. package/dist/task-mcp.js +143 -75
  89. package/dist/task-settings.d.ts +41 -5
  90. package/dist/task-settings.js +39 -25
  91. package/dist/trace/core-keyset-guard.d.ts +2 -2
  92. package/dist/trace/ledger-sink.d.ts +29 -0
  93. package/dist/trace/ledger-sink.js +11 -0
  94. package/dist/trace/project.d.ts +6 -0
  95. package/dist/trace/project.js +11 -0
  96. package/dist/trace/wire-projection-faces.d.ts +1 -1
  97. package/dist/trace/wire-projection-faces.js +1 -1
  98. package/dist/user-authored-text.d.ts +7 -0
  99. package/dist/user-authored-text.js +3 -0
  100. package/dist/web-admin-client/apply-effective.js +10 -5
  101. package/dist/web-admin-client/types.d.ts +1 -1
  102. package/package.json +2 -2
  103. package/scripts/drain-check-local.mjs +3 -2
@@ -103,7 +103,6 @@ export async function runStage10(ctx) {
103
103
  sessionMirrorRuling: principalCaps
104
104
  ? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
105
105
  : undefined,
106
- scenarioGate: createScenarioGate({ config, scenarios, principalCaps, logger }, "peek"),
107
106
  instrumentDegenerate,
108
107
  sideQueryAccounting,
109
108
  parkedReviveSurface: parkedReviveSurface ? parkedReviveSurface : undefined,
@@ -212,7 +211,8 @@ export async function runStage10(ctx) {
212
211
  });
213
212
  const runnerFor = createHandsRunnerSelector({ full: runner, handsless: handslessRunner });
214
213
  const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => webAdmin.getKeyResolver() });
215
- const server = createHttpServer({ runner, runnerFor, sideQuery, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
214
+ const scenarioGate = createScenarioGate({ config, scenarios, principalCaps, logger }, "peek");
215
+ const server = createHttpServer({ runner, runnerFor, sideQuery, scenarioGate, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
216
216
  bindServerLateSeats({ config, logger, server });
217
217
  const fleetReconcile = startFleetReconciler({
218
218
  bus: fleetBus,
@@ -284,8 +284,11 @@ export async function runStage10(ctx) {
284
284
  const failoverFirstByte = buildFailoverFirstByteNotice(config);
285
285
  if (failoverFirstByte)
286
286
  logger.warn("failover_first_byte_lane", failoverFirstByte);
287
+ const bound = server.address();
288
+ if (bound === null || typeof bound === "string")
289
+ throw new Error(`http server listening without a TCP address (server.address() = ${JSON.stringify(bound)})`);
287
290
  logger.info("listening", {
288
- port: config.port,
291
+ port: bound.port,
289
292
  bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
290
293
  ...(bindHost === "127.0.0.1" && !config.bindHost
291
294
  ? { bindHostNote: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
@@ -1,7 +1,8 @@
1
1
  import { Type } from "typebox";
2
+ import { NOW_TOOL_NAME } from "./host-tool-names.js";
2
3
  export function nowTool() {
3
4
  return {
4
- name: "Now",
5
+ name: NOW_TOOL_NAME,
5
6
  description: "Return the current date-time as an ISO-8601 UTC string. Use when the task involves dates/deadlines/'today'.",
6
7
  effect: "read",
7
8
  parameters: Type.Object({}),
@@ -54,6 +54,7 @@ export declare const COMPOSITE_CAPABILITY_MEMBERS: {
54
54
  readonly workflowsGate: readonly ["engineCan", "denial"];
55
55
  readonly sealedKey: readonly ["alg", "publicKeyId", "publicKey"];
56
56
  readonly workspaceSync: readonly ["persist", "keep", "timeoutMs", "cache"];
57
+ readonly userAttachments: readonly ["entries", "kinds", "supplies", "limits"];
57
58
  };
58
59
  type CompositeTable = typeof COMPOSITE_CAPABILITY_MEMBERS;
59
60
  /** 值的**具名成员对象臂**(标量 / 字面词 / `false` / `null` / `undefined` / 数组 / 函数臂剥掉)。 */
@@ -15,6 +15,7 @@ export const COMPOSITE_CAPABILITY_MEMBERS = {
15
15
  workflowsGate: ["engineCan", "denial"],
16
16
  sealedKey: ["alg", "publicKeyId", "publicKey"],
17
17
  workspaceSync: ["persist", "keep", "timeoutMs", "cache"],
18
+ userAttachments: ["entries", "kinds", "supplies", "limits"],
18
19
  };
19
20
  export function gateCompositeMembers(body) {
20
21
  return body;
@@ -1,5 +1,5 @@
1
1
  import { handBandEffects, judgeRuleToolName, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
2
- import { NO_DECLARED_TOOL_NAMES } from "../namespaced-rule-name.js";
2
+ import { TOOL_NAME_ROSTER } from "../namespaced-rule-name.js";
3
3
  export function withoutHands(deps) {
4
4
  const { executionEnvFactory: _handsDropped, lspManager: _lspDropped, ...rest } = deps;
5
5
  return rest;
@@ -20,7 +20,7 @@ export const HANDS_BAND_TOOL_NAMES = Object.freeze([
20
20
  "EnterWorktree",
21
21
  "ExitWorktree",
22
22
  "ListAgents",
23
- ].filter((n, i, all) => judgeRuleToolName(n, NO_DECLARED_TOOL_NAMES).kind === "current" && all.indexOf(n) === i));
23
+ ].filter((n, i, all) => judgeRuleToolName(n, TOOL_NAME_ROSTER).kind === "current" && all.indexOf(n) === i));
24
24
  export const HANDS_DUAL_USE_TOOL_NAMES = Object.freeze(["TaskOutput", "TaskStop", "SendMessage", "AgentTranscript", "ListAgents"]);
25
25
  export const HANDS_EXCLUSIVE_TOOL_NAMES = Object.freeze(HANDS_BAND_TOOL_NAMES.filter((n) => !HANDS_DUAL_USE_TOOL_NAMES.includes(n)));
26
26
  export function handsDenyPolicy() {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * **本仓自己铸给模型的工具的名字** —— 唯一的名字源(叶模块,零依赖)。铸它们的地方(`builtin-tools.ts` / `send-user-file-tool.ts` / `select-environment-tool.ts` / `repo-tools.ts`)
3
+ * 与读它们的地方(工具名清单的受理判:`rule-entry-list.ts` 的名册;场景详情的工具名单;长尾延迟披露集)都从这里取,不手抄。
4
+ *
5
+ * 为什么受理口要知道它们:请求的 `allowTools` / `excludeTools` / 子代的 `allowTools` / `denyTools` 里把 `SendUserFile` 写成 `SENDUSERFILE`,引擎只在开跑时按这一 run 真挂着的名册告运维;
6
+ * 受理口读不到那份名册,读得到的是「引擎目录 ∪ 本仓自铸的这几只」—— 不把这几只放进名册,它们的孪生就是受理口漏掉的同一类静默失效。
7
+ * 宿主之外的调用方工具、MCP / A2A 对端的工具不在其中(前者本仓不挂,后者是协议名,由协议段判)。
8
+ */
9
+ export declare const NOW_TOOL_NAME = "Now";
10
+ export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
11
+ export declare const SELECT_ENVIRONMENT_TOOL_NAME = "SelectEnvironment";
12
+ export declare const REPO_TREE_TOOL_NAME = "repo_tree";
13
+ export declare const REPO_READ_FILE_TOOL_NAME = "repo_read_file";
14
+ export declare const REPO_PULL_DIFF_TOOL_NAME = "repo_pull_diff";
15
+ /** 本仓自铸的全部工具名(受理口工具名清单的名册成员;加一只自铸工具 ⇒ 这里加一行)。 */
16
+ export declare const HOST_TOOL_NAMES: ReadonlySet<string>;
17
+ //# sourceMappingURL=host-tool-names.d.ts.map
@@ -0,0 +1,15 @@
1
+ export const NOW_TOOL_NAME = "Now";
2
+ export const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
3
+ export const SELECT_ENVIRONMENT_TOOL_NAME = "SelectEnvironment";
4
+ export const REPO_TREE_TOOL_NAME = "repo_tree";
5
+ export const REPO_READ_FILE_TOOL_NAME = "repo_read_file";
6
+ export const REPO_PULL_DIFF_TOOL_NAME = "repo_pull_diff";
7
+ export const HOST_TOOL_NAMES = new Set([
8
+ NOW_TOOL_NAME,
9
+ SEND_USER_FILE_TOOL_NAME,
10
+ SELECT_ENVIRONMENT_TOOL_NAME,
11
+ REPO_TREE_TOOL_NAME,
12
+ REPO_READ_FILE_TOOL_NAME,
13
+ REPO_PULL_DIFF_TOOL_NAME,
14
+ ]);
15
+ //# sourceMappingURL=host-tool-names.js.map
@@ -53,7 +53,7 @@ function entryKeyIssue(entry, at) {
53
53
  scanKeyClosure(t, `${at}.transport.`, new Set(keys), NO_TOLERATED_KEYS, buckets);
54
54
  }
55
55
  if (buckets.unknown.length === 0) {
56
- const refusal = requestEntryIdentityRefusal(entry);
56
+ const refusal = requestEntryIdentityRefusal(entry, at);
57
57
  if (refusal === undefined)
58
58
  return null;
59
59
  return refusal.errorCode === "request.body_shape" ? shapeRefusal(refusal.message) : fieldRefusal(refusal.message);
@@ -74,7 +74,7 @@ function entryValueIssue(entry, at) {
74
74
  if (dropped !== undefined || read.ok.length !== 1) {
75
75
  const field = dropped?.field ?? "entry";
76
76
  return fieldRefusal(`${at}.${field} is not a usable value${urlEchoFor(entry, field)} — this endpoint refuses the whole list rather than dialing a silently shortened one` +
77
- ` (a task submission would instead drop just this declaration and mount its siblings; a probe answering "can I use these" must not quietly answer about fewer)`);
77
+ ` (a probe answering "can I use these" must not quietly answer about fewer)`);
78
78
  }
79
79
  if (read.badContentOrigin.length > 0) {
80
80
  return fieldRefusal(`${at}.contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(entry.contentOrigin)} (omit the key for the default: this server's tools classify as external content)`);
@@ -90,7 +90,7 @@ function declarationDowngradeIssue(entry, spec, at) {
90
90
  return null;
91
91
  diff.sort();
92
92
  return fieldRefusal(`${keyClosureClause(diff, "unusable value(s)")} — this deployment's declaration reader could not use these exactly as written, and a probe must not dial a configuration that differs from the one you declared` +
93
- " (a task submission would instead keep the server and silently drop just these, which is why the two faces disagree here on purpose; this endpoint refuses, because the reading would then describe a server you did not ask about)");
93
+ " (this endpoint refuses, because the reading would then describe a server you did not ask about)");
94
94
  }
95
95
  function declaredAsJudged(entry) {
96
96
  const declared = entry;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { z } from "zod";
3
+ import { REPO_PULL_DIFF_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_TREE_TOOL_NAME } from "./host-tool-names.js";
3
4
  export function parseRepo(input) {
4
5
  const s = input.trim().replace(/\.git$/, "");
5
6
  const m = /^[a-z]+:\/\/[^/]+\/(.+)$/i.exec(s);
@@ -160,7 +161,7 @@ export function repoToolsFor(client, coords) {
160
161
  const ref = (r) => (r ? Promise.resolve(r) : client.defaultBranch(coords));
161
162
  return [
162
163
  {
163
- name: "repo_tree",
164
+ name: REPO_TREE_TOOL_NAME,
164
165
  description: "List file paths in the repository (recursive). Use this first to understand structure, then read specific files.",
165
166
  effect: "read",
166
167
  parameters: Type.Object({ ref: Type.Optional(Type.String({ description: "branch/tag/sha; default = repo default branch" })) }),
@@ -171,7 +172,7 @@ export function repoToolsFor(client, coords) {
171
172
  },
172
173
  },
173
174
  {
174
- name: "repo_read_file",
175
+ name: REPO_READ_FILE_TOOL_NAME,
175
176
  description: "Read one file's text from the repository. Read only the files you need (just-in-time).",
176
177
  effect: "read",
177
178
  parameters: Type.Object({ path: Type.String(), ref: Type.Optional(Type.String()) }),
@@ -181,7 +182,7 @@ export function repoToolsFor(client, coords) {
181
182
  },
182
183
  },
183
184
  {
184
- name: "repo_pull_diff",
185
+ name: REPO_PULL_DIFF_TOOL_NAME,
185
186
  description: "Get the unified diff of a pull request by its number (for reviewing a PR's changes).",
186
187
  effect: "read",
187
188
  parameters: Type.Object({ index: Type.Integer({ description: "the PR number" }) }),
@@ -296,16 +296,17 @@ export declare function danglingScenarioRefusal(name: string, source: ScenarioNa
296
296
  */
297
297
  export declare function gateScenarioRequest(ruling: ScenarioRuling | undefined, bodyScenario: unknown, defaultScenario: string, known: readonly string[], logger?: Logger, principal?: string): string;
298
298
  /**
299
- * 一条请求点名的场景名过门 —— {@link gateScenarioRequest} 的**唯一一处实参装配**。
299
+ * 一条请求的场景名过门 —— {@link gateScenarioRequest} 的**唯一一处实参装配**。
300
300
  *
301
301
  * 门的六个实参各有来源:中心裁定(`principalCaps.scenarioRuling`,按 principal)、请求点名的名字、部署缺省场景、**在册词表**
302
- * (`Object.keys(scenarios)` —— 场景表活引用,不抄快照)、日志与 principal。「这个场景能看见什么」这类问题有不止一个读面
303
- * (任务提交腿 `resolveSpec`、`GET /v1/capabilities/mcp?scenario=` 的状态面),每个读面自己拼一遍这六个实参 = 第二只门:
304
- * 两面对「在册」「放行」各判各的,读面就开始对不上 run。所以装配只在这里,读面持有的是**绑好的门**。
302
+ * (`Object.keys(scenarios)` —— 场景表活引用,不抄快照)、日志与 principal。「此主体在此场景能看见什么」这类问题有不止一个读口
303
+ * (任务提交腿 `resolveSpec`、`run-local`、`GET /v1/capabilities/mcp` 的状态面、`GET /v1/sessions/:id/mcp` / `…/a2a` 两张会话面板),
304
+ * 每个读口自己拼一遍这六个实参 = 第二只门:两口对「在册」「放行」「缺席取谁」各判各的,读口就开始对不上 run。
305
+ * 所以装配只在这里,读口持有的是**绑好的门**;凡回答这类问题的口,场景名都由门给 —— 点名与缺席都问。
305
306
  *
306
307
  * 契约:`(principal, requested) => 在册的场景名`(返回值一定在册;被拒 ⇒ 抛 {@link gateScenarioRequest} 的那几只 typed 错,
307
- * 读面不改写)。`requested` 非串 ⇒ 门走指派链(ruling 胜者,否则部署缺省)—— 状态面只在**请求点名了**时才问门。
308
- * `principalCaps` 缺席(无中心 / 干跑 / 嵌入方)⇒ 无 ruling,门退回「请求名或部署缺省」那条现状链。
308
+ * 读口不改写)。`requested` 非串(缺席)⇒ 门走指派链(ruling 胜者,否则部署缺省),与 run 时同一读法。
309
+ * `principalCaps` 缺席(无中心 / 干跑 / 嵌入方 / `run-local`)⇒ 无 ruling,门退回「请求名或部署缺省」那条现状链。
309
310
  */
310
311
  export interface ScenarioGateSources {
311
312
  readonly config: {
@@ -323,7 +324,7 @@ export type ScenarioGate = (principal: string | undefined, requested: unknown) =
323
324
  /**
324
325
  * 门问中心裁定走哪条腿(闭集;新增一词 ⇒ 下面的穷尽 `switch` 编译红)—— 与 `resolveRuntimeCaps` / `peekRuntimeCaps` 是同一对:
325
326
  * · `enforce` —— 任务提交腿:中心裁定走执法腿,center 硬失败铸下的 fail-closed 降级项两腿都认;
326
- * · `peek` —— 读面(`GET /v1/capabilities/mcp?scenario=`):只读腿,同一只缓存 / 同一条抓取 / 同一份 fail-open 判决,但硬失败铸的降级项只对读面自己算数,
327
+ * · `peek` —— 读面(`GET /v1/capabilities/mcp` 状态面、`GET /v1/sessions/:id/mcp` / `…/a2a` 两张会话面板):只读腿,同一只缓存 / 同一条抓取 / 同一份 fail-open 判决,但硬失败铸的降级项只对读面自己算数,
327
328
  * 不替这个主体的真提交留下 `{allowWorkflows:false, allowFork:false, autoMode:false}` 的短窗 deny(读面不许成为别人被拒的原因)。
328
329
  */
329
330
  export type ScenarioGateLeg = "enforce" | "peek";
@@ -2,6 +2,7 @@ import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, WEB_FETCH
2
2
  import { HttpError, createCodedHttpError } from "../security.js";
3
3
  import { createSubagentExtraTools, extraToolsErrorFields, nowTool } from "./builtin-tools.js";
4
4
  import { parseRepo, repoToolsFor } from "./repo-tools.js";
5
+ import { NOW_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_TREE_TOOL_NAME } from "./host-tool-names.js";
5
6
  import { stablePrompt } from "./prompt.js";
6
7
  import { createWebSearchBackend, judgeRequestWebSearch } from "../plugins/web-search.js";
7
8
  import { skillsForScenario } from "./skills.js";
@@ -119,16 +120,16 @@ export function builtinScenarioDetails(scenarios, deps) {
119
120
  return fallback;
120
121
  }
121
122
  };
122
- const REPO_TOOLS = ["repo_tree", "repo_read_file", "repo_pull_diff", "Now"];
123
+ const REPO_TOOLS = [REPO_TREE_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, NOW_TOOL_NAME];
123
124
  const mk = (name, summary, toolset, tools, promptSummary) => ({
124
125
  name, source: "builtin", builtin: true, summary, toolset, tools, promptSummary, enabled: true,
125
126
  ...availabilityFields(deps, toolset),
126
127
  });
127
128
  const details = {
128
- code: mk("code", "编码主场景([891] 出厂缺省;CC 编码 persona 蒸馏版,default 全量工具面;终验旋钮独立)", "full-body", probe("code", {}, undefined, ["Now"]), deps.brandIdentity
129
+ code: mk("code", "编码主场景([891] 出厂缺省;CC 编码 persona 蒸馏版,default 全量工具面;终验旋钮独立)", "full-body", probe("code", {}, undefined, [NOW_TOOL_NAME]), deps.brandIdentity
129
130
  ? "Sema 产品身份 + core 编码完全体提示词(CODE_AGENT persona+自治自检;CC 血统对表)"
130
131
  : "core 编码完全体提示词(CODE_AGENT persona+自治自检;CC 血统对表)"),
131
- default: mk("default", "通用/助理场景(域中性 persona,CC agent loop 全量工具面;[891] 起非出厂缺省,显式可选)", "full-body", probe("default", {}, undefined, ["Now"]), deps.brandIdentity ? "Sema 产品身份 + core 默认宪法(结构层由引擎合成)" : "core 默认宪法(中性,无场景附加 persona)"),
132
+ default: mk("default", "通用/助理场景(域中性 persona,CC agent loop 全量工具面;[891] 起非出厂缺省,显式可选)", "full-body", probe("default", {}, undefined, [NOW_TOOL_NAME]), deps.brandIdentity ? "Sema 产品身份 + core 默认宪法(结构层由引擎合成)" : "core 默认宪法(中性,无场景附加 persona)"),
132
133
  "code-review": mk("code-review", "代码评审(clone-free 只读仓库工具)", "repo-readonly", probe("code-review", { repo: "probe/probe" }, undefined, REPO_TOOLS), "评审员提示词(产出契约)"),
133
134
  scan: mk("scan", "中性只读仓库扫描(输出契约由 objective+skills 决定)", "repo-readonly", probe("scan", { repo: "probe/probe" }, undefined, REPO_TOOLS), "无场景附加提示词(objective+center skills 拥有输出契约)"),
134
135
  };
@@ -149,7 +150,7 @@ export function centerScenarioDetails(specs, builtinNames, deps) {
149
150
  builtin: builtinNames.includes(spec.name),
150
151
  summary: "",
151
152
  toolset: spec.toolset,
152
- tools: spec.toolset === "repo-readonly" ? ["repo_tree", "repo_read_file", "repo_pull_diff", "Now"] : ["Now"],
153
+ tools: spec.toolset === "repo-readonly" ? [REPO_TREE_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, NOW_TOOL_NAME] : [NOW_TOOL_NAME],
153
154
  promptSummary: "",
154
155
  enabled: true,
155
156
  ...availabilityFields(deps, spec.toolset),
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { SELECT_ENVIRONMENT_TOOL_NAME } from "./host-tool-names.js";
2
3
  import { BoundedSessionMap } from "../bounded-session-map.js";
3
4
  import { resolveSandboxImageRef } from "../per-task-image.js";
4
5
  export class SessionEnvironmentSelection {
@@ -22,7 +23,7 @@ async function availableProfiles(catalog, viewer) {
22
23
  }
23
24
  export function selectEnvironmentTool(deps) {
24
25
  return {
25
- name: "SelectEnvironment",
26
+ name: SELECT_ENVIRONMENT_TOOL_NAME,
26
27
  description: "Switch this session's sandbox environment (image profile) or discover available ones. " +
27
28
  "Call with no arguments to list available profiles and their capabilities. " +
28
29
  "Call with `profile` (optionally `capabilitiesNeeded`) to select one. " +
@@ -43,7 +43,6 @@ export declare function sendUserFileTool(deps: {
43
43
  }) => Promise<IssuedFileLink>;
44
44
  emitter: SendUserFileEmitter;
45
45
  }): ToolSpec;
46
- export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
47
46
  /**
48
47
  * The deployment's group for the delegated-child injection (`createSubagentExtraTools` in `builtin-tools.ts` composes it with
49
48
  * `Now` into the delegation tool's `extraTools` factory AND its `extraToolNames` roster — one list, read twice): SendUserFile,
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { Type } from "typebox";
3
3
  import { builtinAgentDefinitions } from "@sema-agent/core";
4
+ import { SEND_USER_FILE_TOOL_NAME } from "./host-tool-names.js";
4
5
  export class SendUserFileEmitter {
5
6
  als = new AsyncLocalStorage();
6
7
  runWithContext(ctx, fn) {
@@ -19,7 +20,7 @@ export const SEND_USER_FILE_MAX_BYTES = 100 * 1024 * 1024;
19
20
  export const SEND_USER_FILE_MAX_CAPTION = 500;
20
21
  export function sendUserFileTool(deps) {
21
22
  return {
22
- name: "SendUserFile",
23
+ name: SEND_USER_FILE_TOOL_NAME,
23
24
  description: "Send file(s) to the user — surfaced as downloadable/inline cards in their pane. " +
24
25
  "Use when the file itself is the deliverable (generated charts, reports, screenshots, build artifacts) " +
25
26
  "and you want it surfaced, not just mentioned. Files must already exist on local disk (this tool sends " +
@@ -91,7 +92,6 @@ export function sendUserFileTool(deps) {
91
92
  },
92
93
  };
93
94
  }
94
- export const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
95
95
  const READONLY_BUILTIN_AGENT_TYPES = new Set(builtinAgentDefinitions().map((d) => d.name));
96
96
  export function subagentSendUserFileExtraTools(deps) {
97
97
  return { tools: [deps.spec], forSpawn: (ctx) => !(ctx.agentType !== undefined && READONLY_BUILTIN_AGENT_TYPES.has(ctx.agentType)) };
@@ -1,4 +1,5 @@
1
- export const LONGTAIL_DEFER_TOOLS = new Set(["SelectEnvironment"]);
1
+ import { SELECT_ENVIRONMENT_TOOL_NAME } from "./host-tool-names.js";
2
+ export const LONGTAIL_DEFER_TOOLS = new Set([SELECT_ENVIRONMENT_TOOL_NAME]);
2
3
  export function applyLongtailDefer(tools, enabled) {
3
4
  if (!enabled)
4
5
  return tools;
@@ -16,7 +16,7 @@ import { MODEL_PROVIDER_WORDS } from "./model-provider.js";
16
16
  import { buildDisplaySafeUrl } from "./trace/redact.js";
17
17
  import { describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
18
18
  import { buildOutboundProxyPosture } from "./outbound-dispatcher.js";
19
- import { modelSupportsImages } from "./model-select.js";
19
+ import { modelImageInput, modelReasoningDecl } from "./model-select.js";
20
20
  export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
21
21
  export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
22
22
  export const CATALOG_DANGER_AXES = ["security", "billing", "data"];
@@ -303,14 +303,18 @@ export const CONFIG_CATALOG = [
303
303
  r("MODEL_FIRST_BYTE_TIMEOUT_MS", "modelPlane", "number", `请求发出 → 响应头的等待(ms;缺席 = core 缺省 + 上传宽限;到点 core 自有一次加宽重试,配了 MODEL_GATEWAY_FALLBACK_URLS 也一样(之后才切备;server 不铸 firstByteRetries);0 = 该段不设引擎钟、传输层的响应头 / 响应体钟也关(TCP + TLS 建连仍归传输层自己的钟);自托管慢网关按排队 + prefill 留足;failover 拓扑想快切须显式设小;须为 [${FIRST_BYTE_TIMEOUT_BOUNDS.minMs}, ${FIRST_BYTE_TIMEOUT_BOUNDS.maxMs}] 内十进制整数,其余拒启)`, { derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随 core 缺省(本仓不复制上游数字;core 改缺省本旋钮照映不改)", resolve: (c) => c.resilience.firstByteTimeoutMs ?? null }),
304
304
  r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0 = 该段不设引擎钟、传输层的响应体钟也关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
305
305
  r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
306
- r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(S-623:**无出厂缺省** —— 配置能派发到的每条 openai 兼容路由都必须解析到显式端点(条目自己的 baseUrl,或本键),否则拒启 / 热路径整批拒,机读码 `config.model_route_endpoint_missing`,拒因点名条目并给出本机写法 `MODEL_GATEWAY_BASEURL=<本机网关>/v1`;anthropic 单路由部署不设本键合法;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { derivedDefaultNote: "缺席 ⇒ 本部署没有网关(不铸键);有 openai 路由落不到端点 ⇒ 拒启", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
306
+ r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(S-623:**无出厂缺省** —— 将要服务的那张表(目录被采纳判目录表,否则判 env 表)里每条 openai 兼容路由都必须解析到显式端点(条目自己的 baseUrl,或本键),否则拒启 / 热路径整批拒,机读码 `config.model_route_endpoint_missing`,拒因点名条目并给出本机写法 `MODEL_GATEWAY_BASEURL=<本机网关>/v1`;anthropic 单路由部署不设本键合法;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { derivedDefaultNote: "缺席 ⇒ 本部署没有网关(不铸键);有 openai 路由落不到端点 ⇒ 拒启", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
307
307
  r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-623:主网关 MODEL_GATEWAY_BASEURL 未设而本键设了 ⇒ 拒启,机读码 `config.gateway_fallback_without_primary` —— 备用是主网关的镜像;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
308
- r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
308
+ r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省;env 表的主槽。目录被采纳时整张换掉 env 表、本键不服务,目录第一次采纳那一拍说一行 `config_env_set_but_ineffective`;目录缺席时它是兜底,两者皆无 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
309
309
  r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0 = 该段不设引擎钟、传输层的响应体钟也关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
310
310
  r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
311
311
  r("MODEL_PRESENCE_PENALTY", "modelPlane", "number", "presence_penalty 直通"),
312
312
  r("MODEL_PROVIDER", "modelPlane", "enum", "主模型 provider(路由选 brain;S-461 起是闭集,词表外的值启动期拒启并点名机读码 `config.model_provider_invalid`)", { enumValues: MODEL_PROVIDER_WORDS, derivedDefaultNote: "缺席:ANTHROPIC base+cred 同在 ⇒ anthropic,否则 gateway", resolve: (c) => c.model.provider, center: cModels("models[].provider") }),
313
- r("MODEL_REASONING", "modelPlane", "boolean", "主模型 reasoning 声明", { staticDefault: "true", resolve: (c) => c.model.reasoning ?? null, center: cModels("models[].reasoning") }),
313
+ r("MODEL_REASONING", "modelPlane", "boolean", "主模型 reasoning 声明(三态:true / false / 缺席 = 未声明)", {
314
+ derivedDefaultNote: "缺席 ⇒ 不铸 `Model.reasoning`(没有 true 缺省);引擎按真值读,缺席与 false 同读 —— 思考档不随请求发出;要开思考写 MODEL_REASONING=true。`GET /v1/models` 该行不带 `reasoning` 键",
315
+ resolve: (c) => modelReasoningDecl(c.model) ?? null,
316
+ center: cModels("models[].reasoning"),
317
+ }),
314
318
  r("MODEL_REASONING_EFFORT_LEVELS", "modelPlane", "csv", "openai 端点接受的思考档集(六档子集;高档 clamp-down 不 422;anthropic 腿不铸此键;未知词拒启)", {
315
319
  derivedDefaultNote: "缺席 ⇒ 不铸 compat,core 走保守梯(minimal|low|medium|high),xhigh/max 被 clamp 下来",
316
320
  resolve: (c) => {
@@ -332,7 +336,14 @@ export const CONFIG_CATALOG = [
332
336
  },
333
337
  }),
334
338
  r("MODEL_VERIFIER", "modelPlane", "string", "verify 门的异构 verifier(须命中目录;typo 回 default 并 boot warn=自评自批)", { derivedDefaultNote: "缺席/不在目录 ⇒ default(=implementer,去相关性失效警告)" }),
335
- r("MODEL_VISION", "modelPlane", "boolean", "主模型 vision 声明(false = 明说不收图:带图请求的图被换成占位句交给模型,提交时计数并告警)", { staticDefault: "true", resolve: (c) => modelSupportsImages(c.model), center: cModels("models[].vision") }),
339
+ r("MODEL_VISION", "modelPlane", "boolean", "主模型 vision 声明(三态:true / false / 缺席 = 未声明;false = 明说不收图:带图请求的图被换成占位句交给模型,提交时计数并告警)", {
340
+ derivedDefaultNote: "缺席 ⇒ 不铸 `Model.input`(没有 true 缺省);引擎与带图预检都按「收」读(图原样交给模型,不出信号);`GET /v1/models` 该行不带 `vision` 键。要让预检对纯文本模型出信号写 MODEL_VISION=false",
341
+ resolve: (c) => {
342
+ const v = modelImageInput(c.model);
343
+ return v === "undeclared" ? null : v === "yes";
344
+ },
345
+ center: cModels("models[].vision"),
346
+ }),
336
347
  r("MYSQL_DATABASE", "store", "string", "MySQL 协议库名(选中该引擎时必填)", { danger: DATA }),
337
348
  r("MYSQL_HOST", "store", "string", "MySQL 协议主机(在场即算 SQL 信号,参与 DB_BACKEND 默认推导)", { danger: DATA }),
338
349
  r("MYSQL_PASSWORD", "store", "secret", "MySQL 协议口令", { danger: SEC }),
@@ -232,8 +232,8 @@ export const CONFIG_CROSS_INVARIANTS = [
232
232
  id: "model-route-endpoint",
233
233
  keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic", "models"],
234
234
  restartReason: "model-route-endpoint",
235
- bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:model-route-endpoint" },
236
- check: ({ live, next }) => (next.models === live.models ? undefined : modelRouteEndpointRefusal(next)),
235
+ bootAnchor: { file: "src/boot/stage-02-web-admin-pull.ts", marker: "CROSS-INVARIANT:model-route-endpoint" },
236
+ check: ({ live, next }) => (next.models === live.models ? undefined : modelRouteEndpointRefusal(next, "catalog")),
237
237
  },
238
238
  {
239
239
  id: "default-model-resolvable",
@@ -149,8 +149,8 @@ export function mapToServiceEffective(eff, version) {
149
149
  ...(m.sealedApiKey !== undefined ? { sealedApiKey: m.sealedApiKey } : {}),
150
150
  ...(m.quotaWeight !== undefined ? { quotaWeight: m.quotaWeight } : {}),
151
151
  tier: m.tier,
152
- reasoning: m.reasoning,
153
- vision: m.vision,
152
+ ...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
153
+ ...(m.vision !== undefined ? { vision: m.vision } : {}),
154
154
  ...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}),
155
155
  ...(m.maxTokens !== undefined ? { maxTokens: m.maxTokens } : {}),
156
156
  ...(typeof m.autoCompactTokens === "number" ? { autoCompactTokens: m.autoCompactTokens } : {}),
@@ -548,6 +548,12 @@ export interface ServiceConfigFlat {
548
548
  *
549
549
  * 🔴 **必填、无可选形**:一个 `?` 会立刻招来一条「缺席就回落 `config.model`」的臂,而那条臂正是本键
550
550
  * 要消灭的东西([ref]:同一语义面禁两个写者)。测试夹具与 `loadConfig` 一样,与 `model` 同源铸。
551
+ *
552
+ * 🔴 **同引用是契约**:env 表服务时 `model` 与 `envModel` **恒是同一个对象**(`loadConfig` 把同一只模型放进两个键)。
553
+ * `servedModelTableLeg`(`model-select.ts`)拿这条**引用相等**当「这个进程服务的是哪张表」的唯一状态位 —— `model` 换成目录缺省 ⇒ 不再同引用 ⇒ 服务的是目录表。
554
+ * 所以写口恰好两处:`config.ts` 的铸造点(两键同一个对象)与 `web-admin-client/apply-effective.ts` 的 `commitStaged`(只换 `model`,`envModel` 永不被写);
555
+ * 克隆模型对象、或嵌入方手搓 `ServiceConfig` 时把两键指向两只对象,这一位会被误读成「目录在服务」(拒因句选错视图、`MODEL_ID` 通告误发)。
556
+ * 源码钉(`test/served-model-route.test.ts` 的类钉)把这两处写口列成白名单。
551
557
  */
552
558
  envModel: Model;
553
559
  /** Model catalog by name (1.24): always `default` + the main model's id, plus an optional cheap model.
package/dist/config.d.ts CHANGED
@@ -229,7 +229,7 @@ export declare function parseEnvNameAllowlist(knob: RunnerEnvNameAllowlistKnob):
229
229
  * Shared by the env lane (main + cheap models) and the center lane (web-admin toModel). */
230
230
  export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
231
231
  /**
232
- * **严格名册布防**的唯一判据(boot 就绪门 `boot/web-admin.ts` 与 S-623 端点判据同调这一只)。
232
+ * **严格名册布防**的唯一判据(boot 就绪门 `boot/web-admin.ts` 与装配第 2 段的路由端点判官同调这一只)。
233
233
  *
234
234
  * 布防 = 远程 registry ∧ 非 dryRun ∧ `CONFIG_REQUIRE_ROSTER=true`(严格 `"true"`,直读 env、不进 ServiceConfig —— 纯
235
235
  * boot 期姿势旋钮,原判据逐字搬来,零语义变化)。布防时运维声明 env MODEL_ID 是**占位**:名册落地前计费提交 503、
@@ -332,7 +332,7 @@ export declare function unmatchableToolNamesRefusal(source: string, names: reado
332
332
  * 7.117.0(core 7.42.0)起这张表整份投 `RunnerDeps.allowedTools`(引擎 allow 层的部署成员,构造时读一次),判据是**引擎的读法**,与请求的两张
333
333
  * allow 表同一份(`rule-entry-list.ts`):
334
334
  * ① 值不是字符串表 ⇒ 拒(env 的 CSV 恒给字符串表;判在这里是为了「值一旦换了来源,第一道判仍在」);
335
- * ② 引擎读成空的条目(`compileAllowList(names, "allow", 名册 = 空集).inert` 非空 —— `*`、`mcp__*`、写路径 / 域名形、带空白或畸形的名字,以及
335
+ * ② 引擎读成空的条目(`compileAllowList(names, "allow", 名册).inert` 非空 —— `*`、`mcp__*`、写路径 / 域名形、带空白或畸形的名字,以及
336
336
  * core 7.43.0 起条目名过引擎判官读成 `tool_name` 的退役名 / 无前缀短名 / 对端段铸不出,core 7.48.0 #1400 起含工具段为空的 `mcp__<srv>__`;
337
337
  * 7.116.0–7.122.x 那一形由本仓先拒,随之退役)⇒ 拒,句子点名条目、引擎给的原因词(名字层带判词与现名)、
338
338
  * 该怎么写。请求的 allow 侧对 ② 只收通告;运维这一侧拒启:这是运维自己的配置,写错了当场改,而引擎对读成空的部署条目只在每一跑发一条通告。
package/dist/config.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { DEFAULT_AT_MENTION, DEFAULT_MODEL_REF } from "./model-select.js";
2
+ import { DEFAULT_AT_MENTION, DEFAULT_MODEL_REF, mintCapabilityKeys } from "./model-select.js";
3
3
  import { homedir } from "node:os";
4
4
  import { join, posix as posixPath } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE, RECOMMENDED_SENSITIVE_PATTERNS, judgeRuleToolName, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable } from "@sema-agent/core";
6
+ import { compileAllowList, BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE, RECOMMENDED_SENSITIVE_PATTERNS, judgeRuleToolName, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable } from "@sema-agent/core";
7
7
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES, WEB_ADMIN_ENV, retiredEnvRefusal } from "@sema-agent/settings-schema";
8
8
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
9
9
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
@@ -22,7 +22,7 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
22
22
  import {} from "./posture-source.js";
23
23
  import { COST_QUOTA_WINDOW_SEC_RANGE, costQuotaWindowSecInRange } from "./observability/cost-quota.js";
24
24
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
25
- import { describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
25
+ import { TOOL_NAME_ROSTER, describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
26
26
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
27
27
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
28
28
  import { lkgPathOverrideOf } from "./config-lkg.js";
@@ -33,14 +33,14 @@ import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE
33
33
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
34
34
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
35
35
  import { AUTONOMY_MODES } from "./runtime-governance.js";
36
- import { allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
36
+ import { MAX_NAMED_INERT_ENTRIES, allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, inertRuleEntryGuidance, ruleEntryListShapeOk } from "./rule-entry-list.js";
37
37
  import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
38
38
  import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
39
39
  import { modelProviderRefusal } from "./model-provider.js";
40
40
  import { modelRouteUrlRefusal } from "./model-route-url.js";
41
41
  import { httpUrlFlaws } from "@sema-agent/settings-schema";
42
42
  import { buildHttpUrlRefusal } from "./url-refusal.js";
43
- import { csvOf, envLaneTable, judgeModelRouteGap, mintedCheapId, readModelRouteEnv } from "./model-route-gap.js";
43
+ import { csvOf, envLaneTable, mintedCheapId, readModelRouteEnv } from "./model-route-gap.js";
44
44
  import { displayListEntry } from "./list-entry-display.js";
45
45
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
46
46
  import { allowlistedEnv } from "./server-secret-env.js";
@@ -925,14 +925,14 @@ function parseModelDomain() {
925
925
  thinkingFormat: parseThinkingFormat(process.env.MODEL_THINKING_FORMAT),
926
926
  });
927
927
  const envCompat = provider === "anthropic" ? undefined : envCompatDeclared;
928
+ const modelCapability = { reasoning: parseBoolWord("MODEL_REASONING", process.env.MODEL_REASONING), vision: parseBoolWord("MODEL_VISION", process.env.MODEL_VISION) };
928
929
  const model = {
929
930
  id: modelId,
930
931
  name: modelId,
931
932
  api: provider === "anthropic" ? "anthropic-messages" : "openai-completions",
932
933
  provider,
933
934
  baseUrl: "",
934
- reasoning: boolEnv("MODEL_REASONING", true),
935
- input: boolEnv("MODEL_VISION", true) ? ["text", "image"] : ["text"],
935
+ ...mintCapabilityKeys(modelCapability),
936
936
  ...buildModelCostKey(buildCostOrAbsent({
937
937
  input: costEnv("MODEL_COST_INPUT", "unset"),
938
938
  output: costEnv("MODEL_COST_OUTPUT", "unset"),
@@ -983,7 +983,7 @@ function parseModelDomain() {
983
983
  ...model,
984
984
  id: cheapMintedId,
985
985
  name: cheapMintedId,
986
- input: ((v) => (v === undefined ? model.input : v ? ["text", "image"] : ["text"]))(parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION)),
986
+ ...mintCapabilityKeys({ reasoning: parseBoolWord("MODEL_CHEAP_REASONING", process.env.MODEL_CHEAP_REASONING), vision: parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION) }),
987
987
  ...buildModelCostKey(buildCostOrAbsent({
988
988
  input: costEnv("MODEL_CHEAP_COST_INPUT", "declares-zero"),
989
989
  output: costEnv("MODEL_CHEAP_COST_OUTPUT", "declares-zero"),
@@ -993,7 +993,6 @@ function parseModelDomain() {
993
993
  contextWindow: numEnvOr("MODEL_CHEAP_CONTEXT_WINDOW", model.contextWindow),
994
994
  maxTokens: numEnvOr("MODEL_CHEAP_MAX_TOKENS", model.maxTokens),
995
995
  ...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
996
- reasoning: parseBoolWord("MODEL_CHEAP_REASONING", process.env.MODEL_CHEAP_REASONING) ?? model.reasoning,
997
996
  }
998
997
  : undefined;
999
998
  const mainAct = optFinitePositiveEnv("MODEL_AUTO_COMPACT_TOKENS");
@@ -1183,8 +1182,15 @@ function findUnmatchableToolNames(names, opts) {
1183
1182
  out.push({ name, guidance: uncompilable });
1184
1183
  continue;
1185
1184
  }
1186
- if (verdict !== undefined)
1185
+ if (verdict !== undefined) {
1187
1186
  out.push({ name, guidance: describeUnmatchableRuleName(verdict, subject) });
1187
+ continue;
1188
+ }
1189
+ const nearMiss = compileAllowList([subject], "deny", TOOL_NAME_ROSTER).inert.find((i) => i.reason === "near_miss");
1190
+ if (nearMiss?.currentName !== undefined) {
1191
+ const rest = name.startsWith(subject) ? name.slice(subject.length) : "";
1192
+ out.push({ name, guidance: inertRuleEntryGuidance({ ...nearMiss, currentName: `${nearMiss.currentName}${rest}` }) });
1193
+ }
1188
1194
  }
1189
1195
  return out;
1190
1196
  }
@@ -1192,9 +1198,10 @@ export function unmatchableToolNamesRefusal(source, names, judgment) {
1192
1198
  const bad = findUnmatchableToolNames(names, judgment);
1193
1199
  if (bad.length === 0)
1194
1200
  return undefined;
1201
+ const named = bad.slice(0, MAX_NAMED_INERT_ENTRIES).map((b) => `${displayListEntry(b.name)} → ${b.guidance}`).join("; ");
1195
1202
  const message = (`${source} has ${bad.length === 1 ? "an entry" : "entries"} that can never take effect — ${bad.length === 1 ? "it" : "each"} would never match a live tool or cannot be compiled into a rule ` +
1196
1203
  `(names are read exactly as written: no alias folding, no protocol prefix added): ` +
1197
- bad.map((b) => `${displayListEntry(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1204
+ (bad.length > MAX_NAMED_INERT_ENTRIES ? `${named}; and ${bad.length - MAX_NAMED_INERT_ENTRIES} more` : named)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1198
1205
  return { message, names: bad.map((b) => b.name) };
1199
1206
  }
1200
1207
  const APPROVAL_GRANT_ENV = "APPROVAL_GRANT";
@@ -2521,12 +2528,6 @@ export function loadConfig() {
2521
2528
  if (laneRequiredRefusal !== undefined) {
2522
2529
  throw new Error(laneRequiredRefusal);
2523
2530
  }
2524
- {
2525
- const routeGap = judgeModelRouteGap(process.env, { envLaneDispatchable: !isRosterGateArmed(integrations.webAdmin) });
2526
- if (routeGap.gap !== "none") {
2527
- throw new Error(routeGap.reason);
2528
- }
2529
- }
2530
2531
  EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2531
2532
  return attachConfigGroups({
2532
2533
  ...store,
@@ -19,6 +19,13 @@ export interface ElicitationFrame {
19
19
  * no core-stream anchor, so no `eventId` (absence is honest, not an omission); this is the frame's own time
20
20
  * coordinate instead, same posture as the stream-approval frames' `serverNowMs`. */
21
21
  serverNowMs?: number;
22
+ /** "elicitation" only (additive; the same window pair `question` carries): the coordinator's OWN answer window —
23
+ * at expiry the ask releases as `decline`. Minted from the SAME `ttlMs` the `setTimeout` below arms (one source), so
24
+ * a form replayed from the ledger can be judged still-waiting (no `elicitation_complete` with this id ∧
25
+ * `expiresAtMs` not reached) or lapsed even when the close row never landed (a lost write, a crashed replica). */
26
+ expiresInMs?: number;
27
+ /** "elicitation" only: `serverNowMs + expiresInMs` — the absolute deadline on the server's clock. */
28
+ expiresAtMs?: number;
22
29
  }
23
30
  /** The per-run context `onElicit` recovers via ALS. `emit` delivers a frame to the run's live stream;
24
31
  * `abortSignal` (the run's own cancel signal) lets a run-abort promptly cancel an elicitation parked awaiting a
@@ -112,18 +112,33 @@ export class ElicitationCoordinator {
112
112
  signal?.addEventListener("abort", onAbort, { once: true });
113
113
  ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
114
114
  this.pending.set(id, { settle, owner: ctx.owner, taskId: ctx.taskId });
115
- try {
116
- await ctx.emit({ type: "elicitation", elicitationId: id, mcpServerName: req.server, message, ...(requestedSchema !== undefined ? { requestedSchema } : {}), mode: "form", serverNowMs: this.now() });
115
+ const emitted = (async () => {
116
+ const now = this.now();
117
+ return ctx.emit({ type: "elicitation", elicitationId: id, mcpServerName: req.server, message, ...(requestedSchema !== undefined ? { requestedSchema } : {}), mode: "form", serverNowMs: now, expiresInMs: this.throttle.ttlMs, expiresAtMs: now + this.throttle.ttlMs });
118
+ })().then(() => "ok", () => "failed");
119
+ const delivery = await Promise.race([emitted, answerP.then(() => "stalled")]);
120
+ if (delivery === "ok") {
117
121
  rc.total += 1;
118
122
  rc.lastByServer.set(req.server, t);
119
123
  }
120
- catch {
124
+ else if (delivery === "failed") {
121
125
  recordFailOpen("server.elicitation.open-frame-undelivered", `elicitation=${id} server=${req.server}`);
122
126
  settle({ action: "decline" });
123
127
  }
124
128
  const answer = await answerP;
125
- void Promise.resolve()
126
- .then(() => ctx.emit({ type: "elicitation_complete", elicitationId: id, mcpServerName: req.server, action: answer.action, serverNowMs: this.now() }))
129
+ void emitted
130
+ .then((result) => {
131
+ if (result === "failed") {
132
+ if (delivery === "stalled")
133
+ recordFailOpen("server.elicitation.open-frame-undelivered", `elicitation=${id} server=${req.server}`);
134
+ return undefined;
135
+ }
136
+ if (delivery === "stalled") {
137
+ rc.total += 1;
138
+ rc.lastByServer.set(req.server, t);
139
+ }
140
+ return ctx.emit({ type: "elicitation_complete", elicitationId: id, mcpServerName: req.server, action: answer.action, serverNowMs: this.now() });
141
+ })
127
142
  .catch(() => recordFailOpen("server.elicitation.complete-breadcrumb-dropped", `elicitation=${id}`));
128
143
  return answer;
129
144
  };