@sema-agent/server 7.77.0 → 7.78.1

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 (43) hide show
  1. package/dist/approval-card.d.ts +24 -1
  2. package/dist/approval-card.js +13 -0
  3. package/dist/boot/resolve-spec.js +3 -0
  4. package/dist/boot/runner-deps.js +3 -1
  5. package/dist/config-catalog.js +1 -0
  6. package/dist/config-center/read-warnings.d.ts +10 -1
  7. package/dist/config-center/read-warnings.js +5 -1
  8. package/dist/config-provider.js +2 -2
  9. package/dist/config-types.d.ts +13 -1
  10. package/dist/config.js +2 -1
  11. package/dist/http/routes/approvals-assistant.d.ts +22 -0
  12. package/dist/http/routes/approvals-assistant.js +4 -1
  13. package/dist/http/routes/tasks.js +14 -9
  14. package/dist/http/routes/workflows.js +7 -1
  15. package/dist/http/server.d.ts +28 -4
  16. package/dist/http/server.js +47 -13
  17. package/dist/observability/fail-open.d.ts +4 -0
  18. package/dist/observability/fail-open.js +4 -0
  19. package/dist/observability/run-terminal-log.js +1 -0
  20. package/dist/orchestration/workflow-agent-session-index.d.ts +7 -3
  21. package/dist/parked-decide.d.ts +27 -12
  22. package/dist/parked-decide.js +1 -1
  23. package/dist/plugins/workflow-run-store-sql.d.ts +7 -3
  24. package/dist/request-key-closure.d.ts +30 -0
  25. package/dist/request-key-closure.js +56 -0
  26. package/dist/runs.js +3 -1
  27. package/dist/runtime-governance.d.ts +18 -4
  28. package/dist/runtime-governance.js +2 -2
  29. package/dist/spec-fields.d.ts +3 -1
  30. package/dist/spec-fields.js +4 -5
  31. package/dist/task-settings.d.ts +18 -16
  32. package/dist/task-settings.js +17 -29
  33. package/dist/tool-approval.d.ts +35 -3
  34. package/dist/tool-approval.js +41 -19
  35. package/dist/trace/core-keyset-guard.d.ts +8 -8
  36. package/dist/trace/ledger-events.d.ts +7 -1
  37. package/dist/trace/ledger-sink.js +19 -6
  38. package/dist/trace/model-segment.d.ts +32 -2
  39. package/dist/trace/model-segment.js +16 -6
  40. package/dist/trace/project.d.ts +95 -3
  41. package/dist/trace/project.js +128 -12
  42. package/dist/trace/redact.js +159 -13
  43. package/package.json +4 -4
@@ -49,16 +49,26 @@ export type ParkOrigin = {
49
49
  park: WorkflowRunParkKey;
50
50
  }
51
51
  /**
52
- * S-252 —— **这条记录的 park 真源读不出来**(第四种出身,不是第四条车道)。
52
+ * S-252 —— **这条记录的 park 真源在本车道读不出来**(第四种出身,不是第四条车道)。
53
53
  *
54
54
  * 索引行还在、run 行也还在,但记录上没有一份读得出来的 `parks[]`:旧引擎(core < 7.17.0)写下的存量行
55
- * 根本没有这一位,或者某一条条目的形坏了。而这一位是**引擎自己**的 park 责任表 —— core 的 resume 准入
56
- * 读不到它就整条拒(`workflow.park_truth_unreadable` / `parks_unreadable`,亲读安装树
57
- * `dist/orchestration/workflow.js:2217-2218`;条目形坏 ⇒ `park_binding_broken`,`:2222-2224`)。
58
- * ⇒ 这条决定**结构上**送不出去:答 200「已交给宿主的下一跑」就是一张兑付不了的回执。
55
+ * 根本没有这一位,或者某一条条目的形坏了。而这一位是**引擎自己**的 park 责任表,也是本车道把调用方的
56
+ * checkpoint join 到某一条 park 的**单一真源** ⇒ 这条决定**结构上**送不出去:答 200「已交给宿主的
57
+ * 下一跑」就是一张兑付不了的回执。
59
58
  * 此前这一格与「索引落后一拍 / 这条 run 已不再负责它」两种**正常** miss 完全同形,于是落 legacy 腿拿
60
59
  * 一个错因 409。判别式据真源的**可读性**把这一格分出来,三个消费点各自响亮处置(decide 拒 + 不驱动
61
60
  * 宿主续跑;SLA sweep 照旧静默跳过——它同样复核不了)。
61
+ *
62
+ * 🔴 **7.78.0 / core 7.18.0([ref]):本臂的「为什么拒」没变,「拒了之后干什么」变了。**
63
+ * 旧注(与旧拒文)断言 core 的 resume 准入对 `!Array.isArray(prior.parks)` 一律整条拒,并据此告诉运维
64
+ * 「这条 run 恢复不了,开一条新的」。core 7.18.0 把那条准入拆成**三臂**([ref]「走 a″」),该断言
65
+ * 自此为假:①记录带 `parks` ⇒ 那张表独自作数(不变);②**`parks` 缺席 ⇒ 候选从 journal 的 `parked` 臂
66
+ * 与记录自己 `parked` 腿行的 `parkedCheckpointToken` 取并集派生**,非空即准入(每个派生候选仍逐个
67
+ * `checkpointStore.get(token)` 验真);③缺席且并集为空 ⇒ 仍拒(`parks_unreadable`,条件收窄到这一格)。
68
+ * ⇒ 存量记录**多数可以被 resume 救回来**,而旧拒文让人去开新 run = 把一条救得回来的 run 劝弃。
69
+ * 本车道**仍然拒**(core 逐字:那种记录的赎回键是在准入期派生的、**从记录上读不出来**;本腿要么拒、
70
+ * 要么就得在服务端重算 core 的三臂 —— 后者正是这次统一掉的双读),但恢复动作改成真话:**去 resume
71
+ * 这条 run**。准入一旦走完,core 会把 `parks` 写上记录([ref]),下一次 `/decide` 就走正常的 workflow 臂。
62
72
  */
63
73
  | {
64
74
  kind: "workflow_parks_unreadable";
@@ -228,13 +238,18 @@ export type WorkflowParkedDecidePlan = {
228
238
  /**
229
239
  * S-252 —— **park 真源读不出来的那条记录的响亮拒**(与上面那族拒臂同一种返回形)。
230
240
  *
231
- * 为什么是拒而不是「尽力而为地投递一次」(机理按安装树 core 7.17.1 `dist/orchestration/workflow.js` 亲读):
232
- * · 记录上的 `parks[]` 是**引擎自己**的 park 责任表,而且是**下一次 resume 的准入输入**:
233
- * `!Array.isArray(prior.parks)` ⇒ 整条拒 resume(`workflow.park_truth_unreadable` / `parks_unreadable`,
234
- * :2217-2218);某一条条目形坏 ⇒ `park_binding_broken`(:2222-2224)。两者都不是「少一条 park」,
235
- * 是**这条 run 再也 resume 不了** —— 而本车道的投递腿恰恰是让宿主去 `Workflow({resumeFromRunId})`。
236
- * ⇒ 决定结构上送不出去。答 200「已交给宿主的下一跑」就是一张兑付不了的回执:卡还 pending、人以为决完了。
237
- * ⇒ 拒,并把真话说出来:这条 run 恢复不了,开一条新的;卡仍 pending,什么都没有被伪造。
241
+ * 为什么是拒而不是「尽力而为地投递一次」:记录上的 `parks[]` 是本车道把调用方的 checkpoint join 到某一条
242
+ * park 的**单一真源**。读不出来 ⇒ 这条决定结构上送不出去,答 200「已交给宿主的下一跑」就是一张兑付不了的
243
+ * 回执:卡还 pending、人以为决完了。⇒ 拒,并把真话说出来;卡仍 pending,什么都没有被伪造。
244
+ *
245
+ * 🔴 **7.78.0 / core 7.18.0([ref]):真话变了一句,拒没变。** 亲读安装树 7.18.0 的三臂准入(判别式顶注逐字
246
+ * 记着):`parks` 缺席时 core **从 journal 的 `parked` 臂 + 记录自己 `parked` 腿行的 `parkedCheckpointToken`
247
+ * 派生候选**并逐个向 checkpoint 店验真,非空即准入 —— 所以「这条 run 恢复不了」自 7.18.0 起对**多数**存量
248
+ * 记录是假的,照旧文答就是劝人弃掉一条救得回来的 run。本腿仍拒的理由是另一条(core 逐字):那种记录的赎回键
249
+ * 是**在准入期派生的、从记录上读不出来**,服务端要么拒、要么重算 core 的三臂(被这次统一掉的双读)。
250
+ * ⇒ 恢复动作 = **去 resume 这条 run**(`Workflow({resumeFromRunId})`):准入走完 core 会把 `parks` 写上
251
+ * 记录([ref]),下一次 `/decide` 走正常 workflow 臂。若引擎那一步也拒(journal 与腿行都给不出候选,
252
+ * core 的第三臂),那才是真的恢复不了。
238
253
  *
239
254
  * 这条臂的受害集 = **存量持久数据**(core < 7.17.0 的引擎写下、升级后还 parked 着的 run 记录)。硬 breaking
240
255
  * 三句里持久数据那一句要的就是这个形:响亮拒,不做静默双读(读 `agents[]` 补一份身份正是被退役的双算)。
@@ -89,7 +89,7 @@ export function denyWorkflowParksUnreadable(origin) {
89
89
  return {
90
90
  status: 409,
91
91
  body: {
92
- error: "this parked workflow agent's run record carries no readable list of the parks the run owns — the engine writes that list when it parks, and it is the only proof that this checkpoint token belongs to that run. A record written by an engine older than the one deployed here (or one whose entries are damaged) is refused by a resume of the run itself for the same reason, so the decision is NOT delivered rather than answered with a receipt nothing can redeem. The approval card stays pending and nothing is forged: this run cannot be resumed — start a new run",
92
+ error: "this parked workflow agent's run record carries no readable list of the parks the run owns — the engine writes that list when it parks, and it is the only proof that this checkpoint token belongs to that run. This route cannot join your checkpoint to a park without it, so the decision is NOT delivered rather than answered with a receipt nothing can redeem. The approval card stays pending and nothing is forged. Resume the run itself instead: the engine derives the parks such a record owns from its journal and its own parked legs, proves each against the checkpoint store, and writes the list onto the record once that admission completes — after which this route works normally. Only if the engine also refuses that resume is the run truly unrecoverable",
93
93
  errorCode: "decide.workflow_parks_unreadable",
94
94
  runId: origin.runId,
95
95
  },
@@ -80,9 +80,13 @@ export declare function withRedactedError<T extends {
80
80
  *
81
81
  * 🔴 **`parks[]` 是这条阶梯唯一不许碰的东西**(core 7.17.0 [ref] 义务②)。
82
82
  * 它不是观测位,是**下一次 resume 的准入输入**:core 的 resume 准入在任何 agent 被派发之前读记录上的
83
- * 这一位,读不到就整条拒(`workflow.park_truth_unreadable` / `parks_unreadable`)——不是「少一条 park」,
84
- * 是**这条 run 再也 resume 不了**;它同时也是本仓 `/decide` 的 workflow 车道复核「这条 park 属于这条 run」
85
- * 的**单一真源**(S-252)。实现上三级全部走 `{...上一级}` 展开、没有任何一级点名裁它,机器背书 =
83
+ * 这一位。🔴 **7.78.0 / core 7.18.0([ref])口径更新**:读不到**不再**是整条拒 —— core 改成三臂,缺席时
84
+ * 从 journal 的 `parked` 臂 + 记录自己 `parked` 腿行的 `parkedCheckpointToken` 派生候选逐个验真,非空即
85
+ * 准入,空才拒(`parks_unreadable` 条件收窄到那一格)。但**本店剥它仍是灾难级**:带 `parks` 的记录走
86
+ * 第一臂,「那张表独自作数」—— 剥成缺席就把一条走第一臂的记录推去走派生臂,而派生的两个缓存只装这条 run
87
+ * **自己做出**的 park,继承来、从未跑到的那一条**只存在于被剥掉的那张表里**,于是它要么被漏掉、要么被
88
+ * 当成不存在而在 resume 时活着重跑([ref] 正要消灭的形)。它同时也是本仓 `/decide` 的 workflow 车道复核
89
+ * 「这条 park 属于这条 run」的**单一真源**(S-252)。实现上三级全部走 `{...上一级}` 展开、没有任何一级点名裁它,机器背书 =
86
90
  * `test/slim-oversize-run.test.ts` 里那两格(最狠的一级上 `parks` 仍逐字完整 + 负控:记录本来没有
87
91
  * 这一位时不许凭空补 `[]`)。缺席 ⇒ 不补:补 `[]` 会把「这版引擎没写这个位」伪造成「这条 run 没有 park」,
88
92
  * 而 core 对两者的处置相反。
@@ -0,0 +1,30 @@
1
+ /** 拒体里逐字回显的键名条数上限。键名原样回显(不猜语义),但**条数有界** —— 一个塞满键的体不许把 400 响应
2
+ * 放大成流量面。超出部分只报数。 */
3
+ export declare const MAX_KEY_ECHO = 20;
4
+ /** 键名回显的清洗:剥控制/双向字符(键名进的是 JSON 错误体与运维日志,一个 RLO 能把两行字读反),
5
+ * 再按 {@link MAX_KEY_ECHO_CHARS} 截断。清洗之外**逐字**,不做大小写/近似猜测。 */
6
+ export declare function echoKeyName(prefix: string, key: string): string;
7
+ /** 一次键闭集裁决的产物:一句 wire 文案 + 两张分类键名表(消费端可机读,不必解析文案)。 */
8
+ export interface KeyClosureIssue {
9
+ message: string;
10
+ /** 词表外的键(拼错/未来键),全路径形,已清洗+截断+排序+封顶。 */
11
+ unknownKeys: string[];
12
+ /** 上游合约(CC / SDK / 引擎)里合法、但本 service 不在这一面接线的键,同形。 */
13
+ unsupportedKeys: string[];
14
+ }
15
+ /** 逐键分拣:受理集内跳过,`tolerated` 命中进 `unsupported`,其余进 `unknown`。两桶都拒,标签只影响文案。 */
16
+ export declare function scanKeyClosure(bag: object, prefix: string, accepted: ReadonlySet<string>, tolerated: ReadonlySet<string>, into: {
17
+ unknown: string[];
18
+ unsupported: string[];
19
+ }): void;
20
+ /** 一张分类表的文案片段(封顶后只报数)。 */
21
+ export declare function keyClosureClause(names: string[], label: string): string;
22
+ /**
23
+ * FRESH 提交面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
24
+ * 全部受理 ⇒ `null`。
25
+ *
26
+ * 判据边界(与 settings 那只同源,有意窄):只判**键名**;体本身非对象 ⇒ 本门不判(留给既有的
27
+ * 「missing 'objective' string」那道门,免得同一个体被两句话抢答)。
28
+ */
29
+ export declare function taskBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
30
+ //# sourceMappingURL=request-key-closure.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
2
+ export const MAX_KEY_ECHO = 20;
3
+ const MAX_KEY_ECHO_CHARS = 64;
4
+ export function echoKeyName(prefix, key) {
5
+ const clean = key.replace(CONTROL_AND_BIDI_STRIP_RE, "");
6
+ return `${prefix}${clean.length > MAX_KEY_ECHO_CHARS ? `${clean.slice(0, MAX_KEY_ECHO_CHARS)}…` : clean}`;
7
+ }
8
+ export function scanKeyClosure(bag, prefix, accepted, tolerated, into) {
9
+ for (const k of Object.keys(bag)) {
10
+ if (accepted.has(k))
11
+ continue;
12
+ (tolerated.has(k) ? into.unsupported : into.unknown).push(echoKeyName(prefix, k));
13
+ }
14
+ }
15
+ export function keyClosureClause(names, label) {
16
+ const shown = names.slice(0, MAX_KEY_ECHO);
17
+ const more = names.length - shown.length;
18
+ return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
19
+ }
20
+ const TASK_BODY_DECLARED_KEYS = [
21
+ "a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
22
+ "appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
23
+ "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "enableFork", "excludeTools",
24
+ "finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
25
+ "maxTokens", "mcpServers", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
26
+ "outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
27
+ "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
28
+ "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
29
+ "sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
30
+ "verify", "verifyRounds",
31
+ ];
32
+ const TASK_BODY_SCENARIO_KEYS = ["businessContext", "lenses", "repo", "rounds", "team"];
33
+ const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
34
+ const UNSUPPORTED_BODY = new Set(["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "writeFace"]);
35
+ export function taskBodyKeyIssue(raw) {
36
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
37
+ return null;
38
+ const buckets = { unknown: [], unsupported: [] };
39
+ scanKeyClosure(raw, "", ACCEPTED_BODY, UNSUPPORTED_BODY, buckets);
40
+ if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
41
+ return null;
42
+ buckets.unknown.sort();
43
+ buckets.unsupported.sort();
44
+ const parts = [
45
+ ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a task-request key — check the spelling)")] : []),
46
+ ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (an engine seat this service derives, never reads from the body)")] : []),
47
+ ];
48
+ return {
49
+ message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
50
+ `${parts.join("; ")}. ` +
51
+ `Accepted: ${[...ACCEPTED_BODY].sort().join(",")}`,
52
+ unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
53
+ unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
54
+ };
55
+ }
56
+ //# sourceMappingURL=request-key-closure.js.map
package/dist/runs.js CHANGED
@@ -95,6 +95,8 @@ export function resumeAtHttpStatus(result) {
95
95
  return 400;
96
96
  if (code === "config.invalid_checkpoint_store")
97
97
  return 400;
98
+ if (code === "config.skill_listing_budget_invalid")
99
+ return 400;
98
100
  if (code === "config.memory_capture_unsupported")
99
101
  return 409;
100
102
  if (!code.startsWith("resume_at."))
@@ -196,7 +198,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
196
198
  if (approval) {
197
199
  const coordinator = approval.coordinator;
198
200
  spec.onAsk = approvalLeg.windowZero
199
- ? () => Promise.resolve(coordinator.unattendedAskOutcome())
201
+ ? (askReq) => Promise.resolve(coordinator.unattendedAskOutcome({ taskId, owner: elicitOwner ?? owner, toolName: askReq.toolName }))
200
202
  : coordinator.boundAsk({
201
203
  owner: elicitOwner ?? owner,
202
204
  taskId,
@@ -84,13 +84,27 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
84
84
  */
85
85
  export declare function compileCommandPolicy(rules: CommandRule[] | undefined): ToolPolicy | undefined;
86
86
  /**
87
- * Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the three primitives — TRAP #2):
88
- * - `read-only` / `plan` → `handsReadOnly: true` (read + propose, never mutate; the §6 verifier read-only
89
- * boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan for approval" intent
90
- * is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands.
87
+ * Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
88
+ * - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
89
+ * §6 verifier read-only boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan
90
+ * for approval" intent is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands.
91
91
  * - `ask` → `shellGate: "always"` (every `Bash` command tightens to an `irreversible_ask` durable suspend —
92
92
  * the fail-closed default for an unattended deployment with no parsed classifier).
93
93
  * - `auto` / `undefined` → `{}` (no extra tightening; still subject to the deployment baseline + commandPolicy).
94
+ *
95
+ * 🔴 **为什么只读那两档同时说 `writeFace:"roots"`**([ref] 随批;不是补丁,是把这一档的表态说全):
96
+ * core 7.18.0 的 `resolveContainmentFace` 第 1 行把「只读(verifier)挂载 × 任务层 `writeFace:"open"`」
97
+ * 判成**真实矛盾**并**抛** `config.write_face_readonly_conflict`(理由逐字:那种挂载根本不带写面,
98
+ * 声明一个开放写面 = 为一次调用没有的工具表态)。而 `permissionMode:"bypassPermissions"` 自 [ref] 起
99
+ * 会在阶段④铸出 `writeFace:"open"` —— 于是「`AUTONOMY=read-only` 的部署 × 一个 bypass 请求」这一格
100
+ * 不修就是**整条 run 当场配置错误失败**。
101
+ *
102
+ * 修在这里而不是在写点加一条「若 handsReadOnly 则别写」的判据:两个字段**在这一档里是同一件事**
103
+ * (「这条 run 没有写面」),而 `tightenTaskSpec` 本来就把 `writeFace` 当 tighten-only 安全字段收
104
+ * (`base:"open"` × `override:"roots"` ⇒ `"roots"`,反向 throw)—— 用既有的合并表表达它,规则数不变;
105
+ * 在写点加判据则是让阶段④去读一个它不拥有的治理事实,那才是第二个判官。
106
+ * 与 `ask → shellGate:"always"` 同姿势:这一档**自己就是**一次「围栏在」的表态,所以无表态的请求上
107
+ * 它照样把键写出来(不是「只在有 open 时才压」——那又是一条判据)。
94
108
  */
95
109
  export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial<TaskSpec>;
96
110
  /**
@@ -85,7 +85,7 @@ export function autonomyOverrides(autonomy) {
85
85
  switch (autonomy) {
86
86
  case "read-only":
87
87
  case "plan":
88
- return { handsReadOnly: true };
88
+ return { handsReadOnly: true, writeFace: "roots" };
89
89
  case "ask":
90
90
  return { shellGate: "always" };
91
91
  case "auto":
@@ -163,7 +163,7 @@ export function applyRuntimeGovernance(base, governance) {
163
163
  const shellObserver = createGovernanceShellGateMarkPolicy(marksOf);
164
164
  overrides.toolPolicy = governancePolicy === undefined ? shellObserver : combinePolicies(governancePolicy, shellObserver);
165
165
  }
166
- if (overrides.toolPolicy === undefined && overrides.handsReadOnly === undefined && overrides.shellGate === undefined) {
166
+ if (Object.keys(overrides).length === 0) {
167
167
  return base;
168
168
  }
169
169
  return tightenTaskSpec(base, overrides);
@@ -38,7 +38,9 @@ export declare function normalizeAttachments(v: unknown): TaskSpec["attachments"
38
38
  export declare function normalizeResumeAtMode(v: unknown, resumeAtResolved: boolean): "at" | "before" | undefined;
39
39
  /** [ref] 防御上限:一份请求最多带多少个 agent 定义。core 侧无上限;这里封一个防御值 —— 每个定义都进
40
40
  * 系统提示(whenToUse/skills/systemPrompt 全是 prompt 载荷),不设界就是 per-turn token 成本洞(与
41
- * MAX_USER_SKILLS 同轴)。32 足够覆盖 CC 式 `.claude/agents/` 全家桶还有余量。 */
41
+ * 上限)。32 足够覆盖 CC 式 `.claude/agents/` 全家桶还有余量。⚠️ 这条帽守的是 **agent 定义**的条数
42
+ * (每个定义的 whenToUse/systemPrompt 都是无条件进系统提示的 prompt 载荷),与 [ref] 退役的
43
+ * **技能**条数帽不是同一轴:技能清单由引擎按预算渲染(超了裁描述),agent 定义没有那个预算面。 */
42
44
  export declare const MAX_TASK_AGENTS = 32;
43
45
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
44
46
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
@@ -58,7 +58,6 @@ const MAX_AGENT_TEXT_CHARS = 4_096;
58
58
  const MAX_AGENT_SYSTEM_PROMPT_CHARS = 65_536;
59
59
  const MAX_AGENT_TOOLS = 64;
60
60
  const MAX_AGENT_TOOL_NAME_CHARS = 128;
61
- const MAX_AGENT_SKILLS = 10;
62
61
  const MAX_AGENT_SKILL_CONTENT_CHARS = 1_048_576;
63
62
  const MAX_AGENT_MAX_TURNS = 1_000;
64
63
  const boundedString = (max) => (v) => typeof v === "string" && v.length <= max;
@@ -73,9 +72,9 @@ const skillOk = (s) => {
73
72
  if (k !== "name" && k !== "description" && k !== "content" && k !== "manifest" && k !== "files")
74
73
  return false;
75
74
  }
76
- if (typeof s.name !== "string" || s.name.length === 0 || s.name.length > MAX_AGENT_NAME_CHARS)
75
+ if (typeof s.name !== "string" || s.name.length === 0)
77
76
  return false;
78
- if (typeof s.description !== "string" || s.description.length > 1_024)
77
+ if (typeof s.description !== "string")
79
78
  return false;
80
79
  if (typeof s.content !== "string" || s.content.length === 0 || s.content.length > MAX_AGENT_SKILL_CONTENT_CHARS)
81
80
  return false;
@@ -135,8 +134,8 @@ const TASK_AGENT_FIELD_SHAPES = {
135
134
  allowTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
136
135
  denyTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
137
136
  skills: {
138
- desc: `an array of at most ${MAX_AGENT_SKILLS} { name, description, content } skill objects (optional manifest/files; files entries need { path, content })`,
139
- ok: (v) => Array.isArray(v) && v.length <= MAX_AGENT_SKILLS && v.every(skillOk),
137
+ desc: "an array of { name, description, content } skill objects (optional manifest/files; files entries need { path, content }) — no count cap; the engine renders the listing within its own byte budget",
138
+ ok: (v) => Array.isArray(v) && v.every(skillOk),
140
139
  },
141
140
  background: { desc: "a boolean", ok: (v) => typeof v === "boolean" },
142
141
  isolation: { desc: '"worktree"', ok: (v) => v === "worktree" },
@@ -37,6 +37,7 @@
37
37
  */
38
38
  import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core";
39
39
  import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
40
+ import { type KeyClosureIssue } from "./request-key-closure.js";
40
41
  /** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [ref]/[ref] five-word
41
42
  * table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set ([ref] P2-1 corpus check): CC's own
42
43
  * full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`,
@@ -160,6 +161,13 @@ export interface ParsedTaskSettings {
160
161
  * 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
161
162
  * core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
162
163
  hooks?: HooksConfig;
164
+ /** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
165
+ * 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
166
+ * 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。
167
+ * server 零加工透传到 `TaskSpec`,默认值属主是引擎(缺席 ⇒ 整键不写)。 */
168
+ skillListingBudgetFraction?: number;
169
+ /** 同族第二键:每条技能描述在清单里的字符帽(正整数;CC 默认 1536)。超出由引擎**截断**,不是拒。 */
170
+ skillListingMaxDescChars?: number;
163
171
  }
164
172
  /** The result of parsing an untrusted `body.settings`: the applied subset + the names of present-but-deferred
165
173
  * fields (so the caller can log/advertise them — no silent drop). `null` settings ⇒ `{ settings: undefined }`. */
@@ -180,10 +188,15 @@ export declare const MAX_SETTINGS_ENV_VARS = 256;
180
188
  export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768;
181
189
  export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
182
190
  /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
183
- * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 在 {@link parseTaskSettings};
184
- * `webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
185
- * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。 */
186
- export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
191
+ * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在
192
+ * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
193
+ * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。
194
+ *
195
+ * 🔴 两个 `skillListing*`([ref],7.77.0)= **CC settings 同名键**,值域抄 CC 的 schema 声明
196
+ * (`skillListingBudgetFraction` 是 `gt(0).lte(1)`、`skillListingMaxDescChars` 是 `int().positive()`);
197
+ * 消费点 = 本文件的 parse + `boot/resolve-spec.ts` 的 `TaskSpec` 透传。语义(预算怎么花、描述怎么裁、
198
+ * 溢出怎么退成 name-only)整个归引擎,本仓一个字节都不渲染,也**不复刻默认值**——缺席即整键不写。 */
199
+ export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
187
200
  /** 本 service 消费的 `settings.permissions` 子键 —— 闭集(值形校验另有既有门,本门只判键名)。 */
188
201
  export declare const TASK_SETTINGS_PERMISSION_KEYS: readonly ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
189
202
  /** CC/SDK 合约里合法、本 service **不接线**的顶层键。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响
@@ -202,17 +215,6 @@ export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "
202
215
  /** 同上,`settings.permissions` 子层(出处 = SDK `SettingsPermissions` 的三个声明键)。
203
216
  * `additionalDirectories` 有**同请求替代面**:顶层 `body.additionalDirectories`([ref],resolve-spec 真消费)。 */
204
217
  export declare const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS: readonly ["additionalDirectories", "disableBypassPermissionsMode"];
205
- /** 拒体里逐字回显的键名条数上限。键名原样回显(不猜语义),但**条数有界** —— 一个塞满键的体不许把 400 响应
206
- * 放大成流量面。超出部分只报数。 */
207
- export declare const MAX_SETTINGS_KEY_ECHO = 20;
208
- /** {@link taskSettingsKeyIssue} 的拒体:一句 wire 文案 + 两张分类键名表(消费端可机读,不必解析文案)。 */
209
- export interface TaskSettingsKeyIssue {
210
- message: string;
211
- /** 词表外的键(拼错/未来键),`settings.x` / `settings.permissions.x` 全路径形,已清洗+截断+排序+封顶。 */
212
- unknownKeys: string[];
213
- /** CC/SDK 合约合法但本 service 不接线的键,同形。 */
214
- unsupportedKeys: string[];
215
- }
216
218
  /**
217
219
  * FRESH 提交面的 `settings` 键闭集裁决:顶层或 `permissions` 子层出现受理集之外的键 ⇒ 返回拒体(调用方 400
218
220
  * `request.body_shape`);全部受理 ⇒ `null`。
@@ -224,7 +226,7 @@ export interface TaskSettingsKeyIssue {
224
226
  * 「settings must be an object」/「settings.permissions must be an object」两道门,免得同一个体被两句话抢答。
225
227
  * · `permissions: null` 是既有钉住的「容器缺席」形,照旧不咬。
226
228
  */
227
- export declare function taskSettingsKeyIssue(raw: unknown): TaskSettingsKeyIssue | null;
229
+ export declare function taskSettingsKeyIssue(raw: unknown): KeyClosureIssue | null;
228
230
  /**
229
231
  * Validate an untrusted `body.settings` (raw `unknown` off the wire) into the service-trusted {@link
230
232
  * ParsedTaskSettings}. NEVER trusts the wire type — every field is shape-checked. `defaultMode` accepts the FIVE
@@ -1,6 +1,7 @@
1
1
  import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
+ import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
4
5
  import { parseHooksConfig } from "./hooks/hook-runner.js";
5
6
  export function coercePermissionMode(raw) {
6
7
  const strict = coercePermissionModeStrict(raw);
@@ -72,49 +73,31 @@ function cleanStringArray(v) {
72
73
  const out = v.filter((x) => typeof x === "string" && x.length > 0).slice(0, MAX_SETTINGS_PERMISSION_RULES);
73
74
  return out.length > 0 ? out : undefined;
74
75
  }
75
- export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
76
+ export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
76
77
  export const TASK_SETTINGS_PERMISSION_KEYS = ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
77
78
  export const TASK_SETTINGS_UNSUPPORTED_KEYS = ["apiKeyHelper", "crossSessionInbound", "dialogExpiry", "sandbox", "statusLine"];
78
79
  export const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS = ["additionalDirectories", "disableBypassPermissionsMode"];
79
- export const MAX_SETTINGS_KEY_ECHO = 20;
80
- const MAX_SETTINGS_KEY_ECHO_CHARS = 64;
81
80
  const ACCEPTED_TOP = new Set(TASK_SETTINGS_KEYS);
82
81
  const ACCEPTED_PERM = new Set(TASK_SETTINGS_PERMISSION_KEYS);
83
82
  const UNSUPPORTED_TOP = new Set(TASK_SETTINGS_UNSUPPORTED_KEYS);
84
83
  const UNSUPPORTED_PERM = new Set(TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS);
85
- function echoSettingsKey(prefix, key) {
86
- const clean = key.replace(CONTROL_AND_BIDI_STRIP_RE, "");
87
- return `${prefix}${clean.length > MAX_SETTINGS_KEY_ECHO_CHARS ? `${clean.slice(0, MAX_SETTINGS_KEY_ECHO_CHARS)}…` : clean}`;
88
- }
89
84
  export function taskSettingsKeyIssue(raw) {
90
85
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
91
86
  return null;
92
- const unknown = [];
93
- const unsupported = [];
94
- const scan = (bag, prefix, accepted, tolerated) => {
95
- for (const k of Object.keys(bag)) {
96
- if (accepted.has(k))
97
- continue;
98
- (tolerated.has(k) ? unsupported : unknown).push(echoSettingsKey(prefix, k));
99
- }
100
- };
101
- scan(raw, "settings.", ACCEPTED_TOP, UNSUPPORTED_TOP);
87
+ const buckets = { unknown: [], unsupported: [] };
88
+ scanKeyClosure(raw, "settings.", ACCEPTED_TOP, UNSUPPORTED_TOP, buckets);
102
89
  const perms = raw.permissions;
103
90
  if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
104
- scan(perms, "settings.permissions.", ACCEPTED_PERM, UNSUPPORTED_PERM);
91
+ scanKeyClosure(perms, "settings.permissions.", ACCEPTED_PERM, UNSUPPORTED_PERM, buckets);
105
92
  }
93
+ const { unknown, unsupported } = buckets;
106
94
  if (unknown.length === 0 && unsupported.length === 0)
107
95
  return null;
108
96
  unknown.sort();
109
97
  unsupported.sort();
110
- const clause = (names, label) => {
111
- const shown = names.slice(0, MAX_SETTINGS_KEY_ECHO);
112
- const more = names.length - shown.length;
113
- return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
114
- };
115
98
  const parts = [
116
- ...(unknown.length > 0 ? [clause(unknown, "unknown (not a settings key here — check the spelling)")] : []),
117
- ...(unsupported.length > 0 ? [clause(unsupported, "unsupported (a CC settings key this service does not wire)")] : []),
99
+ ...(unknown.length > 0 ? [keyClosureClause(unknown, "unknown (not a settings key here — check the spelling)")] : []),
100
+ ...(unsupported.length > 0 ? [keyClosureClause(unsupported, "unsupported (a CC settings key this service does not wire)")] : []),
118
101
  ];
119
102
  const dirHint = unsupported.some((n) => n.endsWith(".additionalDirectories"))
120
103
  ? " Extra directories ride the top-level body.additionalDirectories instead."
@@ -123,8 +106,8 @@ export function taskSettingsKeyIssue(raw) {
123
106
  message: `settings: unrecognized key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
124
107
  `${parts.join("; ")}.${dirHint} ` +
125
108
  `Accepted: settings.{${[...TASK_SETTINGS_KEYS].join(",")}} and settings.permissions.{${[...TASK_SETTINGS_PERMISSION_KEYS].join(",")}}`,
126
- unknownKeys: unknown.slice(0, MAX_SETTINGS_KEY_ECHO),
127
- unsupportedKeys: unsupported.slice(0, MAX_SETTINGS_KEY_ECHO),
109
+ unknownKeys: unknown.slice(0, MAX_KEY_ECHO),
110
+ unsupportedKeys: unsupported.slice(0, MAX_KEY_ECHO),
128
111
  };
129
112
  }
130
113
  export function parseTaskSettings(raw) {
@@ -178,8 +161,13 @@ export function parseTaskSettings(raw) {
178
161
  out.ultracode = true;
179
162
  if (hooksConfig)
180
163
  out.hooks = hooksConfig;
181
- const hasApplied = out.permissions !== undefined || out.model !== undefined || out.outputStyle !== undefined || out.shellEnv !== undefined || out.ultracode !== undefined || out.hooks !== undefined;
182
- return { settings: hasApplied ? out : undefined, deferred };
164
+ const fraction = s.skillListingBudgetFraction;
165
+ if (typeof fraction === "number" && Number.isFinite(fraction) && fraction > 0 && fraction <= 1)
166
+ out.skillListingBudgetFraction = fraction;
167
+ const maxDesc = s.skillListingMaxDescChars;
168
+ if (typeof maxDesc === "number" && Number.isSafeInteger(maxDesc) && maxDesc > 0)
169
+ out.skillListingMaxDescChars = maxDesc;
170
+ return { settings: Object.keys(out).length > 0 ? out : undefined, deferred };
183
171
  }
184
172
  export function hasConstitutionAnchors(text) {
185
173
  return typeof text === "string" && text.includes(CYBER_RISK) && text.includes(URL_SAFETY) && text.includes(HARNESS_SECTION_ANCHOR);
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage } from "node:http";
2
- import { type AskRequest, type AskOutcome, type RuleOffer } from "@sema-agent/core";
2
+ import { type AskRequest, type AskOutcome, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
3
3
  import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
4
4
  import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
5
5
  import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
@@ -169,6 +169,27 @@ export interface ToolApprovalFrame {
169
169
  * `GateDisposition.denied.cause`(`tool_end.gate` / `permissionDenied.gate`),ask 面自此没有这一位。
170
170
  */
171
171
  ruleStoreUnreadable?: string;
172
+ /**
173
+ * **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
174
+ * 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
175
+ *
176
+ * 🔴 **在场才读,永远不读缺席**。缺席不是「无计可施」:它同时覆盖敏感路径 deny 行(按模式判,加任何
177
+ * 根都不动它)、读边界压根读不懂的命令(heredoc / 换行 / 命令替换 / 参数当程序)、没展开的 glob、
178
+ * 递归遍历、以及每一只**不是**读边界提的 ask —— 7.18.0 及以前的**每**一只 ask 都是这一形。
179
+ *
180
+ * 🔴 **原样回传**(core 契约 `shell.read_boundary.grant_candidate_is_the_grant`):`dir` 是绝对、已做词法
181
+ * 折叠、用读边界自己的拼法写的串;**显示的串就是要加的串**。壳可以为了好看渲一个缩短形,回传必须是
182
+ * 本键里的原串。⇒ 本仓的姿势是「整只逐字带,或整只丢」,**绝不截**(见 `MAX_READ_ROOT_CANDIDATE_DIR`)。
183
+ *
184
+ * 🔴 **echo-only,零判读**:server 不从命令文本重推目录,也不复核它真不真能清掉这只 ask —— core 已经
185
+ * 把提议的根放回读边界又走了一遍才递出来,下游再判一遍就是同一事实的第二个判官(与 `origin` /
186
+ * `ruleStoreUnreadable` / `denialLimitFallback` 三条顶注的同一条禁令)。
187
+ *
188
+ * 🔴 **只此一面**:core 的负控逐字写着「the durable park row has no twin of this seat in this version」——
189
+ * 一只 PARKED 的根外读 ask 渲的仍是单独那张确认卡。所以 `card_json` / 耐久面**没有**这一座,本仓也不
190
+ * 自铸一个(自铸 = 同一事实的第二个源,而且那个源会在 park 兑现时说谎)。
191
+ */
192
+ readRootCandidate?: ReadRootGrantCandidate;
172
193
  /**
173
194
  * **E-14**([ref]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**工具输入**
174
195
  * (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类属主 `text-bidi.ts`)。
@@ -858,9 +879,20 @@ export declare class ToolApprovalCoordinator {
858
879
  * 残余 = 那类行的归因会记成 `routing_failure`(其实是政策 deny),归因精化登记为候件。
859
880
  */
860
881
  private unattendedOutcome;
882
+ /** 政策**读**口(不是终局铸口)—— 回答「这台部署把无人可答翻成 park 吗」。
883
+ *
884
+ * 🔴 与 {@link unattendedOutcome} 分家是 S-187 的一半:此前两件事共用一只方法,于是墓碑腿那次
885
+ * 纯粹的**政策查询**(`unattendedOutcome() !== false`)会和同一只 ask 的真终局各记一条遥测 ——
886
+ * 「一只 ask 一条线」当场不成立。读口不落日志,铸口落;谁也不兼职。 */
887
+ private get unattendedRoutesToPark();
861
888
  /** {@link unattendedOutcome} 的**公开**口:`windowZero` 的 immediate-unavailable 闭包长在装配层
862
- * (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。 */
863
- unattendedAskOutcome(): "unavailable" | false;
889
+ * (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。
890
+ * 出处随调用方给(那条腿知道自己是哪一只 ask)。 */
891
+ unattendedAskOutcome(site: {
892
+ taskId?: string;
893
+ owner?: string | null;
894
+ toolName?: string;
895
+ }): "unavailable" | false;
864
896
  /**
865
897
  * [ref] 车3 刀 3b —— [ref] §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
866
898
  *