@sema-agent/server 7.78.0 → 7.78.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -0
  2. package/USAGE.md +4 -0
  3. package/dist/approval-card.d.ts +37 -4
  4. package/dist/approval-card.js +22 -2
  5. package/dist/boot/resolve-spec.js +6 -3
  6. package/dist/config-catalog.d.ts +18 -1
  7. package/dist/config-catalog.js +3 -3
  8. package/dist/governance-http-status.d.ts +27 -0
  9. package/dist/governance-http-status.js +11 -0
  10. package/dist/http/routes/approvals-assistant.d.ts +35 -0
  11. package/dist/http/routes/approvals-assistant.js +39 -1
  12. package/dist/http/routes/tasks.js +4 -9
  13. package/dist/http/server.d.ts +28 -4
  14. package/dist/http/server.js +33 -13
  15. package/dist/observability/fail-open.d.ts +4 -0
  16. package/dist/observability/fail-open.js +4 -0
  17. package/dist/parked-decide.d.ts +7 -2
  18. package/dist/plugins/host-workspace-registry.d.ts +27 -0
  19. package/dist/plugins/host-workspace-registry.js +27 -0
  20. package/dist/plugins/one-time-migrations.d.ts +3 -0
  21. package/dist/plugins/one-time-migrations.js +18 -11
  22. package/dist/plugins/remote-env-host.d.ts +15 -0
  23. package/dist/plugins/remote-env-host.js +41 -1
  24. package/dist/request-key-closure.d.ts +30 -0
  25. package/dist/request-key-closure.js +56 -0
  26. package/dist/runs.d.ts +51 -0
  27. package/dist/runs.js +4 -11
  28. package/dist/runtime-governance.d.ts +18 -4
  29. package/dist/runtime-governance.js +2 -2
  30. package/dist/spec-fields.d.ts +4 -2
  31. package/dist/spec-fields.js +4 -5
  32. package/dist/task-a2a.js +8 -6
  33. package/dist/task-mcp.js +5 -3
  34. package/dist/task-settings.d.ts +49 -17
  35. package/dist/task-settings.js +37 -38
  36. package/dist/tool-approval.d.ts +35 -3
  37. package/dist/tool-approval.js +41 -19
  38. package/dist/tool-axis-words.d.ts +30 -0
  39. package/dist/tool-axis-words.js +9 -0
  40. package/dist/trace/core-keyset-guard.d.ts +3 -3
  41. package/dist/trace/ledger-sink.js +12 -5
  42. package/dist/trace/model-segment.d.ts +32 -2
  43. package/dist/trace/model-segment.js +15 -5
  44. package/dist/trace/project.d.ts +37 -3
  45. package/dist/trace/project.js +14 -10
  46. package/package.json +3 -3
package/README.md CHANGED
@@ -114,6 +114,12 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
114
114
  # To pin a model: add "model":"<catalog id>" to the body; see GET /v1/capabilities for what is available
115
115
  ```
116
116
 
117
+ - **Where your files have to be**: a caller-supplied `cwd` is a path on the machine the **server process**
118
+ runs on, and it is honored only on the single-user `REMOTE_EXEC=host` lane — that lane is by definition
119
+ *this* box (no container boundary). So "server on box A, my repo on box B" does not work: nothing maps
120
+ B's paths into A. Run the server on the box that holds the files (or `run-local` there), or use a
121
+ container lane (`e2b` / `k8s` / `local-docker`) where the workspace is the sandbox's own and `cwd` is
122
+ ignored.
117
123
  - **Distribution coordinates**: npm = [`@sema-agent/server`](https://www.npmjs.com/package/@sema-agent/server)
118
124
  (public on npmjs) · images = `ghcr.io/sema-agent/sema-server` + `docker.io/claybobby/sema-server`
119
125
  (both public; `:latest` rolling, `:<sha>` pinned).
package/USAGE.md CHANGED
@@ -404,6 +404,10 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
404
404
  `SERVICE_AUTH_TOKEN`/TOKENS 名录)⇒ 自动绑 `127.0.0.1`,boot 日志点名原因。理由:该形常配
405
405
  `REMOTE_EXEC=host`(用户真机、非沙箱),绑全接口=同网段任何人可无鉴权提交任务并执行。
406
406
  - 配了凭证的部署(云形/k8s/compose)缺省**不变**(全接口),既有部署零影响。
407
+ - **跨机不映射 cwd**:请求里的 `cwd` 是**服务进程所在那台机器**上的路径,且只有单用户 `REMOTE_EXEC=host`
408
+ 车道会尊重它(那条车道的定义就是"运维自己这台机、无容器边界")。所以「服务跑在 A 机、代码在 B 机」不成立
409
+ —— 没有任何东西把 B 的路径映到 A。要么把服务(或 `run-local`)跑在放着文件的那台机器上,要么走容器车道
410
+ (`e2b`/`k8s`/`local-docker`),那里的工作区是沙箱自己的,`cwd` 被忽略。
407
411
 
408
412
  **workspace 浏览面 —— 已随整树快照纪元退役**
409
413
  - 旧四端点(list/tree/file/archive)恒 `501 capability.workspace_retired`,`capabilities.workspace`
@@ -21,7 +21,7 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { type AskEvidenceAbsence, type RuleOffer } from "@sema-agent/core";
24
+ import { type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
25
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
26
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
27
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
@@ -269,6 +269,29 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
269
269
  * (截出来的是另一个机器码);`shown` 超限 ⇒ **截条目**,`total` 逐字保真。
270
270
  */
271
271
  export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
272
+ /**
273
+ * [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。core 自己的同族界是
274
+ * `READ_ROOT_CANDIDATE_DIR_MAX = 1024`(`src/core/read-root-candidate.ts`),**未从包根导出**,故钉本地
275
+ * 孪生 + 本出处注记(与 `MAX_SKILL_CONTENT_CHARS` 同款处置)。
276
+ *
277
+ * 🔴 超限的方向是**整只丢,不截** —— core 契约 `shell.read_boundary.grant_candidate_is_the_grant`:卡上
278
+ * 显示的串就是人要加进读目录的那个串,截过的目录是**另一个**目录,加进去也清不掉这只 ask。与
279
+ * {@link MAX_PROBE_CAUSE_CODE} 的 `code` 超限整只丢逐字同判据。
280
+ */
281
+ export declare const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
282
+ /** 窄读的输出形 —— **就是 core 的那只型**,不在本仓再声明一份同形结构(重声明 = 上游改形那天本仓静默不红)。 */
283
+ export type ReadRootCandidateProjection = ReadRootGrantCandidate;
284
+ /**
285
+ * [ref] —— `AskRequest.readRootCandidate` 的边界窄读:**把这个目录加进本会话的读目录,这只 ask 就不会
286
+ * 再出现**。活卡帧的**唯一**铸造点。
287
+ *
288
+ * 🔴 **零判读**:server 既不从命令文本重推目录、也不校验它是不是真的能清掉这只 ask —— 那是 core 已经
289
+ * 做过的判断(它把提议的根放回读边界又走了一遍),在下游再判一次就是同一个事实的第二个判官。
290
+ * 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(见 {@link MAX_READ_ROOT_CANDIDATE_DIR})。
291
+ * 🔴 **缺席不是断言**:它同时覆盖「这一族 ask 没有任何目录能清掉」(敏感路径 deny 行、读边界压根读不懂
292
+ * 的命令、递归遍历、非读边界 ask……)与「老引擎」两形 —— 消费端只读在场,永远不读缺席。
293
+ */
294
+ export declare function readReadRootCandidate(req: unknown): ReadRootCandidateProjection | undefined;
272
295
  /**
273
296
  * [ref] 件 G2 —— `AskRequest.ruleEvidence` 的形(帧 / `card_json` / 读面共用;窄读入口是
274
297
  * {@link readRuleEvidence})。三对成员各 = 值**或**五词命名缺席(`AskEvidenceAbsence`),core 引擎
@@ -299,14 +322,24 @@ export type RuleEvidenceProjection = z.infer<typeof RuleEvidenceSchema>;
299
322
  * · `orgRevision` / 缺席词 verbatim(数值 / 闭集词,非内容)。
300
323
  */
301
324
  export declare function readRuleEvidence(req: unknown): RuleEvidenceProjection | undefined;
302
- /** [ref] 第五单(core [ref] 修②)—— `ruleOffers` 缺席因由的**闭三词集**(core d.ts 逐字;词表是闭集,
303
- * 未知词=形不合=按缺席处置,绝不透传一个消费端词表外的值)。 */
325
+ /**
326
+ * [ref] 第五单(core [ref] 修②)—— `ruleOffers` 缺席因由的**闭三词集**。
327
+ *
328
+ * 🔴 **型从 core 派生,不手抄**(S-249):core 刻意**不导出**这个名字,也不导出运行期常量
329
+ * (`permission-rule-lanes.d.ts` 顶注逐字:「Deliberately not exported: this is a wire vocabulary, and its
330
+ * two faces declare it literally … so a consumer reads the closed set on the type it is holding」)——
331
+ * 于是本仓**必须**自持一份运行期词表(zod 要字面元组),但**不必**自持一份型。做法:型 =
332
+ * `AskRequest["ruleOffersAbsence"]` 的联合(那正是 core 要我们读的那张脸),运行期词表另立一行,
333
+ * 两者之间架**两向编译围栏** ⇒ core 加词 / 改词 / 退役任一,本文件当场 tsc 红。
334
+ * (对照 {@link AskEvidenceAbsenceSchema}:那一族 core **给了**运行期常量,所以那边零词表、用 `z.custom`。
335
+ * 两族的姿势不同不是分歧,是「上游给什么就用什么」的同一条纪律的两种兑现。)
336
+ */
337
+ export type RuleOffersAbsence = NonNullable<CoreAskRequest["ruleOffersAbsence"]>;
304
338
  export declare const RuleOffersAbsenceSchema: z.ZodEnum<{
305
339
  mandated: "mandated";
306
340
  lane_cannot_speak: "lane_cannot_speak";
307
341
  shadowed: "shadowed";
308
342
  }>;
309
- export type RuleOffersAbsence = z.infer<typeof RuleOffersAbsenceSchema>;
310
343
  /**
311
344
  * `AskRequest.ruleOffersAbsence`(core [ref] 修②)的**边界窄读** —— 活卡帧与 `card_json` 的唯一铸造点
312
345
  * (与 {@link readProbeCause} 同款分工),两面结构性同值。闭集词 verbatim(非内容族,零 redact);
@@ -160,6 +160,19 @@ export function readProbeCause(req) {
160
160
  ...(raw.further !== undefined ? { further: family(raw.further) } : {}),
161
161
  };
162
162
  }
163
+ export const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
164
+ const ReadRootCandidateEnvelopeSchema = z.object({
165
+ readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true) }).optional(),
166
+ });
167
+ export function readReadRootCandidate(req) {
168
+ const parsed = ReadRootCandidateEnvelopeSchema.safeParse(req);
169
+ const raw = parsed.success ? parsed.data.readRootCandidate : undefined;
170
+ if (raw === undefined)
171
+ return undefined;
172
+ if (raw.dir.length > MAX_READ_ROOT_CANDIDATE_DIR)
173
+ return undefined;
174
+ return { dir: raw.dir, clearsThisAsk: true };
175
+ }
163
176
  const MAX_EVIDENCE_DOT_ACTOR = 256;
164
177
  const MAX_EVIDENCE_DOTS = 16;
165
178
  const AskEvidenceAbsenceSchema = z.custom((v) => typeof v === "string" && ASK_EVIDENCE_ABSENCE_VALUES.includes(v));
@@ -220,11 +233,18 @@ export function readRuleEvidence(req) {
220
233
  ...(raw.personalRuleDotsAbsent !== undefined ? { personalRuleDotsAbsent: raw.personalRuleDotsAbsent } : {}),
221
234
  };
222
235
  }
223
- export const RuleOffersAbsenceSchema = z.enum(["mandated", "lane_cannot_speak", "shadowed"]);
236
+ const RULE_OFFERS_ABSENCE_VALUES = ["mandated", "lane_cannot_speak", "shadowed"];
237
+ export const RuleOffersAbsenceSchema = z.enum(RULE_OFFERS_ABSENCE_VALUES);
224
238
  const RuleOffersAbsenceEnvelopeSchema = z.object({ ruleOffersAbsence: RuleOffersAbsenceSchema.optional() });
225
239
  export function readRuleOffersAbsence(req) {
226
240
  const parsed = RuleOffersAbsenceEnvelopeSchema.safeParse(req);
227
- return parsed.success ? parsed.data.ruleOffersAbsence : undefined;
241
+ if (parsed.success)
242
+ return parsed.data.ruleOffersAbsence;
243
+ const raw = req !== null && typeof req === "object" ? req.ruleOffersAbsence : undefined;
244
+ if (raw === undefined)
245
+ return undefined;
246
+ recordFailOpen("server.approval-card.closed-word-out-of-set", `ruleOffersAbsence=${redactSecrets(String(raw)).slice(0, 40)}`);
247
+ return undefined;
228
248
  }
229
249
  const DenialLimitFallbackSchema = z
230
250
  .object({
@@ -30,7 +30,7 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
30
30
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
31
31
  import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
32
32
  import { assertRequestMcpContentOrigin, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
33
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, shellGateForMode, withPermissionMode } from "../task-settings.js";
33
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, autoModeRequestedForMode, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, shellGateForMode, withPermissionMode, writeFaceForMode } from "../task-settings.js";
34
34
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
35
35
  import { redactSecrets } from "../trace/redact.js";
36
36
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
@@ -53,7 +53,7 @@ export function createResolveSpec(ctx) {
53
53
  assertRequestA2aUnlocked(body.a2aPeers, lockedKeys);
54
54
  const gated = await gateScenarioAndAppend(body, auth, opts);
55
55
  const effMode = effectivePermissionMode(body, gated.parsedSettings.settings);
56
- noteSessionShellGate?.(auth?.sessionId, effMode === "bypassPermissions");
56
+ noteSessionShellGate?.(auth?.sessionId, effMode !== undefined && shellGateForMode(effMode) === "off");
57
57
  const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode, opts);
58
58
  const anchors = await resolveHistoryAnchors(body, auth);
59
59
  const spec = assembleSpecLiteral(body, auth, req, opts, { ...gated, ...lane, ...anchors }, effMode);
@@ -279,7 +279,8 @@ export function createResolveSpec(ctx) {
279
279
  additionalReadDirectories,
280
280
  enablePlanMode: config.planModeEnabled ? true : undefined,
281
281
  ...(effMode ? { shellGate: shellGateForMode(effMode) } : {}),
282
- ...(effMode === "auto" ? { autoModeRequested: true } : {}),
282
+ ...((f) => (f !== undefined ? { writeFace: f } : {}))(effMode !== undefined ? writeFaceForMode(effMode) : undefined),
283
+ ...((f) => (f !== undefined ? { autoModeRequested: f } : {}))(effMode !== undefined ? autoModeRequestedForMode(effMode) : undefined),
283
284
  selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
284
285
  forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
285
286
  retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
@@ -367,6 +368,8 @@ export function createResolveSpec(ctx) {
367
368
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
368
369
  })(cap.tools),
369
370
  skills: mergeUserSkills(cap.skills, body.skills, logger),
371
+ ...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
372
+ ...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
370
373
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
371
374
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
372
375
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
@@ -34,6 +34,23 @@ export interface ConfigCatalogSpec {
34
34
  readonly derivedDefaultNote?: string;
35
35
  readonly danger?: readonly CatalogDangerAxis[];
36
36
  readonly enumValues?: readonly string[];
37
+ /**
38
+ * S-201① —— 本行**回显**得出的词集(`enumValues` 的子集;缺席 ⇒ 与 `enumValues` 同集)。
39
+ *
40
+ * 🔴 `enumValues` 一直是**两义**的:它既是行对外宣告的「你可以写哪些词」([ref] 件⑤/⑧ 两条钉逐字锚
41
+ * 这一义:窄了=少宣告一档、宽了=宣告一个 boot 会拒的词),又被回显校验当成「实算值合不合法」的闭集。
42
+ * 两义在**多数**行上碰巧同集,于是差别只活在散文里 —— `MANUAL_MODE_SHELL_GATE` 的
43
+ * `derivedDefaultNote` 写着「真姿态只有 always/classify 两个」,`MODEL_DEFAULT_THINKING` 写着
44
+ * 「缺席/off ⇒ 不铸」,而**没有任何一条腿读那两句话**:`off` 照样留在回显闭集里,回显校验对它放行。
45
+ *
46
+ * ⇒ 把那半句散文变成一处**校验腿真读的声明**:`sanitizeEnumValue` 问 `echoWords ?? enumValues`,
47
+ * 于是「这一格的合法回显值」从此只有一个属主,不再靠人脑对两个词表做差集。
48
+ *
49
+ * 🔴 **不上 wire**:消费方(UI 下拉、非法值的纠错提示)要的是「可写集」= `enumValues`,那一义不变;
50
+ * 本字段服务的是服务端自己的回显纪律。一个词进得了 `enumValues` 却不在这里 ⇒ 它的语义恒是
51
+ * 「**缺席的另一种拼法**」(写下去与不写同效),那正是目前仅有的两行的形。
52
+ */
53
+ readonly echoWords?: readonly string[];
37
54
  /**
38
55
  * S-100 —— **底层布尔 ↔ 闭集词**的行内映射,只给「实算腿回布尔、而行对外说词」的 enum 行。
39
56
  *
@@ -91,7 +108,7 @@ export interface ConfigCatalogRow {
91
108
  readonly effectiveValue: CatalogValue;
92
109
  /**
93
110
  * S-100(additive;**只在真非法时铸,恒不铸 `false`**)—— 本行的实算值**落在它自己宣告的
94
- * `enumValues` 之外**,或它的实算腿回了一个本行没有声明映射的布尔。
111
+ * 回显闭集之外**(`echoWords ?? enumValues`,S-201①),或它的实算腿回了一个本行没有声明映射的布尔。
95
112
  *
96
113
  * 在场时 `effectiveValue` 恒 `null`:回显一个集外值等于目录在教消费方写一个写回去就非法的词,
97
114
  * 而**静默丢**会让「配错了」与「真的没设」在 wire 上同形([ref] 响亮臂)。
@@ -204,7 +204,7 @@ export const CONFIG_CATALOG = [
204
204
  r("LOG_LEVEL", "observability", "string", "日志档位", { staticDefault: "info", configKey: "logLevel" }),
205
205
  r("LSP_ENABLED", "orchestration", "boolean", "LSP 沙箱腿 opt-in(需烤好的 LSP 模板;host 腿另有 LSP_HOST_ENABLED)", { staticDefault: "false", configKey: "lspEnabled" }),
206
206
  r("LSP_HOST_ENABLED", "orchestration", "boolean", "LSP host 腿开关(本机 language server,优雅降级)", { staticDefault: "true", configKey: "lspHostEnabled" }),
207
- r("MANUAL_MODE_SHELL_GATE", "approval", "enum", "手动模式 shell 门收紧腿(off=no-op 非放松 —— 本键只有收紧半场;拼错拒启)", { danger: SEC, enumValues: ["always", "classify", "off"], resolve: (c) => c.manualModeShellGate ?? null, derivedDefaultNote: "off/未设 ⇒ 本键不贡献任何东西(归一成键缺席)⇒ 回显 null;真姿态只有 always/classify 两个" }),
207
+ r("MANUAL_MODE_SHELL_GATE", "approval", "enum", "手动模式 shell 门收紧腿(off=no-op 非放松 —— 本键只有收紧半场;拼错拒启)", { danger: SEC, enumValues: ["always", "classify", "off"], echoWords: ["always", "classify"], resolve: (c) => c.manualModeShellGate ?? null, derivedDefaultNote: "off/未设 ⇒ 本键不贡献任何东西(归一成键缺席)⇒ 回显 null;真姿态只有 always/classify 两个" }),
208
208
  r("MAX_PRINCIPAL_COST_USD", "limitsHttp", "number", "per-principal 窗内成本天花板(0=关)", { staticDefault: "0", danger: BILL, configKey: "maxPrincipalCostUsd", center: cLimits("maxPrincipalCostUsd") }),
209
209
  r("MAX_TASK_COST_USD", "limitsHttp", "number", "单任务成本天花板(0=关)", { staticDefault: "0", danger: BILL, configKey: "maxTaskCostUsd", center: cLimits("maxTaskCostUsd") }),
210
210
  r("MAX_TASK_TOKENS", "limitsHttp", "number", "单任务 token 天花板(per-slice 窗;0=关)", { staticDefault: "0", danger: BILL, configKey: "maxTaskTokens", center: cLimits("maxTaskTokens") }),
@@ -270,7 +270,7 @@ export const CONFIG_CATALOG = [
270
270
  r("MODEL_COST_CACHE_WRITE", "modelPlane", "number", "主模型 cache-write 单价", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.cacheWrite ?? null, center: cModels("models[].cost.cacheWrite") }),
271
271
  r("MODEL_COST_INPUT", "modelPlane", "number", "主模型 input 单价(NaN 会让成本天花板整条消失 ⇒ 坏值拒启)", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.input ?? null, center: cModels("models[].cost.input") }),
272
272
  r("MODEL_COST_OUTPUT", "modelPlane", "number", "主模型 output 单价", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.output ?? null, center: cModels("models[].cost.output") }),
273
- r("MODEL_DEFAULT_THINKING", "modelPlane", "enum", "主模型默认思考档(llm-core ThinkingLevel;off=不设模型级默认;未知词拒启)", { enumValues: ["minimal", "low", "medium", "high", "xhigh", "max", "off"], derivedDefaultNote: "缺席/off ⇒ 不铸 defaultThinking(逐任务档由 spec>role 决定)", resolve: (c) => c.model.defaultThinking ?? null }),
273
+ r("MODEL_DEFAULT_THINKING", "modelPlane", "enum", "主模型默认思考档(llm-core ThinkingLevel;off=不设模型级默认;未知词拒启)", { enumValues: ["minimal", "low", "medium", "high", "xhigh", "max", "off"], echoWords: ["minimal", "low", "medium", "high", "xhigh", "max"], derivedDefaultNote: "缺席/off ⇒ 不铸 defaultThinking(逐任务档由 spec>role 决定)", resolve: (c) => c.model.defaultThinking ?? null }),
274
274
  r("MODEL_DEGRADE_AT_COST_FRACTION", "modelPlane", "number", "预算降级触发比(spent/budget;>1=实际别降级)", { staticDefault: "0.7", danger: BILL }),
275
275
  r("MODEL_DEGRADE_ON", "modelPlane", "csv", "反应式降级触发词表(core DegradeReason 全集;拼错拒启)", { center: { domain: "models", path: "degrade", centerKey: "models", precedence: "center-wins", timing: "restart", restartSlice: "degrade-route" } }),
276
276
  r("MODEL_DEGRADE_REACTIVE", "modelPlane", "boolean", "反应式降级开关(degrade-route 重启片只在它开时活)", { staticDefault: "false", center: { domain: "models", path: "degrade", centerKey: "models", precedence: "center-wins", timing: "restart", restartSlice: "degrade-route" } }),
@@ -489,7 +489,7 @@ function stripUrlUserinfo(v) {
489
489
  return u.href;
490
490
  }
491
491
  function sanitizeEnumValue(spec, v) {
492
- const words = spec.enumValues;
492
+ const words = spec.echoWords ?? spec.enumValues;
493
493
  if (v === null)
494
494
  return { value: null };
495
495
  if (words === undefined)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * S-189 —— core `GOVERNANCE_CODES` **闭集**在本分诊面上的逐码处置表。
3
+ *
4
+ * 为什么单独一张表而不是继续往下面的 if 链里加行:`failureCodeOf` 是**开集**(顶注写死「消费点一律带
5
+ * default 臂,禁按穷举写」),而开集里嵌着 core 这一个**闭集注册表**。闭集的成员共享两个性质——同一条
6
+ * prepare-throw→failed-result 管道、**零计费**(prepare 期就拒了)——所以它们要么被分诊成 4xx/5xx,要么
7
+ * 就落进 200-with-failed-body 并被 `finalizeTaskResult` **计费**。此前四个成员在 if 链里被点名、另外四个
8
+ * 静默落 200;core 往注册表加一个码时,server 这边不红不报,新码当场进「200 + 计费」。
9
+ *
10
+ * 🔴 表的类型是 `Record<GovernanceCode, …>`:**core 加成员 ⇒ 这里缺行 ⇒ tsc 红**,逼人当场判档。
11
+ * 值 `null` = **有意**不在本腿分诊(理由逐条写在行上),与「忘了写」在类型上不同形 —— 这是本表存在的
12
+ * 全部意义:让「没表态」不可能静默发生。开集那一半(下面的 if 链 + `resume_at.` 前缀臂 + default)不动。
13
+ *
14
+ * 逐码预期的**行为面**镜像在 `test/resume-anchor.test.ts` 的 S-189 段(同型同义、刻意不共用这张表:
15
+ * 测试喂码进函数看状态码,不读本表,否则表写错了两边一起错还全绿)。
16
+ */
17
+ export declare const GOVERNANCE_HTTP_STATUS: {
18
+ readonly "memory.admission_denied": 403;
19
+ readonly "memory.admission_required": 503;
20
+ readonly "memory.capture_optout_denied": 403;
21
+ readonly "config.tool_mount_denied": 400;
22
+ readonly "config.locked_key": 400;
23
+ readonly "usage.window_exhausted": null;
24
+ readonly "config.compliance_required": null;
25
+ readonly "config.compliance_denied": null;
26
+ };
27
+ //# sourceMappingURL=governance-http-status.d.ts.map
@@ -0,0 +1,11 @@
1
+ export const GOVERNANCE_HTTP_STATUS = {
2
+ "memory.admission_denied": 403,
3
+ "memory.admission_required": 503,
4
+ "memory.capture_optout_denied": 403,
5
+ "config.tool_mount_denied": 400,
6
+ "config.locked_key": 400,
7
+ "usage.window_exhausted": null,
8
+ "config.compliance_required": null,
9
+ "config.compliance_denied": null,
10
+ };
11
+ //# sourceMappingURL=governance-http-status.js.map
@@ -3,9 +3,44 @@ import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type GovernancePosture } from "../active-run-conflict.js";
5
5
  import { type RouteCtx } from "../route-ctx.js";
6
+ import { type KeyClosureIssue } from "../../request-key-closure.js";
7
+ /**
8
+ * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
9
+ * 全部受理 ⇒ `null`。判据边界与孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue`)逐字同一条:
10
+ * **只判键名**(值形校验归各自既有的门,本门零抢话);体本身非对象 ⇒ 本门不判(留给「body must be
11
+ * { decision … }」那道根形门,免得同一个体被两句话抢答)。
12
+ *
13
+ * 🔴 **重放不额外过门**,也不需要:本腿的幂等回放(`decideIdempotentReplay`)重放的是**同一次请求的同一份
14
+ * 体**,它与首决走同一条路由、同一道门;而 checkpoint 的 resume 重建腿根本不读这个体(它读盘上的 body,
15
+ * 走 `resolveSpec` 的宽容层)⇒ 4xx 砖不到任何存量 parked 任务。这与 tasks 面「RESUME 重放不过门」是同一条律
16
+ * 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
17
+ */
18
+ export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
6
19
  export declare const ASSISTANT_PREEMPT_RE: RegExp;
7
20
  export declare const ASSISTANT_RESUME_RE: RegExp;
8
21
  export declare const ASSISTANT_PLAN_REVIEW_RE: RegExp;
22
+ /**
23
+ * S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
24
+ *
25
+ * ## 三个域,三个属主(这才是归一:每个域的读法只写一次,而不是三处各挑一种写法)
26
+ * `body.reason` 在这条路由上会被读三次,而它们的属主**不是同一个**:
27
+ * · **签名域**(直连门的 HMAC 信封第五位)—— 属主是**跨仓字节契约**(§6.2 / `approvalHmacMessage`,
28
+ * 签名器在 cli / SDK)。那份契约明写空串与 `null` 是两个不同的被签值。⇒ **逐字线值,永不折叠**;
29
+ * 在这里折一刀,一份合法签发的空理由证明就永远 401(合并复审 R1 [high] 真复现过)。
30
+ * · **上送域**(`resumePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
31
+ * (7.19.0 / [ref] 把「人的结算」收进一个铸点,空文本在那里折成缺席,live 与 durable decide 两条路径同形)。
32
+ * ⇒ **原样透传**;跨仓宪法「源头修复禁下游旁路」:在下游再折一次就是同一语义的第二个写者,
33
+ * 今天两者答案相同,上游哪天改法就变成一条谁都查不出来的分歧。
34
+ * · **本仓自铸域**(幂等回放的**身份**)—— 属主就是本仓:它回答「这次重试与首决是不是同一个决议」。
35
+ * ⇒ **本函数**。此前那里是一个裸真值判(`body.reason ? …`),与上面两处混在一起看就是三种拼法;
36
+ * 收进一只具名读法之后,「什么算一个理由」在本仓只有这一个答案,而另外两个域各自点名了自己的属主。
37
+ *
38
+ * 值域前置门(非串 400 / 超长 413)在两条路由里各自先跑,本函数只做「空串 ⇒ 缺席」这一件事。
39
+ * 纯空白**仍是**一条理由(上游把这一形逐字钉成 current behaviour,本仓跟着它、不自作主张多折一层)。
40
+ */
41
+ export declare function decisionReasonOf(body: {
42
+ reason?: unknown;
43
+ }): string | undefined;
9
44
  /** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
10
45
  * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
11
46
  * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
@@ -17,9 +17,40 @@ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_
17
17
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
18
18
  import { governanceOriginOf } from "../active-run-conflict.js";
19
19
  import { sendResumeOutcome } from "../route-ctx.js";
20
+ import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
21
+ const DECIDE_BODY_DECLARED_KEYS = [
22
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "updatedInput",
23
+ ];
24
+ const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
25
+ const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
26
+ export function decideBodyKeyIssue(raw) {
27
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
28
+ return null;
29
+ const buckets = { unknown: [], unsupported: [] };
30
+ scanKeyClosure(raw, "", ACCEPTED_DECIDE_BODY, UNSUPPORTED_DECIDE_BODY, buckets);
31
+ if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
32
+ return null;
33
+ buckets.unknown.sort();
34
+ buckets.unsupported.sort();
35
+ const parts = [
36
+ ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a decide-request key — check the spelling)")] : []),
37
+ ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, or a path parameter — never read from this body)")] : []),
38
+ ];
39
+ return {
40
+ message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
41
+ `(a misspelled \`remember\` or \`boundInputHash\` silently turns off an exemption / the decision binding). ` +
42
+ `${parts.join("; ")}. ` +
43
+ `Accepted: ${[...ACCEPTED_DECIDE_BODY].sort().join(",")}`,
44
+ unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
45
+ unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
46
+ };
47
+ }
20
48
  export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
21
49
  export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
22
50
  export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
51
+ export function decisionReasonOf(body) {
52
+ return typeof body.reason === "string" && body.reason !== "" ? body.reason : undefined;
53
+ }
23
54
  export function isQuestionAnswer(v) {
24
55
  if (!v || typeof v !== "object" || Array.isArray(v))
25
56
  return false;
@@ -452,6 +483,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
452
483
  sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
453
484
  return;
454
485
  }
486
+ {
487
+ const keyIssue = decideBodyKeyIssue(body);
488
+ if (keyIssue) {
489
+ sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
490
+ return;
491
+ }
492
+ }
455
493
  if (body.reason !== undefined && typeof body.reason !== "string") {
456
494
  sendError(res, 400, "request.field_invalid", "reason must be a string");
457
495
  return;
@@ -564,7 +602,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
564
602
  payload: {
565
603
  ...(decision === "approve" && binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
566
604
  ...(answer !== undefined ? { answer } : {}),
567
- ...(body.reason ? { reason: body.reason } : {}),
605
+ ...((r) => (r !== undefined ? { reason: r } : {}))(decisionReasonOf(body)),
568
606
  },
569
607
  deciderPrincipal,
570
608
  explicitOperator,
@@ -13,7 +13,7 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
13
13
  import { mintCancelledResult } from "../../run-cancel-context.js";
14
14
  import { recordResourceWindow } from "../../resource-window.js";
15
15
  import { redactSecrets } from "../../trace/redact.js";
16
- import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData } from "../../trace/project.js";
16
+ import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
17
17
  import { cascadeConfig, runMeta } from "../run-meta.js";
18
18
  import { scopedIdempotencyKey } from "../idempotency.js";
19
19
  import { sendJson, sendError, sseHeaders } from "../send.js";
@@ -502,12 +502,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
502
502
  }
503
503
  else if (ev.type === "text_delta" || ev.type === "text_end" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
504
504
  const e = ev;
505
- const ident = {
506
- ...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
507
- ...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}),
508
- ...(e.sourceTaskId !== undefined ? { sourceTaskId: e.sourceTaskId } : {}),
509
- ...(e.bgAgentId !== undefined ? { bgAgentId: e.bgAgentId } : {}),
510
- };
505
+ const ident = liveIdentityFields(e);
511
506
  const textEnd = ev.type === "text_end" ? textEndEventData(e) : undefined;
512
507
  const arm = ev.type === "text_delta"
513
508
  ? { type: "text_delta", delta: e.delta, ...ident }
@@ -523,7 +518,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
523
518
  sseData(res, arm);
524
519
  }
525
520
  else {
526
- sseData(res, ev);
521
+ sseData(res, { ...ev });
527
522
  }
528
523
  }
529
524
  });
@@ -616,7 +611,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
616
611
  if (deps.toolApproval) {
617
612
  const toolApproval = deps.toolApproval;
618
613
  prepared.spec.onAsk = windowZero
619
- ? () => Promise.resolve(toolApproval.unattendedAskOutcome())
614
+ ? (askReq) => Promise.resolve(toolApproval.unattendedAskOutcome({ taskId: askTaskId, owner: askOwner, toolName: askReq.toolName }))
620
615
  : toolApproval.boundAsk({
621
616
  owner: askOwner,
622
617
  taskId: askTaskId,
@@ -730,18 +730,42 @@ export type FlatServiceDeps = ServiceCoreDeps & Partial<ServiceStoreDeps & Servi
730
730
  export type ServiceDeps = FlatServiceDeps & ServiceDepGroups;
731
731
  /** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
732
732
  export declare function flattenServiceDeps(deps: ServiceDeps): FlatServiceDeps;
733
- /** Personalization caps: server-pinned hard limits — a UI may be stricter, never looser. */
734
- export declare const MAX_USER_SKILLS = 10;
735
733
  /** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
736
734
  * dist/core/runner/synthetic-tools.js:84,未从 core 包根导出,故钉本地孪生+本出处注记)。修7 曾对齐旧
737
735
  * 20k 截断门([ref] 飞轮考据:20k 是压缩保留区口径,invoke 时刻用错),core 1.293 撤门后本门跟随回收
738
- * ([ref]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。 */
736
+ * ([ref]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。
737
+ *
738
+ * 🔴 **这是技能面**唯一**剩下的尺寸门,而且它不是超集帽**:引擎在装配期对超 1 MiB 的技能本就整拒
739
+ * (config 阶段错误),门上先答只是把同一个判决搬到调用方看得懂的地方。[ref] 退役的三条帽
740
+ * (条数 / description / name 长度)都没有这个性质——它们是本仓自造的、CC 与引擎都没有的门。 */
739
741
  export declare const MAX_SKILL_CONTENT_CHARS = 1048576;
740
742
  export declare const MAX_SYSTEM_PROMPT_CHARS = 16384;
741
743
  /** Cap on a request's `outputSchema` (JSON-serialized) — an uncapped schema bloats the prompt + the per-turn
742
744
  * validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
743
745
  export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
744
- /** Validate `body.skills` (caps + shape). Returns an error string (→ 400) or null when acceptable. */
746
+ /**
747
+ * Validate `body.skills` —— **形**校验 + 一条引擎孪生门。返回错误串(→ 400)或 null。
748
+ *
749
+ * 🔴 **[ref](7.77.0,硬 BREAKING;零别名零双读)**:此前这里有三条本仓自造的拒绝臂,全部退役 ——
750
+ * · `at most 10 skills per request`(条数帽);
751
+ * · `skill.description must be a string of at most 1024 characters`(描述长度帽);
752
+ * · `skill.name must be a non-empty string of at most 64 characters` 里的 **长度**那一半。
753
+ *
754
+ * 退役的判据是**上游两侧都没有同形门**:CC 2.1.259 的技能清单渲染(`cli259.js:23195` 的预算算法)
755
+ * 在超预算时把**描述裁短**、再不够就退成 name-only,**从不**因为条数多或描述长而拒掉一条技能;
756
+ * 它的 `Ir()` 名字校验也只判非空 / 无配对代理 / 无首尾空白 / 无括号逗号控制字符,没有长度帽。
757
+ * 引擎侧同理:唯一整拒的是 1 MiB 的单技能加载门。于是那三条帽是「装了 11 个技能就 400」这种
758
+ * 纯本仓面的伤——收益是零(预算由渲染侧按字节兜),代价是用户装不进自己的技能库。
759
+ *
760
+ * **补偿**(三问的第三问):`settings.skillListingBudgetFraction` / `settings.skillListingMaxDescChars`
761
+ * 两个旋钮直达引擎的渲染预算(per-turn 成本的真属主)。
762
+ * ⚠️ **fraction 是**单向**的**:引擎的预算 = `min(窗口 × fraction, 8 000 字节)`,8 000 是**投递车道的结构
763
+ * 上限**(清单帧与同 turn 的其它附件共用一份完整送达额度)⇒ **调低生效、调高被夹**;调高唯一的用处是让
764
+ * 一个声明窗口小于 200 000 token 的模型把清单预算拿回大窗口的水平。别把这句读成「调用方可以随便加大」。
765
+ * 体积面照旧由**非技能专属**的 8 MiB 请求体上限兜住(`MAX_BODY`),那是一条一直都在的门。
766
+ *
767
+ * 🔴 **server 侧禁裁列表 / 禁截描述**:渲染归引擎(单一属主)。本函数只判「这一条技能的形能不能用」。
768
+ */
745
769
  export declare function validateUserSkills(skills: unknown): string | null;
746
770
  /**
747
771
  * Managed-Agents-style HTTP/SSE service surface.
@@ -9,7 +9,8 @@ import {} from "../config-center/facade.js";
9
9
  import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
10
10
  import { verifyDirectDoorProof } from "../principal-jwt.js";
11
11
  import {} from "../memory-sync.js";
12
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, taskSettingsKeyIssue } from "../task-settings.js";
12
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
13
+ import { taskBodyKeyIssue } from "../request-key-closure.js";
13
14
  import { selfOrchestrationDenial } from "../task-workflow.js";
14
15
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
15
16
  import { parseHooksConfig } from "../hooks/hook-runner.js";
@@ -37,6 +38,7 @@ import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartE
37
38
  import { redactSecrets } from "../trace/redact.js";
38
39
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
39
40
  import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
41
+ import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-segment.js";
40
42
  import { errorText } from "../observability/err-text.js";
41
43
  import { recordFailOpen } from "../observability/fail-open.js";
42
44
  import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
@@ -95,7 +97,6 @@ export function flattenServiceDeps(deps) {
95
97
  return deps;
96
98
  return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
97
99
  }
98
- export const MAX_USER_SKILLS = 10;
99
100
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
100
101
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
101
102
  export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
@@ -104,15 +105,13 @@ export function validateUserSkills(skills) {
104
105
  return null;
105
106
  if (!Array.isArray(skills))
106
107
  return "skills must be an array of { name, description, content }";
107
- if (skills.length > MAX_USER_SKILLS)
108
- return `at most ${MAX_USER_SKILLS} skills per request`;
109
108
  for (const s of skills) {
110
109
  if (s === null || typeof s !== "object")
111
110
  return "each skill must be an object { name, description, content }";
112
- if (typeof s["name"] !== "string" || s["name"].length === 0 || s["name"].length > 64)
113
- return "skill.name must be a non-empty string of at most 64 characters";
114
- if (typeof s["description"] !== "string" || s["description"].length > 1024)
115
- return "skill.description must be a string of at most 1024 characters";
111
+ if (typeof s["name"] !== "string" || s["name"].length === 0)
112
+ return "skill.name must be a non-empty string";
113
+ if (typeof s["description"] !== "string")
114
+ return "skill.description must be a string";
116
115
  if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
117
116
  return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
118
117
  }
@@ -525,6 +524,13 @@ export function createHttpServer(rawDeps) {
525
524
  sendError(res, 400, "request.field_invalid", "objective must not be empty or whitespace-only (an empty user message poisons the session history on strict providers)");
526
525
  return null;
527
526
  }
527
+ {
528
+ const bodyKeyIssue = taskBodyKeyIssue(body);
529
+ if (bodyKeyIssue) {
530
+ sendError(res, 400, "request.body_shape", bodyKeyIssue.message, { unknownKeys: bodyKeyIssue.unknownKeys, unsupportedKeys: bodyKeyIssue.unsupportedKeys });
531
+ return null;
532
+ }
533
+ }
528
534
  if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
529
535
  sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
530
536
  return null;
@@ -644,6 +650,14 @@ export function createHttpServer(rawDeps) {
644
650
  sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
645
651
  return null;
646
652
  }
653
+ if (st.skillListingBudgetFraction !== undefined && !skillListingBudgetFractionOk(st.skillListingBudgetFraction)) {
654
+ sendError(res, 400, "request.field_invalid", "settings.skillListingBudgetFraction must be a number greater than 0 and at most 1");
655
+ return null;
656
+ }
657
+ if (st.skillListingMaxDescChars !== undefined && !skillListingMaxDescOk(st.skillListingMaxDescChars)) {
658
+ sendError(res, 400, "request.field_invalid", "settings.skillListingMaxDescChars must be a positive integer");
659
+ return null;
660
+ }
647
661
  if (st.hooks !== undefined && st.hooks !== null) {
648
662
  const hooksParsed = parseHooksConfig(st.hooks);
649
663
  if (!hooksParsed.config) {
@@ -1355,7 +1369,7 @@ export function createHttpServer(rawDeps) {
1355
1369
  if (deps.toolApproval && taskId) {
1356
1370
  const toolApproval = deps.toolApproval;
1357
1371
  resumeTaskConfig.onAsk = approvalLeg.windowZero
1358
- ? () => Promise.resolve(toolApproval.unattendedAskOutcome())
1372
+ ? (askReq) => Promise.resolve(toolApproval.unattendedAskOutcome({ taskId, owner: principal ?? null, toolName: askReq.toolName }))
1359
1373
  : toolApproval.boundAsk({
1360
1374
  owner: principal ?? null,
1361
1375
  taskId,
@@ -1532,6 +1546,7 @@ export function createHttpServer(rawDeps) {
1532
1546
  const startSeq = rs && taskId ? await rs.maxSeq(taskId) : 0;
1533
1547
  let text = "";
1534
1548
  let reasoning = "";
1549
+ let reasoningEventId;
1535
1550
  const persistThinking = deps.config.traceThinking;
1536
1551
  const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
1537
1552
  const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
@@ -1555,8 +1570,9 @@ export function createHttpServer(rawDeps) {
1555
1570
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
1556
1571
  const flush = async () => {
1557
1572
  if (reasoning) {
1558
- await append("reasoning", modelTextEventData({ text: reasoning }));
1573
+ await append("reasoning", modelTextEventData({ text: reasoning, eventId: reasoningEventId }));
1559
1574
  reasoning = "";
1575
+ reasoningEventId = undefined;
1560
1576
  }
1561
1577
  if (text) {
1562
1578
  await append("text", modelTextEventData({ text, eventId: anchor.firstTextEventId }));
@@ -1632,10 +1648,14 @@ export function createHttpServer(rawDeps) {
1632
1648
  text += ev.delta;
1633
1649
  anchor.onText(ev.eventId);
1634
1650
  break;
1635
- case "reasoning_delta":
1636
- if (persistThinking)
1637
- reasoning += ev.delta;
1651
+ case "reasoning_delta": {
1652
+ const rb = reasoningDeltaBytes(ev);
1653
+ if (persistThinking && rb !== undefined) {
1654
+ reasoning += rb;
1655
+ reasoningEventId ??= reasoningSegmentEventId(ev.eventId);
1656
+ }
1638
1657
  break;
1658
+ }
1639
1659
  case "tool_start":
1640
1660
  anchor.onTool();
1641
1661
  await flush();