@sema-agent/server 7.64.0 → 7.65.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.
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
 
@@ -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);
@@ -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。 */
@@ -82,19 +83,58 @@ export declare function tightenWithHandsDeny(base: ToolPolicy | undefined): Tool
82
83
  /** 装配点的取用形:`full` 原样放行基线,`none` 叠 deny。**穷举 switch**,无缺省臂。 */
83
84
  export declare function toolPolicyForHands(hands: ScenarioHands, base: ToolPolicy | undefined): ToolPolicy | undefined;
84
85
  /**
85
- * 请求级 lane 登记簿 —— resolveSpec 在产出 TaskSpec 时登记本请求的场景表态,执行点凭 spec 取回选 Runner。
86
+ * 执行车道在 TaskSpec 上的**显式字段键**(S-149;修前是一张以 spec **对象身份**为键的 WeakMap 旁表)。
86
87
  *
87
- * 为什么不把 lane 写进 TaskSpec:那是 core 的类型,server 无自有位;而 `resolveSpec` 的返回形是
88
- * ServiceDeps 的公开签名,80+ 测试文件在桩它 —— 改返回形的射程远大于本件。登记簿以 spec **对象身份**
89
- * 为键(每请求一只新对象,并发无串台;WeakMap 弱引用,零清理欠账),由同一只 boot 装配同时交给
90
- * resolveSpec 与 HTTP 执行点,配对是结构性的。
88
+ * 为什么是 symbol 而不是字符串键:`TaskSpec` 是**上游 core 的类型面**,一个字符串键会进
89
+ * `Object.keys` / `JSON.stringify` / core 自己的键审计,等于 server 在别人的契约上私自加一个键;
90
+ * symbol 键随 spread(`{...spec}`)与 rest 解构(`const {objective, ...taskConfig} = spec`)结构性
91
+ * 地跟着走,却对上游的键面**不可见**(结构化克隆与 JSON 同样跳过 symbol 键)。于是「车道是 spec 上
92
+ * 的一个字段、复制 spec 不丢车道」与「不污染上游契约」两件事同时成立。
91
93
  *
92
- * 未登记 ⇒ `full`:唯一可达形是「这只 spec 不是本 resolver 产的」(测试桩)。生产链上 stamp 与 laneOf
93
- * 同一实例,不存在半接;真误配时纵深 deny 仍在(见 {@link tightenWithHandsDeny})。
94
+ * 修前的旁表要求「从 resolveSpec 出来的那一只对象必须一路原样活到执行点」——一条**隐形**的对象身份
95
+ * 契约:任何一次 `{...spec}` 都会静默把车道降级成最宽的 `full`(实测:登记 `none` 的 spec 复制后
96
+ * `laneOf` 答 `full`)。字段化把那条隐形要求整条删掉。
94
97
  */
95
- export interface HandsLaneRegistry {
96
- stamp(spec: TaskSpec, hands: ScenarioHands): void;
97
- laneOf(spec: TaskSpec): ScenarioHands;
98
+ export declare const HANDS_LANE_FIELD: unique symbol;
99
+ /** 已铸车道的 TaskSpec。铸点唯一(`boot/resolve-spec.ts` 的 resolveSpec 出口),读点唯一({@link handsLaneOf})。 */
100
+ export type LanedTaskSpec = TaskSpec & {
101
+ readonly [HANDS_LANE_FIELD]: ScenarioHands;
102
+ };
103
+ /**
104
+ * 铸车道(**单一属主**:只允许 resolveSpec 的出口调用)。
105
+ *
106
+ * 字段是 `enumerable`(所以复制得走)、`writable:false` + `configurable:false`(所以同一只 spec 被
107
+ * 第二次铸车道会当场 TypeError —— 两个写者是漂移的成因,响亮好过后到者悄悄赢)。
108
+ */
109
+ export declare function stampHandsLane<T extends TaskSpec>(spec: T, hands: ScenarioHands): T & LanedTaskSpec;
110
+ /**
111
+ * 车道字段缺席的拒因 —— 具名类,便于调用点/测试按类型判,而不是嗅文案。
112
+ *
113
+ * 🔴 **刻意不是 `HttpError`、刻意不铸新的 wire 错误码**:它落到 `http/server.ts` 的兜底 catch,
114
+ * wire 上是 500 `internal.error`(内部文本不回显,只进 `request_error` 日志行)。理由两条:
115
+ * ① 对**消费端**它与任何一次服务端内部故障无差别可操作性(重试一次,仍复现即报运维)—— 附录 A 多一个
116
+ * 码却给不出不同的动作,是没有价值的契约面;
117
+ * ② 本仓的中继码登记表(`test/error-code-catalog-live.test.ts` 的 `RELAYED`,上限 24 条)明写「超了说明
118
+ * 『码不写字面量』正在变成默认写法,该反过来改代码」—— 一个只能靠登记才看得见的新码正是它拦的东西。
119
+ * 需要分辨的是**运维**,而运维读的是日志行(本 message 逐字在 `request_error` 里)。
120
+ */
121
+ export declare class HandsLaneUnstampedError extends Error {
122
+ constructor();
98
123
  }
99
- export declare function createHandsLaneRegistry(): HandsLaneRegistry;
124
+ /**
125
+ * 读车道 —— **缺席即拒**([ref] P 类:车道选择是保护型量,fail-closed;`observability/fail-open.ts`
126
+ * 头注把「车道选择」逐字列在「正解是 fail-closed,不是留痕后继续放行」那一族里)。
127
+ *
128
+ * 修前此处是 `?? "full"`:一只没经过本仓 resolver 的 spec 会**静默拿到最宽的车道**(全量可写手带)。
129
+ * 「唯一可达形是测试桩」这个论证不是防线 —— 它是一句关于今天调用图的断言,而防线要防的正是明天多出
130
+ * 来的第二个执行点。缺席现在整只拒(500 + 日志留原句),而不是把一个未经裁定的任务放进有手车道。
131
+ */
132
+ export declare function handsLaneOf(spec: TaskSpec): ScenarioHands;
133
+ /**
134
+ * 装配点的选路席:`{full, handsless}` 两只 Runner + 车道字段 ⇒ 本任务跑哪一只。
135
+ *
136
+ * 具名工厂而不是在 main.ts 手搓闭包:选路规则(读字段 → 穷举 switch)于是只有**一份实现**,
137
+ * 测试与生产装配拿的是同一只(手搓等价闭包 = 第二真源,正是本件要删的那一类)。
138
+ */
139
+ export declare function createHandsRunnerSelector(pair: HandsRunnerPair): (spec: TaskSpec) => Runner;
100
140
  //# sourceMappingURL=hands-lane.d.ts.map
@@ -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)
@@ -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";
@@ -1539,9 +1540,17 @@ export interface ServiceConfigFlat {
1539
1540
  /** Task Trace API (S1): persist the agent's reasoning/thinking into the durable run event log (for the CC-style
1540
1541
  * trace UI). Default ON; `TRACE_THINKING=false` disables capture for sensitive deployments. Redacted on persist. */
1541
1542
  traceThinking: boolean;
1542
- /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。缺席=不铸(core 引擎默认 roots,server 不写死
1543
- * 上游默认);显式坏词启动期响亮拒。只影响读面,写面永不受它管(core 契约)。 */
1543
+ /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。显式坏词启动期响亮拒;只影响读面,写面永不
1544
+ * 受它管(core 契约)。
1545
+ * 🔴 **S-167(7.65.0)起「缺席」按 posture 分两形**(clay 裁定 2):单用户 turnkey(`REQUIRE_PRINCIPAL`
1546
+ * 未设)⇒ 铸 `open`(工作区外读默认放行);多租户 ⇒ 仍**不铸**(core 引擎默认 roots,server 不写死
1547
+ * 上游默认)。谁定的这一档由 {@link ServiceConfigFlat.readFaceSource} 答 —— 值与来源**同一处写**。 */
1544
1548
  readFace?: "open" | "roots";
1549
+ /** S-167:上面那一档**是谁定的**(闭集四词,见 `read-face-posture.ts`)。不变量:
1550
+ * `readFace === undefined` ⟺ `readFaceSource === "engine-default"`。
1551
+ * 🔴 承重面**不是**装饰:`config-center/read-face.ts` 的「env 恒赢」逐键判据读的就是它(修前读的是
1552
+ * 「键在不在场」这个代理,posture 会铸键 ⇒ 代理当场失真,center 的 face 会被从没设过 env 的机器挡掉)。 */
1553
+ readFaceSource: ReadFaceSource;
1545
1554
  /** [ref]:部署级 READ deny 追加表(additive-only,core 内建表永不可缩)。env=JSON 数组
1546
1555
  * [{pattern, caseSensitive?}],boot 时用 core `compileReadDeny` 预验——坏形拒启。缺席=不铸。 */
1547
1556
  readDenyPatterns?: {
@@ -1674,7 +1683,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1674
1683
  * config 这一层它只是一句声明。 */
1675
1684
  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
1685
  /** 组: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">;
1686
+ 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
1687
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1679
1688
  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
1689
  /** 组: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,14 +1,3 @@
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
1
  import type { IncomingMessage, ServerResponse } from "node:http";
13
2
  import type { WiringManifest } from "@sema-agent/core";
14
3
  import { type StreamApprovalGate, type StreamApprovalGateInput } from "../../tool-approval.js";
@@ -1,3 +1,4 @@
1
+ import { buildReadFaceWiring } from "../../read-face-posture.js";
1
2
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
2
3
  import { sendJson, sendError } from "../send.js";
3
4
  import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
@@ -90,6 +91,7 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
90
91
  memoryPosture: deps.memoryPosture ?? null,
91
92
  sqlEngine: deps.sqlEngineFacts?.() ?? null,
92
93
  writeProtection: deps.writeProtection ?? null,
94
+ readFace: buildReadFaceWiring(deps.config),
93
95
  configApply: deps.configApplyState?.() ?? null,
94
96
  mcpRevocations: deps.mcpRevocationState?.() ?? null,
95
97
  serverGates: buildServerWiringGates({
package/dist/main.js CHANGED
@@ -20,7 +20,7 @@ import { assertGateIntentServiceable, buildForceDurableGateInertNotice, hasOpera
20
20
  import { loadSkills } from "./capabilities/skills.js";
21
21
  import { createRepoClient } from "./capabilities/repo-tools.js";
22
22
  import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
23
- import { createHandsLaneRegistry, pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
23
+ import { createHandsRunnerSelector, withoutExecutionEnv } from "./capabilities/hands-lane.js";
24
24
  import { createLogger } from "./observability/logger.js";
25
25
  import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
26
26
  import { createRegistryJwtVerifier } from "./auth-bridge.js";
@@ -730,9 +730,7 @@ async function main() {
730
730
  : undefined,
731
731
  sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
732
732
  };
733
- const handsLanes = createHandsLaneRegistry();
734
733
  const resolveSpec = createResolveSpec({
735
- handsLanes,
736
734
  config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
737
735
  getCenterPrompts: () => configCenter.getCenterPrompts(),
738
736
  getKeyResolver: () => configCenter.getKeyResolver(),
@@ -746,7 +744,7 @@ async function main() {
746
744
  liveQuestionFace: question,
747
745
  approverSeat: createRunnerDepsOnAsk(toolApproval),
748
746
  });
749
- const runnerFor = (spec) => pickHandsRunner(handsLanes.laneOf(spec), { full: runner, handsless: handslessRunner });
747
+ const runnerFor = createHandsRunnerSelector({ full: runner, handsless: handslessRunner });
750
748
  const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => configCenter.getKeyResolver() });
751
749
  const server = createHttpServer({ runner, runnerFor, sideQuery, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
752
750
  runDenySweep = server.denyExpiredApprovals;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * S-167(clay 裁定 2,[ref] 转达 / [ref] ②)—— **READ 容纳面档位的来源**(单一属主)。
3
+ *
4
+ * ## 它解决的问题
5
+ *
6
+ * `READ_FACE` 此前只有「显式设了就铸、缺席就不铸」两态,而**缺席**这一态里其实塞了两件完全不同的事:
7
+ * · 单机 turnkey(`REQUIRE_PRINCIPAL` 未设,CC 的信任模型:一台机器一个可信主人)—— 运维没表态是因为
8
+ * 他压根不认为工作区外的读需要围栏;引擎默认的 `roots` 于是成了一道**没人要求过**的墙(clay 裁:
9
+ * 「工作区外读默认放行(单机形)」)。
10
+ * · 多租户 —— 缺席就是缺席,引擎默认 `roots` 正是想要的收敛姿态。
11
+ *
12
+ * 于是本模块把「档位」与「档位是谁定的」拆成两个字段:值仍是 `open | roots | 缺席`,来源是本文件的
13
+ * **四词闭集**。来源不是装饰:
14
+ * ① `config-center/read-face.ts` 的 **env 恒赢**判据此前用的是「`config.readFace` 键在不在场」这个
15
+ * **代理**;posture 会铸键,代理当场失真(center 的 face 会被一台从没设过 env 的机器挡掉)。
16
+ * 换成读来源之后判据说的就是它真正想说的那件事:**这台机器的 env 显式占住了这个键吗**。
17
+ * ② 运维面:`GET /v1/diagnostics/wiring` 报 `{face, source, note}`,「为什么这台机器是 open」有答案
18
+ * (裁定的补偿项:升级即放开,得让运维看得见并知道怎么钉回)。
19
+ *
20
+ * ## 优先序(三态,与 `config.ts` 的 `postureOn` 同形)
21
+ * `env 显式` > `center 发布` > `posture 派生` > `引擎默认`。env 恒赢的论证逐字见 `config-center/read-face.ts`
22
+ * 头注①(READ 容纳面描述的是**这台机器的磁盘上有什么**,组织的下发面不知道)。
23
+ *
24
+ * ## 定界
25
+ * 本模块**只**管 `face` 一档。内建读拒表(`readDenyPatterns` / `readDenyBuiltinTiers` /
26
+ * `readDenyBuiltinExclude`)一个字节不动 —— 放开容纳面不等于放开凭证/shell 历史/钱包那几档 deny,
27
+ * 两者在 core 里是并列机制(deny 表在 open 面上照样拦)。core 的 `config.read_face_deployment_clamped`
28
+ * (只读挂载钳制)同样不受本件影响。
29
+ */
30
+ /** 档位来源的闭集词表(顺序 = 优先序:env > center > posture > 引擎默认)。 */
31
+ export declare const READ_FACE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
32
+ /** 档位的来源(闭集四词)。加词 ⇒ {@link readFaceSourceNote} 的穷举 switch 编译红。 */
33
+ export type ReadFaceSource = (typeof READ_FACE_SOURCE_WORDS)[number];
34
+ /** 一台 worker 的 READ 面档位事实(值 + 来源)。`ServiceConfigFlat` 结构上满足本形。 */
35
+ export interface ReadFacePostureFacts {
36
+ readonly readFace?: "open" | "roots" | undefined;
37
+ readonly readFaceSource: ReadFaceSource;
38
+ }
39
+ /**
40
+ * 缺席 `READ_FACE` 时的 posture 派生(`config.ts` 的铸点调用;显式值不经此口)。
41
+ * 单用户 turnkey ⇒ `open`;多租户 ⇒ 不铸(引擎默认 `roots`,server 不复制上游默认)。
42
+ */
43
+ export declare function readFaceForPosture(singleUserTurnkey: boolean): ReadFacePostureFacts;
44
+ /** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
45
+ export declare function readFaceSourceNote(source: ReadFaceSource): string;
46
+ /** operator 诊断面的投影(`GET /v1/diagnostics/wiring`)。`face:null` = 本部署没钉档,引擎默认接管。 */
47
+ export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
48
+ face: "open" | "roots" | null;
49
+ source: ReadFaceSource;
50
+ note: string;
51
+ };
52
+ //# sourceMappingURL=read-face-posture.d.ts.map
@@ -0,0 +1,20 @@
1
+ export const READ_FACE_SOURCE_WORDS = ["env", "center", "posture", "engine-default"];
2
+ export function readFaceForPosture(singleUserTurnkey) {
3
+ return singleUserTurnkey ? { readFace: "open", readFaceSource: "posture" } : { readFaceSource: "engine-default" };
4
+ }
5
+ export function readFaceSourceNote(source) {
6
+ switch (source) {
7
+ case "env":
8
+ return "pinned by this machine's READ_FACE env var (deployment sovereignty — it wins over any published readFace domain)";
9
+ case "center":
10
+ return "applied from the config-center readFace domain (restart-to-apply); set READ_FACE on this machine to take the key back";
11
+ case "posture":
12
+ return "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back";
13
+ case "engine-default":
14
+ return "not pinned by this deployment — the engine's own default (roots) is in force";
15
+ }
16
+ }
17
+ export function buildReadFaceWiring(facts) {
18
+ return { face: facts.readFace ?? null, source: facts.readFaceSource, note: readFaceSourceNote(facts.readFaceSource) };
19
+ }
20
+ //# sourceMappingURL=read-face-posture.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.64.0",
3
+ "version": "7.65.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",