@sema-agent/server 7.114.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 (68) hide show
  1. package/MIGRATION.md +73 -1
  2. package/USAGE.md +35 -2
  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/resolve-spec.js +4 -1
  15. package/dist/boot/shutdown.d.ts +19 -0
  16. package/dist/boot/shutdown.js +12 -1
  17. package/dist/capabilities/composite-members.d.ts +1 -0
  18. package/dist/capabilities/composite-members.js +1 -0
  19. package/dist/config-catalog.js +2 -1
  20. package/dist/config-center/apply-effective.js +11 -11
  21. package/dist/config-provider.d.ts +16 -0
  22. package/dist/config-provider.js +11 -2
  23. package/dist/config-types.d.ts +20 -1
  24. package/dist/config.d.ts +41 -18
  25. package/dist/config.js +36 -13
  26. package/dist/deployment-governance.d.ts +15 -4
  27. package/dist/deployment-governance.js +20 -2
  28. package/dist/fleet/fleet-bus.d.ts +1 -22
  29. package/dist/hooks/hook-runner.d.ts +29 -3
  30. package/dist/hooks/hook-runner.js +30 -17
  31. package/dist/http/admission.js +7 -2
  32. package/dist/http/approval-sweeps.js +20 -0
  33. package/dist/http/checkpoint-ctx.d.ts +68 -0
  34. package/dist/http/checkpoint-ctx.js +36 -0
  35. package/dist/http/durable-run-leg.js +2 -1
  36. package/dist/http/resume-legs.js +9 -0
  37. package/dist/http/route-ctx.d.ts +7 -0
  38. package/dist/http/routes/a2a-serve.d.ts +1 -3
  39. package/dist/http/routes/a2a-serve.js +0 -2
  40. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  41. package/dist/http/routes/admin-memory-optout.js +0 -1
  42. package/dist/http/routes/capabilities.js +1 -0
  43. package/dist/http/routes/diagnostics.d.ts +5 -0
  44. package/dist/http/routes/diagnostics.js +2 -0
  45. package/dist/http/routes/sessions.js +1 -1
  46. package/dist/http/routes/tasks.js +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/leader/wire.js +2 -1
  49. package/dist/local-data-root.d.ts +1 -1
  50. package/dist/mcp-content-origin.d.ts +0 -8
  51. package/dist/model-entry-refusal.d.ts +10 -2
  52. package/dist/model-entry-refusal.js +19 -3
  53. package/dist/observability/fail-open.d.ts +5 -8
  54. package/dist/permission-rule-list.d.ts +81 -0
  55. package/dist/permission-rule-list.js +17 -0
  56. package/dist/run-local.js +4 -1
  57. package/dist/runtime-governance.d.ts +21 -1
  58. package/dist/runtime-governance.js +6 -1
  59. package/dist/security.js +2 -2
  60. package/dist/session-policy-wire.d.ts +5 -4
  61. package/dist/task-settings.d.ts +6 -5
  62. package/dist/task-settings.js +16 -30
  63. package/dist/tool-approval.d.ts +16 -4
  64. package/dist/tool-approval.js +2 -1
  65. package/dist/trace/core-keyset-guard.d.ts +9 -4
  66. package/dist/trace/project.js +9 -0
  67. package/package.json +4 -4
  68. package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
@@ -2,11 +2,18 @@ import { Runner } from "@sema-agent/core";
2
2
  import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
3
3
  import { createSighupIdleHandler } from "../sighup-idle.js";
4
4
  import { createParentWatch } from "../parent-watch.js";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { HOST_BG_KILL_GRACE_MS } from "../plugins/remote-env-host.js";
5
7
  import { installProcessExitBoundary } from "./exit-boundary.js";
6
8
  import { createEngineLeaseTracker } from "./engine-lease.js";
7
9
  import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
8
10
  import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
9
11
  const HARD_STOP_GRACE_MS = 10_000;
12
+ export function backgroundGraceRemainingMs(a) {
13
+ if (a.softStopped <= 0)
14
+ return 0;
15
+ return Math.max(0, Math.min(a.softStoppedAt + a.graceMs, a.deadlineMs) - a.now);
16
+ }
10
17
  export function installShutdownHandlers(ctx) {
11
18
  const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, deviceHub, toolApproval, parkCommits, } = ctx;
12
19
  const dataRoot = config.localDataRoot;
@@ -102,7 +109,10 @@ export function installShutdownHandlers(ctx) {
102
109
  subRunner.sessions.dispose();
103
110
  void lspManager?.dispose?.();
104
111
  void workflowNotifyJournal?.close?.();
105
- exitBoundary.reapChildProcessesEarly();
112
+ const backgroundSoftStopped = exitBoundary.reapChildProcessesEarly();
113
+ const softStoppedAt = Date.now();
114
+ const backgroundGraceMs = backgroundGraceRemainingMs({ softStopped: backgroundSoftStopped, softStoppedAt, now: softStoppedAt, graceMs: HOST_BG_KILL_GRACE_MS, deadlineMs: graceDeadlineMs });
115
+ const backgroundGrace = backgroundGraceMs > 0 ? delay(backgroundGraceMs) : Promise.resolve();
106
116
  const drained = approvalDrain
107
117
  .then(() => parkCommits?.quiesce(graceDeadlineMs))
108
118
  .then((q) => {
@@ -120,6 +130,7 @@ export function installShutdownHandlers(ctx) {
120
130
  void Promise.all([retentionTeardown, ledgerStopped])
121
131
  .then(() => (backend ? backend.close() : Promise.resolve()))
122
132
  .then(() => routingStopped)
133
+ .finally(() => backgroundGrace)
123
134
  .finally(() => process.exit(0));
124
135
  });
125
136
  const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
@@ -46,6 +46,7 @@ export declare const COMPOSITE_CAPABILITY_MEMBERS: {
46
46
  readonly webSearch: readonly ["backend"];
47
47
  readonly background: readonly ["listFace", "exitFaces"];
48
48
  readonly taskSettings: readonly ["permissions", "permissionMode", "model", "outputStyle", "env", "hooks"];
49
+ readonly settings: readonly ["permissions"];
49
50
  readonly permissionModeAuto: readonly ["accepted", "classifierSeat", "entitlementSource", "armed", "reason", "model"];
50
51
  readonly writeProtection: readonly ["armed", "rows", "replaced"];
51
52
  readonly executionLane: readonly ["provider", "toolsOnThisHost", "isolated"];
@@ -7,6 +7,7 @@ export const COMPOSITE_CAPABILITY_MEMBERS = {
7
7
  webSearch: ["backend"],
8
8
  background: ["listFace", "exitFaces"],
9
9
  taskSettings: ["permissions", "permissionMode", "model", "outputStyle", "env", "hooks"],
10
+ settings: ["permissions"],
10
11
  permissionModeAuto: ["accepted", "classifierSeat", "entitlementSource", "armed", "reason", "model"],
11
12
  writeProtection: ["armed", "rows", "replaced"],
12
13
  executionLane: ["provider", "toolsOnThisHost", "isolated"],
@@ -58,6 +58,7 @@ export const CONFIG_CATALOG = [
58
58
  r("ANTHROPIC_VERSION", "modelPlane", "string", "Anthropic API version 头(缺省由 SDK 定)"),
59
59
  r("APPROVAL_AUTO_BUDGET", "approval", "number", "每 run-leg 自动放行的普通门控工具调用数上限(0=关;防审批循环的熔断,不是安全 ask 的帽)", { staticDefault: "0", danger: SEC, configKey: "approvalAutoBudget" }),
60
60
  r("APPROVAL_DENY", "approval", "csv", "始终拒绝的工具名单(装配期拒不可匹配名)", { danger: SEC, configKey: "approvalDeny" }),
61
+ r("APPROVAL_GRANT", "approval", "csv", "运维的 allow 规则(工具名规则:精确名 / mcp__<server>__* / a2a__<peer>__*;命中 = 带理由放行,不再为这些调用出「没人表态」卡(不放掉任何一层自己的问);HTTP 提交 / durable 续跑 / 停驻后台 agent 赎回 / run-local 四腿同装(leader worker 首跑与修复腿到不了);不越过任何一层的 deny / ask;不是白名单;坏值拒启)", { danger: SEC, configKey: "approvalGrant", derivedDefaultNote: "缺席 = 空表(本层不表态,折叠与未设逐字同)" }),
61
62
  r("APPROVAL_HMAC_KEYS", "approval", "secret", "直连门(D-G)HMAC 完整性密钥集(空=直连门不激活)", { danger: SEC }),
62
63
  r("APPROVAL_NEVER_AUTO", "approval", "csv", "永不被 AUTO_BUDGET 自动放行的工具集(不可逆动作恒要人)", { danger: SEC, configKey: "approvalNeverAuto" }),
63
64
  r("APPROVAL_REQUIRE", "approval", "csv", "必须人审的工具名单(F4 门;center 腿热:refresh 拍整表换代,无 checkpoint 店的部署发非空名单 ⇒ 审批组整组拒)", { danger: SEC, configKey: "approvalRequire", center: cGov("approvalRequire") }),
@@ -156,7 +157,7 @@ export const CONFIG_CATALOG = [
156
157
  r("HOST_BG_DEFAULT_TIMEOUT_SEC", "orchestration", "number", "host 后台 shell 缺省超时(秒)", { staticDefault: "300" }),
157
158
  r("HOST_BG_ENABLED", "orchestration", "boolean", "host 车道后台 shell 开关", { staticDefault: "true" }),
158
159
  r("HOST_BG_FILE_CAP", "orchestration", "number", "host 后台 shell 磁盘护栏(字节;失控输出被 reap)", { staticDefault: String(64 * 1024 * 1024) }),
159
- r("HOST_BG_KILL_GRACE_MS", "orchestration", "number", "host 后台 shell SIGTERM→SIGKILL 宽限(ms);会话删除 / 会话停止口在软停与硬收之间等同一只", { staticDefault: "1000" }),
160
+ r("HOST_BG_KILL_GRACE_MS", "orchestration", "number", "host 后台 shell SIGTERM→SIGKILL 宽限(ms);会话删除 / 会话停止口在软停与硬收之间等同一只,受控退出(SIGTERM / SIGINT)在软停之后、退出之前也等一次(封顶在停机宽限窗内)", { staticDefault: "1000" }),
160
161
  r("HOST_BG_MAX_CONCURRENT", "orchestration", "number", "host 后台 shell 并发帽", { staticDefault: "8" }),
161
162
  r("HOST_BG_MAX_TIMEOUT_SEC", "orchestration", "number", "host 后台 shell 超时上限(秒)", { staticDefault: "1800" }),
162
163
  r("HOST_BG_MEM_CAP", "orchestration", "number", "host 后台 shell 内存流护栏(字节)", { staticDefault: String(8 * 1024 * 1024) }),
@@ -1,6 +1,6 @@
1
1
  import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
2
  import { modelKeyPoison } from "../key-resolver.js";
3
- import { applyAutoCompactWindow, buildModelCostKey, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
3
+ import { applyAutoCompactWindow, buildModelCostKey, parseAutonomy, unmatchableToolNamesRefusal } from "../config.js";
4
4
  import { buildModelCompat, readModelCompat } from "../model-compat.js";
5
5
  import { buildModelHeadersKey } from "../model-entry-headers.js";
6
6
  import { validateCommandRules } from "../runtime-governance.js";
@@ -9,7 +9,7 @@ import { stageLimits } from "./stage-limits.js";
9
9
  import { APPROVAL_GROUP_KEYS, BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
10
10
  import { buildDisplaySafeUrl } from "../trace/redact.js";
11
11
  import { crossInvariant } from "../config-invariants.js";
12
- import { DEFAULT_MODEL_MISSING_CODE, REFUSED_NAME_MINTED_CODE, judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, malformedDroppedCarrierRefusal } from "../model-entry-refusal.js";
12
+ import { DEFAULT_MODEL_MISSING_CODE, REFUSED_NAME_MINTED_CODE, judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, refusedNameLogField, refusedNamesLogFields, malformedDroppedCarrierRefusal } from "../model-entry-refusal.js";
13
13
  import { judgeModelRouteEndpoint } from "../model-route-endpoint.js";
14
14
  import { gateIntentUnserviceableReason } from "../approval.js";
15
15
  import { refusedGroups } from "./apply-ledger.js";
@@ -314,7 +314,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
314
314
  if (usable.length > 0) {
315
315
  picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, usable[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", {
316
316
  source,
317
- name,
317
+ ...(Object.hasOwn(verdict.refused, name) ? refusedNameLogField("name", name) : { name }),
318
318
  ...(Object.hasOwn(verdict.refused, name) ? { unavailable: verdict.refused[name].code } : {}),
319
319
  hint: "explicit default names a model that is not in the usable catalog (not enabled, or unavailable on this host — see `unavailable`) — falling to the next source",
320
320
  }));
@@ -327,7 +327,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
327
327
  continue;
328
328
  }
329
329
  const refusal = Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
330
- logger?.warn("sema_registry_tier_binding_unavailable", { tier, model: target, ...(refusal !== undefined ? { code: refusal.code } : {}), hint: "this tier binding names a model that is not in this generation's usable catalog — the binding is dropped (the tier word falls to the next lower bound tier, core's ladder); fix the entry or rebind the tier" });
330
+ logger?.warn("sema_registry_tier_binding_unavailable", { tier, ...(refusal !== undefined ? refusedNameLogField("model", target) : { model: target }), ...(refusal !== undefined ? { code: refusal.code } : {}), hint: "this tier binding names a model that is not in this generation's usable catalog — the binding is dropped (the tier word falls to the next lower bound tier, core's ladder); fix the entry or rebind the tier" });
331
331
  }
332
332
  const nextView = { ...config, models, tiers: activeTiers, modelRefusals: verdict.refused };
333
333
  const noDefault = crossInvariant("default-model-resolvable").check({ live: config, next: nextView });
@@ -343,7 +343,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
343
343
  const target = typeof spec === "string" ? spec : spec !== null && typeof spec === "object" && "model" in spec && typeof spec.model === "string" ? spec.model : undefined;
344
344
  const refusal = target !== undefined && Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
345
345
  if (refusal !== undefined) {
346
- logger?.warn("sema_registry_role_binding_unavailable", { role, model: target, code: refusal.code, hint: ROLE_BINDING_UNAVAILABLE_HINT });
346
+ logger?.warn("sema_registry_role_binding_unavailable", { role, ...refusedNameLogField("model", target), code: refusal.code, hint: ROLE_BINDING_UNAVAILABLE_HINT });
347
347
  }
348
348
  }
349
349
  const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
@@ -357,7 +357,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
357
357
  roles,
358
358
  activeTiers,
359
359
  atModelAllowlist,
360
- infoLine: { count: usable.length, ...(Object.keys(verdict.refused).length > 0 ? { unavailable: Object.keys(verdict.refused) } : {}), default: defaultModel.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
360
+ infoLine: { count: usable.length, ...(Object.keys(verdict.refused).length > 0 ? refusedNamesLogFields("unavailable", Object.keys(verdict.refused)) : {}), default: defaultModel.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
361
361
  };
362
362
  return { stages: { models: plane, collab: stageCollabDomain(eff) }, defaultName: picked.name, refused: verdict.refused };
363
363
  }
@@ -476,13 +476,13 @@ function stageApprovalRequire(raw) {
476
476
  return { ok: false, detail, reason: detail, names: nonString.map((e) => `[${e.i}]`), kept: KEPT_WOULD_REMOVE_GATE };
477
477
  }
478
478
  const names = raw.filter((n) => typeof n === "string").map((n) => n.trim()).filter((n) => n.length > 0);
479
- const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
480
- if (bad.length > 0) {
479
+ const unmatchable = unmatchableToolNamesRefusal("published governance.approvalRequire", names, { consumer: "tool-name-table", behavior: "deny" });
480
+ if (unmatchable !== undefined) {
481
481
  return {
482
482
  ok: false,
483
- detail: formatUnmatchableToolNames("published governance.approvalRequire", bad),
484
- reason: `published governance.approvalRequire has ${bad.length} entr${bad.length === 1 ? "y" : "ies"} that match no live tool (retired name / incomplete MCP name / CC-form rule) — the offending entries are named in the sema_registry_approval_require_invalid log line`,
485
- names: bad.map((b) => b.name),
483
+ detail: unmatchable.message,
484
+ reason: `published governance.approvalRequire has ${unmatchable.names.length} entr${unmatchable.names.length === 1 ? "y" : "ies"} that match no live tool (retired name / incomplete MCP name / CC-form rule) — the offending entries are named in the sema_registry_approval_require_invalid log line`,
485
+ names: unmatchable.names,
486
486
  kept: KEPT_WOULD_MATCH_NOTHING,
487
487
  };
488
488
  }
@@ -35,6 +35,22 @@ export type FetchEffectiveResult = {
35
35
  readWarnings: readonly EffectiveReadWarning[];
36
36
  credentialIdentity?: string;
37
37
  } | null;
38
+ /**
39
+ * 引号段 = **一条规则**:先过 `redactSecrets`;文本里没有引号字符(`"` / `'`)⇒ 原样返回;有 ⇒ 从**第一个**引号到**最后一个**引号(含两端,
40
+ * 两种引号不分种类)整段换成**一个**指纹;`"` 的个数是奇数、或 `'` 的个数是奇数(落单 / 句子被截断)⇒ 从第一个引号遮到**文末**。
41
+ * 本函数不辨认任何一个引号是「开头」「收尾」「转义」还是「撇号」—— 生产方原文插值的值自己带引号时,句子里没有信号能把「值里的引号」与「值的边界」
42
+ * 分开,逐个辨认边界的读法每补一条豁免就多一种漏形。
43
+ *
44
+ * 为什么是超集:
45
+ * · 配对读法(`/"[^"]*"|'[^']*'/g`)遮住的每一段都夹在两个引号之间,因此落在「第一个引号 … 最后一个引号」之内;
46
+ * · 生产方放进引号里的值(不论值里带几个引号、怎么转义、撇号在哪)也都落在这一段之内 —— 值左边至少有一个引号、右边至少有一个引号;
47
+ * · 引号个数不成对时遮到文末,只会更宽。
48
+ * 代价:两个操作数之间的固定词与第一个操作数之后的句子不再可读(例:`roster "#…" (not in the models catalog)`,原句里 `references unknown model`
49
+ * 一并进了指纹);首个引号之前的部分(引用完整性腿拼在前面的坐标 `rosters.0.models.0: `、固定句头)与末个引号之后的句尾仍是明文。
50
+ * 仍不覆盖:生产方**不加引号**直接插值的值(本函数看不见值在哪);句子被截断、而两种引号的个数恰好都是偶数的形。真收法在消息生产方给出结构化操作数、
51
+ * 不回声值(上游 settings-schema)。
52
+ */
53
+ export declare function redactConfigError(text: string): string;
38
54
  /** Sentinel returned by {@link raceBootFetch} when the boot pull outlived its budget — the caller
39
55
  * starts serving on the env fallback and the SAME promise settles in the background (never cancelled). */
40
56
  export declare const BOOT_FETCH_DEFERRED: unique symbol;
@@ -8,8 +8,17 @@ import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetc
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  const OPERAND_FINGERPRINT_KEY = randomBytes(16);
11
- function redactConfigError(text) {
12
- return redactSecrets(text).replace(/"[^"]*"|'[^']*'/g, (m) => `"#${createHmac("sha256", OPERAND_FINGERPRINT_KEY).update(m.slice(1, -1)).digest("hex").slice(0, 8)}"`);
11
+ export function redactConfigError(text) {
12
+ const s = redactSecrets(text);
13
+ const first = s.search(/["']/);
14
+ if (first === -1)
15
+ return s;
16
+ const count = (q) => s.split(q).length - 1;
17
+ const fingerprint = (operand) => `"#${createHmac("sha256", OPERAND_FINGERPRINT_KEY).update(operand).digest("hex").slice(0, 8)}"`;
18
+ if (count('"') % 2 === 1 || count("'") % 2 === 1)
19
+ return `${s.slice(0, first)}${fingerprint(s.slice(first + 1))}`;
20
+ const last = Math.max(s.lastIndexOf('"'), s.lastIndexOf("'"));
21
+ return `${s.slice(0, first)}${fingerprint(s.slice(first + 1, last))}${s.slice(last + 1)}`;
13
22
  }
14
23
  export const BOOT_FETCH_DEFERRED = Symbol("boot-fetch-deferred");
15
24
  export function raceBootFetch(fetch, budgetMs) {
@@ -1324,6 +1324,25 @@ export interface ServiceConfigFlat {
1324
1324
  * 「只配 APPROVAL_NEVER_AUTO」这一形被判成"零门意图" ⇒ 落进单用户 allow-all 基线 ⇒ 那张表
1325
1325
  * 从未进入任何 policy。判定现在单源在 `hasOperatorGateIntent()`(与 inv#2 同文件)。 */
1326
1326
  approvalNeverAuto: string[];
1327
+ /**
1328
+ * 7.115.0 —— **运维的 allow 规则**(`APPROVAL_GRANT`,逗号分隔的工具名规则:精确名 / `mcp__<server>__*` / `a2a__<peer>__*`)。命中 = 带理由的
1329
+ * 放行(core `decisionReason: "rule"`,运维替这些调用表了态 —— 回答没人表态的服务器工具 / A2A 技能调用;内置工具上引擎的写门、shell 检查照问);未命中 = 不表态。与请求腿的 `settings.permissions.allow` 是**同一种东西**:
1330
+ * 同一只编译口(`permission-rule-list.ts` `createRuleListPolicy`)、同一只校验(`config.ts` `approvalGrantRefusal` → `unmatchableToolNamesRefusal`,
1331
+ * 坏值拒启)。施加点 = 部署治理链的单一构造口(`deployment-governance.ts` → `runtime-governance.ts` `applyRuntimeGovernance` 的一片叶),
1332
+ * 所以 HTTP 提交 / durable 续跑(含 leader worker 停驻后经 `/decide` 续跑的那一段)/ 停驻后台 agent 的跨副本赎回 / run-local 四条腿同装(leader worker 首跑与修复腿到不了),每次现取(不 memoize)。**不越过**任何一层的 deny / ask(折叠 deny > ask > allow)。
1333
+ *
1334
+ * ⚠️ 不是白名单:名字不叫 `…_ALLOW`,与会话规则的 `toolAllow`(「只许这些」)刻意分开 —— 本键只给出表态,不收窄任何东西。
1335
+ *
1336
+ * 三问:谁需要 = 在没有请求体的腿上接了 MCP 服务器的运维 —— `run-local`,以及 durable 续跑(每一次从持久化体重建,含 leader worker 停驻后经 `/decide`
1337
+ * 续跑的那一段;A2A 不在射程:运维声明的对端恒带外发标记,那一问本键不放) —— core 7.40.0 起没人表态的服务器工具调用要问,无席 ⇒ 拒、有 durable 席 ⇒ 每次停驻等人,
1338
+ * 这里是运维为自己接的服务器表态的唯一位置。**到不了**:leader worker 的首跑与修复腿(工具策略在 leader 编排里自己折出来 —— `leader/wire.ts` 的
1339
+ * `combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy(…))` —— 不经上面那只构造口);这两条腿今天不挂 MCP / A2A,本键在那里没有对象。谁受伤 = 缺省空表,无人受伤 —— 设了宽规则
1340
+ * (`mcp__<server>__*`,含那台服务器将来新增的工具)= 对**每一个租户、每一条腿**的那些调用都不再因「没人表态」而问人,风险随规则的宽度走
1341
+ * (射程只到这一问:任何一层自己的问照旧压过它 —— 内置工具的精确名写进来,引擎的写门 / 形状层照问);补偿 = 本键就是
1342
+ * core 7.40.0 那条收紧的补偿,deny / ask(`APPROVAL_DENY` / `APPROVAL_REQUIRE` / 请求 settings / 会话规则)照旧压过它。
1343
+ * v1 只有 env 一个写入通道(无 config-center 座位,不进 settings-schema)。空表 = 本层不铸叶,折叠逐字与修前同。
1344
+ */
1345
+ approvalGrant: string[];
1327
1346
  /** [ref] D-G (HMAC = integrity, not identity; ✅ WIRED — the "SCAFFOLD, not yet wired" note here was STALE,
1328
1347
  * corrected 2026-07-25: consumed by `verifyDirectDoorProof` → `verifyApprovalHmac`, reached from the live decide
1329
1348
  * routes — the two `verifyDirectDoorProof(` call sites in `http/routes/approvals-assistant.ts`): the
@@ -2101,7 +2120,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
2101
2120
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelRefusals" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
2102
2121
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
2103
2122
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
2104
- export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "durableApprovalSource" | "streamAskWindowMsSource" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "autoModeClassifierFallback" | "autoModeClassifierTimeoutMs" | "crossSessionInbound" | "crossSessionDialogExpiry">;
2123
+ export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalGrant" | "approvalHmacKeys" | "durableApproval" | "durableApprovalSource" | "streamAskWindowMsSource" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "autoModeClassifierFallback" | "autoModeClassifierTimeoutMs" | "crossSessionInbound" | "crossSessionDialogExpiry">;
2105
2124
  /** 组:memory(记忆面 + TOC 同步腿)。 */
2106
2125
  export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "memoryConsolidationDriver" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
2107
2126
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
package/dist/config.d.ts CHANGED
@@ -285,7 +285,8 @@ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCen
285
285
  * ① env 腿(`APPROVAL_REQUIRE`/`DENY`/`NEVER_AUTO`,下方)—— boot 拒启;
286
286
  * ② 中心下发腿(`runtime.approvalRequire`,config-center/apply-effective.ts)—— 域级拒绝+保前值+warn
287
287
  * (常驻进程不 crash);
288
- * ③ per-request 腿(`settings.permissions.{ask,deny}`,task-settings.ts)—— 该请求 422。
288
+ * ③ per-request 腿(`settings.permissions.{ask,deny}`,task-settings.ts;S-654 (c) 起 `allow` 同判)—— 该请求 422;
289
+ * ④ env 腿 `APPROVAL_GRANT`(7.115.0,运维的 allow 规则,与 ③ 的 allow 桶同一个消费者 `rule-policy`)—— boot 拒启({@link approvalGrantRefusal})。
289
290
  *
290
291
  * 五种拼法:
291
292
  * - CC 形规则条目(`Bash(ps:*)`/`Edit(src/**)`,[ref] [ref]§七 装机实测):CC 的权限规则 DSL 写法。这些
@@ -327,28 +328,50 @@ export interface UnmatchableToolName {
327
328
  * S-177(core 7.9.0 [ref])—— **这条名单条目喂给谁**,决定「永不匹配」这句话怎么判。两个消费者形态:
328
329
  *
329
330
  * · `"tool-name-table"` —— 消费者按**整串工具名**比对(精确 ∪ covering):env `APPROVAL_*` 三键与
330
- * 中心下发 `runtime.approvalRequire`(消费者 `createDurableAskPolicy`),**以及**
331
- * `settings.permissions.allow`(消费者 core 的 `createAllowDenyPolicy` 的 allow 位)。带括号的内容形
331
+ * 中心下发 `runtime.approvalRequire`(消费者 `createDurableAskPolicy`)。带括号的内容形
332
332
  * 在这里永远匹配不上任何活工具名 —— 与引擎的规则文法认不认得它**无关**,是这条腿的比对方式决定的,
333
333
  * 所以内容形照旧响亮拒 + 指路。
334
334
  * · `"rule-policy"` —— 消费者是 core 的**规则编译器**(`createPermissionRulePolicy`):
335
- * `settings.permissions.deny` 与 `.ask`(S-177 / core 7.9.0 起两张表都走编译器)。引擎自此**真的按内容
336
- * 匹配**它们(`Bash(rm -r:*)` / `Edit(~/.sema/**)`),所以本仓不许再替它拒 —— 判据整条交回 core 的
337
- * `validatePermissionRules`,按这张表自己的 behavior 问。
338
- *
339
- * 🔴 **不对称不是特判,是从「消费者是谁」自然导出的**:`allow` 之所以豁免名字形与文法两族(退役名 /
340
- * 短名 / 反斜杠 / 超帽),是因为它是**白名单** —— 一个匹配不上的条目让白名单空掉 ⇒ 一切都不在白名单
341
- * ⇒ fail-CLOSED 且当场可见,那是可接受的另一种失败形;而内容形不同,它是一个 CC 用户为了**少弹窗**
342
- * 写下的拼法族错,静默代价是整只 Bash 不可用。
335
+ * `settings.permissions.deny` / `.ask`(S-177 / core 7.9.0 起)与 `.allow`(S-654 (c) / 7.115.0 起;修前是
336
+ * `createAllowDenyPolicy` 的名单位)。引擎**真的按内容匹配** deny / ask 的内容形(`Bash(rm -r:*)` /
337
+ * `Edit(~/.sema/**)`),所以本仓不许再替它拒 —— 判据整条交回 core 的 `validatePermissionRules`,按这张表
338
+ * 自己的 behavior 问(allow 侧 core 只收工具名形,内容形由 core 判拒)。
339
+ *
340
+ * S-654 (c) 前这里还有一条「`allow` 豁免名字形与文法两族」的不对称,前提是 allow 为白名单(匹配不上 ⇒ 全拒,
341
+ * fail-closed 且当场可见);allow 改成规则后那条前提不在了(匹配不上 ⇒ 什么也不放行、照常问,写的人收不到信号),豁免随之删除。
343
342
  */
344
343
  export type RuleEntryConsumer = "tool-name-table" | "rule-policy";
345
- export declare function findUnmatchableToolNames(names: readonly string[], opts: {
346
- consumer: RuleEntryConsumer;
347
- behavior: RuleBehavior;
348
- pathBases?: PathRuleBases;
349
- }): UnmatchableToolName[];
350
- /** {@link findUnmatchableToolNames} 的成句形——三条腿的文案同源(只有前缀/出口不同)。 */
351
- export declare function formatUnmatchableToolNames(source: string, bad: readonly UnmatchableToolName[]): string;
344
+ /** {@link unmatchableToolNamesRefusal} 的判据入参:这张表喂给谁(名表 / 规则编译器)、按哪种态度问 core 的文法、路径基。 */
345
+ export interface UnmatchableToolNameJudgment {
346
+ readonly consumer: RuleEntryConsumer;
347
+ readonly behavior: RuleBehavior;
348
+ readonly pathBases?: PathRuleBases;
349
+ }
350
+ /**
351
+ * 「这张表里有永远不会生效的条目」的**判据与拒因句,同一只函数**(返回 `undefined` = 通过;拒 ⇒ 句子 + 被点名的条目)。
352
+ * 五条腿全调它:env `APPROVAL_REQUIRE` / `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO`(拒启)、中心下发 `governance.approvalRequire`(整组拒)、
353
+ * 请求体 `settings.permissions.{allow,ask,deny}`(422)、运维 `APPROVAL_GRANT`(拒启)。修前判据(`findUnmatchableToolNames`)与成句
354
+ * (`formatUnmatchableToolNames`)是两只导出,句子里那半句(「这些表都是整串逐字比对」)只对名表成立 —— 规则编译器的
355
+ * 三只桶(与 `APPROVAL_GRANT`)是按规则文法编、按 covering 匹配的,不是整串比对。
356
+ *
357
+ * 句子对两种消费者、三种态度都成立:每一条要么永远匹配不到活工具(退役名 / 无协议前缀的短名 / 缺段的 MCP 名 / 名表里的内容形),要么编不成
358
+ * 一条规则(core 文法的判词原样带出);名字按写的样子读,不做别名折叠、不补协议前缀。逐条的「为什么 / 怎么改」在 `→` 之后。
359
+ */
360
+ export declare function unmatchableToolNamesRefusal(source: string, names: readonly string[], judgment: UnmatchableToolNameJudgment): {
361
+ readonly message: string;
362
+ readonly names: readonly string[];
363
+ } | undefined;
364
+ /**
365
+ * 7.115.0 —— 运维 allow 规则 `APPROVAL_GRANT` 的**拒启判据与拒因句**(同一只函数;`config-invariants.ts` 的 `…Refusal` 形:
366
+ * 返回句子 = 拒,`undefined` = 通过)。判据与请求腿的 `settings.permissions.allow` 是**同一只**:
367
+ * ① 名字形与 core 文法 —— {@link unmatchableToolNamesRefusal},消费者 `rule-policy`、behavior `allow`(退役名 / 无前缀短名 / 缺段 MCP 名 /
368
+ * 反斜杠 / 超长 / 内容形与参数级的 allow;句子点名哪一条、为什么、怎么改);
369
+ * ② 整表试编一次 —— 编的就是治理折叠里那一只(`runtime-governance.ts` {@link compileApprovalGrantLayers}),于是 core 的整表帽这类
370
+ * 只在编译时才抛的判据也在启动时响亮,而不是每个任务一次 500。
371
+ * 没有 `pathBases`:运维的规则不挂在任何一条任务的根上,而 allow 文法本来只收工具名形(路径形的 allow core 拒编,在 ① 里就拒了)。
372
+ * 坏值**不许**静默丢掉那一条后照常起:运维以为放行了,无人值守腿照旧拒 / 停驻,没有任何信号指回这里。
373
+ */
374
+ export declare function approvalGrantRefusal(names: readonly string[]): string | undefined;
352
375
  /**
353
376
  * [ref] 件B/C/D —— 三条**部署治理声明**的 env 解析(auth 域的一段,fail-loud 全在这里)。
354
377
  *
package/dist/config.js CHANGED
@@ -21,7 +21,7 @@ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
21
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
22
22
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
23
23
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
24
- import { AUTONOMY_MODES } from "./runtime-governance.js";
24
+ import { APPROVAL_GRANT_SOURCE, AUTONOMY_MODES, compileApprovalGrantLayers } from "./runtime-governance.js";
25
25
  import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
26
26
  import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
27
27
  import { modelProviderRefusal } from "./model-provider.js";
@@ -1226,14 +1226,16 @@ function uncompilableRuleGuidance(name, behavior, pathBases) {
1226
1226
  const issue = issues[0];
1227
1227
  if (issue === undefined)
1228
1228
  return undefined;
1229
- const ccGuidance = issue.code === "unsupported.bash_prefix" || issue.code === "unsupported.file_glob" ? ccRuleFormGuidance(name) : undefined;
1230
- if (ccGuidance !== undefined)
1231
- return ccGuidance;
1229
+ if (behavior === "allow" && (issue.code === "unsupported.bash_prefix" || issue.code === "unsupported.file_glob" || issue.code === "invalid.allow_param_rule")) {
1230
+ return (`${issue.code} — ${issue.message}. An allow rule list compiles tool-name rules only (an exact name, or mcp__<server>__* / ` +
1231
+ `a2a__<peer>__* for every tool on that peer); a content-form standing approval lives in the persisted-rule store ` +
1232
+ `(/v1/rules/cc-import, or keep the rule when you answer the call's card)`);
1233
+ }
1232
1234
  return (`${issue.code} — core's rule grammar cannot compile this name (${issue.message}), so a table keyed by it matches nothing. ` +
1233
1235
  `Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
1234
1236
  `mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
1235
1237
  }
1236
- export function findUnmatchableToolNames(names, opts) {
1238
+ function findUnmatchableToolNames(names, opts) {
1237
1239
  const out = [];
1238
1240
  for (const name of names) {
1239
1241
  if (opts.consumer === "tool-name-table") {
@@ -1243,8 +1245,6 @@ export function findUnmatchableToolNames(names, opts) {
1243
1245
  continue;
1244
1246
  }
1245
1247
  }
1246
- if (opts.behavior === "allow")
1247
- continue;
1248
1248
  const retired = RETIRED_TOOL_NAMES.get(name);
1249
1249
  if (retired !== undefined) {
1250
1250
  out.push({ name, guidance: retired });
@@ -1268,9 +1268,26 @@ export function findUnmatchableToolNames(names, opts) {
1268
1268
  }
1269
1269
  return out;
1270
1270
  }
1271
- export function formatUnmatchableToolNames(source, bad) {
1272
- return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
1271
+ export function unmatchableToolNamesRefusal(source, names, judgment) {
1272
+ const bad = findUnmatchableToolNames(names, judgment);
1273
+ if (bad.length === 0)
1274
+ return undefined;
1275
+ const message = (`${source} has ${bad.length === 1 ? "an entry" : "entries"} that can never take effect — ${bad.length === 1 ? "it" : "each"} would never match a live tool or cannot be compiled into a rule ` +
1276
+ `(names are read exactly as written: no alias folding, no protocol prefix added): ` +
1273
1277
  bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1278
+ return { message, names: bad.map((b) => b.name) };
1279
+ }
1280
+ export function approvalGrantRefusal(names) {
1281
+ const unmatchable = unmatchableToolNamesRefusal(APPROVAL_GRANT_SOURCE, names, { consumer: "rule-policy", behavior: "allow" });
1282
+ if (unmatchable !== undefined)
1283
+ return unmatchable.message;
1284
+ try {
1285
+ compileApprovalGrantLayers(names);
1286
+ }
1287
+ catch (e) {
1288
+ return e instanceof Error ? e.message : String(e);
1289
+ }
1290
+ return undefined;
1274
1291
  }
1275
1292
  function streamApprovalConfig(gates) {
1276
1293
  const cfg = {
@@ -1337,9 +1354,9 @@ function parseApprovalDomain(ctx) {
1337
1354
  const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS, announceKeySetRejection("APPROVAL_HMAC_KEYS"));
1338
1355
  const durableApproval = postureOn("DURABLE_APPROVAL", infraReady);
1339
1356
  const rejectUnmatchableToolNames = (envName, names) => {
1340
- const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
1341
- if (bad.length > 0)
1342
- throw new Error(formatUnmatchableToolNames(envName, bad));
1357
+ const unmatchable = unmatchableToolNamesRefusal(envName, names, { consumer: "tool-name-table", behavior: "deny" });
1358
+ if (unmatchable !== undefined)
1359
+ throw new Error(unmatchable.message);
1343
1360
  return names;
1344
1361
  };
1345
1362
  return {
@@ -1353,6 +1370,12 @@ function parseApprovalDomain(ctx) {
1353
1370
  resourceSuspendMaxSlices: nonNegativeIntEnv("RESOURCE_SUSPEND_MAX_SLICES", "跨片 slice 数上限"),
1354
1371
  approvalAutoBudget: clampEnvWithWarn("APPROVAL_AUTO_BUDGET", "0", 0, 10000),
1355
1372
  approvalNeverAuto: rejectUnmatchableToolNames("APPROVAL_NEVER_AUTO", csv("APPROVAL_NEVER_AUTO")),
1373
+ approvalGrant: ((names) => {
1374
+ const refusal = approvalGrantRefusal(names);
1375
+ if (refusal !== undefined)
1376
+ throw new Error(refusal);
1377
+ return names;
1378
+ })(csv("APPROVAL_GRANT")),
1356
1379
  approvalHmacKeys: dgHmacKeys,
1357
1380
  directApprovalDoor,
1358
1381
  sensitiveWritePatterns: ((raw) => {
@@ -2296,7 +2319,7 @@ const MODEL_PLANE_GROUP_KEYS = [
2296
2319
  "modelApiKeyEnv", "modelApiKeys", "modelRefusals", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
2297
2320
  ];
2298
2321
  const APPROVAL_GROUP_KEYS = [
2299
- "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
2322
+ "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto", "approvalGrant",
2300
2323
  "approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
2301
2324
  "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
2302
2325
  "sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "autoModeClassifierTimeoutMs", "crossSessionInbound", "crossSessionDialogExpiry",
@@ -11,6 +11,9 @@ export interface DeploymentGovernanceConfigView {
11
11
  readonly commandPolicy?: ServiceConfig["commandPolicy"];
12
12
  readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
13
13
  readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
14
+ /** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT`)—— 原串交给折叠属主编成叶(`runtime-governance.ts` `compileApprovalGrantLayers`)。
15
+ * 必填:三条消费腿的切面都要点名它,漏了是编译错,不是一条腿上静默没有这只旋钮。 */
16
+ readonly approvalGrant: ServiceConfig["approvalGrant"];
14
17
  /**
15
18
  * 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
16
19
  * **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
@@ -209,7 +212,7 @@ export declare class RelativeTargetLexicalEnv extends StubExecutionEnv {
209
212
  * core 的原因。env 只是编译期的占位(compilePatterns 不碰它),真裁决用的是每请求按 lane 铸的那一个。
210
213
  */
211
214
  export declare function assertGuardPatternsUsable(config: Pick<DeploymentGovernanceConfigView, "sensitiveWritePatterns">): void;
212
- /** 一条 boot 期运维告警的**载荷**(纯数据 ⇒ `build*`;打给谁、用哪只 logger 归消费腿)。 */
215
+ /** 一条 boot 期运维告警 / 通告的**载荷**(纯数据 ⇒ `build*`;打给谁、用哪只 logger、哪一级归消费腿)。 */
213
216
  export interface OperatorWarning {
214
217
  readonly event: string;
215
218
  readonly fields: Record<string, unknown>;
@@ -230,15 +233,17 @@ export interface GateSeatView {
230
233
  * 此前是「这个部署一个门都没接」这个 misconfig 的**唯一**提示,而守卫集只挡那二十来个路径段、其余写
231
234
  * 目标照旧无裁决 —— 信号不能因为我们铺了基线就静默消失,所以由我们自己按同一判据说一次。
232
235
  *
233
- * 判据(与信号消失的条件逐字互补):守卫集在场 ∧ 这条腿的审批基线**没有席**(`absent`)∧ 单用户
234
- * auto-accept 基线不适用。三个量都是部署常量 ⇒ 消费腿在 boot 期说一次,不是每任务一次。
236
+ * 判据(与信号消失的条件逐字互补):**非门的部署叶在场**(守卫集,或 7.115.0 起运维的 allow 规则 `APPROVAL_GRANT` —— 两者都让
237
+ * `spec.toolPolicy` 在场、又都不是门:一个只拒守卫段,一个只表态 allow)∧ 这条腿的审批基线**没有席**(`absent`)∧ 单用户
238
+ * auto-accept 基线不适用。事件名 `tool_policy_only_sensitive_baseline` 是allow 规则之前定的,保留不改(运维的日志告警按它匹配),
239
+ * 哪一片叶在场写在 `detail` 与 `guardedPatterns` / `grantRules` 两个计数里。三个量都是部署常量 ⇒ 消费腿在 boot 期说一次,不是每任务一次。
235
240
  * S-694:席读的是喂审批基线的**同一只**({@link ApprovalSeat})—— `durable` / `live` 两形都有人服 ask、名单
236
241
  * 进策略,「守卫集是唯一的策略」这句话在它们上面不成立。今天真发的只剩 HTTP 腿「durable 关 ∧ 多租户」那一形。
237
242
  *
238
243
  * ⚠️ 单一属主([ref] 件三):HTTP 腿与 run-local 腿共用本判据与文案。两处各写一份 = 一处改了另一处
239
244
  * 没改,而两份都长得像对的 —— 那正是本文件存在的理由。
240
245
  */
241
- export declare function buildOnlySensitiveBaselineWarning(config: Pick<DeploymentGovernanceConfigView, "sensitiveWritePatterns">, seat: GateSeatView): OperatorWarning | undefined;
246
+ export declare function buildOnlySensitiveBaselineWarning(config: Pick<DeploymentGovernanceConfigView, "sensitiveWritePatterns" | "approvalGrant">, seat: GateSeatView): OperatorWarning | undefined;
242
247
  /**
243
248
  * `applyRuntimeGovernance` 的 governance 实参预铸([ref] 件一)。
244
249
  *
@@ -253,6 +258,12 @@ export declare function buildOnlySensitiveBaselineWarning(config: Pick<Deploymen
253
258
  * 的读法因此不变。
254
259
  */
255
260
  export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication, roots: DeploymentGovernanceRoots): DeploymentGovernanceInputs;
261
+ /**
262
+ * 7.115.0 —— 运维 allow 规则的**起服通告**载荷(纯数据 ⇒ `build*`;HTTP 腿与 run-local 腿各在装配时说一次,用 info 级)。
263
+ * 规则名不是秘密(它们是工具名),所以原样列出;空表 ⇒ `undefined`(不打)。运维只读面上同一个事实另有一处:
264
+ * `GET /v1/diagnostics/wiring` 的 `serverGates.approvalGrant`。
265
+ */
266
+ export declare function buildApprovalGrantNotice(config: Pick<DeploymentGovernanceConfigView, "approvalGrant">): OperatorWarning | undefined;
256
267
  /**
257
268
  * 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座([ref] 件一)。**恒非
258
269
  * `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
@@ -61,7 +61,7 @@ export function assertGuardPatternsUsable(config) {
61
61
  }
62
62
  }
63
63
  export function buildOnlySensitiveBaselineWarning(config, seat) {
64
- if (config.sensitiveWritePatterns.length === 0 || seat.singleUserAutoAcceptBaseline)
64
+ if ((config.sensitiveWritePatterns.length === 0 && config.approvalGrant.length === 0) || seat.singleUserAutoAcceptBaseline)
65
65
  return undefined;
66
66
  switch (seat.approvalSeat) {
67
67
  case "durable":
@@ -74,12 +74,17 @@ export function buildOnlySensitiveBaselineWarning(config, seat) {
74
74
  throw new Error(`approval seat "${String(unhandled)}" has no arm in the only-sensitive-baseline warning`);
75
75
  }
76
76
  }
77
+ const nonGateLayers = [
78
+ ...(config.sensitiveWritePatterns.length > 0 ? ["the sensitive-path DENY baseline (SENSITIVE_WRITE_PATTERNS)"] : []),
79
+ ...(config.approvalGrant.length > 0 ? ["the APPROVAL_GRANT allow rules"] : []),
80
+ ];
77
81
  return {
78
82
  event: "tool_policy_only_sensitive_baseline",
79
83
  fields: {
80
- detail: "the sensitive-path DENY baseline (SENSITIVE_WRITE_PATTERNS) is this deployment's ONLY tool policy — writes outside the guarded segments are unadjudicated, and core's own UNGATED warning no longer fires because a policy is now always present",
84
+ detail: `${nonGateLayers.join(" and ")} ${nonGateLayers.length > 1 ? "are" : "is"} this deployment's ONLY tool policy and not a gate — writes outside the guarded segments are unadjudicated, and core's own UNGATED warning no longer fires because a policy is now always present`,
81
85
  remedy: "wire an approval face (DURABLE_APPROVAL / APPROVAL_REQUIRE with a store) or accept the single-user auto-accept baseline",
82
86
  guardedPatterns: config.sensitiveWritePatterns.length,
87
+ grantRules: config.approvalGrant.length,
83
88
  },
84
89
  };
85
90
  }
@@ -108,6 +113,19 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
108
113
  commandPolicy: config.commandPolicy,
109
114
  ...(config.manualModeShellGate ? { manualModeShellGate: config.manualModeShellGate } : {}),
110
115
  ...(sensitivePathLayers ? { sensitivePathLayers } : {}),
116
+ ...(config.approvalGrant.length > 0 ? { approvalGrant: config.approvalGrant } : {}),
117
+ };
118
+ }
119
+ export function buildApprovalGrantNotice(config) {
120
+ if (config.approvalGrant.length === 0)
121
+ return undefined;
122
+ return {
123
+ event: "approval_grant_in_force",
124
+ fields: {
125
+ rules: config.approvalGrant.length,
126
+ names: [...config.approvalGrant],
127
+ note: "APPROVAL_GRANT states the allow for the tools these rules name on every leg it is installed on (HTTP submit, durable resume, cross-replica revival of parked background agents, run-local) — those calls are no longer asked for want of a statement; any layer's own deny or ask (approval lists, request settings, session rules, the engine's mode gates) still wins",
128
+ },
111
129
  };
112
130
  }
113
131
  export function createApprovalBaselinePolicy(config, seat) {
@@ -1,6 +1,5 @@
1
1
  import type { TaskNotificationPayload, TerminalCause, WorkflowRun } from "@sema-agent/core";
2
- import type { RunRecord, UnsuccessfulRunStatus } from "../plugins/store-contracts.js";
3
- import type { AssertAllKeysHandled } from "../trace/core-keyset-guard.js";
2
+ import type { RunRecord } from "../plugins/store-contracts.js";
4
3
  /** [ref] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
5
4
  * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
6
5
  * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
@@ -674,24 +673,4 @@ export declare function isTerminalFleetStatus(status: FleetTaskStatus): boolean;
674
673
  export declare function runStatusToFleet(status: RunStatusForFleet, opts?: {
675
674
  awaitingPlanApproval?: boolean;
676
675
  }): FleetTaskStatus;
677
- /**
678
- * S-213③ —— **编译期同源钉**:fleet 的 `"failed"` 桶必须恰好收下 `RUN_STATUS_CLASS` 判为「非成功终局」
679
- * 的每一个 core 词。
680
- *
681
- * 这个词的病史:`blocked` 曾在本仓有**两个独立写者**且答案互斥 —— 本文件读它为「非成功终局(agent
682
- * 放弃)⇒ 行离场」,而 `observability/run-terminal.ts` 读它为「治理判决,不是失败 ⇒ 不打行」。两处
683
- * 各自穷举、各自正确地编译,结果是一条 `report_blocked` 的 run 在面板上离场、在引擎日志里一个字都没有。
684
- * 语义收归 `plugins/store-contracts.ts` 的那张表之后,本行不再自持判断:表把某个词挪出/挪进
685
- * `unsuccessful`(或 core 新增一个非成功终态)而上面的 switch 没跟上 ⇒ 下面这条收不敛到 `never` ⇒ tsc 红。
686
- * (`"error"` 是 server 自有别名、不在那张表的辖域内,故不在本钉的两边出现。)
687
- */
688
- type FleetUnsuccessfulBucket = "failed" | "blocked";
689
- type _GuardFleetUnsuccessfulBucket = AssertAllKeysHandled<Exclude<UnsuccessfulRunStatus, FleetUnsuccessfulBucket>>;
690
- /** 反向门(与 `core-keyset-guard.ts` 的 `_Guard*Reverse` 同形):桶里留了一个表**不再**判为非成功终局的
691
- * 词同样红 —— 只有正向门时,把 `blocked` 挪回 `succeeded` 而 switch 照旧塞进 failed 桶,并集覆盖、
692
- * 差集门一声不吭(本批实测:只加正向门时该变异 tsc rc=0)。两道钉一起加。 */
693
- type _GuardFleetUnsuccessfulBucketReverse = AssertAllKeysHandled<Exclude<FleetUnsuccessfulBucket, UnsuccessfulRunStatus>>;
694
- /** 占位使用(防 noUnusedLocals 形 lint;类型别名本身零运行时)。 */
695
- export type FleetVocabGuards = [_GuardFleetUnsuccessfulBucket, _GuardFleetUnsuccessfulBucketReverse];
696
- export {};
697
676
  //# sourceMappingURL=fleet-bus.d.ts.map