@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
@@ -1,4 +1,4 @@
1
- import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
1
+ import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ExecutionOutcomeRecordWord, type GateOutcome, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
2
2
  import { type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js";
3
3
  export interface LocalCheckpointStoreOptions {
4
4
  /** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
@@ -40,6 +40,20 @@ export declare class LocalCheckpointStore {
40
40
  readonly redecision: {
41
41
  readonly reopen: true;
42
42
  };
43
+ /**
44
+ * `CheckpointStore.execution` 声明(core 7.7.0 [ref];S-160)——**必填**,与上面三格同一条判据:**如实
45
+ * 按内核判**。内核 core `FileCheckpointStore` 自己声明 `{ outcome: true }`,并把执行记录落成 ledger 的
46
+ * 一个新事件种类(`execution`,与 resolve 同款「先 append+fsync 再翻内存」的崩溃序),
47
+ * {@link LocalCheckpointStore.recordExecutionOutcome} 与 {@link LocalCheckpointStore.reopen} 都是纯委派
48
+ * ⇒ 抄内核的读数是诚实的。包装类**不继承**被包装者的声明(委派不是子类),所以这一格必须自己写。
49
+ *
50
+ * ⚠️ 升级序的诚实披露(DEPLOY-PREREQS 同句):ledger 里一旦出现 `execution` 事件,**7.7.0 之前的
51
+ * binary 会拒绝重放这个目录**(响亮,不是静默跳过);而压缩把事件折进快照行之后,那个旧 binary 读到
52
+ * 一个它不认识的字段、且它的 `reopen` 不认这条记录 —— 跨记录行回滚不受支持。
53
+ */
54
+ readonly execution: {
55
+ readonly outcome: true;
56
+ };
43
57
  private readonly inner;
44
58
  private readonly scopesPath;
45
59
  private readonly sessionTokensPath;
@@ -63,6 +77,10 @@ export declare class LocalCheckpointStore {
63
77
  get(token: CheckpointToken): Promise<Checkpoint | null>;
64
78
  resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
65
79
  reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
80
+ /** core 7.7.0 [ref](S-160):执行判词的 CAS —— 语义、四词、幂等的结构相等与冲突抛全在被包的内核里
81
+ * (它自己走 core 的共享判词 `executionRecordDisposition`,ledger 事件 `execution` 是它的 commit 半场),
82
+ * 本 wrapper 只做转发。服务扩展面(运维队列/会话索引)与这条记录无关:它不改 status,也不新增可枚举行。 */
83
+ recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
66
84
  /** core 5.14.0([ref]):入参是 `PendingSteerInput`(text/trusted + 可选 actor/inputId/priority),不是
67
85
  * 两键单座 —— 形参窄成 `{text,trusted}` 会**静默丢掉**署名与幂等键(TS 的方法参数是双变的,窄形不
68
86
  * 报错,只是把三个可选键从类型上抹掉,调用点再也传不进来)。队列语义与边界全在被包的 core
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { isApprovalGateKind } from "../tool-approval.js";
6
- import { FileCheckpointStore, atomicWriteFile, sanitizePathComponent, } from "@sema-agent/core";
6
+ import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
7
7
  import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, pendingRowPrecedes, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
8
8
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
9
9
  function terminalAtOf(cp) {
@@ -17,6 +17,7 @@ export class LocalCheckpointStore {
17
17
  durability = "durable";
18
18
  fidelity = "json";
19
19
  redecision = { reopen: true };
20
+ execution = { outcome: true };
20
21
  inner;
21
22
  scopesPath;
22
23
  sessionTokensPath;
@@ -95,6 +96,9 @@ export class LocalCheckpointStore {
95
96
  reopen(token, scope, reason) {
96
97
  return this.inner.reopen(token, scope, reason);
97
98
  }
99
+ recordExecutionOutcome(token, scope, gate) {
100
+ return this.inner.recordExecutionOutcome(token, scope, gate);
101
+ }
98
102
  setPendingSteer(token, scope, steer) {
99
103
  return this.inner.setPendingSteer(token, scope, steer);
100
104
  }
@@ -227,6 +231,7 @@ export class LocalCheckpointStore {
227
231
  boundInputHash: pa?.boundInputHash ?? null,
228
232
  decision: approvalDecisionOfWinner(cp.resolvedOutcome, boundCallId),
229
233
  payloadFp: cp.resolvedOutcome === undefined ? null : approvalPayloadFingerprint(cp.resolvedOutcome),
234
+ ...((v) => ({ executionOutcome: v.kind === "executed" ? v.gate : null }))(executionVerdict(cp)),
230
235
  });
231
236
  }
232
237
  return out;
@@ -95,6 +95,11 @@ export const PG_SCHEMA_STATEMENTS = [
95
95
  -- has_bidi_controls(S-30① / #356②):bidi 在场位,语义与「1 或 NULL、恒不写 0」见 MySQL 孪生同名列的
96
96
  -- 行内注。clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT。
97
97
  has_bidi_controls SMALLINT,
98
+ -- execution_outcome / execution_at_ms(S-160;core 7.7.0 #602):语义、两轴的分工、CAS 与冲突的
99
+ -- 「记录物理不可变」由来见 MySQL 孪生(tidb-pool.ts 同两列的行内注)。载荷是 core 的 GateOutcome
100
+ -- JSON ⇒ 与同表 outcome 列同取 JSONB(协议信封无损,IS NULL 谓词两方言同义)。
101
+ execution_outcome JSONB,
102
+ execution_at_ms BIGINT,
98
103
  PRIMARY KEY (token)
99
104
  )`,
100
105
  `CREATE INDEX IF NOT EXISTS idx_checkpoint_status_deadline ON checkpoint (status, deadline)`,
@@ -266,6 +266,17 @@ export const SCHEMA_STATEMENTS = [
266
266
  -- (缺席 = 没检出 ≠ 干净),一个 0 会把「没检出」谎成「已确认干净」。老行(本列诞生前 park 的)
267
267
  -- NULL ⇒ 读口省键。clay 批 2026-07-26 TINYINT(1) 归一先例:布尔列 TiDB 侧 TINYINT(1)。
268
268
  has_bidi_controls TINYINT(1) NULL,
269
+ -- execution_outcome / execution_at_ms(S-160;core 7.7.0 #602 的耐久半场):**决定被怎么处置了**的
270
+ -- 结算记录 —— resume 腿上那次调用的 tool_end 帧携带的 GateOutcome,与 outcome 列(决定本身,不可变)
271
+ -- 是两个轴:一个人批了(outcome 里 disposition=allowed),部署自己的复核在 post-CAS 否了他改过的参数
272
+ -- (execution_outcome 里 denied/policy)—— 流断了之后从行重建这条 run 的消费者,此前只读得到前半句。
273
+ -- 写口是 recordExecutionOutcome 的 CAS(status='resolved' AND execution_outcome IS NULL),**恰好一次**;
274
+ -- 第二次写同一份记录是幂等,写不同的一份是 checkpoint.execution_outcome_conflict(记录物理不可变 ——
275
+ -- 两份不同的执行记录意味着同一个已决动作被结算了两次,那是引擎缺陷,不是店可以吸收的条件)。
276
+ -- 缺席只有一个含义:**未知**,绝不是「放行过」(读面单口 core executionVerdict(cp))。
277
+ -- 两列同生同灭(stamp 在场 ⇔ 记录在场);reopen 见到记录即拒(已结算的动作不可重开)。
278
+ execution_outcome JSON NULL,
279
+ execution_at_ms BIGINT NULL,
269
280
  PRIMARY KEY (token),
270
281
  KEY idx_checkpoint_status_deadline (status, deadline),
271
282
  KEY idx_checkpoint_session_status (session_id, status)
@@ -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
@@ -110,11 +110,21 @@ export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: R
110
110
  */
111
111
  export declare function assertRequestMcpContentOrigin(bodyMcp: unknown): void;
112
112
  /**
113
- * Merge GATED per-request MCP servers OVER the deployment baseline (center/config). The baseline WINS on a name
114
- * clash — a caller can ADD servers but can never SHADOW a configured one (the security baseline is authoritative,
115
- * same posture as `mergeUserSkills`). Returns the baseline unchanged when there is nothing to add.
113
+ * The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
114
+ * this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
115
+ * A caller can ADD servers but can never SHADOW or REFILL a deployment-declared one: a center entry that was skipped
116
+ * (disabled / missing env ref / malformed `toolFaces`) still occupies its name, otherwise "skipped" would silently
117
+ * mean "free for the caller to remount without the operator's `allowTools` / `toolFaces`" (车CF codex r2 [high]).
118
+ * Same posture as `mergeUserSkills`.
116
119
  */
117
- export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, gated: McpServerSpec[]): McpServerSpec[] | undefined;
120
+ export declare function deploymentClosedMcpNames(baseline: McpServerSpec[] | undefined, reservedNames: readonly string[]): ReadonlySet<string>;
121
+ /**
122
+ * Merge GATED per-request MCP servers OVER the deployment baseline (center/config). Names closed by the deployment
123
+ * ({@link deploymentClosedMcpNames}) are dropped from `gated`. `reservedNames` has NO default: a security predicate's
124
+ * input is never implied — a caller with no deployment declaration passes `[]` and says so. Returns the baseline unchanged
125
+ * when nothing is added.
126
+ */
127
+ export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, gated: McpServerSpec[], reservedNames: readonly string[]): McpServerSpec[] | undefined;
118
128
  /**
119
129
  * The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
120
130
  * caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
@@ -127,6 +137,10 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
127
137
  };
128
138
  requirePrincipal?: boolean;
129
139
  lockedConfigKeys?: readonly LockedKey[];
140
+ /** The deployment's declared-name set rides on the same config object the gate reads (one owner, no second arg). */
141
+ mcpServers?: {
142
+ reservedNames: readonly string[];
143
+ };
130
144
  }, logger?: {
131
145
  info?(m: string, meta?: unknown): void;
132
146
  warn(m: string, meta?: unknown): void;
package/dist/task-mcp.js CHANGED
@@ -2,6 +2,7 @@ import { resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
4
4
  import { readMcpSourceLabel } from "./mcp-source-label.js";
5
+ import { readMcpToolFaces } from "./mcp-tool-faces.js";
5
6
  export const MAX_REQUEST_MCP_SERVERS = 32;
6
7
  export function mcpInjectionHonored(config) {
7
8
  if (config.lockedConfigKeys?.includes("mcp") === true)
@@ -79,6 +80,11 @@ function normalizeServer(e, noteBadOrigin) {
79
80
  noteBadOrigin(spec.name, o.contentOrigin);
80
81
  }
81
82
  Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
83
+ const faces = readMcpToolFaces(o.toolFaces);
84
+ if (!faces.ok)
85
+ return null;
86
+ if (faces.faces !== undefined)
87
+ spec.toolFaces = faces.faces;
82
88
  return spec;
83
89
  }
84
90
  export function validateRequestMcp(raw) {
@@ -124,11 +130,14 @@ export function assertRequestMcpContentOrigin(bodyMcp) {
124
130
  ` (omit the key entirely for the default: this server's tools classify as external content)`, { code: "request.field_invalid" });
125
131
  }
126
132
  }
127
- export function mergeRequestMcp(baseline, gated) {
133
+ export function deploymentClosedMcpNames(baseline, reservedNames) {
134
+ return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
135
+ }
136
+ export function mergeRequestMcp(baseline, gated, reservedNames) {
128
137
  if (gated.length === 0)
129
138
  return baseline;
130
- const taken = new Set((baseline ?? []).map((s) => s.name));
131
- const add = gated.filter((s) => !taken.has(s.name));
139
+ const closed = deploymentClosedMcpNames(baseline, reservedNames);
140
+ const add = gated.filter((s) => !closed.has(s.name));
132
141
  return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
133
142
  }
134
143
  export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
@@ -153,6 +162,16 @@ export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
153
162
  note: "declaration DROPPED — these servers keep the pre-378 default (external content, session gets marked); the servers themselves still mount",
154
163
  });
155
164
  }
156
- return mergeRequestMcp(baseline, v.ok);
165
+ const reservedNames = config.mcpServers?.reservedNames ?? [];
166
+ const closed = deploymentClosedMcpNames(baseline, reservedNames);
167
+ const blocked = v.ok.filter((s) => closed.has(s.name)).map((s) => s.name);
168
+ if (blocked.length > 0) {
169
+ logger?.warn("mcp_injection_dropped", {
170
+ reason: "name_reserved_by_deployment",
171
+ dropped: blocked,
172
+ note: "these names are declared by the deployment (center/config) — mounted or skipped, a caller can neither shadow nor refill them; rename the request-side server",
173
+ });
174
+ }
175
+ return mergeRequestMcp(baseline, v.ok, reservedNames);
157
176
  }
158
177
  //# sourceMappingURL=task-mcp.js.map
@@ -1,6 +1,6 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { uuidv7, MAX_RULE_TEXT_CHARS, parseAllowRuleText } from "@sema-agent/core";
3
+ import { uuidv7, MAX_RULE_TEXT_CHARS, parseAllowRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
5
  import { redactDeep, redactSecrets } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
@@ -69,7 +69,7 @@ class ApprovalStoreDeadlineError extends Error {
69
69
  this.name = "ApprovalStoreDeadlineError";
70
70
  }
71
71
  }
72
- const FS_WRITE_TOOLS = new Set(["Write", "Edit", "NotebookEdit"]);
72
+ const FS_WRITE_TOOLS = pathConfinableWriteToolNames();
73
73
  function sessionAllowCategory(toolName) {
74
74
  return FS_WRITE_TOOLS.has(toolName) ? "fs-write" : `tool:${toolName}`;
75
75
  }
@@ -31,7 +31,7 @@ type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjec
31
31
  type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin";
32
32
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
33
33
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
34
- type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end";
34
+ type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end" | "tool_roster_delta";
35
35
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
36
36
  type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
37
37
  type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
@@ -49,7 +49,7 @@ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "seve
49
49
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
50
50
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
51
51
  type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested";
52
- type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
52
+ type TaskSpecUnwired = "excludeAllTools" | "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
53
53
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
54
54
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
55
55
  type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveMemoryScopes" | "stats" | "editedFiles";
@@ -407,6 +407,27 @@ export declare function workspaceChangedEventData(ev: {
407
407
  * 都红不了(2026-09-04 实测:该钉 26/26 全绿,红出 `autoMode` 的是编译期门)。段级 + 成员级的**编译期**门在
408
408
  * `core-keyset-guard.ts` ⑥b —— 那才是「core 加了段/加了成员」的可靠红点,两道并用缺一不可。
409
409
  */
410
+ /**
411
+ * `tool_roster_delta`(core 7.8.0 [ref] ⑪ 新 `TaskEvent` 臂)—— **运行期**名册变更:整只变更后名册
412
+ * (`delta.roster`)+ 一段只有持有 `fromDigest` 的消费者才用得上的摘要。
413
+ *
414
+ * 契约里唯一的硬话是失败方向:`fromDigest` 对不上**不是拒绝** —— 携带的整只名册无论如何都是新状态,
415
+ * 消费端记一次 skew 后按快照重同步。所以本构造器**不**替消费端判 digest,只判「这一帧的载荷是不是
416
+ * core 说的那个形」。
417
+ *
418
+ * 判形与 {@link wiringManifestEventData} 的 `tools` 段同一条姿势(理由逐字见那里):属主是 core 的
419
+ * typebox schema `ToolRosterDelta`,过了整只透传,没过 ⇒ **整帧不发** + F 类留痕。半张名册变更帧比
420
+ * 不发更坏:消费端会据它把没读到的行当成「这只工具被卸了」。
421
+ *
422
+ * ⚠️ **如实披露(core 7.8.0 自述)**:7.8.0 的 runtask **还没有**推这条帧([ref] S3 才接线),座位与
423
+ * 类型先到货。本臂因此是**前瞻接线**:core 开始推的那一版,server 这一侧零改动即到达消费端;在那之前
424
+ * 它永不触发。消费端在那一版之前**不得**依赖本帧(core 契约原文)。
425
+ */
426
+ export declare function toolRosterDeltaEventData(ev: {
427
+ delta?: unknown;
428
+ eventId?: unknown;
429
+ parentToolCallId?: unknown;
430
+ }): Record<string, unknown> | undefined;
410
431
  export declare function wiringManifestEventData(ev: {
411
432
  manifest?: unknown;
412
433
  eventId?: unknown;
@@ -1,5 +1,6 @@
1
1
  import { redactErrorMessage } from "../observability/run-terminal-log.js";
2
- import { MCP_DELIVERY_VERDICTS, screenGateOutcome } from "@sema-agent/core";
2
+ import { MCP_DELIVERY_VERDICTS, screenGateOutcome, ToolRoster, ToolRosterDelta } from "@sema-agent/core";
3
+ import { Value } from "typebox/value";
3
4
  import { recordFailOpen } from "../observability/fail-open.js";
4
5
  import { redactSecrets, redactDeep } from "./redact.js";
5
6
  import { injectionTierOrAbsent } from "./injection-tier.js";
@@ -94,6 +95,15 @@ export function screenedGate(v) {
94
95
  recordFailOpen("server.trace.gate-outcome-defective", "disposition is not one of the two arms");
95
96
  return undefined;
96
97
  }
98
+ const shapeDefect = (d.kind === "denied" && typeof d.deniedBy !== "string" && "disposition.deniedBy is not a string") ||
99
+ (g.settlement !== undefined && (g.settlement === null || typeof g.settlement !== "object") && "settlement is not an object") ||
100
+ (g.settlement !== undefined && g.settlement !== null && typeof g.settlement.kind !== "string" && "settlement.kind is not a string") ||
101
+ (g.origin !== undefined && typeof g.origin !== "string" && "origin is not a string") ||
102
+ undefined;
103
+ if (shapeDefect !== undefined) {
104
+ recordFailOpen("server.trace.gate-outcome-defective", shapeDefect);
105
+ return undefined;
106
+ }
97
107
  const defects = screenGateOutcome(g);
98
108
  if (defects.length > 0) {
99
109
  recordFailOpen("server.trace.gate-outcome-defective", defects.join("; "));
@@ -250,6 +260,13 @@ export function workspaceChangedEventData(ev) {
250
260
  ...identityFields(ev),
251
261
  };
252
262
  }
263
+ export function toolRosterDeltaEventData(ev) {
264
+ if (!Value.Check(ToolRosterDelta, ev.delta)) {
265
+ recordFailOpen("server.trace.tool-roster-malformed", "tool_roster_delta.delta did not fit core ToolRosterDelta");
266
+ return undefined;
267
+ }
268
+ return { delta: ev.delta, ...identityFields(ev) };
269
+ }
253
270
  export function wiringManifestEventData(ev) {
254
271
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
255
272
  const bool = (v) => (typeof v === "boolean" ? v : undefined);
@@ -314,6 +331,8 @@ export function wiringManifestEventData(ev) {
314
331
  return undefined;
315
332
  return armed === (reason === "armed") ? { armed, reason } : undefined;
316
333
  })();
334
+ const toolsIn = m.tools;
335
+ const tools = toolsIn === undefined ? undefined : Value.Check(ToolRoster, toolsIn) ? toolsIn : (recordFailOpen("server.trace.tool-roster-malformed", "wiring_manifest.tools did not fit core ToolRoster"), undefined);
317
336
  const mcpIn = m.mcp;
318
337
  const mcp = (() => {
319
338
  if (!Array.isArray(mcpIn))
@@ -352,6 +371,7 @@ export function wiringManifestEventData(ev) {
352
371
  ...(permissionRules !== undefined ? { permissionRules } : {}),
353
372
  ...(modelGate !== undefined ? { modelGate } : {}),
354
373
  ...(autoMode !== undefined ? { autoMode } : {}),
374
+ ...(tools !== undefined ? { tools } : {}),
355
375
  ...(mcp !== undefined ? { mcp } : {}),
356
376
  ...identityFields(ev),
357
377
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.64.0",
3
+ "version": "7.66.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",
@@ -54,8 +54,8 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.6.2",
58
- "@sema-agent/settings-schema": "1.8.0",
57
+ "@sema-agent/core": "7.8.0",
58
+ "@sema-agent/settings-schema": "1.9.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
61
61
  "mysql2": "^3.22.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "8.4.0",
72
+ "@sema-agent/sdk": "8.5.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",