@sema-agent/server 7.113.0 → 7.115.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 (87) hide show
  1. package/MIGRATION.md +109 -2
  2. package/USAGE.md +36 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-card.d.ts +21 -24
  10. package/dist/approval-card.js +12 -0
  11. package/dist/approval.js +5 -2
  12. package/dist/boot/exit-boundary.d.ts +3 -2
  13. package/dist/boot/exit-boundary.js +3 -0
  14. package/dist/boot/memory-boundary.d.ts +2 -1
  15. package/dist/boot/memory-consolidation.d.ts +1 -1
  16. package/dist/boot/resolve-spec.js +6 -3
  17. package/dist/boot/shutdown.d.ts +19 -0
  18. package/dist/boot/shutdown.js +12 -1
  19. package/dist/capabilities/composite-members.d.ts +1 -0
  20. package/dist/capabilities/composite-members.js +1 -0
  21. package/dist/config-catalog.js +8 -6
  22. package/dist/config-center/apply-effective.js +11 -11
  23. package/dist/config-invariants.d.ts +27 -0
  24. package/dist/config-invariants.js +54 -39
  25. package/dist/config-provider.d.ts +16 -0
  26. package/dist/config-provider.js +11 -2
  27. package/dist/config-types.d.ts +24 -11
  28. package/dist/config-types.js +0 -1
  29. package/dist/config.d.ts +41 -34
  30. package/dist/config.js +78 -81
  31. package/dist/deployment-governance.d.ts +15 -4
  32. package/dist/deployment-governance.js +20 -2
  33. package/dist/env-facts.d.ts +3 -1
  34. package/dist/env-facts.js +5 -1
  35. package/dist/execution-lane-caps.d.ts +54 -30
  36. package/dist/execution-lane-caps.js +48 -20
  37. package/dist/fleet/fleet-bus.d.ts +1 -22
  38. package/dist/hooks/hook-runner.d.ts +112 -6
  39. package/dist/hooks/hook-runner.js +138 -32
  40. package/dist/hosted-posture.d.ts +2 -2
  41. package/dist/http/admission.js +9 -4
  42. package/dist/http/approval-sweeps.js +20 -0
  43. package/dist/http/checkpoint-ctx.d.ts +68 -0
  44. package/dist/http/checkpoint-ctx.js +36 -0
  45. package/dist/http/durable-run-leg.js +2 -1
  46. package/dist/http/resume-legs.js +9 -0
  47. package/dist/http/route-ctx.d.ts +7 -0
  48. package/dist/http/routes/a2a-serve.d.ts +1 -3
  49. package/dist/http/routes/a2a-serve.js +0 -2
  50. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  51. package/dist/http/routes/admin-memory-optout.js +0 -1
  52. package/dist/http/routes/capabilities.js +3 -2
  53. package/dist/http/routes/diagnostics.d.ts +5 -0
  54. package/dist/http/routes/diagnostics.js +2 -0
  55. package/dist/http/routes/sessions.js +1 -1
  56. package/dist/http/routes/tasks.js +3 -2
  57. package/dist/index.d.ts +1 -1
  58. package/dist/leader/wire.js +2 -1
  59. package/dist/local-data-root.d.ts +1 -1
  60. package/dist/mcp-content-origin.d.ts +0 -8
  61. package/dist/model-entry-refusal.d.ts +10 -2
  62. package/dist/model-entry-refusal.js +19 -3
  63. package/dist/observability/corrupt-read-seat.d.ts +3 -3
  64. package/dist/observability/corrupt-read-seat.js +5 -4
  65. package/dist/observability/fail-open.d.ts +10 -9
  66. package/dist/observability/fail-open.js +5 -1
  67. package/dist/permission-rule-list.d.ts +81 -0
  68. package/dist/permission-rule-list.js +17 -0
  69. package/dist/plugins/remote-env-host.d.ts +0 -35
  70. package/dist/plugins/remote-env-host.js +0 -8
  71. package/dist/plugins/remote-env-local-docker.d.ts +5 -13
  72. package/dist/plugins/remote-env-local-docker.js +2 -4
  73. package/dist/run-local.js +4 -1
  74. package/dist/runtime-governance.d.ts +21 -1
  75. package/dist/runtime-governance.js +6 -1
  76. package/dist/security.js +2 -2
  77. package/dist/session-policy-wire.d.ts +5 -4
  78. package/dist/store-posture.d.ts +7 -7
  79. package/dist/store-posture.js +0 -1
  80. package/dist/task-settings.d.ts +19 -5
  81. package/dist/task-settings.js +19 -30
  82. package/dist/tool-approval.d.ts +16 -4
  83. package/dist/tool-approval.js +2 -1
  84. package/dist/trace/core-keyset-guard.d.ts +9 -4
  85. package/dist/trace/project.js +9 -0
  86. package/package.json +4 -4
  87. package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
@@ -35,6 +35,7 @@ export function buildServerWiringGates(input) {
35
35
  postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
36
36
  }),
37
37
  checkpointStore: input.checkpointStore !== undefined,
38
+ approvalGrant: [...input.approvalGrant],
38
39
  };
39
40
  }
40
41
  export function buildStaticWiringAudit(facts) {
@@ -121,6 +122,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
121
122
  streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
122
123
  sessionAutoTitle: deps.config.sessionAutoTitle === true,
123
124
  sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
125
+ approvalGrant: deps.config.approvalGrant,
124
126
  }),
125
127
  });
126
128
  return;
@@ -5,7 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
8
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
8
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
9
9
  import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
10
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
11
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
27
27
  import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
28
28
  import { buildSyncSubmitBody } from "../sync-submit-body.js";
29
29
  import { assertNever } from "../route-ctx.js";
30
+ import { buildCheckpointCtx } from "../checkpoint-ctx.js";
30
31
  import { withModelReachScope } from "../../model-reach-scope.js";
31
32
  import { createInstructionSelectionLeg } from "../../instruction-selection.js";
32
33
  const DISCONNECT_PARK_GRACE_MS = 10_000;
@@ -252,7 +253,7 @@ export async function handleTasks(req, res, match, ctx) {
252
253
  let ledgerSink;
253
254
  let ledgerTerminal = false;
254
255
  if (deps.checkpointStore && prepared.spec.sessionId) {
255
- await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
256
+ await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
256
257
  }
257
258
  if (durableTaskId && deps.runStore) {
258
259
  const rs = deps.runStore;
@@ -804,7 +805,7 @@ export async function handleTasks(req, res, match, ctx) {
804
805
  }
805
806
  commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
806
807
  if (deps.checkpointStore && prepared.spec.sessionId) {
807
- await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
808
+ await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
808
809
  }
809
810
  const syncCancelCtrl = createRunAbort();
810
811
  if (prepared.spec.sessionId)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { loadConfig, type ServiceConfig } from "./config.js";
2
- export { composeExecutionLane, assertExecutionLaneInvariants } from "./boot/execution-lane.js";
2
+ export { composeExecutionLane, assertExecutionLaneInvariants, type ExecutionLaneComposeCtx } from "./boot/execution-lane.js";
3
3
  export { createBrain } from "./brain.js";
4
4
  export { createSessionStore } from "./plugins/session-store.js";
5
5
  export { createStoreBackend, type StoreBackend, type SessionCaptureRecordSource } from "./plugins/store-backend.js";
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
8
  import { withUnattendedLeg } from "../unattended-permission-mode.js";
9
+ import { buildCheckpointCtx } from "../http/checkpoint-ctx.js";
9
10
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
10
11
  import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
11
12
  import { createDurableAskPolicy } from "../approval.js";
@@ -267,7 +268,7 @@ export function createLeaderRunner(cfg) {
267
268
  const provisionWorker = async (sub) => {
268
269
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
269
270
  const durable = cfg.durable?.();
270
- const resumeCtx = { body: { objective: sub.spec.objective }, unattendedLeg: true };
271
+ const resumeCtx = buildCheckpointCtx({ body: { objective: sub.spec.objective }, unattendedLeg: true });
271
272
  let durableSpec = {};
272
273
  if (durable) {
273
274
  const d = durable;
@@ -4,7 +4,7 @@
4
4
  * 链(第一个非空者胜,空串视作缺席):`LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `<home>/.ai-agent`。
5
5
  *
6
6
  * 为什么是一只**纯函数、env 与 home 都由调用方递入**:本函数有两个读者 —— 服务自己的 `loadConfig`(递 `process.env` / `os.homedir()`)与随包发的运维脚本
7
- * `scripts/retired-mode-seed-drain-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
7
+ * `scripts/drain-check-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
8
8
  * 设了 `AGENT_DATA_DIR` 的部署不带参数跑脚本会读到另一个目录并报「干净」)。纯叶、零反向依赖 ⇒ 脚本引它不会把整个服务装载起来。
9
9
  *
10
10
  * ⚠️ 与**配置根**(`configLocalRoot`:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,不吃 `LOCAL_DATA_ROOT`)不是同一条链 —— 两根分家的那一形由
@@ -19,15 +19,8 @@
19
19
  * 单向钉挡不住第二种(手滑把 `"local"` 打成 `"locale"` 只会让一个合法声明被静默拒),所以两条都要。
20
20
  */
21
21
  import type { ToolContentOrigin } from "@sema-agent/core";
22
- /** 编译期断言:`T` 必须收敛到 `never`(有残余 ⇒ tsc 红)。 */
23
- type AssertNever<T extends never> = T;
24
22
  /** core `ToolContentOrigin` 的执法副本 —— **唯一**一份(见模块头注)。 */
25
23
  export declare const CONTENT_ORIGIN_WORDS: readonly ["local", "execution", "external"];
26
- type ContentOriginWord = (typeof CONTENT_ORIGIN_WORDS)[number];
27
- type _ContentOriginCovers = AssertNever<Exclude<ToolContentOrigin, ContentOriginWord>>;
28
- type _ContentOriginNoStray = AssertNever<Exclude<ContentOriginWord, ToolContentOrigin>>;
29
- /** (占位使用,防 noUnusedLocals;类型别名零运行时。) */
30
- export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOriginNoStray];
31
24
  /**
32
25
  * 词表内?—— 两个平面共用的**那一只**谓词。
33
26
  *
@@ -38,5 +31,4 @@ export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOrigin
38
31
  export declare function isContentOriginWord(v: unknown): v is ToolContentOrigin;
39
32
  /** 坏值的**可打印**摘要(进 400 文案 / warn 元数据)——中和 + 截长,绝不把任意长度的调用方输入原样回显。 */
40
33
  export declare function describeBadContentOrigin(v: unknown): string;
41
- export {};
42
34
  //# sourceMappingURL=mcp-content-origin.d.ts.map
@@ -90,13 +90,21 @@ export interface ModelRefusalVerdict {
90
90
  * 名 → 码,**值只码**(拒因事实里的上游 id / provider 词 / 坏字段键 / 出路句都不上 wire —— `entry_invalid` 行是没人验过的行,id / provider
91
91
  * 可能就是坏行原样;句子是给本机运维的散文,不是契约)。**名筛在这里、只在这里**:键过 settings-schema `isReportableRefusalName`(唯一判据,
92
92
  * import 不复制;6.3.0 起 = 标识符白名单 ∧ 无密钥形 ∧ 无 URL userinfo)—— 秘密形 / 空串 / `__proto__` / 不是标识符的串这类出不了门的名不进键、
93
- * 不计数;引擎内部的判决不受影响(本机日志的写名行为与基线同)。
93
+ * 不计数;引擎内部的判决不受影响;本机日志 / 拒因句里的写名同样先问这一只(S-805,{@link disclosableRefusalName}:不可披露的名只写码)。
94
94
  * 逐条空且无组拒 ⇒ `undefined`(键缺席:「没有」只有一种拼法)。
95
95
  */
96
96
  export declare function buildModelRefusalsSignal(v: ModelRefusalVerdict | undefined): ModelRefusalsSignal | undefined;
97
+ /**
98
+ * 一个被拒条目名的**日志字段**:可披露 ⇒ `{ [key]: name }`;不可披露 ⇒ `{ [`${key}NameWithheld`]: true }`(缺 `key` 不是无声的:读日志的人知道是被扣下、
99
+ * 不是漏写)。**所有**把被拒条目名写进结构化日志字段的站点调这一只(`refusalWarnFields` / `apply-effective.ts` 的角色与档位绑定 warn、缺省悬空 warn),
100
+ * 不各写一份 `isReportableRefusalName` 判断。
101
+ */
102
+ export declare function refusedNameLogField(key: string, name: string): Record<string, unknown>;
103
+ /** 一串被拒条目名的日志字段(`sema_registry_models` 行的 `unavailable`):只列可披露的名,被扣下的名**只数个数**(`<key>NamesWithheld`,0 个时键缺席)。 */
104
+ export declare function refusedNamesLogFields(key: string, names: readonly string[]): Record<string, unknown>;
97
105
  /** 成因 → 它那一条 warn 的事件名(**穷举**)。两个名字是票面点名的既有/新事件,运维的告警规则按名挂。 */
98
106
  export declare function refusalEventOf(reason: ModelEntryRefusalReason): "sema_registry_model_no_brain" | "sema_registry_model_entry_invalid";
99
- /** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。 */
107
+ /** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。S-805:`model` 只在名可披露时写。 */
100
108
  export declare function refusalWarnFields(name: string, r: ModelEntryRefusal): Record<string, unknown>;
101
109
  /**
102
110
  * 一次**显式选择**命中了不可用条目时的拒因句(HTTP `request.model_unavailable` 的 `error`、run-local 的 stderr、
@@ -94,6 +94,22 @@ export function buildModelRefusalsSignal(v) {
94
94
  return undefined;
95
95
  return v.plane === undefined ? { entries } : { entries, plane: v.plane };
96
96
  }
97
+ function disclosableRefusalName(name) {
98
+ return isReportableRefusalName(name) ? name : undefined;
99
+ }
100
+ export function refusedNameLogField(key, name) {
101
+ const shown = disclosableRefusalName(name);
102
+ return shown !== undefined ? { [key]: shown } : { [`${key}NameWithheld`]: true };
103
+ }
104
+ export function refusedNamesLogFields(key, names) {
105
+ const shown = names.filter((n) => disclosableRefusalName(n) !== undefined);
106
+ const withheld = names.length - shown.length;
107
+ return { [key]: shown, ...(withheld > 0 ? { [`${key}NamesWithheld`]: withheld } : {}) };
108
+ }
109
+ function nameSlot(name) {
110
+ const shown = disclosableRefusalName(name);
111
+ return shown === undefined ? "(name withheld)" : `"${shown}"`;
112
+ }
97
113
  export function refusalEventOf(reason) {
98
114
  switch (reason) {
99
115
  case "no_brain":
@@ -104,7 +120,7 @@ export function refusalEventOf(reason) {
104
120
  }
105
121
  export function refusalWarnFields(name, r) {
106
122
  return {
107
- model: name,
123
+ ...refusedNameLogField("model", name),
108
124
  provider: r.provider,
109
125
  code: r.code,
110
126
  ...(r.fields.length > 0 ? { field: r.fields[0], fields: [...r.fields] } : {}),
@@ -118,7 +134,7 @@ export function modelUnavailableMessage(field, requested, name, r) {
118
134
  export function defaultModelRefusal(view, leg) {
119
135
  if (Object.hasOwn(view.models, DEFAULT_MODEL_REF) && view.models[DEFAULT_MODEL_REF] !== undefined)
120
136
  return undefined;
121
- const refused = Object.entries(view.modelRefusals).map(([n, r]) => `"${n}" [${r.code}]`);
137
+ const refused = Object.entries(view.modelRefusals).map(([n, r]) => `${nameSlot(n)} [${r.code}]`);
122
138
  const unavailable = refused.length > 0 ? ` (every enabled catalog entry is unavailable on this host: ${refused.join(", ")})` : "";
123
139
  switch (leg) {
124
140
  case "env":
@@ -163,7 +179,7 @@ export function refusedNameMintedRefusal(view) {
163
179
  const minted = mintedRefusedNames(view);
164
180
  if (minted.length === 0)
165
181
  return undefined;
166
- const pairs = minted.map((m) => `"${m.name}" [${m.code}] → "${m.target}" (${m.via})`);
182
+ const pairs = minted.map((m) => `${nameSlot(m.name)} [${m.code}] → ${nameSlot(m.target)} (${m.via})`);
167
183
  return (`unavailable catalog entries would be re-minted onto other entries by the tier/alias expansion [${REFUSED_NAME_MINTED_CODE}]: ${pairs.join(", ")} — ` +
168
184
  "an explicit pick of that name (or a role bound to it) would silently run on the other entry's endpoint and account; " +
169
185
  "rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
@@ -43,9 +43,9 @@ export declare function createCorruptReadFailOpenSeat(tag: FailOpenTag, leg?: (i
43
43
  *
44
44
  * ## 逐命中响亮不降级
45
45
  * 这一座里有安全轴的脸(session-policy),所以每命中一行 warn 保留(降成「每进程一次」是降级,S-990 的裁定);**在此之上**,认出的脸另记账:
46
- * · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn。该脸的方向是 fail-open
47
- * (core 原装店读不出规则文件 ⇒ 披露后答「没有会话规则」放行,而会话规则只许收紧 ⇒ 被丢掉的恰是调用方加的收紧;SQL 孪生同形则响亮抛)。
48
- * 放行是 core 的裁定、本仓不包层(源头修复纪律),所以**只登债、不改放行**;
46
+ * · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn。core 7.39.2 起按键读拒,
47
+ * 但枚举 / 别的会话抹除 / 数据根重绑三张脸仍按「无规则」(会话规则只许收紧 ⇒ 丢的恰是收紧);座上分不出读法 ⇒ 这一脸每一发都计(含已拒的按键读)。
48
+ * 别的会话抹除的常见形 core **不披露** ⇒ 这一形座上数不到,只能在 CENSUS / CHANGELOG 披露。放行是 core 的裁定、本仓不包层(源头修复纪律),所以**只登债、不改放行**;
49
49
  * · 其余脸 ⇒ 不计(见 {@link DurableAbsenceFace})。
50
50
  * 记账**先于** warn:记账是总函数,warn 可能抛(core 的 guard 会吞掉那个抛),一次日志故障不许吃掉计数。
51
51
  *
@@ -85,10 +85,11 @@ export function createDurableAbsenceSeat(warn, root, thirdFace) {
85
85
  };
86
86
  }
87
87
  function buildDurableAbsenceNote(thirdFace) {
88
- return ("a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
89
- "Consequence depends on which store read it: session-policy = that run lost its SUBTRACTIVE session-rule " +
90
- "layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
91
- `rows; ${thirdFace}. Inspect the named path before deciding.`);
88
+ return ("a durable read hit unreadable bytes (never a plain ENOENT). " +
89
+ "Consequence depends on which store read it: session-policy = a keyed read (a task's session rules, GET/PUT .../policy) was REFUSED " +
90
+ "(the run fails with code \"corrupt\"), but enumeration (session sync export), another session's erase sweep and a data-root rebind still " +
91
+ "read that row as NO RULES — fix or delete the named file (deleting it means no session rules); session repo = a listing silently " +
92
+ `omitted rows; ${thirdFace}. Inspect the named path before deciding.`);
92
93
  }
93
94
  const RULE_LANE_REMEDY = "the permission-rule lane is fail-closed until a person inspects the file (moving it aside restarts the lane with an empty log)";
94
95
  export const JSONL_REPLAY_SEATS = {
@@ -1,13 +1,10 @@
1
1
  import type { Logger } from "./logger.js";
2
2
  import type { Metrics } from "./metrics.js";
3
- /** 登记项:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装),`note` 说清"放行的最坏后果"。 */
4
- export interface FailOpenTagEntry {
5
- /** `F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";`P-DEBT`=保护型欠账,方向明知不对、显式记债。 */
6
- readonly cls: "F" | "P-DEBT";
7
- /** 这条兜底放行了什么、最坏后果是什么。写给未来读遥测的人,不写来历。 */
8
- readonly note: string;
9
- }
10
- /** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
3
+ /**
4
+ * 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。
5
+ * 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
6
+ * `P-DEBT`=保护型欠账,方向明知不对、显式记债。`note` 说清这条兜底放行了什么、最坏后果是什么(写给未来读遥测的人,不写来历)。
7
+ */
11
8
  export declare const FAIL_OPEN_TAGS: {
12
9
  readonly "server.approval-ask-journal.compaction-failed": {
13
10
  readonly cls: "F";
@@ -209,6 +206,10 @@ export declare const FAIL_OPEN_TAGS: {
209
206
  readonly cls: "F";
210
207
  readonly note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。";
211
208
  };
209
+ readonly "server.hooks.matcher-unjudged": {
210
+ readonly cls: "F";
211
+ readonly note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。";
212
+ };
212
213
  readonly "server.hooks.notice-sink-threw": {
213
214
  readonly cls: "F";
214
215
  readonly note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。";
@@ -367,7 +368,7 @@ export declare const FAIL_OPEN_TAGS: {
367
368
  };
368
369
  readonly "server.session-policy.corrupt-read-fail-open": {
369
370
  readonly cls: "P-DEBT";
370
- readonly note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)读不出 ⇒ core 原装 `FileSessionPolicyStore.read` 披露后 `return null` ⇒ 引擎按「没有会话规则」放行,而会话规则**只许收紧**(非 operator 写入遇放宽即拒)⇒ 被丢掉的恰是调用方加的那层收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**:SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛、体读不出形走 `SessionPolicyError(\"corrupt\")`(CENSUS 第 81 行 P 终态 fail-closed)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层(请托帖 [8906] 已发,core 认领 #1299([8908],7.39.2 patch) core),所以只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留、放行行为一格不改(🚧 钉 `test/corrupt-read-f-seats-s990.test.ts`)。**出路 = core 把该面改成 fail-closed,届时撤本 tag**。计数非零 = 有会话的收紧规则文件坏了而该会话正按「无规则」运行,运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。";
371
+ readonly note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。";
371
372
  };
372
373
  readonly "server.outbound.connect-observer-threw": {
373
374
  readonly cls: "F";
@@ -200,6 +200,10 @@ export const FAIL_OPEN_TAGS = {
200
200
  cls: "F",
201
201
  note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。",
202
202
  },
203
+ "server.hooks.matcher-unjudged": {
204
+ cls: "F",
205
+ note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。",
206
+ },
203
207
  "server.hooks.notice-sink-threw": {
204
208
  cls: "F",
205
209
  note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。",
@@ -337,7 +341,7 @@ export const FAIL_OPEN_TAGS = {
337
341
  },
338
342
  "server.session-policy.corrupt-read-fail-open": {
339
343
  cls: "P-DEBT",
340
- note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)读不出 ⇒ core 原装 `FileSessionPolicyStore.read` 披露后 `return null` ⇒ 引擎按「没有会话规则」放行,而会话规则**只许收紧**(非 operator 写入遇放宽即拒)⇒ 被丢掉的恰是调用方加的那层收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**:SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛、体读不出形走 `SessionPolicyError(\"corrupt\")`(CENSUS 第 81 行 P 终态 fail-closed)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层(请托帖 [8906] 已发,core 认领 #1299([8908],7.39.2 patch) core),所以只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留、放行行为一格不改(🚧 钉 `test/corrupt-read-f-seats-s990.test.ts`)。**出路 = core 把该面改成 fail-closed,届时撤本 tag**。计数非零 = 有会话的收紧规则文件坏了而该会话正按「无规则」运行,运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。",
344
+ note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。",
341
345
  },
342
346
  "server.outbound.connect-observer-threw": {
343
347
  cls: "F",
@@ -0,0 +1,81 @@
1
+ /**
2
+ * **规则表的唯一编译口** —— 一张工具名规则表(`allow` / `ask` / `deny` 之一)编成一只 `ToolPolicy`。
3
+ *
4
+ * 两类读者,同一只编译口(编译与校验各只有一份,不在别处另写第二份文法):
5
+ * · 请求腿的 `settings.permissions.{allow,ask,deny}` 三桶(`task-settings.ts` 的 `deriveSettingsPolicy`);
6
+ * · 运维的 allow 规则 `APPROVAL_GRANT`(`runtime-governance.ts` 的 `applyRuntimeGovernance`,经部署治理链的单一构造口
7
+ * `deployment-governance.ts` 交进来 —— HTTP 提交 / durable 续跑 / 停驻后台 agent 的跨副本赎回 / run-local 四条腿同装)。
8
+ *
9
+ * 为什么住在一只叶模块里(不留在 `task-settings.ts`):运维那一侧的启动校验在 `config.ts` 里跑(坏值 = 拒启),而
10
+ * `task-settings.ts` 值引 `config.ts` —— 编译口留在那里,`config.ts` 要调它就闭合一只 ESM 装载环。本模块只 import core 与
11
+ * `text-bidi.ts`(纯叶),三处读者引它都不成环。
12
+ */
13
+ import { type PathRuleBases, type RuleBehavior, type ToolPolicy } from "@sema-agent/core";
14
+ /** 一张表里规则条数的上限 —— 请求腿 wire 门(`task-settings.ts` 的三桶判形)读的也是这一个名字:core 的缺省帽是 256,
15
+ * 不对齐的话第 257 条起编译当场抛,等于服务端自己收下的东西自己反悔。 */
16
+ export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
17
+ /** A name-keyed rule-list ToolPolicy: a tool named by `names` gets `behavior` (an `allow` hit states the allow with
18
+ * `decisionReason: "rule"`; `ask` tightens to an approval gate; `deny` hard-blocks); every other tool is out of
19
+ * scope (`defaultAction: "allow"` = this policy has no opinion about anything it does not name). Composed deny-wins
20
+ * with the rest of the fold, so `ask` / `deny` only ever tighten and an `allow` never overrides a deny or an ask.
21
+ *
22
+ * `source` = 这张表**写在哪**(请求腿 `settings.permissions.<桶>`;运维腿 `APPROVAL_GRANT`)。它盖进每条规则(core 的 deny / ask
23
+ * 句里点名它:`flagged by permission rule "mcp__figma__*" (settings.permissions.ask)`),编不出来时的错误句也点名它;只是出处,
24
+ * 不带任何优先级(层序在各自的折叠里)。
25
+ *
26
+ * 🔴 [ref] / core [ref] —— 本函数曾是 `new Set(askNames).has(req.toolName)` 的**逐字**比对,那是
27
+ * **安全轴上的静默 fail-open**:CC 自家 settings validator 推荐的 covering 拼法
28
+ * (`"MCP rules do not support patterns in parentheses … use mcp__srv__*"`)在逐字比对下永远配不上真工具名
29
+ * `mcp__figma__get_file` ⇒ 一次不问、直跑、审计静音。两道兜底也各自放行:`unmatchableToolNamesRefusal`
30
+ * 对 `mcp__<peer>__*` 判「合法 MCP 全名」(server 不持挂载表,动态宇宙不能按名单否定),`nameSets` 审计面
31
+ * 报的是「没匹配上的名字」而 covering 名在 core 眼里**是**匹配得上的。同一份 settings 里
32
+ * `deny:["mcp__figma__*"]` 却**真拦** —— deny 走 `createAllowDenyPolicy`,它用的是 core 的
33
+ * `namespacedCoveringEntries/Hit`。同族分家,差的只是解析器。
34
+ *
35
+ * ⇒ **规则表的 policy 面现在同源**:三种态度都由 `createPermissionRulePolicy` 的 covering 车道解,底下是
36
+ * core 的**同一只** `namespacedRuleNameCovers`(协议无关:`mcp__`/`a2a__` 同解;peer 段精确比;工具段 `*`/glob 覆盖)。
37
+ * 本仓**不自写第二套 glob** —— 那正是当年分家的成因。
38
+ *
39
+ * ⚠️ 「同源」的**射程**说准了免得下一个人当成全仓结论(codex 交叉轮 finding-1 追出来的边):
40
+ * · 同源的是**本编译口**认的 covering。表**进门前**还有一道更外层的判据(`config.ts` 的 `unmatchableToolNamesRefusal`,
41
+ * 请求腿经 `task-settings.ts` 的 `rejectUnmatchableSettingsNames`、运维腿经 `config.ts` 的 `approvalGrantRefusal`),它把
42
+ * `<ns>__<peer>` **两段形**(`mcp__figma`)判成「缺段名」拒掉 —— 而 core 三只 policy 都认这个形(实测:
43
+ * `createAllowDenyPolicy`/`createPermissionRulePolicy`/`createApprovalPolicy` 对 `mcp__figma` 一律解成 covering)。
44
+ * 所以各表收 `mcp__figma__*`(等价、且被推荐)、拒 `mcp__figma`。
45
+ * · **同源的射程已扩到 operator 名单腿**([ref] 孪生修,[ref]):`createDurableAskPolicy`(`src/approval.ts`)的
46
+ * deny/neverAuto/require 三集合过去是逐字 `Set.has`,自 core 5.60.1 谓词根导出后改成「精确 ∪ covering」,用的是
47
+ * **同一只** `namespacedRuleNameCovers`。⇒ env `APPROVAL_*` 腿 / 中心下发 `runtime.approvalRequire` 腿 / 规则表三家
48
+ * 现在对 covering 拼法的解读逐字一致,`mcp__figma__*` 在哪条腿上都是真判。
49
+ * · **两段形为什么仍不放开**(理由已换,别再引「approval.ts 还不认」):它一名两义——「整台服务器」与「三段名写漏一段」
50
+ * 在 boot 期不可分辨,所以各条腿一律响亮拒 + 指引给无歧义的 `mcp__figma__*`。放宽是各腿同时变宽的 behavior-facing
51
+ * 改动,走独立批的三问。详见 `unmatchableToolNamesRefusal` 顶注。
52
+ *
53
+ * 三处刻意的取舍,都是亲读 core dist 后定的:
54
+ * · `onInvalidRule:"throw"`(缺省)—— 一条编不出来的规则整表拒(请求腿 → resolve-spec 翻 422;运维腿 → 拒启),抛之前补上
55
+ * 出处名(core 的原文只说 "createPermissionRulePolicy: …",不说是哪张表出的错)。[ref] 的括号形在**更外层**已被拒,够不着
56
+ * 这里;真正新增的是「名字里带反斜杠」「单条 > 512 字符」这两族永不匹配的拼法。
57
+ * ✅ **不对称已收口([ref]①,[ref] 销)**:codex finding-2 当年如实登记的「同样两族拼法在 `deny`
58
+ * 上仍是静默 no-op」已修 —— 判据搬进单点成文的 `unmatchableToolNamesRefusal`,它拿 core 自己的 `validatePermissionRules`
59
+ * 对同一串跑一遍文法(不自铸第二套判词),于是三张表对这两族说同一句话。S-654 (c) 起 `allow` 也收。
60
+ * ⚠️ 存量 resume:带这两族拼法的**已存 body** 在 resume 腿会 422(resolve-spec 翻)。这**不是**新造的姿态——A3
61
+ * (`rejectUnmatchableSettingsNames`)早就对存量 body 里的退役名/短名同样抛,且 `applyTaskSettings` 腿有专门的回归钉。
62
+ * 判据同一条:一个**静默不存在的审批门**比一次响亮失败更贵。codex 建议的「给非法存量条目保留旧的逐字比对」被**驳回**:
63
+ * 那正是把 fail-open 请回来。
64
+ * · `caps.maxRules` 对齐 {@link MAX_PERMISSION_RULES_PER_LIST}(理由见那只常量)。core 的 set-level cap 连 `skip` 都照抛,
65
+ * 所以运维腿的启动校验还要**试编一次**(`approvalGrantRefusal`)才拦得住超帽 —— 判据仍是本函数这一只。
66
+ * · 不再自铸 `decisionReason:"rule"` —— core 的规则策略本就不铸这个键(allow 命中由 core 自己铸 `"rule"`),且 core 侧唯一区分它的
67
+ * 两处(prepare-task 的继承 grant 复用判据、park 的 rule-suggestion 门)对 `"rule"` 与 `undefined` **同待**(只把 `"hook"`
68
+ * 排除在外),dist 亲读在案。审计归因改由 `matchedAskRule` + `source` 承担,后者比前者强:它还让 core 的 auto-mode 分类器
69
+ * **拒绝**替人清掉这条 ask(「运维写下的『每次问我』不是分类器的犹豫」),方向是收紧。
70
+ *
71
+ * 🔴 **S-654 (c)(7.115.0,core 7.40.0 #1065):三种态度同一个编译口,`allow` 也走这里** —— `behavior: "allow"` 的规则命中 = core 带
72
+ * `decisionReason: "rule"` 的放行 —— allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢(与参照实现的 `permissions.allow` 在内置工具上**不同**:那边 `allow: ["Edit"]` 跳过编辑确认,这里不放掉引擎的写门);未命中 = `defaultAction: "allow"` 的裸 allow
73
+ * = 不表态、照常判。修前请求腿的 allow 编成 `createAllowDenyPolicy({ allow })` 的**名单**(名单在场 ⇒ 名单外的工具一律拒),与写它的人要的
74
+ * 「替这些工具表态」方向相反;终端因此从不投这只桶。core 7.40.0 起没人表态的服务器工具调用要问,allow 规则就是那张卡的常设答案
75
+ * (`mcp__<server>__*` 一条覆盖整台服务器)。allow 规则**不越过** deny / ask:折叠 deny > ask > allow,任何一层的 deny / ask 照赢;
76
+ * 它只在没人反对时给出那句表态。core 的 allow 文法只收工具名形(精确名 / `mcp__`·`a2a__` covering 形),内容形放行(`Bash(npm:*)` /
77
+ * `Edit(src/**)` / 参数级)core 拒编,两条腿与 deny / ask 同一个出口(判据 `unmatchableToolNamesRefusal`)。
78
+ * 7.115.0 起运维的 `APPROVAL_GRANT` 也编在这里(无人值守腿没有请求体,那是运维为自己接的服务器表态的唯一位置)。 */
79
+ /** `behavior` = 这张表的态度 —— 型直接是 core 的 `RuleBehavior`(不手抄:core 加一种态度,这里与它同步、读者 tsc 上见到)。 */
80
+ export declare function createRuleListPolicy(names: readonly string[], behavior: RuleBehavior, source: string, pathBases: PathRuleBases | undefined): ToolPolicy;
81
+ //# sourceMappingURL=permission-rule-list.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { createPermissionRulePolicy } from "@sema-agent/core";
2
+ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
3
+ export const MAX_PERMISSION_RULES_PER_LIST = 1_000;
4
+ export function createRuleListPolicy(names, behavior, source, pathBases) {
5
+ try {
6
+ return createPermissionRulePolicy(names.map((name) => ({ rule: name, behavior, source })), {
7
+ defaultAction: "allow",
8
+ caps: { maxRules: MAX_PERMISSION_RULES_PER_LIST },
9
+ ...(pathBases !== undefined ? { pathBases } : {}),
10
+ });
11
+ }
12
+ catch (e) {
13
+ const coreMsg = (e instanceof Error ? e.message : String(e)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
14
+ throw new Error(`${source.replace("settings.", "")} has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
15
+ }
16
+ }
17
+ //# sourceMappingURL=permission-rule-list.js.map
@@ -419,39 +419,4 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
419
419
  */
420
420
  export declare function hostExecutionEnvFactory(config?: HostEnvConfig): ExecutionEnvFactory;
421
421
  export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, type LocalDockerEnvDeps, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
422
- import { type LocalDockerEnvConfig, type LocalDockerEnvDeps } from "./remote-env-local-docker.js";
423
- /**
424
- * Config for the `remote-docker` provider: run each task in a container on a REMOTE docker daemon
425
- * (DUAL-MODE-DESIGN §5 — "a user-specified REMOTE host's docker"; registry-core `RemoteExecSpec`).
426
- *
427
- * It is {@link LocalDockerEnvConfig} with the endpoint made REQUIRED — that is the whole difference, by design:
428
- * the container adapter is already daemon-agnostic (workspace `mkdir`'d INSIDE the container, never a host
429
- * bind-mount; bytes over `docker cp` / `docker exec cat`, which stream through the daemon API), and the endpoint
430
- * rides as `-H <dockerHost>` on every invocation. So every local-docker knob (memory/cpus/pidsLimit/dropAllCaps/
431
- * network/timeouts/env/dockerPath) applies unchanged to the remote lane.
432
- */
433
- export interface RemoteDockerEnvConfig extends Omit<LocalDockerEnvConfig, "dockerHost" | "providerLabel"> {
434
- /** Remote docker daemon endpoint (e.g. `tcp://host:2376` or `ssh://user@host`). REQUIRED for remote-docker. */
435
- dockerHost: string;
436
- }
437
- /**
438
- * `ExecutionEnvFactory` for the TOC `remote-docker` backend (DUAL-MODE-DESIGN §5): a per-task container on
439
- * SOMEONE ELSE'S docker daemon (offload — the operator's box stays free; isolation:true, suspendable:false).
440
- * Same lifetime contract as local-docker: unconnected/lazy, one container per task, `destroy()` = `docker rm -f`.
441
- *
442
- * 🔴 **Endpoint credentials are the docker CLI's own** — this adapter deliberately carries none. The docker CLI
443
- * reads `DOCKER_CERT_PATH` + `DOCKER_TLS_VERIFY` from the worker's environment (inherited by every spawn) for a
444
- * `tcp://` + TLS daemon, and uses `ssh-agent` / `~/.ssh/config` for an `ssh://` one. That keeps key material out
445
- * of this process's config objects entirely. Consequence to know: `tcp://…:2376` WITHOUT those env vars fails
446
- * loud at connect (the daemon refuses plaintext) — it does not silently downgrade; a plaintext `tcp://…:2375`
447
- * daemon is unauthenticated and remains the operator's decision, exactly as in the registry-core schema.
448
- * (The registry-core `remote-docker` arm additionally models `tlsCertEnv`/`tlsKeyEnv`/`tlsCaEnv`/`sshKeyEnv` =
449
- * env-NAMEs the control plane resolves to PEM BYTES. Materializing those bytes to short-lived 0600 files and
450
- * passing `--tlsverify --tlscert/--tlskey/--tlscacert` is the remaining config-surface follow-up, together with
451
- * the `provider:"remote-docker"` arm on `AppConfig.remoteExec` — see the note on that union in config-types.ts.)
452
- *
453
- * Throws at factory-BUILD time on a missing/blank endpoint: a "remote" lane with no endpoint would otherwise
454
- * quietly become the LOCAL daemon — an isolation-topology surprise, so it fails loud instead.
455
- */
456
- export declare function remoteDockerExecutionEnvFactory(config: RemoteDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
457
422
  //# sourceMappingURL=remote-env-host.d.ts.map
@@ -1373,12 +1373,4 @@ export function hostExecutionEnvFactory(config = {}) {
1373
1373
  });
1374
1374
  }
1375
1375
  export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
1376
- import { localDockerExecutionEnvFactory } from "./remote-env-local-docker.js";
1377
- export function remoteDockerExecutionEnvFactory(config, deps = {}) {
1378
- const dockerHost = config.dockerHost?.trim();
1379
- if (!dockerHost) {
1380
- throw new RemoteExecutionError("connect_failed", "remoteExec provider 'remote-docker' requires a non-empty dockerHost (the remote daemon endpoint, e.g. tcp://host:2376 or ssh://user@host) — use provider 'local-docker' for THIS machine's daemon");
1381
- }
1382
- return localDockerExecutionEnvFactory({ ...config, dockerHost, providerLabel: "remote-docker" }, deps);
1383
- }
1384
1376
  //# sourceMappingURL=remote-env-host.js.map
@@ -27,19 +27,13 @@ export interface LocalDockerEnvConfig {
27
27
  * DOCKER_HOST override for the docker CLI (e.g. a non-default socket). Passed as `-H <value>` on EVERY docker
28
28
  * invocation (not the process env) so it can't leak / drift. Absent = the daemon DOCKER_HOST points at.
29
29
  *
30
- * Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is exactly the DUAL-MODE-DESIGN §5
31
- * **`remote-docker`** provider — see {@link remoteDockerExecutionEnvFactory} (remote-env-host.ts), which is
32
- * this adapter with the endpoint required. Nothing else about the class is host-local: the workspace is
33
- * `mkdir`'d INSIDE the container (never a bind-mount of a host path) and bytes move by `docker cp` /
34
- * `docker exec cat`, which stream over the daemon API.
30
+ * Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is how a REMOTE docker daemon is used — the
31
+ * lane stays `local-docker` (`REMOTE_EXEC=local-docker` + `DOCKER_HOST`; the settings-schema lane table lists
32
+ * `DOCKER_HOST` as this lane's optional coordinate, and there is no separate `remote-docker` lane since 7.0.0).
33
+ * Nothing about the class is host-local: the workspace is `mkdir`'d INSIDE the container (never a bind-mount of a
34
+ * host path) and bytes move by `docker cp` / `docker exec cat`, which stream over the daemon API.
35
35
  */
36
36
  dockerHost?: string;
37
- /**
38
- * Provider NAME reported on the {@link WorkspaceHandle}. Default `"local-docker"`; the `remote-docker` arm
39
- * passes `"remote-docker"` so a handle never mislabels which lane it is (the CLASS is shared — `remote-docker`
40
- * IS this adapter aimed at a remote daemon). Cosmetic/observability only: it selects no behavior.
41
- */
42
- providerLabel?: string;
43
37
  /** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */
44
38
  memory?: string;
45
39
  /** Container CPU limit (docker `--cpus`, e.g. 1.5). Absent = no limit. */
@@ -88,8 +82,6 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
88
82
  private readonly spawnFn;
89
83
  /** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
90
84
  private readonly containerName;
91
- /** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
92
- private readonly providerName;
93
85
  /** Set once the container is up + the workspace dir created. */
94
86
  private containerId?;
95
87
  private handle?;
@@ -24,7 +24,6 @@ export class RemoteLocalDockerExecutionEnv {
24
24
  cfg;
25
25
  spawnFn;
26
26
  containerName;
27
- providerName;
28
27
  containerId;
29
28
  handle;
30
29
  destroyed = false;
@@ -47,7 +46,6 @@ export class RemoteLocalDockerExecutionEnv {
47
46
  const id = config.id ?? randomBytes(6).toString("hex");
48
47
  const suffix = randomBytes(4).toString("hex");
49
48
  this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
50
- this.providerName = config.providerLabel?.trim() || PROVIDER;
51
49
  }
52
50
  resolve(p) {
53
51
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
@@ -113,7 +111,7 @@ export class RemoteLocalDockerExecutionEnv {
113
111
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
114
112
  }
115
113
  this.containerId = run.value.stdout.trim() || this.containerName;
116
- this.handle = { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
114
+ this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
117
115
  if (config?.secrets?.length) {
118
116
  }
119
117
  return ok(this.handle);
@@ -136,7 +134,7 @@ export class RemoteLocalDockerExecutionEnv {
136
134
  const insp = await this.docker(["inspect", "-f", "{{.State.Running}}", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs });
137
135
  if (insp.ok && insp.value.exitCode === 0 && insp.value.stdout.trim() === "true") {
138
136
  this.containerId = this.containerName;
139
- this.handle ??= { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
137
+ this.handle ??= { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
140
138
  return ok(this.handle);
141
139
  }
142
140
  return { ok: false, error: new RemoteExecutionError("connect_failed", `container '${this.containerName}' is not running (gone or stopped) — no managed snapshot to resume`) };
package/dist/run-local.js CHANGED
@@ -12,7 +12,7 @@ import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
12
12
  import { withUnattendedLeg } from "./unattended-permission-mode.js";
13
13
  import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
14
14
  import { createBrain } from "./brain.js";
15
- import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
15
+ import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildApprovalGrantNotice, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
16
16
  import { applyRuntimeGovernance } from "./runtime-governance.js";
17
17
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
18
18
  import { taskWallClockSec } from "./task-workflow.js";
@@ -334,6 +334,9 @@ export async function runLocal(argv, deps = {}) {
334
334
  });
335
335
  if (ungatedCompensation)
336
336
  logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
337
+ const approvalGrantNotice = buildApprovalGrantNotice(config);
338
+ if (approvalGrantNotice)
339
+ logger.info(approvalGrantNotice.event, approvalGrantNotice.fields);
337
340
  if (!deps.brain) {
338
341
  const missing = missingGatewayRequirements(config);
339
342
  if (missing.length > 0) {