@sema-agent/server 7.66.0 → 7.68.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 (48) hide show
  1. package/USAGE.md +6 -3
  2. package/dist/approval-card.d.ts +8 -0
  3. package/dist/approval.d.ts +3 -0
  4. package/dist/approval.js +2 -1
  5. package/dist/boot/resolve-spec.js +13 -6
  6. package/dist/config-catalog.js +2 -2
  7. package/dist/config-center/apply-effective.js +1 -1
  8. package/dist/config-types.d.ts +15 -2
  9. package/dist/config.d.ts +25 -3
  10. package/dist/config.js +58 -30
  11. package/dist/deployment-governance.d.ts +8 -0
  12. package/dist/http/route-ctx.d.ts +5 -1
  13. package/dist/http/routes/diagnostics.d.ts +27 -2
  14. package/dist/http/routes/diagnostics.js +21 -1
  15. package/dist/http/routes/rules.js +15 -10
  16. package/dist/http/routes/runs.js +1 -1
  17. package/dist/http/routes/tasks.js +4 -8
  18. package/dist/http/server.d.ts +6 -1
  19. package/dist/http/server.js +7 -21
  20. package/dist/mcp-tool-faces.js +11 -2
  21. package/dist/observability/fail-open.d.ts +4 -0
  22. package/dist/observability/fail-open.js +4 -0
  23. package/dist/observability/metrics.js +1 -1
  24. package/dist/permission-rule-vocab.d.ts +13 -21
  25. package/dist/permission-rule-vocab.js +6 -5
  26. package/dist/plugins/permission-rule-store-file.d.ts +3 -5
  27. package/dist/plugins/permission-rule-store-file.js +2 -1
  28. package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
  29. package/dist/plugins/permission-rule-store-sql.js +35 -20
  30. package/dist/posture-source.d.ts +57 -0
  31. package/dist/posture-source.js +27 -0
  32. package/dist/read-face-posture.d.ts +9 -4
  33. package/dist/read-face-posture.js +7 -11
  34. package/dist/rules-consent.d.ts +15 -10
  35. package/dist/rules-consent.js +41 -21
  36. package/dist/runs.d.ts +10 -1
  37. package/dist/runs.js +2 -1
  38. package/dist/session-titler.js +3 -12
  39. package/dist/task-mcp.d.ts +50 -6
  40. package/dist/task-mcp.js +71 -35
  41. package/dist/task-settings.d.ts +30 -0
  42. package/dist/task-settings.js +39 -18
  43. package/dist/tool-approval.d.ts +5 -4
  44. package/dist/tool-approval.js +2 -2
  45. package/dist/trace/engine-notice-wire.d.ts +17 -1
  46. package/dist/trace/engine-notice-wire.js +6 -1
  47. package/dist/trace/project.d.ts +4 -3
  48. package/package.json +3 -3
package/USAGE.md CHANGED
@@ -307,8 +307,11 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
307
307
  - 入口:`import "@sema-agent/server/main"`(exports 正门;此前宿主只能用 node_modules 路径字符串定位
308
308
  `dist/main.js`,那是未承诺的内部路径)。语义承诺三条:**import 即 boot**(副作用模块形,无需调用);
309
309
  **不读 `process.argv`**(宿主可用 argv 传自己的标记,`ps` args 判据不受干扰);`./package.json` 可读。
310
- - `/health` 身份字段承诺:`pid`(= 引擎进程)与 `dataRoot`(= 生效数据根,解析恒回退 `~/.ai-agent`,
311
- 与 DB_BACKEND 无关)**恒在**——宿主用它们验证「这个端口上的 /health 是不是我起的那个引擎」。
310
+ - `/health` 身份字段承诺:`pid`(= 引擎进程)、`startedAt`(**7.67.0+**,= 引擎进程起点的 epoch ms,
311
+ 一次铸)与 `dataRoot`(= 生效数据根,解析恒回退 `~/.ai-agent`,与 DB_BACKEND 无关)**恒在**——宿主用
312
+ 它们验证「这个端口上的 /health 是不是我起的那个引擎」。`startedAt` 变了 = 这个端口后面换了一条命
313
+ (另一个宿主重启了共享引擎):壳据此把「引擎换代」当**一等事实**处理,不必等某个带鉴权的请求先撞一次
314
+ 401 才发现(`/health` 无鉴权,心跳恒绿)。⚠️ 只给这一个数:service token 本体与代际计数**不上**这一面。
312
315
  钉:`test/health-identity-contract.test.ts`。
313
316
  - `/health` 活体 store 探测(5.14.0,SQL 后端专属 additive):后台探针(`STORE_PROBE_INTERVAL_MS`,
314
317
  默认 15000;0=关;负/非数启动响亮拒)缓存一份 DB 真往返结果,`storeProbe:{live,ageMs,error?}` 在
@@ -961,7 +964,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
961
964
  | `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。编排壳在发 SIGTERM **之前**调它;server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
962
965
  | `GET /v1/config/catalog` | **配置目录自描述**(operator-only —— `explicitOperatorOk`,空名单=谁都不是)。逐 env 键一行:`{name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, staticDefault?/derivedDefaultNote?, effectiveValue, valueClass, danger, center?{domain,path,precedence,timing,restartSlice?,domainExists,managedNow,effectiveNow}}`;顶层带 `restartSlices`(重启片闭集)与 `centerDomains`(settings-schema 现行域表)。**两轴不混**:`writeLanes`=谁能写,`effectiveLane`=现在谁在生效(env-wins 族 env 占位 ⇒ center 腿 `effectiveNow:false`)。secret 类值**永不回传**(只回在场位;opaque 回在场位+长度;URL 剥 userinfo)。完备性由仓内 AST 双向对账门执法(分母=全 `src/` 生产 env 读取点)。旋钮 `CONFIG_CATALOG_ENABLED=false`(合规部署)⇒ 路径 404 + 能力位 `configCatalog:false`。响应 `Cache-Control: no-store`。**7.57.0 起**:`enum` 行的 `effectiveValue` **恒落在该行自己的 `enumValues` 闭集内**(仓内机器门执法)——此前 `SESSION_BACKEND` 回的是**内部标签** `tidb`(自 5.0.0 起是**退役公名**,写回去 boot 拒),现归一成公名 `mysql`;`MODEL_DEFAULT_THINKING` 补上一直被解析器接受、却漏在目录闭集外的 `xhigh`,并改为回**实算值**(`off`/未设 ⇒ `null`)而不是 env 原文。 |
963
966
  | `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
964
- | `GET /health` | 健康(无需鉴权)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
967
+ | `GET /health` | 健康(无需鉴权)。**7.67.0+** 恒带 `startedAt`(引擎进程起点 epoch ms,一次铸;与 `pid` 并列的换代锚——值变了就是换了一条命)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
965
968
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
966
969
 
967
970
  > **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
@@ -81,6 +81,7 @@ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
81
81
  kind: z.ZodLiteral<"single">;
82
82
  rule: z.ZodString;
83
83
  match: z.ZodEnum<{
84
+ path: "path";
84
85
  exact: "exact";
85
86
  prefix: "prefix";
86
87
  wildcard: "wildcard";
@@ -93,6 +94,7 @@ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
93
94
  kind: z.ZodLiteral<"command">;
94
95
  rule: z.ZodString;
95
96
  match: z.ZodEnum<{
97
+ path: "path";
96
98
  exact: "exact";
97
99
  prefix: "prefix";
98
100
  wildcard: "wildcard";
@@ -156,6 +158,7 @@ export declare const RuleOfferRawSchema: z.ZodUnion<readonly [z.ZodObject<{
156
158
  kind: z.ZodLiteral<"single">;
157
159
  rule: z.ZodString;
158
160
  match: z.ZodEnum<{
161
+ path: "path";
159
162
  exact: "exact";
160
163
  prefix: "prefix";
161
164
  wildcard: "wildcard";
@@ -168,6 +171,7 @@ export declare const RuleOfferRawSchema: z.ZodUnion<readonly [z.ZodObject<{
168
171
  kind: z.ZodLiteral<"command">;
169
172
  rule: z.ZodString;
170
173
  match: z.ZodEnum<{
174
+ path: "path";
171
175
  exact: "exact";
172
176
  prefix: "prefix";
173
177
  wildcard: "wildcard";
@@ -367,6 +371,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
367
371
  kind: z.ZodLiteral<"single">;
368
372
  rule: z.ZodString;
369
373
  match: z.ZodEnum<{
374
+ path: "path";
370
375
  exact: "exact";
371
376
  prefix: "prefix";
372
377
  wildcard: "wildcard";
@@ -379,6 +384,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
379
384
  kind: z.ZodLiteral<"command">;
380
385
  rule: z.ZodString;
381
386
  match: z.ZodEnum<{
387
+ path: "path";
382
388
  exact: "exact";
383
389
  prefix: "prefix";
384
390
  wildcard: "wildcard";
@@ -475,6 +481,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
475
481
  kind: z.ZodLiteral<"single">;
476
482
  rule: z.ZodString;
477
483
  match: z.ZodEnum<{
484
+ path: "path";
478
485
  exact: "exact";
479
486
  prefix: "prefix";
480
487
  wildcard: "wildcard";
@@ -487,6 +494,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
487
494
  kind: z.ZodLiteral<"command">;
488
495
  rule: z.ZodString;
489
496
  match: z.ZodEnum<{
497
+ path: "path";
490
498
  exact: "exact";
491
499
  prefix: "prefix";
492
500
  wildcard: "wildcard";
@@ -4,6 +4,7 @@
4
4
  * suspend/resume,见 createDurableAskPolicy 与 /v1/approvals 的 checkpointStore 腿)。
5
5
  */
6
6
  import { type ToolPolicy } from "@sema-agent/core";
7
+ import { type PostureSource } from "./posture-source.js";
7
8
  /**
8
9
  * 运维**是否表达了门意图** —— boot 的单用户 allow-all 基线只在"零门意图"时才允许铺开,
9
10
  * 所以这个谓词漏一格 = 那一格的意图被 allow-all 静默吞掉。
@@ -23,6 +24,7 @@ export declare function hasOperatorGateIntent(config: {
23
24
  approvalDeny: readonly string[];
24
25
  approvalNeverAuto: readonly string[];
25
26
  durableApproval: boolean;
27
+ durableApprovalSource: PostureSource;
26
28
  }): boolean;
27
29
  /**
28
30
  * 门意图不可服务=boot 拒启(全窗复审 D3-F1,HIGH;与 env 墓碑族同谱系)。
@@ -35,6 +37,7 @@ export declare function assertGateIntentServiceable(config: {
35
37
  approvalDeny: readonly string[];
36
38
  approvalNeverAuto: readonly string[];
37
39
  durableApproval: boolean;
40
+ durableApprovalSource: PostureSource;
38
41
  }, checkpointStorePresent: boolean): void;
39
42
  /**
40
43
  * [ref] 件⑩ —— **center 的 `forceDurableGate` 在无 checkpoint 店的部署上会静默 inert**,补一条 boot 披露。
package/dist/approval.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "@sema-agent/core";
2
+ import { isExplicitPosture } from "./posture-source.js";
2
3
  export function hasOperatorGateIntent(config) {
3
4
  return (config.approvalRequire.length > 0 ||
4
5
  config.approvalDeny.length > 0 ||
5
6
  config.approvalNeverAuto.length > 0 ||
6
- config.durableApproval);
7
+ (config.durableApproval && isExplicitPosture(config.durableApprovalSource)));
7
8
  }
8
9
  export function assertGateIntentServiceable(config, checkpointStorePresent) {
9
10
  if (!hasOperatorGateIntent(config) || checkpointStorePresent)
@@ -1,5 +1,6 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, deriveAskEffective, expandTiers, resolveAskSeamForm, tightenTaskSpec } from "@sema-agent/core";
4
5
  import { cappedCeiling } from "../budget.js";
5
6
  import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
@@ -366,7 +367,7 @@ export function createResolveSpec(ctx) {
366
367
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
367
368
  })(cap.tools),
368
369
  skills: mergeUserSkills(cap.skills, body.skills, logger),
369
- mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
370
+ mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
370
371
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
371
372
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
372
373
  toolPolicy: toolPolicyForHands(hands, durableEnabled
@@ -411,11 +412,15 @@ export function createResolveSpec(ctx) {
411
412
  const pathAdjudication = hostSemanticsLane
412
413
  ? (() => {
413
414
  const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
414
- const gateCwd = sessionCwd ??
415
- (config.remoteExec === undefined
416
- ? process.cwd()
417
- : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
418
- return { env: new NodeExecutionEnv({ cwd: gateCwd }), cwd: gateCwd };
415
+ const realRoot = sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
416
+ const gateCwd = realRoot ?? join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted");
417
+ const realHome = config.remoteExec === undefined ? homedir() : undefined;
418
+ return {
419
+ env: new NodeExecutionEnv({ cwd: gateCwd }),
420
+ cwd: gateCwd,
421
+ ...(realRoot !== undefined ? { taskRoot: realRoot } : {}),
422
+ ...(realHome !== undefined ? { taskHome: realHome } : {}),
423
+ };
419
424
  })()
420
425
  : {
421
426
  env: new DeferredSandboxPathEnv({
@@ -461,6 +466,8 @@ export function createResolveSpec(ctx) {
461
466
  ? {
462
467
  env: pathAdjudication.env,
463
468
  ...(pathAdjudication.cwd !== undefined ? { cwd: pathAdjudication.cwd } : {}),
469
+ ...(pathAdjudication.taskRoot !== undefined ? { taskRoot: pathAdjudication.taskRoot } : {}),
470
+ ...(pathAdjudication.taskHome !== undefined ? { taskHome: pathAdjudication.taskHome } : {}),
464
471
  ...(scratchpadDir ? { scratchpadDir } : {}),
465
472
  ...gateCommon,
466
473
  }
@@ -111,7 +111,7 @@ export const CONFIG_CATALOG = [
111
111
  r("DOCKER_PIDS_LIMIT", "orchestration", "number", "local-docker fork-bomb 背栓(--pids-limit;非法值回 adapter 默认 512+warn)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ adapter 默认 512" }),
112
112
  r("DOCKER_SANDBOX_ENV", "orchestration", "csv", "转发进容器的本进程 env **名字**表(值只在 worker env,不进 spec/center)", { danger: SEC }),
113
113
  r("DRAIN_GRACE_MS", "limitsHttp", "number", "SIGTERM 优雅排空窗(ms;期间新计费提交 503+Retry-After)", { staticDefault: String(10 * 60 * 1000), configKey: "drainGraceMs", center: cLimits("drainGraceMs") }),
114
- r("DURABLE_APPROVAL", "approval", "boolean", "durable suspend/resume HITL(需 SQL 店;决定 checkpointStore 与 /v1/approvals 面的存在性)", { staticDefault: "false", danger: SEC, configKey: "durableApproval" }),
114
+ r("DURABLE_APPROVAL", "approval", "boolean", "durable suspend/resume HITL(需 SQL 店;决定 checkpointStore 与 /v1/approvals 面的存在性)", { danger: SEC, derivedDefaultNote: "posture 三态(S-178):显式词恒赢,缺席 = 单用户(REQUIRE_PRINCIPAL≠true)且有耐久店 ⇒ on / 多租户或无耐久店 ⇒ off;生效来源见 GET /v1/diagnostics/wiring 的 serverGates.durableApproval.source", configKey: "durableApproval" }),
115
115
  r("E2B_ALLOW_NET", "orchestration", "boolean", "e2b 沙箱出网许可", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 不铸键(adapter 默认允许)" }),
116
116
  r("E2B_API_KEY", "orchestration", "secret", "E2B 车道 API 凭据(REMOTE_EXEC=e2b 必需件)", { danger: [...SEC, ...BILL] }),
117
117
  r("E2B_BG_DEFAULT_TIMEOUT_SEC", "orchestration", "number", "e2b 后台 shell 缺省超时(秒)", { staticDefault: "300" }),
@@ -403,7 +403,7 @@ export const CONFIG_CATALOG = [
403
403
  r("STREAM_APPROVAL_RECONCILE_BATCH", "approval", "number", "对账收敛器每 tick 每段行数帽(整数)", { staticDefault: "200" }),
404
404
  r("STREAM_APPROVAL_REPLAY_MAX", "approval", "number", "开流 preamble 重放帽", { staticDefault: "50" }),
405
405
  r("STREAM_ASK_WINDOW_MARGIN_MS", "approval", "number", "审批窗长三元安全余量(ms)", { staticDefault: "10000", configKey: "streamAskWindowMarginMs" }),
406
- r("STREAM_ASK_WINDOW_MS", "approval", "number", "活卡审批窗长(ms;所有腿生效,与流内协议上不上场无关;0=关窗⇒不发卡恒走无人值守终局;负值与超 Node 定时器上界 2147483647 的值拒启)", { staticDefault: "300000" }),
406
+ r("STREAM_ASK_WINDOW_MS", "approval", "number", "活卡审批窗长(ms;所有腿生效,与流内协议上不上场无关;0=关窗⇒不发卡恒走无人值守终局;负值与超 Node 定时器上界 2147483647 的值拒启)", { derivedDefaultNote: "posture 三态(S-178):显式值恒赢,缺席 = 单用户(REQUIRE_PRINCIPAL≠true)⇒ 86400000(24h;若与显式 STREAM_APPROVAL_ORPHAN_TTL_MS 撞跨旋钮不变量则让位回 300000 并 warn)/ 多租户 ⇒ 300000;生效来源见 GET /v1/diagnostics/wiring 的 serverGates.streamAskWindowMs.source" }),
407
407
  r("SYNC_IMPORT_LEASE_STALE_SEC", "memory", "number", "session-sync staged import 租约陈旧阈(秒)", { staticDefault: "600", configKey: "syncImportLeaseStaleSec" }),
408
408
  r("TASK_MAX_OUTPUT_TOKENS_MAX", "limitsHttp", "number", "调用方自报 limits.maxOutputTokens 的运营方封顶(Math.min 钳显式请求值;坏值=封顶不生效+响亮 warn)", { danger: BILL, derivedDefaultNote: "缺席 ⇒ 不封顶" }),
409
409
  r("TASK_MAX_TURNS_MAX", "limitsHttp", "number", "调用方自报 limits.maxTurns 的运营方封顶(同族三旋钮;只钳显式请求值,不是全队默认限额)", { danger: BILL, derivedDefaultNote: "缺席 ⇒ 不封顶" }),
@@ -373,7 +373,7 @@ function stageApprovalRequire(raw, logger) {
373
373
  `Refusing the domain instead of dropping them: dropping every entry would SILENTLY remove the approval gate.`, nonString.map((e) => `[${e.i}]`), KEPT_WOULD_REMOVE_GATE);
374
374
  }
375
375
  const names = raw.filter((n) => typeof n === "string").map((n) => n.trim()).filter((n) => n.length > 0);
376
- const bad = findUnmatchableToolNames(names);
376
+ const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
377
377
  if (bad.length > 0) {
378
378
  return refuse(formatUnmatchableToolNames("published runtime.approvalRequire", bad), bad.map((b) => b.name), KEPT_WOULD_MATCH_NOTHING);
379
379
  }
@@ -6,6 +6,7 @@
6
6
  * compiling unchanged. All imports below are top-level `import type` (erased — never a load edge,
7
7
  * see test/module-cycle-gate.test.ts header on verbatimModuleSyntax). */
8
8
  import type { ReadFaceSource } from "./read-face-posture.js";
9
+ import type { PostureSource } from "./posture-source.js";
9
10
  import type { SealedKeyPoison } from "./sealed-key.js";
10
11
  import type { A2aServerSpec, CompliancePosture, CrossSessionDialogExpiry, CrossSessionInboundSetting, FileHistoryRetentionPolicy, LockedKey, McpServerSpec, Model, ModelRoles, RetentionPolicy, WriteProtectedEntry } from "@sema-agent/core";
11
12
  import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
@@ -962,6 +963,14 @@ export interface ServiceConfigFlat {
962
963
  * operator resumes it on any replica. Default OFF([ref] A-5:轮询车道已退役,OFF+门意图 ⇒ core UNGATED
963
964
  * 告警,不再有回退门形). `DURABLE_APPROVAL=true`. */
964
965
  durableApproval: boolean;
966
+ /** S-178:上面那一位**是谁定的**(闭集四词,见 `posture-source.ts` —— 与 `readFaceSource` 同一张表)。
967
+ * 7.67.0 起本键进 posture 家族:单机 turnkey ∧ 有耐久店 ⇒ 默认 ON,多租户/内存底座仍 OFF,显式 env
968
+ * 两个方向恒赢。来源上 operator 读面(`GET /v1/diagnostics/wiring` 的 `serverGates`),因为「这台机器
969
+ * 为什么开着 park/resume」在升级后是运维必须看得见的一句话(与 S-167 的补偿项同族)。 */
970
+ durableApprovalSource: PostureSource;
971
+ /** S-178:`STREAM_ASK_WINDOW_MS` 的来源(同一张四词表)。值在 {@link StreamApprovalConfig.windowMs};
972
+ * 单机 turnkey 缺席 ⇒ 24h(posture),多租户缺席 ⇒ 5min(engine-default),显式 env ⇒ env。 */
973
+ streamAskWindowMsSource: PostureSource;
965
974
  /** [ref] seam #2 (preemption) / [ref] ("失败≠没做完" third state): when true (+ durable mode), the
966
975
  * /v1/runs durable path opts every task into core `resourceSuspend` — a budget/turns/walltime limit OR a
967
976
  * scheduler preempt (`POST /v1/assistant/tasks/:id/preempt`) durably SUSPENDS the task (resumable via
@@ -1247,6 +1256,10 @@ export interface ServiceConfigFlat {
1247
1256
  /** Auto-generate a session title at first submit (one cheap-model call per
1248
1257
  * session, write-once; the user rename layer is BFF-side). Default ON; `SESSION_AUTO_TITLE=false` opts out. */
1249
1258
  sessionAutoTitle: boolean;
1259
+ /** S-178:上面那一位的来源(同一张四词表)。本键**不进 posture**(缺省恒 true,两形同值)——
1260
+ * 报来源是为了让运维分辨「这台机器显式关了自动标题」与「它本来就开着」;壳此前注 `false` 的那条
1261
+ * 理由(gateway 不可达时的 warn 噪音)已在 `session-titler.ts` 侧修掉。 */
1262
+ sessionAutoTitleSource: PostureSource;
1250
1263
  /** RFC A2 (clay 拍 A 切片先行): mount the model-facing `SelectEnvironment` tool (spec.tools, 判定②(a)) when
1251
1264
  * the per-task image chain is live (k8s + image catalog). Default ON; `SELECT_ENVIRONMENT_TOOL=false` opts out. */
1252
1265
  selectEnvironmentTool: boolean;
@@ -1682,7 +1695,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
1682
1695
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
1683
1696
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
1684
1697
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1685
- export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "crossSessionInbound" | "crossSessionDialogExpiry">;
1698
+ 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" | "crossSessionInbound" | "crossSessionDialogExpiry">;
1686
1699
  /** 组:memory(记忆面 + TOC 同步腿)。 */
1687
1700
  export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "memoryConsolidationDriver" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1688
1701
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
@@ -1693,7 +1706,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1693
1706
  * config 这一层它只是一句声明。 */
1694
1707
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
1695
1708
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
1696
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readFaceSource" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1709
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readFaceSource" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1697
1710
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1698
1711
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
1699
1712
  /** 组:observability(可观测)。 */
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Model } from "@sema-agent/core";
1
+ import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
2
2
  import type { Autonomy } from "./runtime-governance.js";
3
3
  import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
4
4
  /** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
@@ -237,8 +237,30 @@ export interface UnmatchableToolName {
237
237
  name: string;
238
238
  guidance: string;
239
239
  }
240
- export declare function findCcRuleFormNames(names: readonly string[]): UnmatchableToolName[];
241
- export declare function findUnmatchableToolNames(names: readonly string[]): UnmatchableToolName[];
240
+ /**
241
+ * S-177(core 7.9.0 [ref])—— **这条名单条目喂给谁**,决定「永不匹配」这句话怎么判。两个消费者形态:
242
+ *
243
+ * · `"tool-name-table"` —— 消费者按**整串工具名**比对(精确 ∪ covering):env `APPROVAL_*` 三键与
244
+ * 中心下发 `runtime.approvalRequire`(消费者 `createDurableAskPolicy`),**以及**
245
+ * `settings.permissions.allow`(消费者 core 的 `createAllowDenyPolicy` 的 allow 位)。带括号的内容形
246
+ * 在这里永远匹配不上任何活工具名 —— 与引擎的规则文法认不认得它**无关**,是这条腿的比对方式决定的,
247
+ * 所以内容形照旧响亮拒 + 指路。
248
+ * · `"rule-policy"` —— 消费者是 core 的**规则编译器**(`createPermissionRulePolicy`):
249
+ * `settings.permissions.deny` 与 `.ask`(S-177 / core 7.9.0 起两张表都走编译器)。引擎自此**真的按内容
250
+ * 匹配**它们(`Bash(rm -r:*)` / `Edit(~/.sema/**)`),所以本仓不许再替它拒 —— 判据整条交回 core 的
251
+ * `validatePermissionRules`,按这张表自己的 behavior 问。
252
+ *
253
+ * 🔴 **不对称不是特判,是从「消费者是谁」自然导出的**:`allow` 之所以豁免名字形与文法两族(退役名 /
254
+ * 短名 / 反斜杠 / 超帽),是因为它是**白名单** —— 一个匹配不上的条目让白名单空掉 ⇒ 一切都不在白名单
255
+ * ⇒ fail-CLOSED 且当场可见,那是可接受的另一种失败形;而内容形不同,它是一个 CC 用户为了**少弹窗**
256
+ * 写下的拼法族错,静默代价是整只 Bash 不可用。
257
+ */
258
+ export type RuleEntryConsumer = "tool-name-table" | "rule-policy";
259
+ export declare function findUnmatchableToolNames(names: readonly string[], opts: {
260
+ consumer: RuleEntryConsumer;
261
+ behavior: RuleBehavior;
262
+ pathBases?: PathRuleBases;
263
+ }): UnmatchableToolName[];
242
264
  /** {@link findUnmatchableToolNames} 的成句形——三条腿的文案同源(只有前缀/出口不同)。 */
243
265
  export declare function formatUnmatchableToolNames(source: string, bad: readonly UnmatchableToolName[]): string;
244
266
  /**
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable } from "@sema-agent/core";
5
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
6
6
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
@@ -13,6 +13,7 @@ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
15
  import { readFaceForPosture } from "./read-face-posture.js";
16
+ import {} from "./posture-source.js";
16
17
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
17
18
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
18
19
  function csv(name) {
@@ -321,6 +322,15 @@ export function configKnobTable() {
321
322
  function registerKnob(rec) {
322
323
  CONFIG_KNOBS.set(rec.name, rec);
323
324
  }
325
+ function postureSourceOfKnob(name) {
326
+ const source = CONFIG_KNOBS.get(name)?.source;
327
+ return source === "env" ? "env" : source === "posture" ? "posture" : "engine-default";
328
+ }
329
+ function postureSourceOfNumEnv(raw, singleUserTurnkey) {
330
+ if (raw !== undefined && raw !== "")
331
+ return "env";
332
+ return singleUserTurnkey ? "posture" : "engine-default";
333
+ }
324
334
  const BOOL_TRUE_WORDS = new Set(["true", "1", "yes", "on"]);
325
335
  const BOOL_FALSE_WORDS = new Set(["false", "0", "no", "off"]);
326
336
  function parseBoolWord(name, raw) {
@@ -918,49 +928,48 @@ function ccRuleFormGuidance(name) {
918
928
  `to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
919
929
  `to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
920
930
  }
921
- export function findCcRuleFormNames(names) {
922
- const out = [];
923
- for (const name of names) {
924
- const guidance = ccRuleFormGuidance(name);
925
- if (guidance !== undefined)
926
- out.push({ name, guidance });
927
- }
928
- return out;
929
- }
930
- function uncompilableRuleGuidance(name) {
931
- const issues = validatePermissionRules([{ rule: name, behavior: "deny" }]);
931
+ function uncompilableRuleGuidance(name, behavior, pathBases) {
932
+ const issues = validatePermissionRules([{ rule: name, behavior }], pathBases !== undefined ? { pathBases } : undefined);
932
933
  const issue = issues[0];
933
934
  if (issue === undefined)
934
935
  return undefined;
936
+ const ccGuidance = issue.code === "unsupported.bash_prefix" || issue.code === "unsupported.file_glob" ? ccRuleFormGuidance(name) : undefined;
937
+ if (ccGuidance !== undefined)
938
+ return ccGuidance;
935
939
  return (`${issue.code} — core's rule grammar cannot compile this name (${issue.message}), so a table keyed by it matches nothing. ` +
936
940
  `Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
937
941
  `mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
938
942
  }
939
- export function findUnmatchableToolNames(names) {
943
+ export function findUnmatchableToolNames(names, opts) {
940
944
  const out = [];
941
945
  for (const name of names) {
942
- const ccGuidance = ccRuleFormGuidance(name);
943
- if (ccGuidance !== undefined) {
944
- out.push({ name, guidance: ccGuidance });
945
- continue;
946
+ if (opts.consumer === "tool-name-table") {
947
+ const ccGuidance = ccRuleFormGuidance(name);
948
+ if (ccGuidance !== undefined) {
949
+ out.push({ name, guidance: ccGuidance });
950
+ continue;
951
+ }
946
952
  }
953
+ if (opts.behavior === "allow")
954
+ continue;
947
955
  const retired = RETIRED_TOOL_NAMES.get(name);
948
956
  if (retired !== undefined) {
949
957
  out.push({ name, guidance: retired });
950
958
  continue;
951
959
  }
952
- const uncompilable = uncompilableRuleGuidance(name);
960
+ const uncompilable = uncompilableRuleGuidance(name, opts.behavior, opts.pathBases);
961
+ const subject = opts.consumer === "tool-name-table" ? name : (() => { const parsed = parseRuleText(name, opts.behavior); return "rule" in parsed ? parsed.rule.tool : name; })();
953
962
  if (uncompilable !== undefined) {
954
963
  out.push({ name, guidance: uncompilable });
955
964
  continue;
956
965
  }
957
- const ns = protocolOf(name);
966
+ const ns = protocolOf(subject);
958
967
  if (ns === undefined) {
959
- if (name.includes("__"))
968
+ if (subject.includes("__"))
960
969
  out.push({ name, guidance: `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentToolName(name)}`).join(" or ")}` });
961
970
  continue;
962
971
  }
963
- if (ns.parse(name) === undefined) {
972
+ if (ns.parse(subject) === undefined) {
964
973
  out.push({ name, guidance: `incomplete ${ns.id} name — the live form is ${ns.prefix}<server>__<tool>, or ${ns.prefix}<server>__* to cover every tool on that server` });
965
974
  }
966
975
  }
@@ -974,7 +983,7 @@ const MAX_TIMER_DELAY_MS = 2_147_483_647;
974
983
  function streamApprovalConfig(gates) {
975
984
  const cfg = {
976
985
  enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
977
- windowMs: numEnv("STREAM_ASK_WINDOW_MS", "300000"),
986
+ windowMs: numEnv("STREAM_ASK_WINDOW_MS", gates.windowMsSource === "posture" ? "86400000" : "300000"),
978
987
  replayMax: numEnv("STREAM_APPROVAL_REPLAY_MAX", "50"),
979
988
  admitMaxPerTask: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_TASK", "32"),
980
989
  admitMaxPerOwner: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "256"),
@@ -990,6 +999,10 @@ function streamApprovalConfig(gates) {
990
999
  if (cfg.windowMs > MAX_TIMER_DELAY_MS) {
991
1000
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} exceeds the Node timer ceiling ${MAX_TIMER_DELAY_MS} (2^31-1 ms ≈ 24.8 days) — setTimeout SILENTLY collapses a larger delay to 1ms, so the window you meant to lengthen would expire immediately and every gated ask would take the unattended route on the next tick. Pick a window at or below the ceiling (and remember STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS must stay strictly below STREAM_APPROVAL_ORPHAN_TTL_MS).`);
992
1001
  }
1002
+ if (gates.windowMsSource === "posture" && cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
1003
+ gates.onPostureYield?.({ windowMs: cfg.windowMs, adhocGraceMs: cfg.adhocGraceMs, orphanTtlMs: cfg.orphanTtlMs });
1004
+ cfg.windowMs = 300_000;
1005
+ }
993
1006
  if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
994
1007
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
995
1008
  }
@@ -1014,14 +1027,25 @@ function unattendedApprovalPolicyEnv() {
1014
1027
  `Refusing to boot rather than treating a misspelled value as "unset": that runs the deployment on a policy the operator did not choose, with no trace anywhere`);
1015
1028
  }
1016
1029
  function parseApprovalDomain(ctx) {
1017
- const { postureOn } = ctx;
1030
+ const { postureOn, singleUserTurnkey, infraReady } = ctx;
1018
1031
  const unattendedPolicy = unattendedApprovalPolicyEnv();
1032
+ const streamAskWindowMsSource = postureSourceOfNumEnv(process.env.STREAM_ASK_WINDOW_MS, singleUserTurnkey);
1019
1033
  const toolApprovalEnabled = postureOn("TOOL_APPROVAL_ENABLED");
1034
+ let streamAskWindowMsEffectiveSource = streamAskWindowMsSource;
1035
+ const streamApproval = streamApprovalConfig({
1036
+ unattendedPolicy,
1037
+ toolApprovalEnabled,
1038
+ windowMsSource: streamAskWindowMsSource,
1039
+ onPostureYield: (facts) => {
1040
+ streamAskWindowMsEffectiveSource = "engine-default";
1041
+ process.stderr.write(`[config] stream_ask_window_posture_yielded: the single-user posture default STREAM_ASK_WINDOW_MS=${facts.windowMs} would violate the explicit STREAM_APPROVAL_ORPHAN_TTL_MS=${facts.orphanTtlMs} (window + ADHOC_GRACE ${facts.adhocGraceMs} must be strictly less) — falling back to 300000; set STREAM_ASK_WINDOW_MS explicitly to choose\n`);
1042
+ },
1043
+ });
1020
1044
  const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
1021
1045
  const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS, announceKeySetRejection("APPROVAL_HMAC_KEYS"));
1022
- const durableApproval = boolEnv("DURABLE_APPROVAL", false);
1046
+ const durableApproval = postureOn("DURABLE_APPROVAL", infraReady);
1023
1047
  const rejectUnmatchableToolNames = (envName, names) => {
1024
- const bad = findUnmatchableToolNames(names);
1048
+ const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
1025
1049
  if (bad.length > 0)
1026
1050
  throw new Error(formatUnmatchableToolNames(envName, bad));
1027
1051
  return names;
@@ -1092,7 +1116,9 @@ function parseApprovalDomain(ctx) {
1092
1116
  permissionRulesEnabled: boolEnv("PERMISSION_RULES_ENABLED", true),
1093
1117
  permissionRulesEnabledExplicit: parseBoolWord("PERMISSION_RULES_ENABLED", process.env["PERMISSION_RULES_ENABLED"]) !== undefined,
1094
1118
  streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
1095
- streamApproval: streamApprovalConfig({ unattendedPolicy, toolApprovalEnabled }),
1119
+ streamApproval,
1120
+ streamAskWindowMsSource: streamAskWindowMsEffectiveSource,
1121
+ durableApprovalSource: postureSourceOfKnob("DURABLE_APPROVAL"),
1096
1122
  unattendedApprovalPolicy: unattendedPolicy,
1097
1123
  approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
1098
1124
  };
@@ -1659,6 +1685,7 @@ function parseOrchestrationDomain(ctx) {
1659
1685
  ? { sandboxPkgSource: pkgSourceEnv() }
1660
1686
  : {}),
1661
1687
  sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
1688
+ sessionAutoTitleSource: postureSourceOfKnob("SESSION_AUTO_TITLE"),
1662
1689
  selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
1663
1690
  envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
1664
1691
  toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
@@ -1885,7 +1912,7 @@ const MODEL_PLANE_GROUP_KEYS = [
1885
1912
  ];
1886
1913
  const APPROVAL_GROUP_KEYS = [
1887
1914
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1888
- "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1915
+ "approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1889
1916
  "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
1890
1917
  "sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "crossSessionInbound", "crossSessionDialogExpiry",
1891
1918
  ];
@@ -1907,7 +1934,7 @@ const ORCHESTRATION_GROUP_KEYS = [
1907
1934
  "workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
1908
1935
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
1909
1936
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
1910
- "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1937
+ "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1911
1938
  "imageBakes", "readFace", "readFaceSource", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1912
1939
  ];
1913
1940
  const LIMITS_HTTP_GROUP_KEYS = [
@@ -1968,12 +1995,13 @@ export function loadConfig() {
1968
1995
  const postureOn = (name, infraReady = true) => {
1969
1996
  const explicit = parseBoolWord(name, process.env[name]);
1970
1997
  const value = explicit ?? (singleUserTurnkey && infraReady);
1971
- registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
1998
+ registerKnob({ name, polarity: "posture", value, source: explicit !== undefined ? "env" : singleUserTurnkey ? "posture" : "default" });
1972
1999
  return value;
1973
2000
  };
1974
2001
  const store = parseStoreDomain({ requirePrincipal });
1975
2002
  const modelPlane = parseModelDomain();
1976
- const approval = parseApprovalDomain({ postureOn });
2003
+ const durableStoreReady = store.dbBackend !== "memory";
2004
+ const approval = parseApprovalDomain({ postureOn, singleUserTurnkey, infraReady: durableStoreReady });
1977
2005
  const memory = parseMemoryDomain({ requirePrincipal });
1978
2006
  const auth = parseAuthDomain({ requirePrincipal });
1979
2007
  const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
@@ -55,6 +55,14 @@ export interface PathAdjudication {
55
55
  readonly env: ExecutionEnv;
56
56
  /** 相对路径基准(core 的 `rootPath`)。缺席 ⇒ 相对形在真身那层判不了,补词法臂(见下)。 */
57
57
  readonly cwd?: string;
58
+ /** 🔴 S-177:**真任务根**(权限规则模式的基)。与上面那个 `cwd` 分开:后者在 host 腿的无 session-cwd
59
+ * 形上是一个永不创建的哨兵目录 —— 对写门 fail-safe,对规则 fail-open。理由逐字见
60
+ * `task-settings.ts` 的 `FsWriteGateWiring.taskRoot`。缺席 ⇒ 需要基的规则拼法被 core 整表拒(fail-loud)。 */
61
+ readonly taskRoot?: string;
62
+ /** 🔴 S-177 / codex r2:**执行环境的 home**(`~/…` 形规则的基)。只在引擎与执行同机时在场 ——
63
+ * 远端车道上 server 的 home 是另一个用户的目录,回落它 = 规则静默失效。逐字见
64
+ * `task-settings.ts` 的 `FsWriteGateWiring.taskHome`。 */
65
+ readonly taskHome?: string;
58
66
  }
59
67
  /** [ref]([ref] 案二):durable 部署上的 AskUserQuestion 门的活体面探针(QuestionCoordinator 的切面)。 */
60
68
  export interface LiveQuestionFace {
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -122,6 +122,10 @@ export interface PreparedTaskSubmission {
122
122
  cascade?: boolean;
123
123
  jobId?: string;
124
124
  body: TaskRequestBody;
125
+ /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天只有请求腿 MCP 注入被丢弃一族)。
126
+ * 装配跑在这条腿的通告口开张之前,所以铸点交不出去 —— 由腿在 `registerEngineNoticeLeg` 那一拍
127
+ * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
128
+ engineNotices?: readonly EngineNotice[];
125
129
  }
126
130
  /** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
127
131
  export interface DriveResumeArgs {
@@ -1,3 +1,15 @@
1
+ /**
2
+ * [ref] 件二+件三 —— **装配自证**域:启动期静态自检(拒启判据)与 operator 诊断读面
3
+ * (`GET /v1/diagnostics/wiring`)。
4
+ *
5
+ * 🔴 两件为什么同住一个模块:它们回答的是同一个问题(「这台 worker 到底接了什么线」),只是一个在 boot
6
+ * 用它拒绝启动、一个在运行期把它读给 operator。分成两处 = 拒启用的判据与诊断报的读数会各自演化,而
7
+ * 「拒启说没矛盾、诊断页显示矛盾」正是本设计要消灭的那类装配谎言。故 boot 半场是本文件导出的**纯函数**
8
+ * (可单测、无 I/O),路由半场只是它的读口。
9
+ *
10
+ * 分层:本模块不值 import `server.ts`(那条边会闭合运行时装载环),对它只有 `import type`。
11
+ */
12
+ import { type PostureKnobReading, type PostureSource } from "../../posture-source.js";
1
13
  import type { IncomingMessage, ServerResponse } from "node:http";
2
14
  import type { WiringManifest } from "@sema-agent/core";
3
15
  import { type StreamApprovalGate, type StreamApprovalGateInput } from "../../tool-approval.js";
@@ -7,10 +19,17 @@ import type { RouteCtx } from "../route-ctx.js";
7
19
  export type StreamApprovalGateReading = "active" | Extract<StreamApprovalGate, {
8
20
  active: false;
9
21
  }>["reason"];
10
- /** server 自己的装配谓词读数(core 的静态半场之外的那一半:本仓在这台 worker 上做出的判断)。 */
22
+ /** server 自己的装配谓词读数(core 的静态半场之外的那一半:本仓在这台 worker 上做出的判断)。
23
+ *
24
+ * 🔴 S-178:三个 **posture 家族**旋钮改报 `{value, source, note}`(与 S-167 的 `readFace` 同一张四词表、
25
+ * 同一只 note 函数)。裸布尔答不出裁定的补偿项要求的那句话 —— 单机形升级后 `durableApproval` 缺席即
26
+ * ON、审批窗缺席即 24h,运维必须在一个读面上看得见「这是 posture 派生的」以及「怎么钉回」。
27
+ * `streamApproval` 那一格是**门读数**(另一根轴:这条腿现在通不通),不是旋钮,故形不变。 */
11
28
  export interface ServerWiringGates {
12
29
  streamApproval: StreamApprovalGateReading;
13
- durableApproval: boolean;
30
+ durableApproval: PostureKnobReading<boolean>;
31
+ streamAskWindowMs: PostureKnobReading<number>;
32
+ sessionAutoTitle: PostureKnobReading<boolean>;
14
33
  checkpointStore: boolean;
15
34
  }
16
35
  /** `buildServerWiringGates` 的入参 = 三个谓词各自的**真输入**(不接受已算好的布尔:那样这里读到的就
@@ -23,6 +42,12 @@ export interface ServerWiringGateInput {
23
42
  /** 在场性即事实;本面不取实例,故 `unknown` 足够(取窄型会把整棵 store 类型树拖进诊断域)。 */
24
43
  checkpointStore: unknown;
25
44
  durableApproval: boolean;
45
+ /** S-178:三个 posture 旋钮的**来源**,取自 config 上与值同处写的那一格(不在这里重判)。 */
46
+ durableApprovalSource: PostureSource;
47
+ streamAskWindowMs: number;
48
+ streamAskWindowMsSource: PostureSource;
49
+ sessionAutoTitle: boolean;
50
+ sessionAutoTitleSource: PostureSource;
26
51
  }
27
52
  /** 纯数据产物(`build*`):三个门的当前读数。`streamApproval` 走**单一谓词**
28
53
  * `resolveStreamApprovalGate` —— 与 `/v1/capabilities` 的 `streamApproval` 位、与回决口的 501 门同一个