@sema-agent/server 7.115.0 → 7.116.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 (116) hide show
  1. package/MIGRATION.md +167 -4
  2. package/USAGE.md +6 -3
  3. package/dist/approval-ask-audit-store.d.ts +25 -6
  4. package/dist/approval-ask-audit-store.js +6 -1
  5. package/dist/approval-card.d.ts +8 -4
  6. package/dist/approval-card.js +2 -0
  7. package/dist/approval-crash-convergence.d.ts +60 -0
  8. package/dist/approval-crash-convergence.js +75 -0
  9. package/dist/approval-deny-reasons.d.ts +13 -0
  10. package/dist/approval-deny-reasons.js +2 -0
  11. package/dist/approval.js +2 -1
  12. package/dist/boot/coordinators.js +0 -12
  13. package/dist/boot/execution-env.js +2 -1
  14. package/dist/boot/resolve-spec.js +1 -0
  15. package/dist/boot/stage-07-capability-layer.js +21 -15
  16. package/dist/boot/stores.js +4 -4
  17. package/dist/capabilities/hands-lane.d.ts +0 -23
  18. package/dist/capabilities/hands-lane.js +3 -2
  19. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  20. package/dist/capabilities/send-user-file-tool.js +1 -8
  21. package/dist/capability-facts.d.ts +296 -0
  22. package/dist/capability-facts.js +354 -0
  23. package/dist/config-center/apply-effective.js +1 -1
  24. package/dist/config.d.ts +16 -13
  25. package/dist/config.js +12 -12
  26. package/dist/http/admission.d.ts +6 -0
  27. package/dist/http/admission.js +22 -5
  28. package/dist/http/principal-gate.d.ts +3 -0
  29. package/dist/http/principal-gate.js +3 -0
  30. package/dist/http/resume-legs.js +10 -1
  31. package/dist/http/retired-skill-manifest.d.ts +23 -0
  32. package/dist/http/retired-skill-manifest.js +22 -0
  33. package/dist/http/route-table.d.ts +3 -2
  34. package/dist/http/route-table.js +5 -5
  35. package/dist/http/routes/a2a-serve.js +14 -21
  36. package/dist/http/routes/admin-config-refresh.js +7 -2
  37. package/dist/http/routes/admin-drain.js +7 -2
  38. package/dist/http/routes/admin-memory-optout.js +20 -6
  39. package/dist/http/routes/adoption.js +19 -5
  40. package/dist/http/routes/agents-roster.js +4 -2
  41. package/dist/http/routes/approvals-assistant.js +41 -18
  42. package/dist/http/routes/background.d.ts +0 -2
  43. package/dist/http/routes/background.js +4 -5
  44. package/dist/http/routes/capabilities.d.ts +2 -1
  45. package/dist/http/routes/capabilities.js +62 -65
  46. package/dist/http/routes/config-catalog.d.ts +4 -5
  47. package/dist/http/routes/config-catalog.js +16 -2
  48. package/dist/http/routes/devices.js +17 -11
  49. package/dist/http/routes/diagnostics.js +6 -1
  50. package/dist/http/routes/fleet.js +4 -2
  51. package/dist/http/routes/images.js +30 -3
  52. package/dist/http/routes/leader.js +7 -1
  53. package/dist/http/routes/mcp-reconnect.js +17 -10
  54. package/dist/http/routes/memory-bundle.js +20 -6
  55. package/dist/http/routes/memory-compliance.js +19 -5
  56. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  57. package/dist/http/routes/memory-consolidation.js +17 -3
  58. package/dist/http/routes/memory-origin.js +19 -5
  59. package/dist/http/routes/notify-wake.js +5 -3
  60. package/dist/http/routes/observability.js +19 -5
  61. package/dist/http/routes/retention-ops.js +6 -1
  62. package/dist/http/routes/rules.js +49 -21
  63. package/dist/http/routes/run-memory-capture.js +5 -2
  64. package/dist/http/routes/runs.js +90 -60
  65. package/dist/http/routes/session-sync.js +64 -53
  66. package/dist/http/routes/sessions-list.js +8 -5
  67. package/dist/http/routes/sessions.js +39 -24
  68. package/dist/http/routes/shared-memory.js +5 -3
  69. package/dist/http/routes/tasks.js +2 -1
  70. package/dist/http/routes/trace-usage.js +18 -11
  71. package/dist/http/routes/workflows.js +36 -13
  72. package/dist/http/server.js +4 -1
  73. package/dist/http/wire-types.d.ts +6 -0
  74. package/dist/namespaced-rule-name.d.ts +54 -0
  75. package/dist/namespaced-rule-name.js +33 -0
  76. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  77. package/dist/observability/corrupt-read-seat.js +4 -3
  78. package/dist/observability/fail-open.d.ts +5 -1
  79. package/dist/observability/fail-open.js +5 -1
  80. package/dist/observability/permission-rule-events.d.ts +3 -0
  81. package/dist/observability/permission-rule-events.js +4 -0
  82. package/dist/plugins/file-run-store.d.ts +7 -1
  83. package/dist/plugins/file-run-store.js +20 -7
  84. package/dist/plugins/memory-run-store.js +2 -1
  85. package/dist/plugins/one-time-migrations.d.ts +4 -2
  86. package/dist/plugins/one-time-migrations.js +26 -3
  87. package/dist/plugins/pg-session-storage.js +1 -1
  88. package/dist/plugins/run-store-sql.js +4 -4
  89. package/dist/plugins/session-policy-store-sql.js +4 -2
  90. package/dist/plugins/store-backend.d.ts +6 -1
  91. package/dist/plugins/store-backend.js +2 -1
  92. package/dist/plugins/store-contracts.d.ts +27 -8
  93. package/dist/plugins/store-contracts.js +12 -3
  94. package/dist/plugins/tidb-session-store.js +1 -1
  95. package/dist/request-key-closure.js +1 -1
  96. package/dist/run-local.d.ts +37 -3
  97. package/dist/run-local.js +48 -5
  98. package/dist/runs.d.ts +0 -58
  99. package/dist/runs.js +15 -20
  100. package/dist/session-policy-wire.d.ts +40 -60
  101. package/dist/session-policy-wire.js +60 -32
  102. package/dist/spec-fields.d.ts +32 -0
  103. package/dist/spec-fields.js +31 -16
  104. package/dist/standing-answer-grant.d.ts +8 -0
  105. package/dist/standing-answer-grant.js +5 -0
  106. package/dist/task-settings.d.ts +13 -3
  107. package/dist/tool-approval.d.ts +16 -0
  108. package/dist/tool-approval.js +29 -7
  109. package/dist/trace/core-keyset-guard.d.ts +4 -4
  110. package/dist/trace/self-minted-wire.d.ts +42 -0
  111. package/dist/trace/self-minted-wire.js +13 -0
  112. package/dist/user-input-ledger.d.ts +8 -5
  113. package/package.json +5 -3
  114. package/scripts/drain-check-local.mjs +115 -25
  115. package/scripts/drain-check.mysql.sql +67 -0
  116. package/scripts/drain-check.pg.sql +71 -0
@@ -1,21 +1,41 @@
1
- import { PROTOCOL_TABLE, RETIRED_TOOL_NAMES, protocolOf } from "@sema-agent/core";
2
- const RULE_FIELD_ROLE = {
3
- toolAllow: "tool-name",
4
- toolDeny: "tool-name",
5
- allowDirs: "path",
6
- commandAllow: "command",
7
- commandDeny: "command",
1
+ import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
2
+ import { describeUnreachableNamespacedRuleName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
3
+ const RULE_FIELDS = {
4
+ toolAllow: true,
5
+ toolDeny: true,
6
+ allowDirs: true,
7
+ commandAllow: true,
8
+ commandDeny: true,
8
9
  };
9
- export const SESSION_PERMISSION_RULE_FIELDS = Object.keys(RULE_FIELD_ROLE);
10
- const TOOL_NAME_RULE_FIELDS = SESSION_PERMISSION_RULE_FIELDS.filter((f) => RULE_FIELD_ROLE[f] === "tool-name");
11
- export function legacyToolNameOf(name) {
12
- const guidance = RETIRED_TOOL_NAMES.get(name);
13
- if (guidance !== undefined)
14
- return { why: "retired", guidance };
15
- if (name.includes("__") && protocolOf(name) === undefined)
16
- return { why: "unprefixed-namespace" };
10
+ export const SESSION_PERMISSION_RULE_FIELDS = Object.keys(RULE_FIELDS);
11
+ function legacyToolNameRefusalOf(rules) {
12
+ try {
13
+ assertSessionRuleNamesCurrent(rules);
14
+ return undefined;
15
+ }
16
+ catch (e) {
17
+ if (e instanceof SessionPolicyError && e.code === "legacy_tool_name" && e.detail !== undefined)
18
+ return e.detail;
19
+ throw e;
20
+ }
21
+ }
22
+ const TOOL_NAME_FIELD_TABLE = { toolAllow: "toolAllow", toolDeny: "toolDeny" };
23
+ const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
24
+ export function unreachableSessionRuleNameOf(rules) {
25
+ for (const field of TOOL_NAME_FIELDS) {
26
+ for (const name of rules[field] ?? []) {
27
+ const verdict = unreachableNamespacedRuleNameOf(name);
28
+ if (verdict !== undefined)
29
+ return { field, name, verdict };
30
+ }
31
+ }
17
32
  return undefined;
18
33
  }
34
+ export function assertSessionRuleNamesReachable(rules) {
35
+ const unreachable = unreachableSessionRuleNameOf(rules);
36
+ if (unreachable !== undefined)
37
+ throw new Error(describeUnreachableSessionRuleName(unreachable));
38
+ }
19
39
  function protocolToolNameShapes() {
20
40
  return PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(" or ");
21
41
  }
@@ -38,13 +58,12 @@ export function readWritableSessionPermissionRules(raw) {
38
58
  const read = readSessionPermissionRules(raw);
39
59
  if (read.problem !== undefined)
40
60
  return read;
41
- for (const field of TOOL_NAME_RULE_FIELDS) {
42
- for (const name of read.rules[field] ?? []) {
43
- const legacy = legacyToolNameOf(name);
44
- if (legacy !== undefined)
45
- return { problem: { kind: "legacy-tool-name", field, name, legacy } };
46
- }
47
- }
61
+ const refusal = legacyToolNameRefusalOf(read.rules);
62
+ if (refusal !== undefined)
63
+ return { problem: { kind: "legacy-tool-name", refusal } };
64
+ const unreachable = unreachableSessionRuleNameOf(read.rules);
65
+ if (unreachable !== undefined)
66
+ return { problem: { kind: "unreachable-tool-name", unreachable } };
48
67
  return read;
49
68
  }
50
69
  export function readSessionRulesRecords(raw) {
@@ -71,22 +90,29 @@ export function describeSessionPermissionRulesProblem(p) {
71
90
  switch (p.kind) {
72
91
  case "not-an-object": return "rules must be a SessionPermissionRules object";
73
92
  case "field-not-string-array": return `${p.field} must be an array of strings`;
74
- case "legacy-tool-name": return describeLegacyToolName(p.field, p.name, p.legacy);
93
+ case "legacy-tool-name": return describeLegacyToolName(p.refusal);
94
+ case "unreachable-tool-name": return describeUnreachableSessionRuleName(p.unreachable);
75
95
  default: {
76
96
  const unhandled = p;
77
97
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
78
98
  }
79
99
  }
80
100
  }
81
- function describeLegacyToolName(field, name, legacy) {
82
- switch (legacy.why) {
83
- case "retired":
84
- return `${field} names "${name}", a retired tool name (${legacy.guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name`;
85
- case "unprefixed-namespace":
86
- return `${field} names "${name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
101
+ function describeUnreachableSessionRuleName(u) {
102
+ return `${u.field} names "${u.name}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)}), so nothing was written`;
103
+ }
104
+ function describeLegacyToolName(r) {
105
+ switch (r.verdict) {
106
+ case "retired": {
107
+ const v = judgeRuleToolName(r.name, new Set());
108
+ const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
109
+ return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
110
+ }
111
+ case "unprefixed_namespace":
112
+ return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
87
113
  default: {
88
- const unhandled = legacy;
89
- throw new Error(`unhandled legacy tool name arm: ${JSON.stringify(unhandled)}`);
114
+ const unhandled = r.verdict;
115
+ throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
90
116
  }
91
117
  }
92
118
  }
@@ -100,9 +126,10 @@ export function legacyToolNameProblemOf(p) {
100
126
  switch (p.problem.kind) {
101
127
  case "not-an-object":
102
128
  case "field-not-string-array":
129
+ case "unreachable-tool-name":
103
130
  return undefined;
104
131
  case "legacy-tool-name":
105
- return { field: p.problem.field, name: p.problem.name };
132
+ return p.problem.refusal;
106
133
  default: {
107
134
  const unhandled = p.problem;
108
135
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
@@ -131,6 +158,7 @@ export function sessionPolicyErrorHttp(code) {
131
158
  case "conflict": return { status: 409, errorCode: "conflict" };
132
159
  case "loosen_forbidden": return { status: 403, errorCode: "loosen_forbidden" };
133
160
  case "corrupt": return { status: 500, errorCode: "corrupt" };
161
+ case "legacy_tool_name": return { status: 400, errorCode: "rules.legacy_tool_name" };
134
162
  default: {
135
163
  const unhandled = code;
136
164
  throw new Error(`unhandled SessionPolicyError code: ${JSON.stringify(unhandled)}`);
@@ -4,6 +4,7 @@
4
4
  * body WITHOUT the HTTP-layer 400 validation, so a malformed value must fall back to a safe default, never throw.
5
5
  */
6
6
  import { type AgentDefinition, type TaskSpec } from "@sema-agent/core";
7
+ import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
7
8
  /**
8
9
  * E12 — normalize `body.suggestNextPrompts` → `TaskSpec.suggestNextPrompts`. `true` = on with core defaults
9
10
  * (count 3, cheap role); an object tunes `count` (clamped 1..10) / `role` (a free string — core validates it against
@@ -42,6 +43,36 @@ export declare function normalizeResumeAtMode(v: unknown, resumeAtResolved: bool
42
43
  * (每个定义的 whenToUse/systemPrompt 都是无条件进系统提示的 prompt 载荷),与 [ref] 退役的
43
44
  * **技能**条数帽不是同一轴:技能清单由引擎按预算渲染(超了裁描述),agent 定义没有那个预算面。 */
44
45
  export declare const MAX_TASK_AGENTS = 32;
46
+ /**
47
+ * core 7.41.0 #1303 —— 技能的两只清单 `allowedTools` / `disallowedTools`(`dist/core/agent-definition.d.ts`:本次 run 的 allow 规则来源 /
48
+ * deny 规则来源,不是围栏)的**形**谓词。两道技能门(顶层 `body.skills[]` 的 `validateUserSkills`、`agents[].skills[]` 的
49
+ * {@link SKILL_FIELD_SHAPES})**同一只**:一边判一边不判 = 同一个键两种话(S-377 baseDir 吃过的亏)。
50
+ *
51
+ * 本谓词只判「字符串数组」:引擎 `compileSkillGrant` 直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成
52
+ * `B` / `a` / `s` / `h` 四条(deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法**不在受理面判:引擎在开跑时对读不出的条目
53
+ * (`*` / 写路径 / 域名)发 `skills.allowed_tool_not_granted`,文法的属主是引擎(directory loader 同样原样抄、不判)。
54
+ * 形对之后两道门再问一问 {@link unreachableSkillToolListEntryOf}(协议名的服务器段铸不出 / 缺段 ⇒ 400)—— 那一问不是文法,是「写得出却永远匹配不到」,
55
+ * 与配置表、会话规则写口同一只判官。
56
+ * 无条数 / 长度帽([ref]:技能面的受理帽整族退役,体积由 8 MiB 请求体上限兜)。
57
+ */
58
+ export declare const skillToolListOk: (v: unknown) => boolean;
59
+ /** 技能的两只清单(`allowedTools` = allow 规则来源,`disallowedTools` = deny 规则来源;条目的工具名那一段两只同读)。 */
60
+ declare const SKILL_TOOL_LISTS: readonly ["allowedTools", "disallowedTools"];
61
+ /** 技能清单里一条够不着任何挂载工具的协议名条目。 */
62
+ export interface SkillToolListUnreachable {
63
+ readonly key: (typeof SKILL_TOOL_LISTS)[number];
64
+ readonly entry: string;
65
+ readonly verdict: UnreachableNamespacedRuleName;
66
+ }
67
+ /**
68
+ * 一条技能(形已过两道门的字符串数组判)的两只清单里,第一条「协议名写得出、却永远匹配不到」的条目 —— 判官是 `namespaced-rule-name.ts` 那一只
69
+ * (配置表 / 请求三桶 / 会话规则写口同调),条目的工具名那一段经同一只 `ruleEntryToolName` 取(与配置表的规则编译器消费者同读)。都够得着 ⇒ `undefined`。
70
+ * 两处受理口同调:顶层 `skills[]`(`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。
71
+ * 续跑重放**不问**这一问(两层都是:重放腿不过受理口,存量体照旧原样交给引擎 —— 引擎把它编成一条永不命中的规则,与修前同一种命运,不另开读法)。
72
+ */
73
+ export declare function unreachableSkillToolListEntryOf(skill: Record<string, unknown>): SkillToolListUnreachable | undefined;
74
+ /** 两处受理口同一句(句子的后半与配置表 / 会话规则写口同一只 `describeUnreachableNamespacedRuleName`)。 */
75
+ export declare function describeUnreachableSkillToolListEntry(u: SkillToolListUnreachable): string;
45
76
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
46
77
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
47
78
  * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。⚠️ 已知残账(core 半场):
@@ -157,4 +188,5 @@ export declare function normalizeApproachNotice(raw: unknown): false | {
157
188
  * 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
158
189
  */
159
190
  export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean, warn?: (event: string, fields: Record<string, unknown>) => void): TaskSpec["limits"];
191
+ export {};
160
192
  //# sourceMappingURL=spec-fields.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { isThinkingLevel } from "@sema-agent/core";
2
2
  import { taskWallClockSec } from "./task-workflow.js";
3
3
  import { isValidCwd } from "./task-cwd.js";
4
+ import { describeUnreachableNamespacedRuleName, ruleEntryToolName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
4
5
  export function normalizeSuggestNextPrompts(v) {
5
6
  if (v === true)
6
7
  return true;
@@ -65,28 +66,34 @@ const toolList = (v) => Array.isArray(v) &&
65
66
  v.length <= MAX_AGENT_TOOLS &&
66
67
  v.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_AGENT_TOOL_NAME_CHARS);
67
68
  const plainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
68
- const skillManifestOk = (m) => {
69
- if (!plainObject(m))
70
- return false;
71
- for (const k of Object.keys(m)) {
72
- if (k !== "allowTools" && k !== "allowPaths" && k !== "egress" && k !== "lineageId")
73
- return false;
69
+ export const skillToolListOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
70
+ const SKILL_TOOL_LISTS = ["allowedTools", "disallowedTools"];
71
+ export function unreachableSkillToolListEntryOf(skill) {
72
+ for (const key of SKILL_TOOL_LISTS) {
73
+ const list = skill[key];
74
+ if (!Array.isArray(list))
75
+ continue;
76
+ for (const entry of list) {
77
+ if (typeof entry !== "string")
78
+ continue;
79
+ const verdict = unreachableNamespacedRuleNameOf(ruleEntryToolName(entry));
80
+ if (verdict !== undefined)
81
+ return { key, entry, verdict };
82
+ }
74
83
  }
75
- if (!(Array.isArray(m.allowTools) && m.allowTools.every((x) => typeof x === "string")))
76
- return false;
77
- if (m.allowPaths !== undefined && !(Array.isArray(m.allowPaths) && m.allowPaths.every((x) => typeof x === "string")))
78
- return false;
79
- if (m.egress !== undefined && typeof m.egress !== "boolean")
80
- return false;
81
- return typeof m.lineageId === "string" && m.lineageId.length > 0;
82
- };
84
+ return undefined;
85
+ }
86
+ export function describeUnreachableSkillToolListEntry(u) {
87
+ return `skill.${u.key} names "${u.entry}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)})`;
88
+ }
83
89
  const skillFilesOk = (v) => Array.isArray(v) &&
84
90
  v.every((f) => plainObject(f) && typeof f.path === "string" && f.path.length > 0 && typeof f.content === "string");
85
91
  const SKILL_FIELD_SHAPES = {
86
92
  name: { required: true, ok: (v) => typeof v === "string" && v.length > 0 },
87
93
  description: { required: true, ok: (v) => typeof v === "string" },
88
94
  content: { required: true, ok: (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_AGENT_SKILL_CONTENT_CHARS },
89
- manifest: { required: false, ok: skillManifestOk },
95
+ allowedTools: { required: false, ok: skillToolListOk },
96
+ disallowedTools: { required: false, ok: skillToolListOk },
90
97
  files: { required: false, ok: skillFilesOk },
91
98
  baseDir: { required: false, ok: isValidCwd },
92
99
  };
@@ -136,7 +143,7 @@ const TASK_AGENT_FIELD_SHAPES = {
136
143
  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 },
137
144
  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 },
138
145
  skills: {
139
- desc: "an array of { name, description, content } skill objects (optional manifest/files/baseDir; files entries need { path, content }; baseDir must be an absolute path with no \"..\" segment) — no count cap; the engine renders the listing within its own byte budget",
146
+ desc: "an array of { name, description, content } skill objects (optional allowedTools/disallowedTools/files/baseDir; allowedTools/disallowedTools are arrays of strings; files entries need { path, content }; baseDir must be an absolute path with no \"..\" segment) — no count cap; the engine renders the listing within its own byte budget",
140
147
  ok: (v) => Array.isArray(v) && v.every(skillOk),
141
148
  },
142
149
  background: { desc: "a boolean", ok: (v) => typeof v === "boolean" },
@@ -187,6 +194,14 @@ export function validateTaskAgents(v) {
187
194
  const issue = taskAgentIssue(v[i]);
188
195
  if (issue)
189
196
  return `agents[${i}]: ${issue}`;
197
+ const skills = v[i].skills;
198
+ if (Array.isArray(skills)) {
199
+ for (let j = 0; j < skills.length; j++) {
200
+ const unreachable = unreachableSkillToolListEntryOf(skills[j]);
201
+ if (unreachable !== undefined)
202
+ return `agents[${i}]: skills[${j}]: ${describeUnreachableSkillToolListEntry(unreachable)}`;
203
+ }
204
+ }
190
205
  const name = v[i].name;
191
206
  if (names.has(name))
192
207
  return `agents[${i}]: duplicate agent name "${name}"`;
@@ -0,0 +1,8 @@
1
+ /** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest`、待决条目、停驻行的 `tool_approval` 臂)都结构上满足。 */
2
+ export interface StandingAnswerGrantCarrier {
3
+ readonly toolName?: unknown;
4
+ readonly boundGrantDigest?: unknown;
5
+ }
6
+ /** 这一问的应答是否授予这一次调用之外的东西(读法见文件头注)。 */
7
+ export declare function answerGrantsBeyondCall(carrier: StandingAnswerGrantCarrier): boolean;
8
+ //# sourceMappingURL=standing-answer-grant.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { SKILL_TOOL_NAME } from "@sema-agent/core";
2
+ export function answerGrantsBeyondCall(carrier) {
3
+ return Object.hasOwn(carrier, "boundGrantDigest") || carrier.toolName === SKILL_TOOL_NAME;
4
+ }
5
+ //# sourceMappingURL=standing-answer-grant.js.map
@@ -11,13 +11,23 @@
11
11
  * operator/principal axis, [ref] §E6, a different trust axis + lifecycle). Re-applying on a resume leg is
12
12
  * idempotent (the same tighten), so no special resume handling is needed (mirrors runtime-governance).
13
13
  *
14
- * 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3):
14
+ * 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3) —— **scope: the `settings.permissions` channel this module projects, and only it**:
15
15
  * - permissions are folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any override that would
16
- * LOOSEN a stricter base safety field. The client can ADD denies/asks/handsReadOnly on top of the deployment +
17
- * operator baseline, never widen it. Its `allow` rules (S-654 (c), 7.115.0) STATE an allow for the tools they name —
16
+ * LOOSEN a stricter base safety field. Through THIS channel the client can ADD denies/asks/handsReadOnly on top of the
17
+ * deployment + operator baseline, never widen it. Its `allow` rules (S-654 (c), 7.115.0) STATE an allow for the tools they name —
18
18
  * the answer core 7.40.0 wants before it runs an MCP / A2A call nobody stated — and never override a deny or an ask
19
19
  * from any layer (deny > ask > allow); a miss has no opinion. They release no ask a layer raises itself: on built-in
20
20
  * tools the engine's own write gate and shell checks still ask (unlike the reference implementation's `permissions.allow`).
21
+ * - ⚠️ This is NOT a claim about the request as a whole. Since server 7.116.0 (core 7.41.0) a request-carried skill's
22
+ * `allowedTools` — activated by the model's approved `Skill` card, or named in `userInvokedSkills` — IS an accepted
23
+ * loosening on a different channel (`skills[]` → `TaskSpec.skills`, never through this module): for the rest of that run
24
+ * it releases the `unstated` / `ordinary` asks the engine itself raises (including the built-in write gate and the shell's
25
+ * ordinary asks; same predicate as a persisted allow rule), on hosted / multi-tenant deployments too — no deployment
26
+ * switch, by ruling. It still clears no deny / ask rule (request or operator), no governance / shell-floor /
27
+ * write-protection / plan-mode / probe / mandated ask, and opens no write containment. An operator who needs a person
28
+ * asked for a given tool regardless uses the per-tool knobs: `APPROVAL_REQUIRE` (an ask rule), `APPROVAL_NEVER_AUTO`
29
+ * (a person, mandated), `MANUAL_MODE_SHELL_GATE=always` (shell). Pinned on the real assembly:
30
+ * `test/skill-grant-operator-knobs.test.ts`.
21
31
  * - permissionMode(轴B #2 注释保鲜,1.254——[ref]/[ref] 后的真实现):`defaultMode` 收**五**模式
22
32
  * (default | acceptEdits | plan | bypassPermissions | auto),bypass/auto 被 HONOR 为 gate-shape 选择
23
33
  * ——但深层不变量未变:bypass=「不加 ask 门」,恒不越部署基线(tightenTaskSpec deny-wins,基线 deny/
@@ -95,6 +95,10 @@ export interface StandingAnswerFacts {
95
95
  readonly requiresRealApproval: boolean;
96
96
  /** core 闸站盖的强制位([ref]):规则清不掉、站着的答案退不掉,只有对**这一次调用**的应答能答。 */
97
97
  readonly mandated: boolean;
98
+ /** 7.116.0(审计修复批 S4,core 7.41.0 `Skill` 卡):这一问的应答**还授予**这一次调用之外的东西(批准 `Skill` = 激活技能 + 本 run 内
99
+ * 清单免问)。站着的答案要代答它必须读得出授予绑定、且绑定相同;core 7.41.0 在活的问上不给绑定 ⇒ 两侧都读不出 ⇒ 不可记、不被代答。
100
+ * 读者一律经 `standing-answer-grant.ts` 的 `answerGrantsBeyondCall`(唯一读口)折这一位。 */
101
+ readonly grantsBeyondCall: boolean;
98
102
  }
99
103
  /**
100
104
  * 🔴 **S-660 —— 会话级放行的唯一判据:站着的答案能不能退掉这一问。** 纯谓词,三个读者:
@@ -245,6 +249,15 @@ export interface ToolApprovalFrame {
245
249
  * 席照常答得了」说的是**这一次调用的应答者**,本仓不改它。
246
250
  */
247
251
  mandated?: true;
252
+ /**
253
+ * **7.116.0(审计修复批 S4;ADDITIVE,`"tool_approval"` only)—— 这张卡上的「本会话放行」只放行这一次、不会被记住。**
254
+ * 值 = 同一只判据 {@link standingAnswerMayRetire} 的否定(四位:治理强制 / 必须真人 / 强制 / 应答还授予别的东西),回决侧写授权
255
+ * (`finishRespond`)读的是同一份事实 ⇒ 帧说不可记 ⇔ 点了 `allow_session` 回执 `rememberApplied:false`。壳读这一位决定渲不渲
256
+ * 「本会话放行」,不再从 `requiresRealApproval` / `mandated` / `governanceForced` 三位自己推断(那一推断漏掉第四个理由:core 7.41.0
257
+ * 的 `Skill` 卡批准即授予清单,三位全无)。`card_json` 卡面、`livePending` 行、运维队列行同名同义。
258
+ * 🔴 **真才带,缺席绝不编 `false`**:缺席 = 可记(有会话时)或老服务端。没有会话的腿上点了照样只放行一次(回执说明),本位不管那一形。
259
+ */
260
+ rememberRefused?: true;
248
261
  /**
249
262
  * **S-721(core 7.32.0 #1092;ADDITIVE,`"tool_approval"` only)** —— 强制位**站在哪一个词上**:core `PERSISTED_RULE_MANDATES`
250
263
  * 六词之一(`operator_always` / `tool_marks` / `probe_mandate` / `probe_unanswered` / `write_protection` /
@@ -551,6 +564,9 @@ export interface LivePendingRow {
551
564
  * 值 = 条目上那一位(`askBroadcast` 铸条目时取自这一次过闸的 `req.mandated`,与 {@link frame} 上的同名键同源)。此前只在
552
565
  * `frame` 里:同一个 `GET /v1/approvals` 响应的 durable `pending[]` 行顶层带它(S-663 二段),live 行却要进帧里读。 */
553
566
  mandated?: true;
567
+ /** 7.116.0(审计修复批 S4):这张卡上的「本会话放行」不会被记住 —— 与 {@link ToolApprovalFrame.rememberRefused} 同名同义同源
568
+ * (同一只判据,读条目上的四位事实);真才带。 */
569
+ rememberRefused?: true;
554
570
  /** S-721(core 7.32.0 #1092):强制位站在哪一个词上 —— 与 `mandated` 同层同形(在场逐字,缺席不编词),值 = 条目上那一个词
555
571
  * (与 {@link frame} 上同名键同源;帧型该键顶注是全文)。 */
556
572
  mandate?: PersistedRuleMandate;
@@ -13,6 +13,7 @@ import { MAX_SETTLED_BY_CHARS, POLICY_REFUSAL_DECISION, projectAskTerminalClaimT
13
13
  import { settleByPolicy } from "./approval-policy-settlement.js";
14
14
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
15
15
  import { governanceOriginOf } from "./runtime-governance.js";
16
+ import { answerGrantsBeyondCall } from "./standing-answer-grant.js";
16
17
  const defaultLogger = createLogger();
17
18
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
18
19
  export function isApprovalGateKind(k) {
@@ -84,7 +85,7 @@ function sessionAllowKey(owner, sessionId, category) {
84
85
  return JSON.stringify([owner, sessionId, category]);
85
86
  }
86
87
  export function standingAnswerMayRetire(facts) {
87
- return !facts.governanceForced && !facts.requiresRealApproval && !facts.mandated;
88
+ return !facts.governanceForced && !facts.requiresRealApproval && !facts.mandated && !facts.grantsBeyondCall;
88
89
  }
89
90
  export function parkRowStandingAnswerFacts(pending, gate, governance) {
90
91
  const riskDescriptor = "riskDescriptor" in gate ? gate.riskDescriptor : undefined;
@@ -92,6 +93,15 @@ export function parkRowStandingAnswerFacts(pending, gate, governance) {
92
93
  governanceForced: governanceOriginOf({ riskDescriptor }, governance) === true,
93
94
  requiresRealApproval: readParkRowTrueSeat(pending, "requiresRealApproval"),
94
95
  mandated: readParkRowTrueSeat(pending, "mandated") || mandatedFromSeat(readRuleOffersAbsenceSeat(pending)),
96
+ grantsBeyondCall: answerGrantsBeyondCall(pending),
97
+ };
98
+ }
99
+ function pendingEntryStandingAnswerFacts(entry) {
100
+ return {
101
+ governanceForced: entry.governanceForced === true,
102
+ requiresRealApproval: entry.requiresRealApproval === true,
103
+ mandated: entry.mandated === true,
104
+ grantsBeyondCall: answerGrantsBeyondCall(entry),
95
105
  };
96
106
  }
97
107
  function mandatedFromSeat(seat) {
@@ -741,7 +751,7 @@ export class ToolApprovalCoordinator {
741
751
  return (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
742
752
  }
743
753
  async standingGrantSettles(origin, req, governanceForced) {
744
- if (!standingAnswerMayRetire({ governanceForced, requiresRealApproval: req.requiresRealApproval === true, mandated: req.mandated === true }))
754
+ if (!standingAnswerMayRetire({ governanceForced, requiresRealApproval: req.requiresRealApproval === true, mandated: req.mandated === true, grantsBeyondCall: answerGrantsBeyondCall(req) }))
745
755
  return false;
746
756
  const sessionId = origin.sessionId;
747
757
  if (!sessionId)
@@ -1088,6 +1098,7 @@ export class ToolApprovalCoordinator {
1088
1098
  ...(governanceForced ? { governanceForced: true } : {}),
1089
1099
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1090
1100
  ...(req.mandated === true ? { mandated: true } : {}),
1101
+ ...(standingAnswerMayRetire({ governanceForced, requiresRealApproval: req.requiresRealApproval === true, mandated: req.mandated === true, grantsBeyondCall: answerGrantsBeyondCall(req) }) ? {} : { rememberRefused: true }),
1091
1102
  ...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
1092
1103
  ...(originWord !== undefined ? { origin: originWord } : {}),
1093
1104
  ...(askClassPair ?? {}),
@@ -1614,6 +1625,20 @@ export class ToolApprovalCoordinator {
1614
1625
  return shapeOutcome(await allowedP);
1615
1626
  }
1616
1627
  }
1628
+ const rememberRefused = !standingAnswerMayRetire(pendingEntryStandingAnswerFacts({
1629
+ ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1630
+ ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1631
+ ...(req.mandated === true ? { mandated: true } : {}),
1632
+ toolName: req.toolName,
1633
+ }));
1634
+ if (rememberRefused)
1635
+ frame.rememberRefused = true;
1636
+ else
1637
+ delete frame.rememberRefused;
1638
+ if (cardForFrame !== undefined && (cardForFrame.rememberRefused === true) !== rememberRefused) {
1639
+ const { rememberRefused: _stale, ...cardRest } = cardForFrame;
1640
+ cardForFrame = rememberRefused ? { ...cardRest, rememberRefused: true } : cardRest;
1641
+ }
1617
1642
  const pendingEntry = {
1618
1643
  settle,
1619
1644
  forceParkNow,
@@ -2212,11 +2237,7 @@ export class ToolApprovalCoordinator {
2212
2237
  }
2213
2238
  finishRespond(id, entry, parsed, opts) {
2214
2239
  let rememberApplied;
2215
- const grantable = standingAnswerMayRetire({
2216
- governanceForced: entry.governanceForced === true,
2217
- requiresRealApproval: entry.requiresRealApproval === true,
2218
- mandated: entry.mandated === true,
2219
- });
2240
+ const grantable = standingAnswerMayRetire(pendingEntryStandingAnswerFacts(entry));
2220
2241
  if (parsed.value === "allow_session")
2221
2242
  rememberApplied = Boolean(entry.sessionId) && grantable;
2222
2243
  if (parsed.value === "allow_session" && entry.sessionId && grantable) {
@@ -2260,6 +2281,7 @@ export class ToolApprovalCoordinator {
2260
2281
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
2261
2282
  ...(e.governanceForced ? { governanceForced: true } : {}),
2262
2283
  ...(e.mandated ? { mandated: true } : {}),
2284
+ ...(standingAnswerMayRetire(pendingEntryStandingAnswerFacts(e)) ? {} : { rememberRefused: true }),
2263
2285
  ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
2264
2286
  ...(e.origin !== undefined ? { origin: e.origin } : {}),
2265
2287
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
@@ -47,7 +47,7 @@ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTask
47
47
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "hasBidiControls" | "previewWithheld" | "execCwd" | "authoredBy";
48
48
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
49
49
  type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
50
- type AskFrameServerMinted = "type" | "approvalId" | "argsOmitted" | "outcome" | "governanceForced" | "expiresAtMs" | "expiresInMs" | "serverNowMs" | "parked" | "inputHasBidi";
50
+ type AskFrameServerMinted = "type" | "approvalId" | "argsOmitted" | "outcome" | "governanceForced" | "expiresAtMs" | "expiresInMs" | "serverNowMs" | "parked" | "inputHasBidi" | "rememberRefused";
51
51
  type _GuardAskFrame = AssertAllKeysHandled<Exclude<keyof ToolApprovalFrame, AskProjected | AskFrameServerMinted>>;
52
52
  type _GuardAskFrameReverse = AssertAllKeysHandled<Exclude<AskProjected | AskFrameServerMinted, keyof ToolApprovalFrame>>;
53
53
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end" | "tool_roster_delta" | "tool_progress" | "tool_disclosure";
@@ -129,7 +129,7 @@ type WiringGovernanceHandled = "audience" | "lockedConfig" | "compliance" | "mem
129
129
  type _GuardWiringGovernance = AssertAllKeysHandled<Exclude<keyof WiringSection<"governance">, WiringGovernanceHandled>>;
130
130
  type _GuardWiringGovernanceReverse = AssertAllKeysHandled<Exclude<WiringGovernanceHandled, keyof WiringSection<"governance">>>;
131
131
  type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
132
- export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed";
132
+ export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed" | "permission.skill_allowed";
133
133
  type PermissionTraceDropped = "permission.sandbox_admitted" | "permission.rule_sync_resurrected" | "permission.rule_sync_dropped" | "permission.org_snapshot_unavailable";
134
134
  type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped>>;
135
135
  type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped, PermissionTraceKind>>;
@@ -144,11 +144,11 @@ type ParkArmKeys = keyof Extract<PendingAction, {
144
144
  kind: "tool_approval";
145
145
  }>;
146
146
  type ParkArmQueueProjected = "toolName" | "toolCallId" | "boundInputHash" | "ruleOffers" | "hasBidiControls" | keyof import("../approval-card.js").ParkRowFacts;
147
- type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "names" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "authoredBy";
147
+ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "names" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "authoredBy" | "boundGrantDigest";
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" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory";
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" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills";
152
152
  type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "outputStyle";
153
153
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
154
154
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
@@ -113,6 +113,48 @@ export declare const SELF_MINTED_WIRE: {
113
113
  };
114
114
  };
115
115
  };
116
+ /**
117
+ * 审批问的**无人决终态归因**:`POST /v1/tasks/:taskId/asks/:askId/decision` 410 `gone.ask` 体的 `deniedReason`(问账行 `denied_reason` 列原值)。
118
+ * 词表属主 = `approval-deny-reasons.ts` 的 `DENY_REASONS` ∪ `VOID_REASONS`(`satisfies Record<DenyReason | VoidReason, …>`:表加词而本行没跟 ⇒ tsc 红)。
119
+ * 7.116.0 +2:启动收敛(承载它的进程没了)的两个词 —— 与 `crashConvergedCause` 同词(同一问在两个读面上一个成因)。
120
+ */
121
+ readonly askTerminalReason: {
122
+ readonly at: "POST /v1/tasks/:taskId/asks/:askId/decision 410 gone.ask deniedReason";
123
+ readonly members: {
124
+ routing_failure_fail_closed: {
125
+ attr: string;
126
+ since: string;
127
+ };
128
+ run_cancelled: {
129
+ attr: string;
130
+ since: string;
131
+ };
132
+ batch_aborted: {
133
+ attr: string;
134
+ since: string;
135
+ };
136
+ batch_bound_elsewhere: {
137
+ attr: string;
138
+ since: string;
139
+ };
140
+ adhoc_leg_no_durable_domain: {
141
+ attr: string;
142
+ since: string;
143
+ };
144
+ orphan_ttl_exceeded: {
145
+ attr: string;
146
+ since: string;
147
+ };
148
+ crashed_before_park: {
149
+ attr: string;
150
+ since: string;
151
+ };
152
+ shutdown_before_park: {
153
+ attr: string;
154
+ since: string;
155
+ };
156
+ };
157
+ };
116
158
  /** 本仓在出口对 core 值做的变换(值变、键不变)。 */
117
159
  readonly wireValueRule: {
118
160
  readonly at: "出口值变换(<端点>#<键> → 变换)";
@@ -40,6 +40,19 @@ export const SELF_MINTED_WIRE = {
40
40
  "internal.memory_control_plane_corrupt": { attr: 500, since: "7.108.0" },
41
41
  },
42
42
  },
43
+ askTerminalReason: {
44
+ at: "POST /v1/tasks/:taskId/asks/:askId/decision 410 gone.ask deniedReason",
45
+ members: {
46
+ routing_failure_fail_closed: { attr: "-", since: "7.3.0" },
47
+ run_cancelled: { attr: "-", since: "7.3.0" },
48
+ batch_aborted: { attr: "-", since: "7.3.0" },
49
+ batch_bound_elsewhere: { attr: "-", since: "7.3.0" },
50
+ adhoc_leg_no_durable_domain: { attr: "-", since: "7.3.0" },
51
+ orphan_ttl_exceeded: { attr: "-", since: "7.3.0" },
52
+ crashed_before_park: { attr: "-", since: "7.116.0" },
53
+ shutdown_before_park: { attr: "-", since: "7.116.0" },
54
+ },
55
+ },
43
56
  wireValueRule: {
44
57
  at: "出口值变换(<端点>#<键> → 变换)",
45
58
  members: {
@@ -13,15 +13,18 @@
13
13
  * 「本服务以回执向调用方承诺已交给引擎、而消费只发生在进程内存里的用户输入」,**且承载它的那只 `TaskStream` 活在本服务的一条 run 腿里**(S-966)——
14
14
  * 后半句是收敛判词成立的前提:证据面是那条 run 行(下文「收敛判据」)。行的 `taskId` = 回执上的 run、`sessionId` = 那条 run 的会话(人看的那一只),
15
15
  * run 里是哪一只流(run 自己的 / 同步子代的)**不是**账本的事实:告知按 run 并帧(`steer` = 条数),读面行同形。今天只有 steer 一族(`kind:"steer"` =
16
- * core 的车道词:引擎把这些输入记在 steer 车道),三个入口同一个引擎动词 `TaskStream.steer`:
16
+ * 受理它的引擎动词 `TaskStream.steer`,三档 `now` / `next` / `later` 同一个动词同一族),三个入口同一个引擎动词:
17
17
  * · `POST /v1/runs/:id/steer` 的 live 腿(200 applied);
18
18
  * · `POST /v1/runs/:id/interrupt` 的带 text 形(202 queued-immediate,档位 now;设计稿把它划在病面外,车 SW 亲核它进的是同一只进程内队列 ⇒ 同类同修);
19
19
  * · `POST /v1/runs/:id/subagents/:target/steer`(200 applied,S-966)—— 投进**同步子代**自己的流(core `createSteerHandle` → `stream.steer`)。子代由父腿
20
20
  * await,父腿收场时 core 撤掉在飞的复活(`subagentRetain.disposeAll`),所以它活在父 run 的腿里;腿 = 子代句柄,`handle.settled` 兑现 ⇒ 结清。
21
21
  * 规则的反面(不是例外表):workflow agent steer(`POST /v1/workflows/:id/agents/:label/steer`,200 applied)不在本账 —— workflow 是会话锚定的后台件
22
22
  * (core `WorkflowTaskHandle.sessionScoped`),活得比发起它的 run 腿长,没有一条 run 行约束它,而 workflow run 行上没有持有者列 ⇒ 判不出写者死没死(另票);
23
- * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**进程内受理口(`src/http/routes` 零
24
- * follow-up 入口;steer 路由不下传 `priority`,core 缺省 `next` = steer 车道)⇒ 闭集一词,不预留第二词。
23
+ * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**独立的受理口(`src/http/routes` 零
24
+ * follow-up 入口)。steer 的 `later` 档在引擎里走 follow-up 车道(引擎在腿收场时按车道分账:没投出去的 `later` 记进 `task.user_followup_undrained`),
25
+ * 但它仍是一次 `TaskStream.steer` 受理、消费帧同为 `human_input{delivery:"applied"}`(两车道同一只消费口)⇒ 同进本账,消费即结清;崩溃告知走 core 唯一的
26
+ * 服务层臂 `task.user_steer_undrained{cause:"crashed"}`(core 对该臂的定义按动词:服务层受理过的 steer)。follow-up 一族在 core 里没有 `cause` 键、没有
27
+ * 服务层臂 ⇒ 本账无处按车道归档,要按车道归档须 core 先给那一臂(上游待答)⇒ 闭集一词,不预留第二词。
25
28
  *
26
29
  * ## 五形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列 + 删行,同义)
27
30
  * · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
@@ -50,8 +53,8 @@
50
53
  * ## 收敛判据(一条:**受理它的进程已经不在,而承载它的腿没有正常收场**)—— 两车道同一只判词({@link judgeOpen},S-969 L8 合一)
51
54
  * 证据面只有一个:承载它的 **run 行**(run 店;行上的 `instanceId` = 当前持有者,createRun 铸、markResuming 接管)。
52
55
  * · 行在、持有者是写者、仍 `running` ⇒ 腿没收场;写者在不在由车道答(下一段)—— 不在 ⇒ `crashed`,可能在 ⇒ `live`(不动,等 reaper)。
53
- * · 行在、持有者是写者、`failed` 且落款是 run 店的**死亡判决**({@link isHolderDeathVerdict}:`reapStale` 的 `RUN_STALLED_ERROR` /
54
- * File 车道孤儿回收与 claim 接管的 `RUN_ORPHANED_ERROR`)⇒ 写者死在腿收场之前 ⇒ `crashed`。判决落盘之后才收(codex r1-F1 / r3-F1:
56
+ * · 行在、持有者是写者、`failed` 且行上是 run 店的**死亡判决码**({@link isHolderDeathVerdict}:`reapStale` 的 `run.stalled` /
57
+ * File 车道孤儿回收与 claim 接管的 `run.orphaned`;7.116.0 起按码判,句子只给人读)⇒ 写者死在腿收场之前 ⇒ `crashed`。判决落盘之后才收(codex r1-F1 / r3-F1:
55
58
  * 读到的旧心跳不算 —— 读后写者可能恢复;收敛与 reap 的先后因此不承重)。维护 tick 里收敛排在 reapStale **之后**(同一条腿串行)。
56
59
  * · 其余 —— 行正常翻面(完成 / 失败 / 取消)、已 park(suspended / needs_review:引擎把未消费的 steer 迁进检查点,续跑重投)、易手(续跑被别的
57
60
  * 进程接走)⇒ 腿正常收场了,只是没人替它落 settle ⇒ 补落 `settled{leg_end}`(判词 `leg_over`,见 {@link OpenVerdict}),不告知。