@sema-agent/server 7.64.0 → 7.66.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 (40) hide show
  1. package/USAGE.md +18 -5
  2. package/dist/boot/governance-seams.js +1 -1
  3. package/dist/boot/resolve-spec.d.ts +0 -5
  4. package/dist/boot/resolve-spec.js +3 -4
  5. package/dist/boot/stores.js +3 -3
  6. package/dist/capabilities/hands-lane.d.ts +55 -13
  7. package/dist/capabilities/hands-lane.js +22 -11
  8. package/dist/config-catalog.js +1 -1
  9. package/dist/config-center/read-face.js +10 -8
  10. package/dist/config-center/skills-mcp.d.ts +3 -3
  11. package/dist/config-center/skills-mcp.js +17 -4
  12. package/dist/config-center/types.d.ts +11 -0
  13. package/dist/config-provider.js +1 -0
  14. package/dist/config-types.d.ts +24 -5
  15. package/dist/config.js +7 -4
  16. package/dist/http/routes/approvals-assistant.js +18 -0
  17. package/dist/http/routes/diagnostics.d.ts +0 -11
  18. package/dist/http/routes/diagnostics.js +2 -0
  19. package/dist/http/routes/tasks.js +6 -1
  20. package/dist/http/server.js +1 -1
  21. package/dist/main.js +2 -4
  22. package/dist/mcp-tool-faces.d.ts +64 -0
  23. package/dist/mcp-tool-faces.js +93 -0
  24. package/dist/observability/fail-open.d.ts +4 -0
  25. package/dist/observability/fail-open.js +4 -0
  26. package/dist/plugins/checkpoint-store-sql.d.ts +68 -7
  27. package/dist/plugins/checkpoint-store-sql.js +90 -25
  28. package/dist/plugins/local-checkpoint-store.d.ts +19 -1
  29. package/dist/plugins/local-checkpoint-store.js +6 -1
  30. package/dist/plugins/pg-pool.js +5 -0
  31. package/dist/plugins/tidb-pool.js +11 -0
  32. package/dist/read-face-posture.d.ts +52 -0
  33. package/dist/read-face-posture.js +20 -0
  34. package/dist/task-mcp.d.ts +18 -4
  35. package/dist/task-mcp.js +23 -4
  36. package/dist/tool-approval.js +2 -2
  37. package/dist/trace/core-keyset-guard.d.ts +2 -2
  38. package/dist/trace/project.d.ts +21 -0
  39. package/dist/trace/project.js +21 -1
  40. package/package.json +4 -4
package/USAGE.md CHANGED
@@ -594,10 +594,11 @@ WRITE_PROTECTED_TABLE_REPLACE='[{"name":".envrc","kind":"basename"}]' # 整表
594
594
  - **leader 车道同席**:与其余部署姿态席同一个取值点(`buildDeploymentPostureSeats`),主 runner / subRunner /
595
595
  run-local / leader 五只 Runner 同表。
596
596
 
597
- **可选 — READ 面姿态(readFace,7.18.0 起;缺席=引擎当家)**
597
+ **可选 — READ 面姿态(readFace,7.18.0 起;缺席:单机 ⇒ `open`,多租户 ⇒ 引擎当家 —— 7.65.0 起,见下)**
598
598
 
599
599
  ```bash
600
- READ_FACE=open # 词表 open|roots;坏词启动期响亮拒。不设=不传给引擎(引擎默认 roots)
600
+ READ_FACE=open # 词表 open|roots;坏词启动期响亮拒
601
+ # 不设:单用户 turnkey(REQUIRE_PRINCIPAL 未设)⇒ open;多租户 ⇒ 不传给引擎(引擎默认 roots)
601
602
  READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitive":true}]'
602
603
  ```
603
604
  - **`READ_FACE`**:READ 工具面(读文件/遍历/搜索)的部署级姿态。引擎默认 **`roots`** = 读被夹在任务的
@@ -609,8 +610,19 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
609
610
  退出。这些条目**叠在** core 内建 deny 集之上(内建先、追加后,重复折叠),不是替换。
610
611
  - **坏形拒启,不静默折默认**:坏 JSON / 非数组 / 缺 `pattern` / 编不过 core 的 `compileReadDeny`
611
612
  一律启动期点名拒——一个手滑的 deny 集比没有 deny 集更糟(以为挡了其实没挡)。
612
- - **缺席不铸**:两个键不设就**不传给引擎**——引擎抬默认/扩内建集那天,存量部署自动跟上(与重试上限
613
- 同款「引擎当家」纪律)。
613
+ - **缺席的语义按 posture 分两形(7.65.0 起,S-167)**:
614
+ · **单用户 turnkey**(`REQUIRE_PRINCIPAL` 未设 —— 一台机器一个可信主人,CC 的信任模型):`READ_FACE`
615
+ 缺席 ⇒ 铸 **`open`**,工作区外的读默认放行。**这是 7.65.0 的行为变更**:同一台机器升级前后
616
+ 「读工作区外的文件」从被拒变成放行。**要钉回收敛姿态:显式 `READ_FACE=roots`。**
617
+ · **多租户**(`REQUIRE_PRINCIPAL=true`):缺席仍**不铸**、不传给引擎(引擎默认 `roots`)——**一个字节不变**。
618
+ · `READ_DENY_PATTERNS` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE` 三键的「缺席不铸」
619
+ **不变**;**内建读拒表照旧生效** —— `open` 放开的是「工作区外」这道围栏,凭证 / shell 历史 / 浏览器 /
620
+ 钱包那几档 deny 在 `open` 面上照样拦(两者在引擎里是并列机制)。
621
+ · 「这一档到底是谁定的」由 operator 面 `GET /v1/diagnostics/wiring` 的 `readFace` 段答:
622
+ `{face, source, note}`,`source` ∈ `env`(本机 env 显式)/ `center`(组织下发)/ `posture`(单机派生)/
623
+ `engine-default`(没钉,引擎当家),`note` 是指路句。
624
+ - **显式值恒赢**:优先序 `env` > `center` > `posture` > 引擎默认;引擎抬默认/扩内建集那天,没钉过的
625
+ 多租户部署自动跟上(与重试上限同款「引擎当家」纪律)。
614
626
  - **leader 车道同席(7.54 起)**:`LEADER_ENABLED` 的 leader 编排(planner / worker / repair / conflict 四类
615
627
  任务)与主车道吃**同一份**四键(连同 `MEMORY_DELEGATION_EVIDENCE` / `MEMORY_PROVENANCE` / `MEMORY_CAPTURE_POLICY`
616
628
  与委派入口 caps;取值点唯一 = `buildDeploymentPostureSeats`,leader 不另读 env)。此前(≤7.53)那条腿一席都不带:
@@ -625,7 +637,8 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
625
637
  保留 env 前值 + `sema_registry_read_face_invalid` warn(半应用一份 READ 姿态比不应用更危险);
626
638
  ③ **restart-to-apply**——四键在 boot 期被引擎装配捕获,下发改动经 `/health` 的 `restart.reasons`
627
639
  带 `read-face` 通告,由编排器重启兑现。当前生效档可从 `GET /v1/capabilities` 的 `readFace` 位读
628
- (`"open"`/`"roots"`/`null`=本部署未钉,引擎默认当家;deny 表内容属策略内容,**不上**能力面)。
640
+ (`"open"`/`"roots"`/`null`=本部署未钉,引擎默认当家;deny 表内容属策略内容,**不上**能力面);
641
+ **来源**(env / center / posture / engine-default)在 operator 面 `GET /v1/diagnostics/wiring` 的 `readFace.source`。
629
642
  ⚠️ 本地 `config.d/` 车道今天**接不上**这一域(registry-core 的可移植域表里没有它),只有
630
643
  `CONFIG_PROVIDER=remote` 的部署走得通。
631
644
 
@@ -36,7 +36,7 @@ export function createGovernanceSeams(ctx) {
36
36
  assertGovernanceAssemblable({
37
37
  lockedKeys,
38
38
  complianceDenies,
39
- deploymentMcpConfigured: (config.mcpServers?.length ?? 0) > 0,
39
+ deploymentMcpConfigured: (config.mcpServers?.servers.length ?? 0) > 0,
40
40
  webFetchMounted: config.requirePrincipal !== true,
41
41
  });
42
42
  const lockedConfig = config.lockedConfigKeys !== undefined && config.lockedConfigKeys.length > 0 ? { keys: config.lockedConfigKeys } : undefined;
@@ -3,7 +3,6 @@ import type { PromptsDomainFaces } from "../prompts-domain-validate.js";
3
3
  import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
4
4
  import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
5
5
  import { selectScenario } from "../capabilities/scenarios.js";
6
- import { type HandsLaneRegistry } from "../capabilities/hands-lane.js";
7
6
  import type { ServiceConfig } from "../config.js";
8
7
  import { type LiveQuestionFace } from "../deployment-governance.js";
9
8
  import { FleetEventBus } from "../fleet/fleet-bus.js";
@@ -23,10 +22,6 @@ type PrincipalCaps = ReturnType<typeof createPrincipalEntitlementsClient>;
23
22
  /** `resolveSpec` 原先从 `main()` 闭包里拿到的全部 boot 局部量。 */
24
23
  export interface ResolveSpecCtx {
25
24
  config: ServiceConfig;
26
- /** [ref]:hands lane 登记簿。resolveSpec 在产出 TaskSpec 时按场景判别位登记本请求的表态,HTTP 执行点
27
- * 凭同一只登记簿选 Runner —— **必须与 main.ts 传给 `runnerFor` 的是同一实例**(异实例 = 每个请求都
28
- * 落回 full,收窄静默失效)。 */
29
- handsLanes: HandsLaneRegistry;
30
25
  logger: Logger;
31
26
  metrics: Metrics;
32
27
  localRoot: string;
@@ -6,7 +6,7 @@ import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/c
6
6
  import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
7
7
  import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
8
8
  import { gateScenarioRequest, mergeUserSkills, selectScenario } from "../capabilities/scenarios.js";
9
- import { toolPolicyForHands } from "../capabilities/hands-lane.js";
9
+ import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.js";
10
10
  import { shouldDiscloseNoPersistentMemory, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
11
11
  import { applyLongtailDefer } from "../capabilities/tool-defer.js";
12
12
  import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
@@ -36,7 +36,7 @@ import { redactSecrets } from "../trace/redact.js";
36
36
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
37
37
  import { effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
38
38
  export function createResolveSpec(ctx) {
39
- const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, handsLanes, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
39
+ const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
40
40
  assertGuardPatternsUsable(config);
41
41
  const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, { durableEnabled, singleUserAutoAcceptBaseline });
42
42
  if (onlySensitiveBaseline)
@@ -62,8 +62,7 @@ export function createResolveSpec(ctx) {
62
62
  scratchpadDir: folded.scratchpadDir,
63
63
  hostSemanticsLane: folded.hostSemanticsLane,
64
64
  });
65
- handsLanes.stamp(final, gated.hands);
66
- return final;
65
+ return stampHandsLane(final, gated.hands);
67
66
  };
68
67
  const gateScenarioAndAppend = async (body, auth, opts) => {
69
68
  const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario, Object.keys(scenarios), logger);
@@ -24,7 +24,7 @@ import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLa
24
24
  import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
25
25
  import { assertPermissionRuleApprovalSchema, assertPermissionRuleTicketSchema } from "../plugins/permission-rule-store-sql.js";
26
26
  import { assertApprovalAskRuleMaterialSchema } from "../plugins/approval-ask-store-sql.js";
27
- import { assertCheckpointBidiColumnSchema } from "../plugins/checkpoint-store-sql.js";
27
+ import { assertCheckpointColumnSchema } from "../plugins/checkpoint-store-sql.js";
28
28
  import { assertFileHistorySchema } from "../plugins/file-history-store-sql.js";
29
29
  import { resolveStreamApprovalGate } from "../tool-approval.js";
30
30
  import { buildMemoryWriteBoundaryAudit, effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
@@ -56,9 +56,9 @@ export async function openStores(ctx) {
56
56
  else if (mysqlPool)
57
57
  await assertToolResultProvenanceSchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
58
58
  if (pgPool)
59
- await assertCheckpointBidiColumnSchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
59
+ await assertCheckpointColumnSchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
60
60
  else if (mysqlPool)
61
- await assertCheckpointBidiColumnSchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
61
+ await assertCheckpointColumnSchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
62
62
  if (config.permissionRulesEnabled) {
63
63
  if (pgPool)
64
64
  await assertPermissionRuleApprovalSchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
@@ -12,7 +12,8 @@
12
12
  * 不是静默继承 full(词表闭集纪律)。
13
13
  * ② {@link HANDS_EXCLUSIVE_TOOL_NAMES} + {@link handsDenyPolicy} —— 纵深:hands=none 的任务即便被误接到
14
14
  * 有手 Runner 上,手独占名单逐一 deny。**不是**主保证(主保证是根本不挂 factory ⇒ 工具面不可见)。
15
- * ③ {@link createHandsLaneRegistry} —— resolveSpec 产出 spec 与执行点选 Runner 之间的 lane 传递。
15
+ * ③ {@link stampHandsLane} / {@link handsLaneOf} —— 车道作为 TaskSpec 上的**显式字段**(S-149):
16
+ * resolveSpec 出口铸一次,执行点读一次,**缺席即拒**(不是回落最宽车道)。
16
17
  */
17
18
  import { type Runner, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
18
19
  /** 场景对手带工具面的表态(闭集)。`full` = core 手带 band 照常 mount;`none` = 不挂 executionEnvFactory 的 Runner。 */
@@ -36,7 +37,9 @@ export declare function pickHandsRunner(hands: ScenarioHands, pair: HandsRunnerP
36
37
  * core 手带 band 的工具名**全集** —— 「挂 executionEnvFactory 才出现、不挂就消失」的那一批。
37
38
  *
38
39
  * 两段来源,都不是拍脑袋:
39
- * · `HAND_TOOL_EFFECTS`(core 导出面,`tools/fs`)—— band 的 effect 表,11 名。
40
+ * · `handBandEffects()`(core 7.8.0 B1 起的导出面;此前是手抄表常量 `HAND_TOOL_EFFECTS`)—— band 的
41
+ * effect 表,由**静态工具目录**按 `mountedBy.includes("hands")` 派生。名册单源之后这张表不再是一张
42
+ * 手抄名表,加/退工具由目录一处说了算。
40
43
  * · 下方锚定补集 —— `createHandsToolkit` / 手带块还会铸但**不在** effect 表里的 5 名(实测差集,见
41
44
  * test/scenario-hands-lane.test.ts 的对表格:它用真 Runner 跑「有手 roster ∖ 无手 roster」,与本常量逐名对账,
42
45
  * core 加/改 band 成员时红,防漂移)。
@@ -82,19 +85,58 @@ export declare function tightenWithHandsDeny(base: ToolPolicy | undefined): Tool
82
85
  /** 装配点的取用形:`full` 原样放行基线,`none` 叠 deny。**穷举 switch**,无缺省臂。 */
83
86
  export declare function toolPolicyForHands(hands: ScenarioHands, base: ToolPolicy | undefined): ToolPolicy | undefined;
84
87
  /**
85
- * 请求级 lane 登记簿 —— resolveSpec 在产出 TaskSpec 时登记本请求的场景表态,执行点凭 spec 取回选 Runner。
88
+ * 执行车道在 TaskSpec 上的**显式字段键**(S-149;修前是一张以 spec **对象身份**为键的 WeakMap 旁表)。
86
89
  *
87
- * 为什么不把 lane 写进 TaskSpec:那是 core 的类型,server 无自有位;而 `resolveSpec` 的返回形是
88
- * ServiceDeps 的公开签名,80+ 测试文件在桩它 —— 改返回形的射程远大于本件。登记簿以 spec **对象身份**
89
- * 为键(每请求一只新对象,并发无串台;WeakMap 弱引用,零清理欠账),由同一只 boot 装配同时交给
90
- * resolveSpec 与 HTTP 执行点,配对是结构性的。
90
+ * 为什么是 symbol 而不是字符串键:`TaskSpec` 是**上游 core 的类型面**,一个字符串键会进
91
+ * `Object.keys` / `JSON.stringify` / core 自己的键审计,等于 server 在别人的契约上私自加一个键;
92
+ * symbol 键随 spread(`{...spec}`)与 rest 解构(`const {objective, ...taskConfig} = spec`)结构性
93
+ * 地跟着走,却对上游的键面**不可见**(结构化克隆与 JSON 同样跳过 symbol 键)。于是「车道是 spec 上
94
+ * 的一个字段、复制 spec 不丢车道」与「不污染上游契约」两件事同时成立。
91
95
  *
92
- * 未登记 ⇒ `full`:唯一可达形是「这只 spec 不是本 resolver 产的」(测试桩)。生产链上 stamp 与 laneOf
93
- * 同一实例,不存在半接;真误配时纵深 deny 仍在(见 {@link tightenWithHandsDeny})。
96
+ * 修前的旁表要求「从 resolveSpec 出来的那一只对象必须一路原样活到执行点」——一条**隐形**的对象身份
97
+ * 契约:任何一次 `{...spec}` 都会静默把车道降级成最宽的 `full`(实测:登记 `none` 的 spec 复制后
98
+ * `laneOf` 答 `full`)。字段化把那条隐形要求整条删掉。
94
99
  */
95
- export interface HandsLaneRegistry {
96
- stamp(spec: TaskSpec, hands: ScenarioHands): void;
97
- laneOf(spec: TaskSpec): ScenarioHands;
100
+ export declare const HANDS_LANE_FIELD: unique symbol;
101
+ /** 已铸车道的 TaskSpec。铸点唯一(`boot/resolve-spec.ts` 的 resolveSpec 出口),读点唯一({@link handsLaneOf})。 */
102
+ export type LanedTaskSpec = TaskSpec & {
103
+ readonly [HANDS_LANE_FIELD]: ScenarioHands;
104
+ };
105
+ /**
106
+ * 铸车道(**单一属主**:只允许 resolveSpec 的出口调用)。
107
+ *
108
+ * 字段是 `enumerable`(所以复制得走)、`writable:false` + `configurable:false`(所以同一只 spec 被
109
+ * 第二次铸车道会当场 TypeError —— 两个写者是漂移的成因,响亮好过后到者悄悄赢)。
110
+ */
111
+ export declare function stampHandsLane<T extends TaskSpec>(spec: T, hands: ScenarioHands): T & LanedTaskSpec;
112
+ /**
113
+ * 车道字段缺席的拒因 —— 具名类,便于调用点/测试按类型判,而不是嗅文案。
114
+ *
115
+ * 🔴 **刻意不是 `HttpError`、刻意不铸新的 wire 错误码**:它落到 `http/server.ts` 的兜底 catch,
116
+ * wire 上是 500 `internal.error`(内部文本不回显,只进 `request_error` 日志行)。理由两条:
117
+ * ① 对**消费端**它与任何一次服务端内部故障无差别可操作性(重试一次,仍复现即报运维)—— 附录 A 多一个
118
+ * 码却给不出不同的动作,是没有价值的契约面;
119
+ * ② 本仓的中继码登记表(`test/error-code-catalog-live.test.ts` 的 `RELAYED`,上限 24 条)明写「超了说明
120
+ * 『码不写字面量』正在变成默认写法,该反过来改代码」—— 一个只能靠登记才看得见的新码正是它拦的东西。
121
+ * 需要分辨的是**运维**,而运维读的是日志行(本 message 逐字在 `request_error` 里)。
122
+ */
123
+ export declare class HandsLaneUnstampedError extends Error {
124
+ constructor();
98
125
  }
99
- export declare function createHandsLaneRegistry(): HandsLaneRegistry;
126
+ /**
127
+ * 读车道 —— **缺席即拒**([ref] P 类:车道选择是保护型量,fail-closed;`observability/fail-open.ts`
128
+ * 头注把「车道选择」逐字列在「正解是 fail-closed,不是留痕后继续放行」那一族里)。
129
+ *
130
+ * 修前此处是 `?? "full"`:一只没经过本仓 resolver 的 spec 会**静默拿到最宽的车道**(全量可写手带)。
131
+ * 「唯一可达形是测试桩」这个论证不是防线 —— 它是一句关于今天调用图的断言,而防线要防的正是明天多出
132
+ * 来的第二个执行点。缺席现在整只拒(500 + 日志留原句),而不是把一个未经裁定的任务放进有手车道。
133
+ */
134
+ export declare function handsLaneOf(spec: TaskSpec): ScenarioHands;
135
+ /**
136
+ * 装配点的选路席:`{full, handsless}` 两只 Runner + 车道字段 ⇒ 本任务跑哪一只。
137
+ *
138
+ * 具名工厂而不是在 main.ts 手搓闭包:选路规则(读字段 → 穷举 switch)于是只有**一份实现**,
139
+ * 测试与生产装配拿的是同一只(手搓等价闭包 = 第二真源,正是本件要删的那一类)。
140
+ */
141
+ export declare function createHandsRunnerSelector(pair: HandsRunnerPair): (spec: TaskSpec) => Runner;
100
142
  //# sourceMappingURL=hands-lane.d.ts.map
@@ -1,4 +1,4 @@
1
- import { HAND_TOOL_EFFECTS, RETIRED_TOOL_NAMES, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
1
+ import { handBandEffects, RETIRED_TOOL_NAMES, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
2
2
  export function withoutExecutionEnv(deps) {
3
3
  const { executionEnvFactory: _handsDropped, ...rest } = deps;
4
4
  return rest;
@@ -12,13 +12,13 @@ export function pickHandsRunner(hands, pair) {
12
12
  }
13
13
  }
14
14
  export const HANDS_BAND_TOOL_NAMES = Object.freeze([
15
- ...Object.keys(HAND_TOOL_EFFECTS),
15
+ ...handBandEffects().keys(),
16
16
  "SendMessage",
17
17
  "AgentTranscript",
18
18
  "Monitor",
19
19
  "EnterWorktree",
20
20
  "ExitWorktree",
21
- ].filter((n) => !RETIRED_TOOL_NAMES.has(n)));
21
+ ].filter((n, i, all) => !RETIRED_TOOL_NAMES.has(n) && all.indexOf(n) === i));
22
22
  export const HANDS_DUAL_USE_TOOL_NAMES = Object.freeze(["TaskOutput", "TaskStop", "SendMessage", "AgentTranscript"]);
23
23
  export const HANDS_EXCLUSIVE_TOOL_NAMES = Object.freeze(HANDS_BAND_TOOL_NAMES.filter((n) => !HANDS_DUAL_USE_TOOL_NAMES.includes(n)));
24
24
  export function handsDenyPolicy() {
@@ -36,13 +36,24 @@ export function toolPolicyForHands(hands, base) {
36
36
  return tightenWithHandsDeny(base);
37
37
  }
38
38
  }
39
- export function createHandsLaneRegistry() {
40
- const lanes = new WeakMap();
41
- return {
42
- stamp: (spec, hands) => {
43
- lanes.set(spec, hands);
44
- },
45
- laneOf: (spec) => lanes.get(spec) ?? "full",
46
- };
39
+ export const HANDS_LANE_FIELD = Symbol("sema-server.handsLane");
40
+ export function stampHandsLane(spec, hands) {
41
+ Object.defineProperty(spec, HANDS_LANE_FIELD, { value: hands, enumerable: true, writable: false, configurable: false });
42
+ return spec;
43
+ }
44
+ export class HandsLaneUnstampedError extends Error {
45
+ constructor() {
46
+ super("execution lane is not stamped on this task spec — refusing to run it rather than falling back to the widest (full-hands) lane");
47
+ this.name = "HandsLaneUnstampedError";
48
+ }
49
+ }
50
+ export function handsLaneOf(spec) {
51
+ const lane = spec[HANDS_LANE_FIELD];
52
+ if (lane === undefined)
53
+ throw new HandsLaneUnstampedError();
54
+ return lane;
55
+ }
56
+ export function createHandsRunnerSelector(pair) {
57
+ return (spec) => pickHandsRunner(handsLaneOf(spec), pair);
47
58
  }
48
59
  //# sourceMappingURL=hands-lane.js.map
@@ -321,7 +321,7 @@ export const CONFIG_CATALOG = [
321
321
  r("READ_DENY_BUILTIN_EXCLUDE", "orchestration", "csv", "按行名删内建 deny 条目(档位之后生效;绝不削任务显式 deny)", { danger: SEC, configKey: "readDenyBuiltinExclude", center: cRead("denyBuiltinExclude", "readDenyBuiltinExclude") }),
322
322
  r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集四档;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
323
323
  r("READ_DENY_PATTERNS", "orchestration", "csv", "READ 面额外 deny pattern 表", { danger: SEC, configKey: "readDenyPatterns", center: cRead("denyPatterns", "readDenyPatterns") }),
324
- r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 不铸键,core 默认 roots", configKey: "readFace", center: cRead("face", "readFace") }),
324
+ r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
325
325
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
326
326
  r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
327
327
  r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: ["e2b", "k8s", "ssh", "adb", "host", "local-docker", "device"], danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
@@ -1,9 +1,9 @@
1
1
  import { compileReadDeny, resolveReadDenyBuiltins } from "@sema-agent/core";
2
2
  const KEY_MAP = [
3
- ["face", "readFace"],
4
- ["denyPatterns", "readDenyPatterns"],
5
- ["denyBuiltinTiers", "readDenyBuiltinTiers"],
6
- ["denyBuiltinExclude", "readDenyBuiltinExclude"],
3
+ { centerKey: "face", cfgKey: "readFace", envHolds: (c) => c.readFaceSource === "env" },
4
+ { centerKey: "denyPatterns", cfgKey: "readDenyPatterns", envHolds: (c) => c.readDenyPatterns !== undefined },
5
+ { centerKey: "denyBuiltinTiers", cfgKey: "readDenyBuiltinTiers", envHolds: (c) => c.readDenyBuiltinTiers !== undefined },
6
+ { centerKey: "denyBuiltinExclude", cfgKey: "readDenyBuiltinExclude", envHolds: (c) => c.readDenyBuiltinExclude !== undefined },
7
7
  ];
8
8
  class ReadFaceDomainError extends Error {
9
9
  }
@@ -43,7 +43,7 @@ function normalizeNameList(field, raw) {
43
43
  });
44
44
  }
45
45
  export function applyCenterReadFace(config, eff, logger) {
46
- const envHeldKeys = KEY_MAP.filter(([, cfgKey]) => config[cfgKey] !== undefined).map(([centerKey]) => centerKey);
46
+ const envHeldKeys = KEY_MAP.filter((r) => r.envHolds(config)).map((r) => r.centerKey);
47
47
  const domain = eff.readFace;
48
48
  if (domain === undefined || domain === null)
49
49
  return envHeldKeys;
@@ -55,7 +55,7 @@ export function applyCenterReadFace(config, eff, logger) {
55
55
  return envHeldKeys;
56
56
  }
57
57
  const d = domain;
58
- const envHeld = KEY_MAP.filter(([centerKey, cfgKey]) => d[centerKey] !== undefined && config[cfgKey] !== undefined).map(([centerKey, cfgKey]) => `${cfgKey} (readFace.${centerKey})`);
58
+ const envHeld = KEY_MAP.filter((r) => d[r.centerKey] !== undefined && r.envHolds(config)).map((r) => `${r.cfgKey} (readFace.${r.centerKey})`);
59
59
  let staged;
60
60
  try {
61
61
  const normalized = {
@@ -69,7 +69,7 @@ export function applyCenterReadFace(config, eff, logger) {
69
69
  if (normalized.readDenyBuiltinTiers !== undefined || normalized.readDenyBuiltinExclude !== undefined) {
70
70
  resolveReadDenyBuiltins(builtinsArg(normalized.readDenyBuiltinTiers, normalized.readDenyBuiltinExclude));
71
71
  }
72
- staged = Object.fromEntries(KEY_MAP.filter(([, cfgKey]) => config[cfgKey] === undefined).map(([, cfgKey]) => [cfgKey, normalized[cfgKey]]).filter(([, v]) => v !== undefined));
72
+ staged = Object.fromEntries(KEY_MAP.filter((r) => !r.envHolds(config)).map((r) => [r.cfgKey, normalized[r.cfgKey]]).filter(([, v]) => v !== undefined));
73
73
  const finalPatterns = staged.readDenyPatterns ?? config.readDenyPatterns;
74
74
  const finalTiers = staged.readDenyBuiltinTiers ?? config.readDenyBuiltinTiers;
75
75
  const finalExclude = staged.readDenyBuiltinExclude ?? config.readDenyBuiltinExclude;
@@ -85,8 +85,10 @@ export function applyCenterReadFace(config, eff, logger) {
85
85
  });
86
86
  return envHeldKeys;
87
87
  }
88
- if (staged.readFace !== undefined)
88
+ if (staged.readFace !== undefined) {
89
89
  config.readFace = staged.readFace;
90
+ config.readFaceSource = "center";
91
+ }
90
92
  if (staged.readDenyPatterns !== undefined)
91
93
  config.readDenyPatterns = staged.readDenyPatterns;
92
94
  if (staged.readDenyBuiltinTiers !== undefined)
@@ -1,5 +1,5 @@
1
1
  import type { A2aServerSpec, McpServerSpec } from "@sema-agent/core";
2
- import type { ScopedA2aPeer, ScopedMcpServer } from "../config-types.js";
2
+ import type { CenterMcp, ScopedA2aPeer } from "../config-types.js";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { LoadedSkill } from "../capabilities/skills.js";
5
5
  import type { CenterA2aPeer, CenterMcpServer, CenterSkillManifest } from "./types.js";
@@ -26,10 +26,10 @@ diskCacheDir?: string): Promise<LoadedSkill[]>;
26
26
  */
27
27
  export declare function resolveMcpServers(mcp: {
28
28
  servers: CenterMcpServer[];
29
- }, logger?: Logger): ScopedMcpServer[];
29
+ }, logger?: Logger): CenterMcp;
30
30
  /** The MCP servers applicable to a scenario: tagged with it, or untagged (global). undefined when none.
31
31
  * [ref]:alias 窗撤销,单键命中(同 `skillsForScenario`)。 */
32
- export declare function mcpForScenario(servers: ScopedMcpServer[] | undefined, scenario: string): McpServerSpec[] | undefined;
32
+ export declare function mcpForScenario(center: CenterMcp | undefined, scenario: string): McpServerSpec[] | undefined;
33
33
  /**
34
34
  * DESIGN-269 §2.1 —— resolve center A2A peers (`/effective.a2a`) into core `A2aServerSpec`s + the scenarios
35
35
  * each applies to. Exact sibling of {@link resolveMcpServers}, same three rules:
@@ -1,4 +1,5 @@
1
1
  import { readMcpSourceLabel } from "../mcp-source-label.js";
2
+ import { readMcpToolFaces } from "../mcp-tool-faces.js";
2
3
  import { skillContentHash } from "@sema-agent/settings-schema";
3
4
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
4
5
  import { fetchSkillContent } from "./http-client.js";
@@ -79,6 +80,7 @@ function resolveRefs(refs, server, kind, logger, event = "sema_registry_mcp_env_
79
80
  }
80
81
  export function resolveMcpServers(mcp, logger) {
81
82
  const out = [];
83
+ const reservedNames = (mcp.servers ?? []).map((s) => s.name);
82
84
  for (const s of mcp.servers ?? []) {
83
85
  if (s.enabled === false)
84
86
  continue;
@@ -97,13 +99,23 @@ export function resolveMcpServers(mcp, logger) {
97
99
  });
98
100
  }
99
101
  const source = readMcpSourceLabel(s.name, s.source);
102
+ const facesRead = readMcpToolFaces(s.toolFaces);
103
+ if (!facesRead.ok) {
104
+ logger?.warn("sema_registry_mcp_tool_faces_invalid", {
105
+ server: s.name,
106
+ problem: facesRead.problem,
107
+ note: "server SKIPPED — a toolFaces overlay is a NARROWING declaration (its pathTarget is what the sensitive-path / write fences read); mounting the server without it would silently drop the protection you declared. Fix the entry and republish.",
108
+ });
109
+ continue;
110
+ }
111
+ const toolFaces = facesRead.faces === undefined ? {} : { toolFaces: facesRead.faces };
100
112
  if (s.transport.kind === "stdio") {
101
113
  const env = resolveRefs(s.transport.envRefs, s.name, "env", logger);
102
114
  if (env === null)
103
115
  continue;
104
116
  out.push({
105
117
  scenarios: s.scenarios ?? [],
106
- spec: { name: s.name, transport: { kind: "stdio", command: s.transport.command, args: s.transport.args, ...(Object.keys(env).length ? { env } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source },
118
+ spec: { name: s.name, transport: { kind: "stdio", command: s.transport.command, args: s.transport.args, ...(Object.keys(env).length ? { env } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces },
107
119
  });
108
120
  }
109
121
  else {
@@ -112,15 +124,16 @@ export function resolveMcpServers(mcp, logger) {
112
124
  continue;
113
125
  out.push({
114
126
  scenarios: s.scenarios ?? [],
115
- spec: { name: s.name, transport: { kind: "http", url: s.transport.url, ...(Object.keys(headers).length ? { headers } : {}), ...(s.transport.principalHeader ? { principalHeader: s.transport.principalHeader } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source },
127
+ spec: { name: s.name, transport: { kind: "http", url: s.transport.url, ...(Object.keys(headers).length ? { headers } : {}), ...(s.transport.principalHeader ? { principalHeader: s.transport.principalHeader } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces },
116
128
  });
117
129
  }
118
130
  }
119
131
  if (out.length > 0)
120
132
  logger?.info("sema_registry_mcp", { servers: out.map((s) => s.spec.name) });
121
- return out;
133
+ return { servers: out, reservedNames };
122
134
  }
123
- export function mcpForScenario(servers, scenario) {
135
+ export function mcpForScenario(center, scenario) {
136
+ const servers = center?.servers;
124
137
  if (!servers || servers.length === 0)
125
138
  return undefined;
126
139
  const hit = servers.filter((s) => s.scenarios.length === 0 || s.scenarios.includes(scenario)).map((s) => s.spec);
@@ -116,6 +116,17 @@ export interface CenterMcpServer {
116
116
  /** B-036 / S-143:声明方自述的分组标签(开集,非空 ≤80;settings-schema 1.7.0 `McpServerSpec.source`)——上 core
117
117
  * `McpServerSpec.source`,由 `wiring_manifest.mcp[].source` 逐字回显。显示用,不进信任轴。 */
118
118
  source?: string;
119
+ /**
120
+ * 件8(core 7.8.0 [ref] §2.7;settings-schema 1.9.0 `McpServerSpec.toolFaces`)—— 这台 server 的
121
+ * **per-tool 面覆盖层**,键 = server 的**原始(未加命名空间)**工具名,值 = `{ family?, pathTarget?,
122
+ * renderHints?, ruleFace? }`。上 core `McpServerSpec.toolFaces`。
123
+ *
124
+ * 🔴 类型是 `unknown` 而不是那个结构,**刻意的**:本类型是两条 center 腿的**承载形**(本地 config.d 经
125
+ * settings-schema 的 `z.unknown()` 解析透明地过来、远端 /effective 是直读 JSON),判据与坏形处置的单一
126
+ * 属主是 `mcp-tool-faces.ts` 的 `readMcpToolFaces`(`resolveMcpServers` 在铸 spec 那一刻调它)。在承载
127
+ * 形上先写一个结构,等于把同一张判据表抄成两份(而其中一份没有执行力)。
128
+ */
129
+ toolFaces?: unknown;
119
130
  transport: {
120
131
  kind: "stdio";
121
132
  command: string;
@@ -144,6 +144,7 @@ export function mapToServiceEffective(eff, version) {
144
144
  ...(s.elicitation !== undefined ? { elicitation: s.elicitation } : {}),
145
145
  ...(s.contentOrigin !== undefined ? { contentOrigin: s.contentOrigin } : {}),
146
146
  ...readMcpSourceLabel(s.name, s.source),
147
+ ...(s.toolFaces !== undefined ? { toolFaces: s.toolFaces } : {}),
147
148
  };
148
149
  if (s.transport.kind === "stdio") {
149
150
  return {
@@ -5,6 +5,7 @@
5
5
  * in config.ts, which re-exports these names so existing `from "./config.js"` importers keep
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
+ import type { ReadFaceSource } from "./read-face-posture.js";
8
9
  import type { SealedKeyPoison } from "./sealed-key.js";
9
10
  import type { A2aServerSpec, CompliancePosture, CrossSessionDialogExpiry, CrossSessionInboundSetting, FileHistoryRetentionPolicy, LockedKey, McpServerSpec, Model, ModelRoles, RetentionPolicy, WriteProtectedEntry } from "@sema-agent/core";
10
11
  import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
@@ -83,6 +84,15 @@ export interface ScopedMcpServer {
83
84
  scenarios: string[];
84
85
  spec: McpServerSpec;
85
86
  }
87
+ /** The deployment's MCP declaration, resolved (`resolveMcpServers`). `servers` = what actually MOUNTED (env-NAME refs
88
+ * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` entries skipped, each with its own loud warn);
89
+ * `reservedNames` = EVERY name the deployment declared, mounted or not. A name the deployment declared is CLOSED to
90
+ * request-side injection (`resolveRequestMcp`) whether or not it mounted — otherwise "skipped" would silently mean
91
+ * "free for the caller to refill without the operator's `toolFaces` / `allowTools`" (车CF codex r2 [high]). */
92
+ export interface CenterMcp {
93
+ servers: ScopedMcpServer[];
94
+ reservedNames: readonly string[];
95
+ }
86
96
  /** DESIGN-269 §2.1 —— an A2A PEER resolved to a core spec (header-NAME refs already → real values) plus
87
97
  * the scenarios it applies to (empty = all). Exact sibling of {@link ScopedMcpServer}: `resolveSpec`
88
98
  * filters by scenario (`a2aForScenario`) and passes `spec` to core as `TaskSpec.a2a`.
@@ -686,8 +696,9 @@ export interface ServiceConfigFlat {
686
696
  };
687
697
  /** MCP servers from sema-registry (`/effective.mcp`), resolved at startup (env-NAME refs → values) and
688
698
  * filtered per scenario in `resolveSpec`. Not env-derived — populated by the sema-registry adapter when
689
- * present (restart-to-apply, like models/roles); undefined = no center MCP (scenarios use no MCP). */
690
- mcpServers?: ScopedMcpServer[];
699
+ * present (restart-to-apply, like models/roles); undefined = no center MCP (scenarios use no MCP).
700
+ * Carries both the mounted list and the full declared-name set — see {@link CenterMcp}. */
701
+ mcpServers?: CenterMcp;
691
702
  /** DESIGN-269 §2.1 —— A2A peers from the config center (`/effective.a2a`), resolved at startup
692
703
  * (header-NAME refs → values) and filtered per scenario in `resolveSpec` (`a2aForScenario`). Same
693
704
  * posture as {@link mcpServers}: not env-derived (no A2A_* env leg — a peer is a structured record,
@@ -1539,9 +1550,17 @@ export interface ServiceConfigFlat {
1539
1550
  /** Task Trace API (S1): persist the agent's reasoning/thinking into the durable run event log (for the CC-style
1540
1551
  * trace UI). Default ON; `TRACE_THINKING=false` disables capture for sensitive deployments. Redacted on persist. */
1541
1552
  traceThinking: boolean;
1542
- /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。缺席=不铸(core 引擎默认 roots,server 不写死
1543
- * 上游默认);显式坏词启动期响亮拒。只影响读面,写面永不受它管(core 契约)。 */
1553
+ /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。显式坏词启动期响亮拒;只影响读面,写面永不
1554
+ * 受它管(core 契约)。
1555
+ * 🔴 **S-167(7.65.0)起「缺席」按 posture 分两形**(clay 裁定 2):单用户 turnkey(`REQUIRE_PRINCIPAL`
1556
+ * 未设)⇒ 铸 `open`(工作区外读默认放行);多租户 ⇒ 仍**不铸**(core 引擎默认 roots,server 不写死
1557
+ * 上游默认)。谁定的这一档由 {@link ServiceConfigFlat.readFaceSource} 答 —— 值与来源**同一处写**。 */
1544
1558
  readFace?: "open" | "roots";
1559
+ /** S-167:上面那一档**是谁定的**(闭集四词,见 `read-face-posture.ts`)。不变量:
1560
+ * `readFace === undefined` ⟺ `readFaceSource === "engine-default"`。
1561
+ * 🔴 承重面**不是**装饰:`config-center/read-face.ts` 的「env 恒赢」逐键判据读的就是它(修前读的是
1562
+ * 「键在不在场」这个代理,posture 会铸键 ⇒ 代理当场失真,center 的 face 会被从没设过 env 的机器挡掉)。 */
1563
+ readFaceSource: ReadFaceSource;
1545
1564
  /** [ref]:部署级 READ deny 追加表(additive-only,core 内建表永不可缩)。env=JSON 数组
1546
1565
  * [{pattern, caseSensitive?}],boot 时用 core `compileReadDeny` 预验——坏形拒启。缺席=不铸。 */
1547
1566
  readDenyPatterns?: {
@@ -1674,7 +1693,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1674
1693
  * config 这一层它只是一句声明。 */
1675
1694
  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">;
1676
1695
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
1677
- 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" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
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">;
1678
1697
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1679
1698
  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">;
1680
1699
  /** 组:observability(可观测)。 */
package/dist/config.js CHANGED
@@ -12,6 +12,7 @@ import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
12
12
  import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
+ import { readFaceForPosture } from "./read-face-posture.js";
15
16
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
16
17
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
17
18
  function csv(name) {
@@ -1458,7 +1459,7 @@ function parseAuthDomain(ctx) {
1458
1459
  };
1459
1460
  }
1460
1461
  function parseOrchestrationDomain(ctx) {
1461
- const { postureOn } = ctx;
1462
+ const { postureOn, singleUserTurnkey } = ctx;
1462
1463
  const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
1463
1464
  if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
1464
1465
  throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
@@ -1501,7 +1502,9 @@ function parseOrchestrationDomain(ctx) {
1501
1502
  }
1502
1503
  }
1503
1504
  return {
1504
- ...(process.env.READ_FACE !== undefined ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]) } : {}),
1505
+ ...(process.env.READ_FACE !== undefined
1506
+ ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]), readFaceSource: "env" }
1507
+ : readFaceForPosture(singleUserTurnkey)),
1505
1508
  ...(process.env.READ_DENY_PATTERNS !== undefined ? { readDenyPatterns: parseReadDenyPatternsEnv(process.env.READ_DENY_PATTERNS) } : {}),
1506
1509
  ...(() => {
1507
1510
  const tiers = process.env.READ_DENY_BUILTIN_TIERS !== undefined ? parseNameListEnv("READ_DENY_BUILTIN_TIERS", process.env.READ_DENY_BUILTIN_TIERS, { emptySentinel: "none" }) : undefined;
@@ -1905,7 +1908,7 @@ const ORCHESTRATION_GROUP_KEYS = [
1905
1908
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
1906
1909
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
1907
1910
  "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1908
- "imageBakes", "readFace", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1911
+ "imageBakes", "readFace", "readFaceSource", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1909
1912
  ];
1910
1913
  const LIMITS_HTTP_GROUP_KEYS = [
1911
1914
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -1973,7 +1976,7 @@ export function loadConfig() {
1973
1976
  const approval = parseApprovalDomain({ postureOn });
1974
1977
  const memory = parseMemoryDomain({ requirePrincipal });
1975
1978
  const auth = parseAuthDomain({ requirePrincipal });
1976
- const orchestration = parseOrchestrationDomain({ postureOn });
1979
+ const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
1977
1980
  const limitsHttp = parseLimitsHttpDomain();
1978
1981
  const observability = parseObservabilityDomain();
1979
1982
  const integrations = parseIntegrationsDomain();
@@ -1,4 +1,6 @@
1
+ import { screenedGate } from "../../trace/project.js";
1
2
  import { personDecision } from "../../host-decision.js";
3
+ import { sameExecutionRecord } from "@sema-agent/core";
2
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
3
5
  import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
4
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -71,6 +73,21 @@ async function decideIdempotentReplay(cs, args) {
71
73
  if (row.payloadFp === null || row.payloadFp !== requestFp)
72
74
  return undefined;
73
75
  }
76
+ const executionOutcome = (() => {
77
+ const records = rows.map((r) => r.executionOutcome);
78
+ const first = records[0];
79
+ if (first === null || first === undefined)
80
+ return undefined;
81
+ for (const r of records) {
82
+ if (r === null)
83
+ return undefined;
84
+ if (!sameExecutionRecord(r, first)) {
85
+ warn("decide_replay_execution_outcome_divergent", { sessionId, rows: rows.length });
86
+ return undefined;
87
+ }
88
+ }
89
+ return screenedGate(first);
90
+ })();
74
91
  return {
75
92
  status: 200,
76
93
  body: {
@@ -78,6 +95,7 @@ async function decideIdempotentReplay(cs, args) {
78
95
  idempotent: true,
79
96
  decision,
80
97
  bindingEnforced: true,
98
+ ...(executionOutcome !== undefined ? { executionOutcome } : {}),
81
99
  },
82
100
  };
83
101
  }