@sema-agent/server 7.129.1 → 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 (117) hide show
  1. package/MIGRATION.md +102 -0
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +48 -9
  5. package/dist/audit.js +4 -1
  6. package/dist/bench/s1/live-deps.js +2 -0
  7. package/dist/boot/resolve-spec.js +43 -16
  8. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  9. package/dist/boot/stage-10-http-server.d.ts +1 -1
  10. package/dist/boot/stage-10-http-server.js +7 -2
  11. package/dist/capabilities/builtin-tools.js +2 -1
  12. package/dist/capabilities/composite-members.d.ts +1 -0
  13. package/dist/capabilities/composite-members.js +1 -0
  14. package/dist/capabilities/hands-lane.js +2 -2
  15. package/dist/capabilities/host-tool-names.d.ts +17 -0
  16. package/dist/capabilities/host-tool-names.js +15 -0
  17. package/dist/capabilities/mcp-probe.js +19 -6
  18. package/dist/capabilities/repo-tools.js +4 -3
  19. package/dist/capabilities/scenarios.d.ts +34 -0
  20. package/dist/capabilities/scenarios.js +20 -4
  21. package/dist/capabilities/select-environment-tool.js +2 -1
  22. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  23. package/dist/capabilities/send-user-file-tool.js +2 -2
  24. package/dist/capabilities/skills.js +1 -1
  25. package/dist/capabilities/tool-defer.js +2 -1
  26. package/dist/config-catalog.js +16 -5
  27. package/dist/config-invariants.js +2 -2
  28. package/dist/config-provider.js +2 -2
  29. package/dist/config-types.d.ts +6 -0
  30. package/dist/config.d.ts +2 -2
  31. package/dist/config.js +18 -17
  32. package/dist/elicitation.d.ts +7 -0
  33. package/dist/elicitation.js +20 -5
  34. package/dist/exec-bounds.d.ts +6 -6
  35. package/dist/hooks/hook-llm.d.ts +11 -1
  36. package/dist/hooks/hook-llm.js +11 -14
  37. package/dist/hooks/hook-runner.d.ts +55 -9
  38. package/dist/hooks/hook-runner.js +86 -16
  39. package/dist/http/admission.js +2 -2
  40. package/dist/http/approval-sweeps.js +2 -2
  41. package/dist/http/checkpoint-ctx.d.ts +12 -11
  42. package/dist/http/checkpoint-ctx.js +4 -4
  43. package/dist/http/dispatch.js +1 -1
  44. package/dist/http/resume-legs.js +6 -5
  45. package/dist/http/routes/approvals-assistant.js +1 -1
  46. package/dist/http/routes/capabilities.js +13 -5
  47. package/dist/http/routes/runs.js +18 -14
  48. package/dist/http/routes/sessions.js +22 -10
  49. package/dist/http/routes/tasks.js +16 -11
  50. package/dist/http/scenario-read-face.d.ts +14 -0
  51. package/dist/http/scenario-read-face.js +5 -0
  52. package/dist/http/send.d.ts +10 -0
  53. package/dist/http/send.js +6 -0
  54. package/dist/http/server.d.ts +16 -4
  55. package/dist/http/server.js +2 -2
  56. package/dist/http/stored-body-replay.d.ts +10 -3
  57. package/dist/http/stored-body-replay.js +27 -6
  58. package/dist/http/wire-types.d.ts +18 -5
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/mcp-assertion-header.d.ts +2 -2
  62. package/dist/mcp-assertion-header.js +3 -3
  63. package/dist/model-entry-refusal.d.ts +1 -7
  64. package/dist/model-route-endpoint.d.ts +51 -11
  65. package/dist/model-route-endpoint.js +45 -23
  66. package/dist/model-route-gap.d.ts +5 -4
  67. package/dist/model-route-gap.js +1 -1
  68. package/dist/model-select.d.ts +58 -0
  69. package/dist/model-select.js +31 -0
  70. package/dist/namespaced-rule-name.d.ts +7 -4
  71. package/dist/namespaced-rule-name.js +4 -3
  72. package/dist/observability/fail-open.d.ts +16 -7
  73. package/dist/observability/fail-open.js +10 -2
  74. package/dist/plugins/local-session-store.js +2 -2
  75. package/dist/plugins/one-time-migrations.d.ts +21 -1
  76. package/dist/plugins/one-time-migrations.js +33 -1
  77. package/dist/plugins/remote-env-adb.js +5 -4
  78. package/dist/plugins/remote-env-local-docker.js +5 -4
  79. package/dist/plugins/remote-env-ssh.js +5 -4
  80. package/dist/question.d.ts +18 -9
  81. package/dist/question.js +21 -13
  82. package/dist/request-key-closure.d.ts +2 -2
  83. package/dist/request-key-closure.js +16 -9
  84. package/dist/rule-entry-list.d.ts +124 -17
  85. package/dist/rule-entry-list.js +140 -24
  86. package/dist/run-local.d.ts +3 -3
  87. package/dist/run-local.js +8 -7
  88. package/dist/runs.d.ts +42 -2
  89. package/dist/runs.js +45 -2
  90. package/dist/runtime-caps-resolver.d.ts +7 -0
  91. package/dist/runtime-caps-resolver.js +1 -0
  92. package/dist/session-titler.d.ts +4 -1
  93. package/dist/session-titler.js +13 -3
  94. package/dist/spec-fields.d.ts +8 -4
  95. package/dist/stored-request-hooks-m20.d.ts +23 -0
  96. package/dist/stored-request-hooks-m20.js +124 -0
  97. package/dist/stored-request-tool-axes-m19.d.ts +29 -0
  98. package/dist/stored-request-tool-axes-m19.js +27 -0
  99. package/dist/task-mcp.d.ts +55 -60
  100. package/dist/task-mcp.js +197 -96
  101. package/dist/task-settings.d.ts +41 -5
  102. package/dist/task-settings.js +39 -25
  103. package/dist/tool-axis-words.d.ts +14 -17
  104. package/dist/trace/core-keyset-guard.d.ts +2 -2
  105. package/dist/trace/ledger-sink.d.ts +29 -0
  106. package/dist/trace/ledger-sink.js +11 -0
  107. package/dist/trace/project.d.ts +6 -0
  108. package/dist/trace/project.js +11 -0
  109. package/dist/trace/wire-projection-faces.d.ts +1 -1
  110. package/dist/trace/wire-projection-faces.js +1 -1
  111. package/dist/user-authored-text.d.ts +7 -0
  112. package/dist/user-authored-text.js +3 -0
  113. package/dist/web-admin-client/apply-effective.js +10 -5
  114. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  115. package/dist/web-admin-client/types.d.ts +1 -1
  116. package/package.json +2 -2
  117. package/scripts/drain-check-local.mjs +3 -2
@@ -193,18 +193,26 @@ export function taskSettingsKeyIssue(raw) {
193
193
  export function taskHooksHonored(config) {
194
194
  return config.requirePrincipal !== true;
195
195
  }
196
- export function parseTaskSettings(raw) {
196
+ export function parseTaskSettings(raw, seats = {}) {
197
197
  if (raw === null || raw === undefined || typeof raw !== "object")
198
198
  return { deferred: [] };
199
199
  const s = raw;
200
200
  const deferred = [];
201
201
  let hooksConfig;
202
+ let hooksReread;
202
203
  if (s.hooks !== undefined && s.hooks !== null) {
203
204
  const parsedHooks = parseHooksConfig(s.hooks);
204
205
  if (parsedHooks.config)
205
206
  hooksConfig = parsedHooks.config;
206
- else
207
- deferred.push("hooks");
207
+ else {
208
+ const reread = seats.storedHooksReread?.(s.hooks);
209
+ if (reread !== undefined) {
210
+ hooksConfig = reread.config;
211
+ hooksReread = { stripped: reread.stripped };
212
+ }
213
+ else
214
+ deferred.push("hooks");
215
+ }
208
216
  }
209
217
  const shellEnv = cleanEnvMap(s.env);
210
218
  if (s.env !== undefined && s.env !== null && shellEnv === undefined)
@@ -252,7 +260,7 @@ export function parseTaskSettings(raw) {
252
260
  const maxDesc = s.skillListingMaxDescChars;
253
261
  if (skillListingMaxDescOk(maxDesc))
254
262
  out.skillListingMaxDescChars = maxDesc;
255
- return { settings: Object.keys(out).length > 0 ? out : undefined, deferred };
263
+ return { settings: Object.keys(out).length > 0 ? out : undefined, deferred, ...(hooksReread !== undefined ? { hooksReread } : {}) };
256
264
  }
257
265
  export function hasConstitutionAnchors(text) {
258
266
  return typeof text === "string" && text.includes(CYBER_RISK) && text.includes(URL_SAFETY) && text.includes(HARNESS_SECTION_ANCHOR);
@@ -321,33 +329,39 @@ function createWorkflowAskPolicy(isExempt) {
321
329
  };
322
330
  }
323
331
  function rejectUnmatchableSettingsNames(perms, pathBases) {
324
- if (pathBases?.home === undefined) {
325
- for (const [field, names, behavior] of [
326
- ["permissions.ask", perms.ask, "ask"],
327
- ["permissions.deny", perms.deny, "deny"],
328
- ]) {
329
- for (const name of names ?? []) {
330
- const parsed = parseRuleText(name, behavior);
332
+ const refused = rulePolicyBucketsRefusal(perms, { ...(pathBases !== undefined ? { declared: pathBases } : {}), ...(pathBases?.home !== undefined ? { home: pathBases.home } : {}) });
333
+ if (refused !== undefined)
334
+ throw new Error(refused.message);
335
+ }
336
+ export const RULE_POLICY_BUCKETS = ["ask", "deny"];
337
+ export function rulePolicyBucketsRefusal(permissions, bases) {
338
+ for (const bucket of RULE_POLICY_BUCKETS) {
339
+ const names = permissions[bucket];
340
+ if (!names)
341
+ continue;
342
+ const unmatchable = unmatchableToolNamesRefusal(`permissions.${bucket}`, names, { consumer: "rule-policy", behavior: bucket, ...(bases.declared !== undefined ? { pathBases: bases.declared } : {}) });
343
+ if (unmatchable !== undefined)
344
+ return { bucket, message: unmatchable.message };
345
+ }
346
+ if (bases.home === undefined) {
347
+ for (const bucket of RULE_POLICY_BUCKETS) {
348
+ const field = `permissions.${bucket}`;
349
+ for (const name of permissions[bucket] ?? []) {
350
+ const parsed = parseRuleText(name, bucket);
331
351
  if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
332
- throw new Error(`${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
333
- `the engine never guesses one: without a home base the rule resolves to "unreadable" and every matching call stops to ask, so it guards nothing. ` +
334
- `Spell the path absolutely (${"Edit(//abs/path/**)"} form), declare the lane's home with REMOTE_EXEC_HOME_DIR, or run the task on a lane whose home this worker knows.`);
352
+ return {
353
+ bucket,
354
+ message: `${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
355
+ `the engine never guesses one: without a home base the rule resolves to "unreadable" and every matching call stops to ask, so it guards nothing. ` +
356
+ `Spell the path absolutely (${"Edit(//abs/path/**)"} form), declare the lane's home with REMOTE_EXEC_HOME_DIR, or run the task on a lane whose home this worker knows.`,
357
+ };
335
358
  }
336
359
  }
337
360
  }
338
361
  }
339
- const judgments = [
340
- ["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
341
- ["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
342
- ];
343
- for (const [field, names, judgment] of judgments) {
344
- if (!names)
345
- continue;
346
- const unmatchable = unmatchableToolNamesRefusal(field, names, judgment);
347
- if (unmatchable !== undefined)
348
- throw new Error(unmatchable.message);
349
- }
362
+ return undefined;
350
363
  }
364
+ export const REQUEST_SETTINGS_REFUSAL_PREFIX = "request settings cannot be applied as written: ";
351
365
  export function deriveSettingsPolicy(settings, gate, workflowGate) {
352
366
  const perms = settings.permissions;
353
367
  if (!perms)
@@ -1,30 +1,27 @@
1
1
  /**
2
- * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词,**两面共用一份**。
2
+ * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词(读者:**A2A 请求腿**;以及一次性迁移 M19 的升级前读法 `stored-request-tool-axes-m19.ts`,随 M19 的登记行退役)。
3
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 一声不吭。
4
+ * ## 历史与现状
5
+ * 修前 `task-mcp.ts` 与 `task-a2a.ts` 各自在请求腿上逐键校验同一个三成员结构(`effect` / `egress` / `irreversibility`),两个**闭集词表**是手抄的字面串,
6
+ * 没有一份钉在 core 的型上;S-249 把词表收到本文件并**两向钉死在 core 的型上**(core 加词 ⇒ 正向门红;本表留了 core 已没有的词 ⇒ 反向门红),两条腿读同一只谓词。
11
7
  *
12
- * ## 修法
13
- * 词表留在本仓(运行期要它),但**两向钉死在 core 的型上**:core 加词 ⇒ 正向门红;本表留了一个 core
14
- * 已经没有的词 ⇒ 反向门红。两个面共用**同一只**谓词,于是「什么算一个合法的轴值」在本仓只有一个答案。
8
+ * 之后 MCP 面的 `toolAxes` 判官整个换给了**引擎**:core 7.51.2 起根导出 `readMcpToolAxes`(整份声明一坏全拒、不默认),部署腿(`web-admin-client/skills-mcp.ts`)
9
+ * 与请求腿(`task-mcp.ts`)都问它,本仓不再有 MCP 面的自铸读者、也不再 import 本文件的谓词。**A2A 面引擎没有对应的判官**(core 暂无 `readA2aToolAxes`),
10
+ * 所以 A2A 请求腿(`task-a2a.ts`)仍用本文件的谓词;引擎一旦给 A2A 也立一只判官,本文件随之退役。另一个读者是 M19 的迁移读者:续跑重放腿把升级前存下的
11
+ * `mcpServers[].toolAxes` 按升级前那只读者的读法重读一次(产物再交引擎的判官),那段读法当年就读本文件的谓词,逐字搬回;它不是 MCP 面的第二只判官。
15
12
  *
16
- * 🔴 **只管值域,不管处置**:两条腿对坏值的处置**刻意不同**(MCP = 丢这一键、留 server;A2A = 整条拒),
17
- * 判据与理由各写在各自的调用点。合并处置会抹掉那条真实的分歧(MCP 的 `toolAxes` 有 server-hint 腿可折,
18
- * A2A 收下的就是最终值),而分歧本身是裁定过的。
13
+ * 下面的同型钉(A2A 的轴型与 MCP 的轴型逐字同声明)仍有意义:A2A 的词表跟着引擎 MCP 面的轴型走,哪一边加词、改词,另一边在这里当场红。
14
+ *
15
+ * 🔴 **只管值域,不管处置**:A2A 腿对坏值的处置是整条拒(MCP 面是引擎判官的「整份全拒」;两面方向一致),M19 的迁移读者剔坏成员(升级前的读法),判据与理由写在各自的调用点。
19
16
  */
20
17
  import type { McpServerSpec, ToolEffect } from "@sema-agent/core";
21
- /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明两面逐字同声明。 */
18
+ /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明 A2A 面与它逐字同声明。 */
22
19
  export type ToolAxisEntry = NonNullable<McpServerSpec["toolAxes"]>[string];
23
20
  /** 轴覆写上那个**两词**的不可逆档(⚠️ 与 `AskOrigin` 面的**三词** `never|maybe|always` 不是同一个词表:
24
21
  * 覆写面只收两极,`maybe` 是引擎按工具自己的声明定的,调用方说不出口)。 */
25
22
  export type ToolAxisIrreversibility = NonNullable<ToolAxisEntry["irreversibility"]>;
26
- /** 值是不是一个合法的 `effect` 词(两条请求腿共用)。 */
23
+ /** 值是不是一个合法的 `effect` 词(A2A 请求腿与 M19 的迁移读者读它)。 */
27
24
  export declare function isToolEffect(v: unknown): v is ToolEffect;
28
- /** 值是不是一个合法的**覆写面** `irreversibility` 词(两条请求腿共用)。 */
25
+ /** 值是不是一个合法的**覆写面** `irreversibility` 词(A2A 请求腿与 M19 的迁移读者读它)。 */
29
26
  export declare function isToolAxisIrreversibility(v: unknown): v is ToolAxisIrreversibility;
30
27
  //# sourceMappingURL=tool-axis-words.d.ts.map
@@ -148,9 +148,9 @@ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCal
148
148
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
149
149
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
150
150
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
151
- 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" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools";
151
+ 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" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools" | "userAttachments";
152
152
  /** 导出:请求体键闭集门(`request-key-closure.ts`)据它锁「本仓记为未接线的引擎座,提交口不许受理」—— 两处分类只能同向。 */
153
- export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport" | "userAttachments";
153
+ export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport";
154
154
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
155
155
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
156
156
  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" | "responseModel" | "compaction";
@@ -104,6 +104,35 @@ export declare function createSerialLedgerAppend(appendEvent: (seq: number, type
104
104
  * 返回 `.catch` 用的处理器(产物是函数 ⇒ `create*`)。
105
105
  */
106
106
  export declare function createForwardAppendFailedNote(type: string): (err: unknown) => void;
107
+ /**
108
+ * 人在环帧(`question` / `question_complete`、`elicitation` / `elicitation_complete`)的**账本孪生投递口**。
109
+ *
110
+ * 一条律:**凡进 run 流的 HITL 帧都有账本孪生;腿只决定「还要不要同时直写 socket」**。后台腿没有 live 面,
111
+ * 它的投递口就是账本追加本身(`runs.ts`);同步流腿有 live SSE,走本函数 —— 同一只帧对象先落账、再按需直写,
112
+ * 于是两条腿对同一只帧的落法只差「直写要不要」,不差「账本有没有」。
113
+ *
114
+ * 顺序与失败语义(与后台腿同一条):
115
+ * · **先账本后直写**。账本落不下 ⇒ 本次投递**失败**(抛),帧不上 socket —— 一只没有孪生的帧不许单独露面
116
+ * (否则壳看见一张重载就丢的卡)。调用方(协调器)按「开帧投递失败」处理:这一问结算 `unavailable`、记一次既有的
117
+ * P-DEBT 留痕;消卡帧则落「面包屑丢失」的 F 类留痕。
118
+ * · **seq 不自造**:孪生走 `sink.append` —— 本腿账本唯一的那条串行链(与 text / tool / done 行同一个计数器)。
119
+ * · **直写口已不可达**(客户端离开,而 run 因 detach 继续)⇒ 账本孪生即送达,不再触碰那条已死的连接
120
+ * (不抛、也不记「丢帧」—— 帧没丢,在账本里)。
121
+ * · **本腿没有账本**(无 durable run 行的部署:`sink` 缺席)⇒ 直写就是全部投递,死流姿态由 `writeLive` 自己带
122
+ * (question 抛、elicitation 静默记账),字节逐字同孪生落地之前。
123
+ *
124
+ * 命名(CLAUDE.md 工厂命名律):`create*` —— 返回的是捕获了两个口的闭包,不是纯数据。
125
+ */
126
+ export declare function createLedgerTwinEmit<F extends {
127
+ type: LedgerEventType;
128
+ }>(opts: {
129
+ /** 本腿的账本(sync 腿 = 有 durable run 行才有;缺席 ⇒ 无孪生可落)。只用 `append`:seq 由它那条链铸。 */
130
+ sink: Pick<LedgerSink, "append"> | undefined;
131
+ /** 本腿的直写口(sync 腿 = 一个具名 SSE 事件)。 */
132
+ writeLive: (frame: F) => void;
133
+ /** 直写面此刻可达吗 —— 与协调器问「这条腿还够得着人吗」的是**同一只**谓词(`QuestionRunContext.deliverable`)。 */
134
+ liveReachable: () => boolean;
135
+ }): (frame: F) => Promise<void>;
107
136
  export declare function createLedgerSink(opts: {
108
137
  /** The seq-stamped durable append — typically `(seq, type, data) => runStore.appendEvent(taskId, seq, type, data)`. */
109
138
  appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise<void>;
@@ -17,6 +17,17 @@ export function createSerialLedgerAppend(appendEvent, startSeq) {
17
17
  export function createForwardAppendFailedNote(type) {
18
18
  return (err) => recordFailOpen("server.ledger.forward-append-failed", `type=${type} err=${err instanceof Error ? err.message : String(err)}`);
19
19
  }
20
+ export function createLedgerTwinEmit(opts) {
21
+ return async (frame) => {
22
+ if (opts.sink !== undefined) {
23
+ const { type, ...data } = frame;
24
+ await opts.sink.append(type, data);
25
+ if (!opts.liveReachable())
26
+ return;
27
+ }
28
+ opts.writeLive(frame);
29
+ };
30
+ }
20
31
  export function createLedgerSink(opts) {
21
32
  const keys = opts.notifiedKeys ?? new NotifiedKeys();
22
33
  let seq = 0;
@@ -851,6 +851,11 @@ export declare function wiringManifestFaceEventData(ev: {
851
851
  * `[from …]` 渲染就是靠它决定加不加 `(unverified)`),剥掉它比剥掉 id 更坏:剩下一个看不出真假的名字。
852
852
  * · `principal` —— **刻意不投影**(与 ToolApprovalFrame 的 `principal` 同裁):帧本就经 owner-scoped 流
853
853
  * 投递,租户身份不必再由帧自述。要它请先提消费需求,别顺手加。
854
+ * · `attachments[]`(core 7.50.0,只在声明了附件的目标那一帧 `applied` 上)—— 逐成员挑四键:`ordinal`(引擎铸的序号)/
855
+ * `kind`(引擎认出的种类,闭集词;读不出种类时缺席)/ `outcome`(引擎的结局词,闭集)⇒ verbatim;`label` = 调用方**自己声明的**
856
+ * 路径或名字(引擎原样回声,不是解析后的落点 —— 多租户沙箱车道上它是这次 run 自己工作区里的相对 / 绝对路径串,帧上不出现任何别的租户
857
+ * 或宿主盘的信息),与已展示的工具实参 / `workspace_changed.cwd` 同一信任级(UNTRUSTED 的路径串)⇒ 过 `redactSecrets`。长度界由引擎定
858
+ * (`USER_ATTACHMENT_LABEL_MAX`),本仓不另截。非对象的成员丢、集外的成员键不投(同 `actor` 的白名单理由)。
854
859
  *
855
860
  * `actor` 逐键挑(禁 spread):core 已声明 `ActorAssertion` 是冻结三键形,但白名单的意义正是「core 加第
856
861
  * 四键时由人处置」——身份对象上多出来的键裸上线是本文件反复登记的病族。
@@ -866,6 +871,7 @@ export declare function humanInputEventData(ev: {
866
871
  actor?: unknown;
867
872
  principal?: unknown;
868
873
  entryId?: unknown;
874
+ attachments?: unknown;
869
875
  eventId?: unknown;
870
876
  parentToolCallId?: unknown;
871
877
  }): Record<string, unknown>;
@@ -665,6 +665,16 @@ export function wiringManifestFaceEventData(ev, operatorFace, instructions) {
665
665
  export function humanInputEventData(ev) {
666
666
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
667
667
  const a = typeof ev.actor === "object" && ev.actor !== null && !Array.isArray(ev.actor) ? { ...ev.actor } : {};
668
+ const attachments = Array.isArray(ev.attachments)
669
+ ? ev.attachments
670
+ .filter((m) => typeof m === "object" && m !== null && !Array.isArray(m))
671
+ .map((m) => ({
672
+ ...(typeof m.ordinal === "number" && Number.isFinite(m.ordinal) ? { ordinal: m.ordinal } : {}),
673
+ ...(str(m.kind) !== undefined ? { kind: m.kind } : {}),
674
+ ...(str(m.outcome) !== undefined ? { outcome: m.outcome } : {}),
675
+ ...(str(m.label) !== undefined ? { label: redactSecrets(String(m.label)) } : {}),
676
+ }))
677
+ : undefined;
668
678
  const actor = {
669
679
  ...(str(a.id) !== undefined ? { id: redactSecrets(String(a.id)) } : {}),
670
680
  ...(typeof a.hostAsserted === "boolean" ? { hostAsserted: a.hostAsserted } : {}),
@@ -679,6 +689,7 @@ export function humanInputEventData(ev) {
679
689
  ...(str(ev.issuer) !== undefined ? { issuer: redactSecrets(String(ev.issuer)) } : {}),
680
690
  ...(Object.keys(actor).length > 0 ? { actor } : {}),
681
691
  ...(str(ev.entryId) !== undefined ? { entryId: ev.entryId } : {}),
692
+ ...(attachments !== undefined ? { attachments } : {}),
682
693
  ...identityFields(ev),
683
694
  };
684
695
  }
@@ -161,7 +161,7 @@ export declare const WIRE_PROJECTION_FACES: {
161
161
  readonly human_input: {
162
162
  readonly projector: "humanInputEventData";
163
163
  readonly coreType: string;
164
- readonly excluded: readonly ["principal", "attachments"];
164
+ readonly excluded: readonly ["principal"];
165
165
  readonly minted: readonly [];
166
166
  };
167
167
  readonly task_notification: {
@@ -30,7 +30,7 @@ export const WIRE_PROJECTION_FACES = {
30
30
  context_usage: { projector: "contextUsageEventData", coreType: arm("context_usage"), excluded: [], minted: [] },
31
31
  turn_end: { projector: "turnEndEventData", coreType: arm("turn_end"), excluded: [], minted: [] },
32
32
  compaction_outcome: { projector: "compactionOutcomeEventData", coreType: arm("compaction_outcome"), excluded: [], minted: [] },
33
- human_input: { projector: "humanInputEventData", coreType: arm("human_input"), excluded: ["principal", "attachments"], minted: [] },
33
+ human_input: { projector: "humanInputEventData", coreType: arm("human_input"), excluded: ["principal"], minted: [] },
34
34
  task_notification: {
35
35
  projector: "taskNotificationEventData",
36
36
  coreType: `TaskNotificationPayload & ${arm("task_notification")}`,
@@ -28,4 +28,11 @@ export declare function buildObjectivePreview(text: string, chars: number): stri
28
28
  export declare const RUN_OBJECTIVE_PREVIEW_CHARS = 120;
29
29
  /** run 行的预览:用户写的那段 → 脱敏 → 截 {@link RUN_OBJECTIVE_PREVIEW_CHARS};没有用户正文 ⇒ `""`(列写空串 = 有记录、剥空)。 */
30
30
  export declare function buildRunObjectivePreview(objective: string): string;
31
+ /**
32
+ * 这条消息是不是**附件帧**(core 7.50.0 请求层附件:目标声明的每只文件由引擎铸成一条自己的 user 消息,紧跟目标提交)。
33
+ * 判据 = 引擎写在消息上的送达记录 `attachment` **在场**(core:那只记录是「这条消息是附件帧」的唯一来源,文本从不判)。坏形记录同样算在场 ——
34
+ * 两个读者都往「不是人写的话」那一侧落(预览走下一条;会话视图按文件字节脱敏),那是安全的一侧。
35
+ * 一个事实一个读法:会话视图的处置表(`audit.ts`)与本地会话列表的预览(`plugins/local-session-store.ts`)同调这一只。
36
+ */
37
+ export declare function isAttachmentFrame(message: unknown): boolean;
31
38
  //# sourceMappingURL=user-authored-text.d.ts.map
@@ -15,4 +15,7 @@ export const RUN_OBJECTIVE_PREVIEW_CHARS = 120;
15
15
  export function buildRunObjectivePreview(objective) {
16
16
  return buildObjectivePreview(objective, RUN_OBJECTIVE_PREVIEW_CHARS);
17
17
  }
18
+ export function isAttachmentFrame(message) {
19
+ return typeof message === "object" && message !== null && Object.hasOwn(message, "attachment");
20
+ }
18
21
  //# sourceMappingURL=user-authored-text.js.map
@@ -1,7 +1,7 @@
1
1
  import { expandTiers, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
2
2
  import { WEB_ADMIN_ENV, narrowAtMention, resolveActiveTiers, resolveCatalogDefaultModel } from "@sema-agent/settings-schema";
3
3
  import { modelKeyPoison } from "../key-resolver.js";
4
- import { readAtMention } from "../model-select.js";
4
+ import { buildEnvModelIdNotServedNotice, mintCapabilityKeys, readAtMention, servedModelTableLeg } from "../model-select.js";
5
5
  import { applyAutoCompactWindow, buildModelCostKey, parseAutonomy, unmatchableToolNamesRefusal } from "../config.js";
6
6
  import { buildModelCompat, readModelCompat } from "../model-compat.js";
7
7
  import { buildModelHeadersKey } from "../model-entry-headers.js";
@@ -40,8 +40,7 @@ function toModel(m, envDefaults = {}, logger) {
40
40
  provider: m.provider,
41
41
  api: m.api,
42
42
  baseUrl: m.baseUrl ?? "",
43
- reasoning: m.reasoning !== undefined ? Boolean(m.reasoning) : (envDefaults.reasoning ?? true),
44
- input: m.vision ? ["text", "image"] : ["text"],
43
+ ...mintCapabilityKeys({ reasoning: m.reasoning, vision: m.vision }),
45
44
  contextWindow: m.contextWindow ?? envDefaults.contextWindow ?? 262144,
46
45
  maxTokens: m.maxTokens ?? envDefaults.maxTokens ?? 4096,
47
46
  ...(charsPerTokenOf(m.charsPerToken) !== undefined || envDefaults.charsPerToken !== undefined
@@ -115,6 +114,7 @@ export function applyEffective(config, eff, logger, opts) {
115
114
  }
116
115
  }
117
116
  }
117
+ const servedBefore = servedModelTableLeg(config);
118
118
  commitStaged(config, staged);
119
119
  if (ordered)
120
120
  appliedGeneration.set(config, gen);
@@ -138,6 +138,11 @@ export function applyEffective(config, eff, logger, opts) {
138
138
  logger?.info("web_admin_limits", { version: staged.version, keys: staged.limits.changed });
139
139
  if (mp)
140
140
  logger?.info("web_admin_models", mp.infoLine);
141
+ if (servedBefore === "env") {
142
+ const notServed = buildEnvModelIdNotServedNotice(config);
143
+ if (notServed !== undefined)
144
+ logger?.warn(notServed.event, notServed.fields);
145
+ }
141
146
  if (staged.approvalGate.next !== undefined)
142
147
  logger?.info("web_admin_runtime", { version: staged.version, approvalRequire: staged.approvalGate.next });
143
148
  if (Object.keys(staged.hot.infoLine).length > 0)
@@ -248,7 +253,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
248
253
  const modelApiKeyEnv = {};
249
254
  const modelApiKeys = {};
250
255
  const envModel = envDerivedModel(config);
251
- const envDefaults = { maxTokens: envModel.maxTokens, contextWindow: envModel.contextWindow, reasoning: envModel.reasoning, charsPerToken: envModel.charsPerToken, defaultThinking: envModel.defaultThinking, compat: envModel.compat };
256
+ const envDefaults = { maxTokens: envModel.maxTokens, contextWindow: envModel.contextWindow, charsPerToken: envModel.charsPerToken, defaultThinking: envModel.defaultThinking, compat: envModel.compat };
252
257
  for (const m of usable) {
253
258
  if (m.baseUrl)
254
259
  logger?.warn("web_admin_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(m.baseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
@@ -343,7 +348,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
343
348
  const noDefault = crossInvariant("default-model-resolvable").check({ live: config, next: nextView });
344
349
  if (noDefault !== undefined)
345
350
  return refuse(DEFAULT_MODEL_MISSING_CODE, noDefault);
346
- const endpoint = judgeModelRouteEndpoint(nextView);
351
+ const endpoint = judgeModelRouteEndpoint(nextView, "catalog");
347
352
  if (endpoint !== undefined)
348
353
  return refuse(endpoint.code, endpoint.reason);
349
354
  const minted = crossInvariant("refused-names-not-minted").check({ live: config, next: nextView });
@@ -27,7 +27,7 @@ export declare function applySkillsManifest(baseline: LoadedSkill[], manifest: {
27
27
  * Resolve center MCP servers (`/effective.mcp`) into core `McpServerSpec`s + the scenarios each applies to.
28
28
  * env-NAME refs → real values from THIS service's env (secret boundary unchanged); a server with any missing
29
29
  * ref is skipped (`resolveRefs` → null). `enabled:false` skipped. A server whose `toolFaces` / `toolAxes` declaration is
30
- * malformed is skipped whole (both are trust-root declarations — see `mcp-tool-faces.ts` / `mcp-tool-axes.ts`). Restart-to-apply
30
+ * malformed is skipped whole (both are trust-root declarations — see `mcp-tool-faces.ts` / the engine's `readMcpToolAxes`). Restart-to-apply
31
31
  * (filtered per scenario in `resolveSpec` via `mcpForScenario`).
32
32
  */
33
33
  export declare function resolveMcpServers(mcp: {
@@ -125,7 +125,7 @@ export interface CenterMcpServer {
125
125
  * 与传输类型和地址无关(stdio 子进程可以是不可信包,loopback URL 可以是你自己的服务)——core 刻意
126
126
  * 不按拓扑推断,本层同样不推断。它是**部署面**的键(core 顶注:与 `RunnerDeps` 接线同一权威平面),
127
127
  * 所以它的家在 center 下发这条腿;请求腿那半场的门与结构性兑现见 `task-mcp.ts`
128
- * 的 `assertRequestMcpContentOrigin` 顶注。
128
+ * 的 `assertRequestMcpEntries` 顶注。
129
129
  *
130
130
  * ✅ **两条腿已通**([ref],settings-schema 1.4.0 补键 + config-provider 映射行,2026-09-02;双绊线翻正销账)。
131
131
  * 病史([ref]③ / [ref]③ / [ref],2026-08-31 双绊线实测更正 —— 本注更旧版那句
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.129.1",
3
+ "version": "7.131.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@sema-agent/core": "7.51.2",
68
- "@sema-agent/settings-schema": "12.2.1",
68
+ "@sema-agent/settings-schema": "13.0.1",
69
69
  "e2b": "^2.28.0",
70
70
  "libsodium-wrappers": "^0.8.4",
71
71
  "mysql2": "^3.22.4",
@@ -19,8 +19,9 @@
19
19
  * · `skill-deny-inert`(换钉 7.117.0 前,引擎 7.42 逐字读规则条目):pending 行所在会话的续跑上下文里,提交体顶层 `skills[]` 或某只子代的 `agents[].skills[]`
20
20
  * 某只技能的 `disallowedTools` 有引擎读成空的条目(例:带首尾空白的 `"Bash "` —— 7.41 去白后是一条有效的拒,7.42 什么也不拒)—— 这样的行在 7.117.0 上续跑会被拒
21
21
  * (行字节不动;出路 = 取消、改好条目重提,见随包 `MIGRATION.md` 7.117.0 节)。命中行点名是哪一层(`member=skills` / `member=agents.skills`)。
22
- * 判据**就是**服务提交与续跑两腿同经的那一只函数(`src/rule-entry-list.ts` 的 `skillDenyInertEntryOf`,它调引擎包根的 `compileAllowList`),经编译产物
23
- * `dist/rule-entry-list.js` 取 —— 本脚本不写第二份;
22
+ * 判据**就是**服务提交与续跑两腿同经的那一只判官(`src/rule-entry-list.ts`:两腿经 `inertToolListEntryOf` 判全部工具名清单,它先调技能两层的取清单函数 `skillDenyInertEntryOf`,
23
+ * 后者调引擎包根的 `compileAllowList`),本脚本取的是技能两层那一只,经编译产物 `dist/rule-entry-list.js` 取 —— 不写第二份。7.131.0 起读成空即拒的清单扩到根 run 的 `allowTools` / `excludeTools` 与子代定义的 `allowTools` / `denyTools`,
24
+ * 本判据(窗在 7.117.0)与随包 SQL 孪生**仍只数技能两层**,不覆盖那几张清单(续跑时的 409 是它们的响亮道);
24
25
  * · `workflow-park-holder`(换钉 7.118.0 前,引擎 7.44.0 起 workflow 子代卡交给哪条 run 只看记录 `parks[]` 条目上的持有者位 `parkedBy` / `generation`):
25
26
  * pending 行的子会话被 workflow park 索引点名(`<数据根>/workflows/workflow-agent-sessions/index.jsonl`:一行 = 一条 workflow run 的整份指针集
26
27
  * `{ runId, rows: [{ sessionId, runId, agentKey, scope }] }`,同一条 run 后写的一行整份覆盖先写的,空集 = 它不再点名任何子会话)= 一张待批的 workflow 子代卡。