@sema-agent/server 7.121.0 → 7.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/MIGRATION.md +175 -0
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +6 -1
  4. package/USAGE.md +57 -18
  5. package/dist/approval-reconciler.d.ts +8 -0
  6. package/dist/approval-reconciler.js +72 -52
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +52 -22
  9. package/dist/auto-mode-face.d.ts +16 -5
  10. package/dist/auto-mode-face.js +7 -4
  11. package/dist/bench/s1/live-deps.js +1 -0
  12. package/dist/boot/config-center.js +3 -0
  13. package/dist/boot/leader.js +1 -0
  14. package/dist/boot/reapers.d.ts +3 -0
  15. package/dist/boot/reapers.js +2 -1
  16. package/dist/boot/resolve-spec.js +1 -4
  17. package/dist/boot/shutdown.js +6 -1
  18. package/dist/boot/stage-08-reapers.js +2 -2
  19. package/dist/capabilities/memory-notice.d.ts +1 -1
  20. package/dist/config-catalog.js +4 -4
  21. package/dist/config-center/apply-effective.d.ts +1 -1
  22. package/dist/config-center/apply-effective.js +5 -3
  23. package/dist/config-center/center-request.d.ts +6 -9
  24. package/dist/config-center/center-request.js +11 -27
  25. package/dist/config-center/http-client.js +3 -3
  26. package/dist/config-center/read-warnings.d.ts +56 -35
  27. package/dist/config-center/read-warnings.js +67 -19
  28. package/dist/config-center/types.d.ts +7 -8
  29. package/dist/config-lkg.d.ts +12 -9
  30. package/dist/config-lkg.js +38 -39
  31. package/dist/config-provider.d.ts +3 -2
  32. package/dist/config-provider.js +2 -2
  33. package/dist/config-types.d.ts +20 -9
  34. package/dist/config-types.js +1 -1
  35. package/dist/config.d.ts +10 -10
  36. package/dist/config.js +92 -127
  37. package/dist/fleet/drain-entry.d.ts +28 -3
  38. package/dist/fleet/drain-entry.js +3 -0
  39. package/dist/fleet/engine-signal.d.ts +54 -23
  40. package/dist/fleet/engine-signal.js +29 -5
  41. package/dist/fleet/serving-lease.js +13 -15
  42. package/dist/fleet-client.d.ts +8 -6
  43. package/dist/fleet-client.js +4 -3
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +2 -2
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +22 -19
  50. package/dist/http/health-self-report.d.ts +3 -0
  51. package/dist/http/resume-legs.js +8 -8
  52. package/dist/http/route-ctx.d.ts +19 -3
  53. package/dist/http/route-table.d.ts +3 -0
  54. package/dist/http/route-table.js +4 -0
  55. package/dist/http/routes/admin-drain.d.ts +5 -2
  56. package/dist/http/routes/admin-drain.js +14 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -0
  58. package/dist/http/routes/approvals-assistant.js +6 -6
  59. package/dist/http/routes/capabilities.js +7 -5
  60. package/dist/http/routes/fleet.d.ts +1 -0
  61. package/dist/http/routes/fleet.js +1 -1
  62. package/dist/http/routes/images.d.ts +1 -0
  63. package/dist/http/routes/images.js +1 -1
  64. package/dist/http/routes/leader.js +6 -0
  65. package/dist/http/routes/runs.d.ts +2 -0
  66. package/dist/http/routes/runs.js +5 -4
  67. package/dist/http/routes/sessions.d.ts +1 -0
  68. package/dist/http/routes/sessions.js +2 -5
  69. package/dist/http/routes/trace-usage.d.ts +1 -0
  70. package/dist/http/routes/trace-usage.js +1 -1
  71. package/dist/http/routes/workflows.d.ts +1 -0
  72. package/dist/http/routes/workflows.js +1 -1
  73. package/dist/http/send.d.ts +13 -0
  74. package/dist/http/send.js +9 -0
  75. package/dist/http/server.d.ts +3 -0
  76. package/dist/http/stored-body-replay.d.ts +28 -0
  77. package/dist/http/stored-body-replay.js +22 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +1 -0
  80. package/dist/leader/leader.d.ts +23 -1
  81. package/dist/leader/leader.js +6 -1
  82. package/dist/leader/repair-wire.d.ts +1 -1
  83. package/dist/leader/repair-wire.js +2 -2
  84. package/dist/leader/run-entry.d.ts +27 -0
  85. package/dist/leader/run-entry.js +29 -0
  86. package/dist/leader/wire.d.ts +10 -0
  87. package/dist/leader/wire.js +15 -10
  88. package/dist/model-entry-refusal.d.ts +18 -9
  89. package/dist/model-entry-refusal.js +19 -14
  90. package/dist/model-route-endpoint.d.ts +21 -1
  91. package/dist/model-route-endpoint.js +9 -3
  92. package/dist/model-route-gap.d.ts +103 -0
  93. package/dist/model-route-gap.js +80 -0
  94. package/dist/model-route-url.d.ts +13 -20
  95. package/dist/model-route-url.js +28 -24
  96. package/dist/namespaced-rule-name.d.ts +16 -30
  97. package/dist/namespaced-rule-name.js +2 -20
  98. package/dist/observability/fail-open.d.ts +23 -10
  99. package/dist/observability/fail-open.js +8 -8
  100. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  101. package/dist/plugins/approval-ask-store-file.js +3 -0
  102. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  103. package/dist/plugins/approval-ask-store-memory.js +6 -0
  104. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  105. package/dist/plugins/approval-ask-store-sql.js +4 -0
  106. package/dist/plugins/one-time-migrations.js +40 -0
  107. package/dist/plugins/park-commit-watch.d.ts +19 -5
  108. package/dist/plugins/park-commit-watch.js +24 -17
  109. package/dist/plugins/session-policy-store-sql.js +1 -2
  110. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  111. package/dist/router/route-orchestration.d.ts +4 -2
  112. package/dist/router/route-orchestration.js +2 -1
  113. package/dist/rule-entry-list.d.ts +3 -13
  114. package/dist/rule-entry-list.js +1 -12
  115. package/dist/run-local.js +4 -2
  116. package/dist/runtime-governance.d.ts +67 -32
  117. package/dist/runtime-governance.js +28 -9
  118. package/dist/session-policy-wire.d.ts +47 -49
  119. package/dist/session-policy-wire.js +41 -36
  120. package/dist/session-sync.d.ts +5 -1
  121. package/dist/session-sync.js +2 -1
  122. package/dist/sighup-idle.d.ts +7 -1
  123. package/dist/sighup-idle.js +4 -4
  124. package/dist/spec-fields.d.ts +4 -4
  125. package/dist/spec-fields.js +1 -4
  126. package/dist/task-settings.d.ts +36 -14
  127. package/dist/task-settings.js +20 -23
  128. package/dist/tool-approval.d.ts +6 -0
  129. package/dist/tool-approval.js +13 -1
  130. package/dist/trace/redact.d.ts +3 -3
  131. package/dist/unattended-permission-mode.d.ts +17 -13
  132. package/dist/unattended-permission-mode.js +3 -1
  133. package/dist/url-shape.d.ts +31 -0
  134. package/dist/url-shape.js +41 -0
  135. package/package.json +3 -3
  136. package/scripts/drain-check-local.mjs +1 -1
@@ -1,6 +1,5 @@
1
- import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent } from "@sema-agent/core";
1
+ import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, normalizeRules } from "@sema-agent/core";
2
2
  import { displayListEntry } from "./list-entry-display.js";
3
- import { describeEmptyToolSegment, emptyToolSegmentOf } from "./namespaced-rule-name.js";
4
3
  const RULE_FIELDS = {
5
4
  toolAllow: true,
6
5
  toolDeny: true,
@@ -20,33 +19,18 @@ function legacyToolNameRefusalOf(rules) {
20
19
  throw e;
21
20
  }
22
21
  }
23
- const TOOL_NAME_FIELD_TABLE = { toolAllow: "toolAllow", toolDeny: "toolDeny" };
24
- const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
25
- export function unreachableSessionRuleNameOf(rules) {
26
- for (const field of TOOL_NAME_FIELDS) {
27
- for (const name of rules[field] ?? []) {
28
- const ns = emptyToolSegmentOf(name);
29
- if (ns !== undefined)
30
- return { field, name, ns };
31
- }
32
- }
33
- return undefined;
34
- }
35
- export function assertSessionRuleNamesReachable(rules) {
36
- const unreachable = unreachableSessionRuleNameOf(rules);
37
- if (unreachable !== undefined)
38
- throw new Error(describeUnreachableSessionRuleName(unreachable));
39
- }
40
22
  function protocolToolNameShapes() {
41
23
  return PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(" or ");
42
24
  }
25
+ function isRulesObject(raw) {
26
+ return raw !== null && typeof raw === "object" && !Array.isArray(raw);
27
+ }
43
28
  export function readSessionPermissionRules(raw) {
44
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
29
+ if (!isRulesObject(raw))
45
30
  return { problem: { kind: "not-an-object" } };
46
- const src = raw;
47
31
  const rules = {};
48
32
  for (const field of SESSION_PERMISSION_RULE_FIELDS) {
49
- const v = src[field];
33
+ const v = raw[field];
50
34
  if (v === undefined)
51
35
  continue;
52
36
  if (!Array.isArray(v) || !v.every((x) => typeof x === "string"))
@@ -55,17 +39,42 @@ export function readSessionPermissionRules(raw) {
55
39
  }
56
40
  return { rules };
57
41
  }
42
+ function engineRuleListOf(field, value) {
43
+ let normalized;
44
+ try {
45
+ normalized = normalizeRules({ [field]: value });
46
+ }
47
+ catch (e) {
48
+ if (e instanceof TypeError)
49
+ return { refusal: e.message };
50
+ throw e;
51
+ }
52
+ const list = normalized[field];
53
+ return list === undefined ? {} : { list };
54
+ }
55
+ export function unwritableRuleListOf(rules) {
56
+ for (const field of SESSION_PERMISSION_RULE_FIELDS) {
57
+ const judged = engineRuleListOf(field, rules[field]);
58
+ if ("refusal" in judged)
59
+ return { field, refusal: judged.refusal };
60
+ }
61
+ return undefined;
62
+ }
58
63
  export function readWritableSessionPermissionRules(raw) {
59
- const read = readSessionPermissionRules(raw);
60
- if (read.problem !== undefined)
61
- return read;
62
- const refusal = legacyToolNameRefusalOf(read.rules);
64
+ if (!isRulesObject(raw))
65
+ return { problem: { kind: "not-an-object" } };
66
+ const rules = {};
67
+ for (const field of SESSION_PERMISSION_RULE_FIELDS) {
68
+ const judged = engineRuleListOf(field, raw[field]);
69
+ if ("refusal" in judged)
70
+ return { problem: { kind: "field-not-rule-list", field, refusal: judged.refusal } };
71
+ if (judged.list !== undefined)
72
+ rules[field] = judged.list;
73
+ }
74
+ const refusal = legacyToolNameRefusalOf(rules);
63
75
  if (refusal !== undefined)
64
76
  return { problem: { kind: "legacy-tool-name", refusal } };
65
- const unreachable = unreachableSessionRuleNameOf(read.rules);
66
- if (unreachable !== undefined)
67
- return { problem: { kind: "unreachable-tool-name", unreachable } };
68
- return read;
77
+ return { rules };
69
78
  }
70
79
  export function readSessionRulesRecords(raw) {
71
80
  if (!Array.isArray(raw))
@@ -91,17 +100,14 @@ export function describeSessionPermissionRulesProblem(p) {
91
100
  switch (p.kind) {
92
101
  case "not-an-object": return "rules must be a SessionPermissionRules object";
93
102
  case "field-not-string-array": return `${p.field} must be an array of strings`;
103
+ case "field-not-rule-list": return p.refusal;
94
104
  case "legacy-tool-name": return describeLegacyToolName(p.refusal);
95
- case "unreachable-tool-name": return describeUnreachableSessionRuleName(p.unreachable);
96
105
  default: {
97
106
  const unhandled = p;
98
107
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
99
108
  }
100
109
  }
101
110
  }
102
- function describeUnreachableSessionRuleName(u) {
103
- return `${u.field} names ${displayListEntry(u.name)}, which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
104
- }
105
111
  function describeLegacyToolName(r) {
106
112
  const why = r.guidance !== undefined ? ` (${r.guidance})` : "";
107
113
  switch (r.verdict) {
@@ -126,8 +132,7 @@ export function legacyToolNameProblemOf(p) {
126
132
  case "rules":
127
133
  switch (p.problem.kind) {
128
134
  case "not-an-object":
129
- case "field-not-string-array":
130
- case "unreachable-tool-name":
135
+ case "field-not-rule-list":
131
136
  return undefined;
132
137
  case "legacy-tool-name":
133
138
  return p.problem.refusal;
@@ -159,7 +159,11 @@ export declare function planSync(sessionId: string, srcBackend: StoreBackend, ds
159
159
  * 无条件写都 rev+1,而 rev 是 operator 乐观锁的 CAS 令牌(`PutRulesOptions.expectedRev`),
160
160
  * 幂等重试/无害重复同步不该把在途 CAS 打失败。归一化用 core 的 `normalizeRules`(store 落盘
161
161
  * 前走同一函数 ⇒ 两侧同坐标);`stripRev` 去掉记录内嵌的 rev(`listBySession` 的 rules 含 rev,
162
- * 它不是内容)。 */
162
+ * 它不是内容)。
163
+ * 🔴 core 7.48.0 起 `normalizeRules` 对「不是一列非空字符串」的桶抛 `TypeError`。`incoming` 由写口读者保证是引擎肯写的
164
+ * (`session-policy-wire.ts` `readSessionRulesRecords`);`stored` 可能是 7.47.x 及以前写下的空串项存量行 —— 读得出、引擎却不肯再写。
165
+ * 这种行与任何肯写的 `incoming` 都**不同内容**(它多一个空项),所以答 `false`(判官 = 写口读者那一只 `unwritableRuleListOf`,不拿它跑 normalize):
166
+ * 写照常交给店 —— 店的收紧门对它照常判(operator 照写,是清旧行的出路;非 operator 删空串 deny 项被读成放宽 ⇒ 403,与 7.122.0 同答)。 */
163
167
  export declare function sameRulesContent(stored: StoredSessionRules | null, incoming: SessionPermissionRules): boolean;
164
168
  /** [ref] ① 的重放腿共享实现:PUSH 路由的两条 policy 重放腿(Phase A identical 短路支 / Phase B
165
169
  * commit 后支)都从这里走 —— 等内容跳过(rev 零漂移),真变更照常落地(E6 tighten-only 门在
@@ -1,5 +1,6 @@
1
1
  import { classifySyncRelationship } from "./session-sync-kernel.js";
2
2
  import { normalizeRules, stripRev } from "@sema-agent/core";
3
+ import { unwritableRuleListOf } from "./session-policy-wire.js";
3
4
  export function fileHistoryImportFace(fs) {
4
5
  const candidate = fs?.importHistory;
5
6
  if (typeof candidate !== "function")
@@ -54,7 +55,7 @@ export async function planSync(sessionId, srcBackend, dstBackend) {
54
55
  return classifySyncRelationship(srcEntries, dstEntries);
55
56
  }
56
57
  export function sameRulesContent(stored, incoming) {
57
- if (stored === null)
58
+ if (stored === null || unwritableRuleListOf(stored) !== undefined)
58
59
  return false;
59
60
  const norm = (r) => JSON.stringify(normalizeRules(stripRev(r)));
60
61
  return norm(stored) === norm(incoming);
@@ -8,11 +8,17 @@
8
8
  * 有界自灭=T11 保留;第二个 SIGHUP 升级 escalate(既有 drain 路径,操作员硬梯子)。
9
9
  *
10
10
  * 诚实边界:idle-but-live 的 peer(两 turn 之间静默超过窗口)会碰到引擎已退→壳 dead-respawn 1-2s 自愈
11
- * (伤害面从 mid-turn 斩降级为冷启动)。若壳侧确认每活会话对引擎持长连,可把连接数并入 inflight 判据。
11
+ * (伤害面从 mid-turn 斩降级为冷启动)。
12
+ * S-1281:附着(打开着的 SSE 流)是判据的第二格 `attached` —— 只读订阅自此不进 inflight(排空不该等一个看着的壳),而一个只挂着订阅流的
13
+ * peer 壳(cli 附着形:`GET /v1/approvals/stream` 常开)仍然是「有人在用」,靠这一格不被当成孤儿。修前这一形是订阅流被算进 inflight
14
+ * 时顺带挡住的;拆开之后两件事各有一格,规则不变:inflight = 0 ∧ attached = 0 ∧ 活性静默满窗才关。
12
15
  */
13
16
  export interface SighupIdleDeps {
14
17
  /** This replica's in-flight legs (drainState.inflight). */
15
18
  inflight: () => number;
19
+ /** S-1281:打开着的 SSE 流条数(drainState.attachedStreams,附着租约同一只 getter)。> 0 ⇒ 有壳挂着,同 inflight 一样重置窗口。
20
+ * 缺席 ⇒ 视为 0(单测 / 嵌入形)。 */
21
+ attached?: () => number;
16
22
  /** MONOTONIC-clock ms of the last non-probe HTTP request (drainState.lastActivityAt). Covers what inflight
17
23
  * can't: admitted-but-not-yet-registered requests (auth/prepare/claim phases) and sub-tick short requests —
18
24
  * and keeps an idle-but-live peer (still listing/browsing) alive. Absent ⇒ inflight-only semantics.
@@ -22,16 +22,16 @@ export function createSighupIdleHandler(deps) {
22
22
  }
23
23
  armedAt = now();
24
24
  const inflight = deps.inflight();
25
- idleSince = inflight === 0 ? now() : null;
26
- deps.log("sighup_idle_watch_armed", { inflight, idleGraceMs: deps.idleGraceMs });
25
+ const attached = deps.attached?.() ?? 0;
26
+ idleSince = inflight === 0 && attached === 0 ? now() : null;
27
+ deps.log("sighup_idle_watch_armed", { inflight, attached, idleGraceMs: deps.idleGraceMs });
27
28
  watch = setInterval(() => {
28
29
  if (deps.isStopped()) {
29
30
  clearInterval(watch);
30
31
  watch = undefined;
31
32
  return;
32
33
  }
33
- const n = deps.inflight();
34
- if (n > 0) {
34
+ if (deps.inflight() > 0 || (deps.attached?.() ?? 0) > 0) {
35
35
  idleSince = null;
36
36
  return;
37
37
  }
@@ -51,16 +51,16 @@ export declare const MAX_TASK_AGENTS = 32;
51
51
  * `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
52
52
  * (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
53
53
  * `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
54
- * `skillDenyInertEntryOf`,判据 = 引擎的条目读法)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 + 引擎读不到的那一形 ⇒ 400)。
54
+ * `skillDenyInertEntryOf`,判据 = 引擎的条目读法)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 ⇒ 400)。
55
55
  * 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
56
56
  * 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
57
57
  */
58
58
  export declare const skillToolListOk: (v: unknown) => v is readonly string[];
59
59
  /**
60
60
  * 一条技能(形已过两道门的字符串数组判)的两只清单在受理口的拒因 —— 都过 ⇒ `undefined`。两处受理口同调:顶层 `skills[]`
61
- * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。两问:条数帽;引擎读不到、却永远匹配不到的
62
- * 条目(工具段为空的协议名,`rule-entry-list.ts` 那一份,与配置表、会话规则写口同一只)。退役名 / 对端段铸不出 / 读成空的条目是引擎的读法(见上):
63
- * 允侧交引擎通告,拒侧由 `resolveSpec` 拒。
61
+ * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。一问:条数帽。退役名 / 对端段或工具段铸不出、为空 /
62
+ * 读成空的条目是引擎的读法(见上):允侧交引擎通告,拒侧由 `resolveSpec` 拒(7.116.0–7.122.x 本仓在这里另拒工具段为空的协议名,core 7.48.0 #1400 起
63
+ * 它是引擎判官的判词,随之退役)。
64
64
  */
65
65
  export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
66
66
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
@@ -1,7 +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 { allowLayerEntryCountIssue, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
4
+ import { allowLayerEntryCountIssue, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
5
  export function normalizeSuggestNextPrompts(v) {
6
6
  if (v === true)
7
7
  return true;
@@ -76,9 +76,6 @@ export function skillToolListRefusal(skill) {
76
76
  const over = allowLayerEntryCountIssue(`skill.${key}`, list);
77
77
  if (over !== undefined)
78
78
  return over.message;
79
- const empty = emptyToolSegmentEntryOf(list);
80
- if (empty !== undefined)
81
- return describeEmptyToolSegmentEntry(`skill.${key}`, empty);
82
79
  }
83
80
  return undefined;
84
81
  }
@@ -13,8 +13,8 @@
13
13
  *
14
14
  * 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3) —— **scope: the `settings.permissions` channel this module projects, and only it**:
15
15
  * - `deny` / `ask` are folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any override that would
16
- * LOOSEN a stricter base safety field. Through those two buckets the client can only ADD denies / asks / handsReadOnly on
17
- * top of the deployment + operator baseline.
16
+ * LOOSEN a stricter base safety field. Through those two buckets the client can only ADD denies / asks on top of the
17
+ * deployment + operator baseline.
18
18
  * - `allow` (server 7.117.0, core 7.42.0) is NOT a policy: the list rides WHOLE and verbatim into `TaskSpec.allowedTools`,
19
19
  * the request member of the engine's ONE allow layer (beside a deployment's `RunnerDeps.allowedTools`, a person's stored
20
20
  * rows, an active skill's `allowed-tools` and a PreToolUse hook's `allow`), read under one predicate. It IS an accepted
@@ -81,12 +81,13 @@ export declare function coercePermissionModeStrict(raw: unknown): PermissionMode
81
81
  * S-433 —— 一份**被批准的计划**之后,这条会话继续用的 permission mode(`plan_review` 决裁体的
82
82
  * `permissionModeAfter`)。core 的契约句是 `present-plan-tool.js`:「If the plan is approved, the run
83
83
  * continues (and any read-only restriction is lifted)」—— 而 core 顶层 resume 把宿主的续跑配置**整体**
84
- * 展开成新一段 spec、不继承停靠点的 `handsReadOnly` / `enablePlanMode`(`runtask.ts`),所以「松开」这
85
- * 件事的属主在**本仓装配续跑配置的那一处**。批准者要说的正是「松到哪一档」。
84
+ * 展开成新一段 spec、读的是那段 spec 上的模式词(`TaskSpec.permissionMode`),所以「松开」这件事的属主在
85
+ * **本仓装配续跑配置的那一处**(改写续跑体上的模式词)。批准者要说的正是「松到哪一档」。
86
86
  *
87
87
  * 🔴 **闭集为什么只有这两个词**:它是 CC `ExitPlanMode` 三选卡上那两张 Yes 卡(「Yes, and auto-accept
88
- * edits」/「Yes, and manually approve edits」)的对位;`plan` 自己(= 不松开)由 `edit`/`reject` 两个
89
- * 决裁表达,`bypassPermissions` / `auto` 不在这张卡上 —— 一个批准动作不该顺带把部署的 ask 面整个关掉。
88
+ * edits」/「Yes, and manually approve edits」)的对位;`plan` 自己(= 不松开)由 `reject` 表达,
89
+ * `bypassPermissions` / `auto` 不在这张卡上 —— 一个批准动作不该顺带把部署的 ask 面整个关掉。
90
+ * 收这一位的决定 = 把计划带出 plan 的决定(`approve` / `edit`,{@link planReviewDecisionExitsPlan})。
90
91
  *
91
92
  * 🔴 **不手抄词表**:`satisfies readonly PermissionMode[]` 让「本表的成员必须仍是 core 的模式词」成为**编译期**事实 ——
92
93
  * core 某天改名/删词,这里就是红的,而不是静默留一个没人认的词。
@@ -97,16 +98,39 @@ export type PlanApprovePermissionModeAfter = (typeof PLAN_APPROVE_PERMISSION_MOD
97
98
  /** S-433 值闭集判据(wire 门用)。**宽臂一个都没有**:词表外的值 400 响亮拒 —— 这一位选的是批准之后
98
99
  * 模型有没有手,把一个拼错的词折成任何一档都是安全轴上的静默降级([ref]「无静默 fail-open」)。 */
99
100
  export declare function isPlanApprovePermissionModeAfter(raw: unknown): raw is PlanApprovePermissionModeAfter;
101
+ /**
102
+ * `plan_review` 的这个决定把计划**带出 plan** 吗 —— 判据与引擎**同一句**:引擎在 plan_review 的续跑上出 plan 的条件是「决定不是 `reject`」
103
+ * (`approve` 按原计划开干、`edit` 按人改过的计划开干;`reject` 送回去重做计划,plan 保留)。两个读者读这一只:HTTP 门(`permissionModeAfter`
104
+ * 只配出 plan 的决定)与写回(`drivePlanReview` 把存储体的词写成 `permissionModeAfter ?? "default"`)。两处各写一句判据,就会出现
105
+ * 「写回按 approve / edit 写、门只在 approve 上收」那种分家。闭集穷尽 switch:决定词表加一词 ⇒ 编译红。
106
+ */
107
+ export declare function planReviewDecisionExitsPlan(decision: "approve" | "edit" | "reject"): boolean;
108
+ /**
109
+ * 运维 `AUTONOMY=plan` 的**入口词地板**盖进提交体 —— 此后一条 run 的模式词只有存储体顶层 `permissionMode` 一个源。
110
+ *
111
+ * 入口词 = `runtime-governance.ts` `entryPermissionMode(体的生效词, 地板).effective`(「词在地板下变成什么」的唯一一只函数;秩是 core 的)。本函数只加
112
+ * 「写进体」这一半:地板缺席 ⇒ 体**原物**交回(零字节变化);地板在场 ⇒ 新体 `{ ...body, permissionMode: 入口词 }`,调用方的对象不改;bundle 的
113
+ * `settings.permissions.defaultMode` 不动 —— 顶层赢过它({@link effectivePermissionMode} 的优先序),再改它就是同一语义的第二个写者。
114
+ *
115
+ * 调用点一个:提交入口(`http/admission.ts` 的 `prepareSpec`:值形门全部判完之后、`resolveSpec` 之前)。那一份体就是首跑装配读的体,也是
116
+ * `buildCheckpointCtx` 持久化的体 ⇒ 一处盖章同时覆盖首跑与每一次续跑重建;装配腿与续跑腿都不读地板(两处读 = 两个源)。
117
+ * 另一个写者是 `plan_review` 写回(`http/resume-legs.ts` `drivePlanReview`,判据 {@link planReviewDecisionExitsPlan})。
118
+ * 热改 `AUTONOMY` 只影响之后提交的任务:在飞的 run 保持它出生时盖下的词。地板的属主是 `runtime-governance.ts` 的 `autonomyModeFloor`。
119
+ */
120
+ export declare function stampEntryPermissionMode<B extends {
121
+ permissionMode?: unknown;
122
+ settings?: unknown;
123
+ }>(body: B, floor: PermissionMode | undefined): B;
100
124
  /**
101
125
  * S-433 —— **一条已持久化的提交体的生效 permission mode**,单点。
102
126
  *
103
127
  * 它只是把 {@link effectivePermissionMode} 与「拿哪一份 settings 去问」这**一对**绑在一起:后者是
104
128
  * `parseTaskSettings(body.settings)`(resume 重放共用的宽容解析层)。两处各写一遍这一对,就会在
105
129
  * 「某天改成用别的 settings 去算生效模式」的那一拍静默分家 —— 而分家在这条轴上的形态是「wire 门说
106
- * 这条任务不是只读起步、装配腿却按只读起步重建」,两个 4xx/行为各说各话,tsc 一声不响。
130
+ * 这条任务不是 plan 起步、装配腿却按 plan 起步重建」,两个 4xx/行为各说各话,tsc 一声不响。
107
131
  *
108
- * ⚠️ 射程:**只管持久化体这一路**。首跑那一路的生效模式在 `boot/resolve-spec.ts` 单点算(它吃的是
109
- * 场景折叠后的 settings),本函数刻意不去冒充那一处的属主。
132
+ * ⚠️ 射程:**只管持久化体这一路**(含即将成为持久化体的那一份提交体 —— {@link stampEntryPermissionMode} 读它)。首跑那一路的
133
+ * 生效模式在 `boot/resolve-spec.ts` 单点算(它吃的是场景折叠后的 settings),本函数刻意不去冒充那一处的属主。
110
134
  */
111
135
  export declare function storedBodyPermissionMode(body: {
112
136
  permissionMode?: unknown;
@@ -430,14 +454,12 @@ export interface FsWriteGateWiring {
430
454
  }
431
455
  export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
432
456
  toolPolicy?: ToolPolicy;
433
- handsReadOnly?: boolean;
434
- enablePlanMode?: boolean;
435
457
  };
436
458
  /**
437
459
  * Apply a client's parsed per-request settings onto a base `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link
438
- * tightenTaskSpec} pass (so the permission override composes deny-wins onto the baseline — TRAP #1 — and
439
- * `handsReadOnly`/`onAsk` can only narrow). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any override would
440
- * LOOSEN a stricter base — surfaced by the caller as a 4xx (a client settings stamp that tries to widen is a
460
+ * tightenTaskSpec} pass (so the permission override composes deny-wins onto the baseline — TRAP #1). `tightenTaskSpec`
461
+ * THROWS `TaskSpecTightenError` if any override would LOOSEN a stricter base — surfaced by the caller as a 4xx (a
462
+ * client settings stamp that tries to widen is a
441
463
  * client error, never a silent weakening). `model`/`outputStyle` are applied directly (model is the caller's
442
464
  * choice WITHIN the catalog — the caller gates it before calling; outputStyle appends to the system prompt).
443
465
  *
@@ -1,11 +1,12 @@
1
1
  import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, isPermissionMode, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { unmatchableToolNamesRefusal } from "./config.js";
3
3
  import { MAX_PERMISSION_RULES_PER_LIST, createRuleListPolicy } from "./permission-rule-list.js";
4
- import { allowLayerEntryCountIssue, describeAllowLayerOverCap, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
4
+ import { allowLayerEntryCountIssue, describeAllowLayerOverCap, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
5
  import { tightenGoverned } from "./governance-overrides.js";
6
6
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
7
7
  import { parseHooksConfig } from "./hooks/hook-runner.js";
8
8
  import { heldRuleName } from "./tool-rule-names.js";
9
+ import { entryPermissionMode } from "./runtime-governance.js";
9
10
  import { policyAskStandingAnswerFacts, standingAnswerDurablyRecordable } from "./tool-approval.js";
10
11
  export const PERMISSION_MODE_WORDS = PERMISSION_MODES.map((m) => `"${m}"`).join(" | ");
11
12
  export function coercePermissionMode(raw) {
@@ -23,6 +24,19 @@ export const PLAN_APPROVE_PERMISSION_MODES_AFTER = ["default", "acceptEdits"];
23
24
  export function isPlanApprovePermissionModeAfter(raw) {
24
25
  return PLAN_APPROVE_PERMISSION_MODES_AFTER.includes(raw);
25
26
  }
27
+ export function planReviewDecisionExitsPlan(decision) {
28
+ switch (decision) {
29
+ case "approve":
30
+ case "edit":
31
+ return true;
32
+ case "reject":
33
+ return false;
34
+ }
35
+ }
36
+ export function stampEntryPermissionMode(body, floor) {
37
+ const entry = entryPermissionMode(storedBodyPermissionMode(body), floor);
38
+ return entry.floor === undefined ? body : { ...body, permissionMode: entry.effective };
39
+ }
26
40
  export function storedBodyPermissionMode(body) {
27
41
  return effectivePermissionMode(body, parseTaskSettings(body.settings).settings);
28
42
  }
@@ -306,16 +320,6 @@ function createWorkflowAskPolicy(isExempt) {
306
320
  nameSets: [{ ask: WORKFLOW_ASK_NAMES }],
307
321
  };
308
322
  }
309
- function createWorkflowPlanDenyPolicy() {
310
- return {
311
- check(req) {
312
- return heldRuleName(req, isWorkflowName) !== undefined
313
- ? { action: "deny", message: `Plan mode is active — "${req.toolName}" is a write/mutating tool and is read-only-blocked.`, decisionReason: "rule" }
314
- : { action: "allow" };
315
- },
316
- nameSets: [{ deny: WORKFLOW_ASK_NAMES }],
317
- };
318
- }
319
323
  function rejectUnmatchableSettingsNames(perms, pathBases) {
320
324
  if (pathBases?.home === undefined) {
321
325
  for (const [field, names, behavior] of [
@@ -343,9 +347,6 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
343
347
  if (unmatchable !== undefined)
344
348
  throw new Error(unmatchable.message);
345
349
  }
346
- const emptyAllow = perms.allow !== undefined ? emptyToolSegmentEntryOf(perms.allow) : undefined;
347
- if (emptyAllow !== undefined)
348
- throw new Error(describeEmptyToolSegmentEntry("permissions.allow", emptyAllow));
349
350
  }
350
351
  export function deriveSettingsPolicy(settings, gate, workflowGate) {
351
352
  const perms = settings.permissions;
@@ -380,15 +381,14 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
380
381
  };
381
382
  const basePolicy = compose();
382
383
  switch (defaultMode) {
383
- case "plan":
384
- return { toolPolicy: compose(createWorkflowPlanDenyPolicy()), handsReadOnly: true, enablePlanMode: true };
385
384
  case "acceptEdits": {
386
385
  const p = compose(fsWriteGate(true), createWorkflowAskPolicy(workflowGate?.isExempt));
387
386
  return p ? { toolPolicy: p } : {};
388
387
  }
389
388
  case "default":
390
389
  case "auto":
391
- case "dontAsk": {
390
+ case "dontAsk":
391
+ case "plan": {
392
392
  const p = compose(createWorkflowAskPolicy(workflowGate?.isExempt));
393
393
  return p ? { toolPolicy: p } : {};
394
394
  }
@@ -398,13 +398,10 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
398
398
  }
399
399
  }
400
400
  export function applyTaskSettings(base, settings, gate, workflowGate) {
401
- const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
401
+ const { toolPolicy } = deriveSettingsPolicy(settings, gate, workflowGate);
402
402
  let next = base;
403
- if (toolPolicy !== undefined || handsReadOnly !== undefined) {
404
- next = tightenGoverned(next, { ...(toolPolicy !== undefined ? { toolPolicy } : {}), ...(handsReadOnly !== undefined ? { handsReadOnly } : {}) });
405
- }
406
- if (enablePlanMode)
407
- next = { ...next, enablePlanMode: true };
403
+ if (toolPolicy !== undefined)
404
+ next = tightenGoverned(next, { toolPolicy });
408
405
  const allow = settings.permissions?.allow;
409
406
  if (allow !== undefined)
410
407
  next = { ...next, allowedTools: allow };
@@ -19,6 +19,12 @@ export type ApprovalGateKind = (typeof APPROVAL_GATE_KINDS)[number];
19
19
  export declare function isApprovalGateKind(k: string | undefined): k is ApprovalGateKind;
20
20
  /** 两方言同形的 SQL IN 片段(值为闭集常量字面,无注入面)。 */
21
21
  export declare const APPROVAL_GATE_KINDS_SQL_IN: string;
22
+ /**
23
+ * 耐久回决口 `POST /v1/approvals/:sessionId/decide` 的路径串 —— S-1280:迟到回决口 PARKING 臂 202 体的机读指路 `decidePath`。
24
+ * 路由表那一行(`http/routes/approvals-assistant.ts` 的 `approval-decide`)按 pattern 匹配它;两者同形由往返钉守
25
+ * (`test/parked-late-decision.test.ts` G3-c:本函数的输出经 `lookupRoute` 落 `approval-decide`)。会话段按路由的 `safeDecode` 对称编码。
26
+ */
27
+ export declare function approvalDecidePath(sessionId: string): string;
22
28
  /**
23
29
  * 🔴 [ref](案A)追记 F6 —— durable 回决口(`POST /v1/tasks/:taskId/asks/:askId/decision`)的 **200 体
24
30
  * additive 判别位**:这次决议在**本副本**上有没有被一条活着的腿消费掉。
@@ -39,6 +39,12 @@ const DEFAULT_PARK_TOMBSTONE_MAX = 2000;
39
39
  const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
40
40
  const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
41
41
  const PARKING_RETRY_AFTER_MS = 250;
42
+ export function approvalDecidePath(sessionId) {
43
+ return `/v1/approvals/${encodeURIComponent(sessionId)}/decide`;
44
+ }
45
+ function parkSessionOf(row) {
46
+ return row.sourceTaskId === row.sessionId && row.sessionId !== row.taskId ? row.sessionId : undefined;
47
+ }
42
48
  function ruleMaterialCarriesCredentialShape(command) {
43
49
  return redactSecretsWithArms(command, { observe: false }).arms.has("full");
44
50
  }
@@ -1961,17 +1967,23 @@ export class ToolApprovalCoordinator {
1961
1967
  return this.replayDecidedAsk(id, row, parsed);
1962
1968
  case "PARKED":
1963
1969
  return this.redeemParkedAsk(id, tomb, row, parsed, httpReq, principal);
1964
- case "PARKING":
1970
+ case "PARKING": {
1971
+ const parkSession = parkSessionOf(row);
1965
1972
  return {
1966
1973
  status: 202,
1967
1974
  body: {
1968
1975
  approvalId: id,
1969
1976
  askId: row.askId,
1970
1977
  reason: "parking_in_progress",
1978
+ message: parkSession !== undefined
1979
+ ? "parking in progress — not applied; retry after retryAfterMs or answer through decidePath"
1980
+ : "parking in progress — not applied; retry after retryAfterMs or answer through the parent session's durable decide",
1971
1981
  retryAfterMs: PARKING_RETRY_AFTER_MS,
1982
+ ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}),
1972
1983
  ...this.lateRuleRefusal(parsed),
1973
1984
  },
1974
1985
  };
1986
+ }
1975
1987
  case "DENIED":
1976
1988
  return this.notPending("denied");
1977
1989
  case "VOID":
@@ -179,9 +179,9 @@ export declare function redactDeepWith(value: unknown, leaf: (s: string) => stri
179
179
  */
180
180
  export declare function redactedPreview(value: unknown, cap?: number): string | null;
181
181
  /**
182
- * **这一位是不是一个层级式绝对 URL**(有 authority 的那一族)—— 本铸点与**模型路由三键拒启**
183
- * (`model-route-url.ts` 第一臂)共用的**同一只**判据:两处曾各判各的,而 `URL.canParse` 单独为真的
184
- * **不透明** URL 恰好是「凭据位看不见」的那一形。
182
+ * **这一位是不是一个层级式绝对 URL**(有 authority 的那一族)—— 本铸点与 `url-shape.ts` 的 http(s) 形判官
183
+ * (模型路由三键 / 中心基址三席 / 公告地址 / embedder / A2A 的拒启都经它问到这里)共用的**同一只**判据:两处曾各判各的,
184
+ * 而 `URL.canParse` 单独为真的**不透明** URL 恰好是「凭据位看不见」的那一形。
185
185
  *
186
186
  * ⚠️ **射程如实**(车JC 复审去幻觉轮:此处原文写的是「本仓唯一判据」,亲读证伪)—— 仓里**另有一处**仍用
187
187
  * 裸 `URL.canParse` 回答同一个问题,且**刻意不在射程内**(改它是一次新的行为面拒启,按三问另开车):
@@ -5,7 +5,9 @@
5
5
  * 本仓**自己铸** spec、**没有调用方**能在请求上表态模式的腿 —— leader worker(`src/leader/wire.ts` provisionWorker 两车道)、leader 的
6
6
  * 修复腿(集成沙箱修复 `mkRepair`、模块门修复 `leader.ts` `runModuleGate`、单代理修复环 `repair-wire.ts` `attachRepairLoopDeps`)、run-local
7
7
  * (`src/run-local.ts`,一次性 CLI)与这些腿的 durable 续跑重挂(`CheckpointCtx.unattendedLeg`)—— 经 {@link withUnattendedLeg} 这**一只**
8
- * 合并口铸两键:`permissionMode: "default"`(显式:续跑时盖过 checkpoint 记下的旧模式)与 `onAsk` = {@link createUnattendedOnAsk}。
8
+ * 合并口铸两键:`permissionMode`(`"default"`,显式:续跑时盖过 checkpoint 记下的旧模式;**钳在 spec 自带的模式词之下** —— run-local 在
9
+ * 它自建 spec 的出生点盖下的运维 `AUTONOMY=plan` 地板、续跑重建读到的存储体的 `plan` 都是天花板,本腿的 `default` 落在它下面就是 `plan`,
10
+ * 不把它降回去)与 `onAsk` = {@link createUnattendedOnAsk}。
9
11
  * 那只 onAsk 读 `AskRequest.classes` **整体**:集非空且每一类都在 {@link UNATTENDED_ANSWERED_CLASSES}(= `{ordinary}`,常量,不是旋钮)⇒ 答 yes
10
12
  * —— **以策略的名义**(S-953 / core 7.39.0 #1265:`{allow:true, settledBy:"policy", approver:"unattended_class_answer"}` ⇒ core 结算 `policy_allowed` /
11
13
  * `who.party:"policy"`;修前是裸 `true`,core 把机器的放行记成**人**批 `human_allowed` / `person`);
@@ -17,18 +19,17 @@
17
19
  * 不答 ⇒ 整问不答。
18
20
  * · **缺席 = 不答的类**:没盖 `origin` 的站(共享制品读确认,KL-1214-a)、部署自铸的请求、7.37.0 之前 park 的行续跑(KL-1214-b,续跑不补)
19
21
  * 都不带类 ⇒ 不答。
20
- * · **答集只 `ordinary`**:七个保护类(`probe` / `write_protection` / `rule_unreadable` / `tool_marks_mcp` / `tool_marks_a2a` /
21
- * `transcript_write` / `artifact_shared_read`)与 `mode`(plan 的写问)留给人;`RELEASABLE_ASK_CLASSES` 之外的类(hook / ask_rule /
22
- * org_rule / operator_forced / …,权威为人提的问)恒不答。function approver 的 yes 是「这一次调用的答」—— 它连 `requiresRealApproval`
22
+ * · **答集只 `ordinary`**:引擎可放行的类(core `releasableAskClassesFor(face)`,按问的面读 —— 哪些类、哪一面放得行以引擎的表为准,本注不抄名单)里除 `ordinary`
23
+ * 之外的保护类与 `mode`(plan 的写问)留给人;可放行类之外的类(权威为人提的问)恒不答。function approver 的 yes 是「这一次调用的答」—— 它连 `requiresRealApproval`
23
24
  * 都清得掉(引擎分不出函数与人),所以带那一位的类(`transcript_write` / `rule_unreadable` / `artifact_shared_read` / `policy_rra`)不进答集
24
- * (钉:test/unattended-permission-mode.test.ts「答集 ⊆ RELEASABLE_ASK_CLASSES,且与 RRA_EXPLAINERS ∪ {policy_rra} 不交」)。
25
+ * (钉:test/unattended-permission-mode.test.ts「答集 ⊆ 每一面的 releasableAskClassesFor(face),且与 RRA_EXPLAINERS ∪ {policy_rra} 不交」)。
25
26
  * · **`ordinary` 覆盖什么**(core 原句):`Read` 工具的根外读、shell 的根外**写**(`echo x > <outside>` —— 写守卫只判保护表)、模式写门对每一次
26
27
  * covered write 的问、形状层对非只读 shell 的问;shell 的根外**读**(`cat <outside>`)是 `probe`,不答。
27
28
  *
28
29
  * ## 三问
29
30
  * · **谁需要**:用 leader / run-local 的部署 —— 普通写与非只读命令照跑(7.105.0 行为;7.106.0–7.108.0 过渡形靠 `bypassPermissions` 宽放给出的同一结果)。
30
- * · **谁受伤**:① 设了 `UNATTENDED_PERMISSION_MODE` 的部署 —— 起服拒启(`config.ts` 墓碑表,`removed` 族);② 靠过渡形缺省 bypass 放行
31
- * `probe` / MCP·A2A 标记工具 / 转录写 / 规则店读不出 / 共享制品读这六类的无人值守流程 —— 改成不答(有席交席、无席 park 或拒)。安全轴收紧。
31
+ * · **谁受伤**:① 设了 `UNATTENDED_PERMISSION_MODE` 的部署 —— 起服拒启(`config.ts` 墓碑表,`removed` 族);② 靠过渡形缺省 bypass 放行答集之外的保护类
32
+ * (哪些类见 `releasableAskClassesFor` 与答集的不变量钉,本注不抄名单)的无人值守流程 —— 改成不答(有席交席、无席 park 或拒)。安全轴收紧。
32
33
  * · **什么补偿**:有人的腿照问人(run-local 在终端里跑;leader 配 durable 审批块 ⇒ park 到 `/decide`);答集是 core 推荐形的常量 ——
33
34
  * 要放更多类的部署先立案候裁,不开旋钮。
34
35
  *
@@ -43,10 +44,11 @@
43
44
  * · 续跑腿上的协调器席:`http/resume-legs.ts` 只在「run 行在场 ∧ 活卡协调器在场」时给 spec 另装活席(`deps.toolApproval && taskId`)——
44
45
  * leader worker 的会话没有 run 行,那一装今天够不着本腿;哪天够着了,活席会盖掉本层(方向 = 多问人,fail-closed),见收车档 ⑤。
45
46
  */
46
- import type { AskClass, AskRequest, OnAsk, TaskSpec } from "@sema-agent/core";
47
+ import type { AskClass, AskRequest, OnAsk, PermissionMode, TaskSpec } from "@sema-agent/core";
47
48
  /**
48
49
  * 按类作答的**答集** —— core §4.6e 推荐形:机器只答 `ordinary`。**常量,不是旋钮**([ref]):要放更多类是部署级的安全决定,先立案候裁。
49
- * 不变量(测试钉):答集 ⊆ core `RELEASABLE_ASK_CLASSES`,且与带 `requiresRealApproval` 的类(core `RRA_EXPLAINERS` ∪ `policy_rra`)不交;`mode` 不在其中。
50
+ * 不变量(测试钉):答集 ⊆ core 每一面的可放行类 `releasableAskClassesFor(face)`(本层不读问的面,同一只答集答每一面的问),且与带 `requiresRealApproval` 的类
51
+ * (core `RRA_EXPLAINERS` ∪ `policy_rra`)不交;`mode` 不在其中。
50
52
  */
51
53
  export declare const UNATTENDED_ANSWERED_CLASSES: ReadonlySet<AskClass>;
52
54
  /**
@@ -66,13 +68,15 @@ export declare function unattendedAnswers(req: Pick<AskRequest, "classes">): boo
66
68
  export declare function createUnattendedOnAsk(seat: UnattendedSeat | undefined): UnattendedSeat;
67
69
  /** {@link withUnattendedLeg} 铸进 spec 的两键。 */
68
70
  export type UnattendedLegSpec = {
69
- readonly permissionMode: "default";
71
+ readonly permissionMode: PermissionMode;
70
72
  readonly onAsk: UnattendedSeat;
71
73
  };
72
74
  /**
73
- * 无人值守腿并进一条 spec —— 各腿的**唯一**合并口:`permissionMode: "default"` + `onAsk: createUnattendedOnAsk(seat)`。`seat` = 本腿的
74
- * 活审批席(run-local 递它的 `createLocalApprover`;leader 各腿与续跑重挂无席,不递)—— 席只经这个参数进来,从不从 spec 上读。
75
- * 调用点必须在 spec 其余键(含治理折叠)**之后**调它;本口不碰 `toolPolicy`(没有策略层要并:过渡形的写保护拒层已退役)。
75
+ * 无人值守腿并进一条 spec —— 各腿的**唯一**合并口:`permissionMode` = `"default"` 钳在 spec 已有的模式词之下(`runtime-governance.ts`
76
+ * `entryPermissionMode("default", spec 的词)`,与入口地板同一只函数、底下是 core 的钳;spec 无词 ⇒ `"default"`)+ `onAsk: createUnattendedOnAsk(seat)`。`seat` = 本腿的活审批席(run-local
77
+ * 递它的 `createLocalApprover`;leader 各腿与续跑重挂无席,不递)—— 席只经这个参数进来,从不从 spec 上读。
78
+ * 调用点必须在 spec 其余键(含治理折叠)**之后**调它 —— spec 自带的 `plan` 由此被读到、留住;本口不碰 `toolPolicy`(没有策略层要并:过渡形的
79
+ * 写保护拒层已退役)。
76
80
  */
77
81
  export declare function withUnattendedLeg<S extends Partial<TaskSpec>>(spec: S, seat?: UnattendedSeat): S & UnattendedLegSpec;
78
82
  //# sourceMappingURL=unattended-permission-mode.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { entryPermissionMode } from "./runtime-governance.js";
1
2
  export const UNATTENDED_ANSWERED_CLASSES = new Set(["ordinary"]);
2
3
  export const UNATTENDED_POLICY_APPROVER = "unattended_class_answer";
3
4
  function unattendedPolicyYes() {
@@ -11,6 +12,7 @@ export function createUnattendedOnAsk(seat) {
11
12
  return (req, signal) => (unattendedAnswers(req) ? unattendedPolicyYes() : seat !== undefined ? seat(req, signal) : "unavailable");
12
13
  }
13
14
  export function withUnattendedLeg(spec, seat) {
14
- return { ...spec, permissionMode: "default", onAsk: createUnattendedOnAsk(seat) };
15
+ const permissionMode = entryPermissionMode("default", spec.permissionMode).effective;
16
+ return { ...spec, permissionMode, onAsk: createUnattendedOnAsk(seat) };
15
17
  }
16
18
  //# sourceMappingURL=unattended-permission-mode.js.map
@@ -0,0 +1,31 @@
1
+ /** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
2
+ export declare const HTTP_URL_PARTS: readonly ["userinfo", "query", "fragment"];
3
+ export type HttpUrlPart = (typeof HTTP_URL_PARTS)[number];
4
+ /** 判官的拒因词(闭集):形不对,或一件被禁的部件在场。 */
5
+ export type HttpUrlFlaw = "not-http-url" | HttpUrlPart;
6
+ /** 「不是一个 http(s) URL」的规则词 —— 不透明形、不可解析、层级式非 http(s) 同一句;只点名规则,不回显 scheme。钉读这一只,不手抄。 */
7
+ export declare const HTTP_URL_SHAPE_RULE = "is not a parseable http(s) URL (scheme must be http or https; it must look like http(s)://host[:port][/path])";
8
+ /**
9
+ * 判:合格 ⇒ `[]`;否则按判序交回规则词。形不对且不透明 ⇒ 只有 `not-http-url`(部件在不透明形上无从判,
10
+ * 也不需要判 —— 那一形整只不收);层级式非 http(s) ⇒ `not-http-url` 之后照判部件。
11
+ * 返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里(调用方的穷举 switch 因此不需要一条「不可达」臂)。
12
+ */
13
+ export declare function httpUrlFlaws<P extends HttpUrlPart>(raw: string, forbid: readonly P[]): ReadonlyArray<"not-http-url" | P>;
14
+ /** 一个拒因词的规则句(穷举;新词不补 = 编译红)。句子只说规则,不含值。 */
15
+ export declare function httpUrlRule(flaw: HttpUrlFlaw): string;
16
+ /**
17
+ * 拒句里值的样子(纯函数):合格的 http(s) 形 ⇒ 只到 origin(`scheme://host[:port]`,过展示铸点),origin 之后还有东西(userinfo /
18
+ * 路径 / query / fragment)⇒ 接一枚 `/…`;其余(不透明 / 不可解析 / 层级式非 http(s))⇒ 展示铸点的占位 `«redacted:url»` ——
19
+ * 拒句里不出现非 http(s) 的 scheme 词(主会话增补裁:写成 scheme 的令牌 `<48hex>://host` 曾原样进 fatal 行)。
20
+ *
21
+ * 🔴 为什么只到 origin(车内 codex 对抗复审 r1 [high],亲跑复现):展示铸点的路径段**逐字**保留、只靠形状律认令牌形 ——
22
+ * 形状律认不出的令牌(48 位十六进制拉取令牌就是一例)放在路径上会原样进拒句。拒句落在 fatal 行、stderr、每一轮的请求失败日志,
23
+ * 这几处不赌形状律的覆盖面:座位名 + 规则词已经说清了哪一席错在哪,值只需要认得出是哪一台主机。
24
+ */
25
+ export declare function buildRefusalShownUrl(raw: string): string;
26
+ /**
27
+ * 拒句的**唯一**铸点:`<subject> <规则句>[; <规则句>…]; got "<值的样子>"`(`build*` = 纯数据)。
28
+ * 值只经 {@link buildRefusalShownUrl} 出现;`flaws` 来自 {@link httpUrlFlaws}(调用方按首个拒因选自己的码与出路句)。
29
+ */
30
+ export declare function buildHttpUrlRefusal(subject: string, raw: string, flaws: readonly HttpUrlFlaw[]): string;
31
+ //# sourceMappingURL=url-shape.d.ts.map