@sema-agent/server 7.100.0 → 7.101.1

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 (107) hide show
  1. package/MIGRATION.md +22 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +80 -9
  5. package/dist/approval-card.d.ts +24 -12
  6. package/dist/approval-card.js +4 -3
  7. package/dist/approval-policy-settlement.d.ts +31 -0
  8. package/dist/approval-policy-settlement.js +16 -0
  9. package/dist/approval-reconciler.d.ts +8 -1
  10. package/dist/approval-reconciler.js +5 -4
  11. package/dist/boot/config-center.js +86 -20
  12. package/dist/boot/execution-env.js +20 -2
  13. package/dist/boot/parked-revive-gate.js +1 -0
  14. package/dist/boot/reapers.js +1 -0
  15. package/dist/boot/resolve-spec.js +25 -8
  16. package/dist/boot/runner-deps.js +9 -3
  17. package/dist/boot/stage-02-registry-pull.js +9 -0
  18. package/dist/boot/stage-10-http-server.js +2 -0
  19. package/dist/boot/stores.js +4 -0
  20. package/dist/config-catalog.js +5 -3
  21. package/dist/config-center/apply-effective.d.ts +25 -0
  22. package/dist/config-center/apply-effective.js +144 -69
  23. package/dist/config-center/apply-ledger.d.ts +12 -2
  24. package/dist/config-center/apply-ledger.js +4 -1
  25. package/dist/config-center/facade.d.ts +1 -1
  26. package/dist/config-center/facade.js +1 -1
  27. package/dist/config-center/http-client.d.ts +4 -1
  28. package/dist/config-center/http-client.js +15 -10
  29. package/dist/config-center/read-warnings.d.ts +115 -6
  30. package/dist/config-center/read-warnings.js +115 -5
  31. package/dist/config-center/types.d.ts +6 -0
  32. package/dist/config-invariants.d.ts +2 -2
  33. package/dist/config-invariants.js +15 -0
  34. package/dist/config-provider.d.ts +4 -0
  35. package/dist/config-provider.js +64 -9
  36. package/dist/config-types.d.ts +37 -8
  37. package/dist/config.js +87 -22
  38. package/dist/deployment-governance.d.ts +45 -12
  39. package/dist/deployment-governance.js +37 -9
  40. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  41. package/dist/env-name-allowlist-knobs.js +24 -0
  42. package/dist/fleet/fleet-bus.d.ts +30 -4
  43. package/dist/fleet/fleet-bus.js +20 -4
  44. package/dist/fleet/fleet-reconciler.d.ts +29 -6
  45. package/dist/fleet/fleet-reconciler.js +109 -79
  46. package/dist/fleet-client.d.ts +2 -1
  47. package/dist/hooks/hook-llm.js +13 -4
  48. package/dist/host-lsp-manager.d.ts +14 -2
  49. package/dist/host-lsp-manager.js +7 -6
  50. package/dist/http/admission.js +22 -7
  51. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  52. package/dist/http/memory-store-refusal-reply.js +10 -0
  53. package/dist/http/routes/approvals-assistant.js +1 -0
  54. package/dist/http/routes/capabilities.js +3 -3
  55. package/dist/http/routes/memory-bundle.js +3 -0
  56. package/dist/http/routes/memory-compliance.js +3 -5
  57. package/dist/http/routes/memory-origin.js +3 -5
  58. package/dist/http/routes/sessions.js +7 -6
  59. package/dist/http/routes/side-query.js +10 -4
  60. package/dist/instruction-source-notice.d.ts +97 -0
  61. package/dist/instruction-source-notice.js +78 -0
  62. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  63. package/dist/memory-layer-legacy-lock.js +74 -0
  64. package/dist/memory-operator-faces.js +1 -17
  65. package/dist/memory-store-refusal.d.ts +37 -0
  66. package/dist/memory-store-refusal.js +14 -0
  67. package/dist/model-entry-refusal.d.ts +130 -0
  68. package/dist/model-entry-refusal.js +157 -0
  69. package/dist/model-provider.d.ts +9 -3
  70. package/dist/model-route-endpoint.d.ts +1 -1
  71. package/dist/model-select.d.ts +49 -4
  72. package/dist/model-select.js +48 -17
  73. package/dist/observability/fail-open.d.ts +13 -1
  74. package/dist/observability/fail-open.js +13 -1
  75. package/dist/observability/metrics.js +1 -0
  76. package/dist/observability/secret-env-scrub.d.ts +27 -4
  77. package/dist/observability/secret-env-scrub.js +22 -7
  78. package/dist/plugins/caching-session-store.d.ts +6 -0
  79. package/dist/plugins/caching-session-store.js +6 -0
  80. package/dist/plugins/local-session-store.d.ts +20 -25
  81. package/dist/plugins/local-session-store.js +111 -64
  82. package/dist/plugins/pg-session-storage.d.ts +7 -0
  83. package/dist/plugins/pg-session-storage.js +19 -0
  84. package/dist/plugins/remote-env-host.d.ts +3 -1
  85. package/dist/plugins/remote-env-host.js +3 -6
  86. package/dist/plugins/tidb-pool.js +5 -0
  87. package/dist/plugins/tidb-session-store.d.ts +13 -0
  88. package/dist/plugins/tidb-session-store.js +14 -0
  89. package/dist/plugins/web-search.d.ts +2 -1
  90. package/dist/plugins/web-search.js +43 -24
  91. package/dist/project-memory.d.ts +10 -0
  92. package/dist/project-memory.js +28 -14
  93. package/dist/run-local.d.ts +17 -15
  94. package/dist/run-local.js +115 -15
  95. package/dist/sealed-key.d.ts +5 -2
  96. package/dist/server-secret-env.d.ts +23 -1
  97. package/dist/server-secret-env.js +9 -0
  98. package/dist/tool-approval.d.ts +74 -31
  99. package/dist/tool-approval.js +102 -49
  100. package/dist/trace/core-keyset-guard.d.ts +27 -5
  101. package/dist/trace/engine-notice-wire.d.ts +27 -1
  102. package/dist/trace/engine-notice-wire.js +8 -0
  103. package/dist/trace/wire-projection-faces.d.ts +2 -2
  104. package/dist/trace/wire-projection-faces.js +2 -2
  105. package/package.json +2 -2
  106. package/dist/memory-layer-preflight.d.ts +0 -46
  107. package/dist/memory-layer-preflight.js +0 -121
@@ -1,6 +1,7 @@
1
1
  import { expandTiers, isThinkingLevel } from "@sema-agent/core";
2
2
  import { HttpError } from "../../security.js";
3
- import { isModelAllowlisted } from "../../model-select.js";
3
+ import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
4
+ import { modelUnavailableMessage } from "../../model-entry-refusal.js";
4
5
  import { redactSecrets } from "../../trace/redact.js";
5
6
  import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
6
7
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
@@ -68,9 +69,14 @@ export async function handleSideQuery(req, res, match, ctx) {
68
69
  return;
69
70
  }
70
71
  if (typeof body.model === "string" && body.model.length > 0) {
71
- const bare = deps.config.models ?? {};
72
- const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
73
- if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist)) {
72
+ const catalog = selectableCatalog(deps.config.models ?? {}, deps.config.tiers, deps.config.modelRefusals);
73
+ const unavailable = matchCatalogModel(body.model, catalog, deps.config.modelRefusals) === undefined ? refusedCatalogEntry(body.model, deps.config.modelRefusals) : undefined;
74
+ if (unavailable !== undefined) {
75
+ const message = modelUnavailableMessage("model", body.model.slice(0, 120), unavailable.name, unavailable.refusal);
76
+ sendError(res, 400, "request.model_unavailable", message);
77
+ return;
78
+ }
79
+ if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist, deps.config.modelRefusals)) {
74
80
  sendError(res, 400, "request.model_not_allowed", `model is not in this deployment's @-mention allowlist: "${body.model.slice(0, 120)}" — the allowlist is configured by the operator (config-center models.atModelAllowlist)`);
75
81
  return;
76
82
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * S-690 —— 项目指令文件**换源**通告(`engine_notice` 码 `instructions.source_changed`)的唯一铸点与基线属主口。
3
+ *
4
+ * ## 这件事为什么归本仓
5
+ * 选哪一份指令文件是**部署的内容半场**(`project-memory.ts` 的 `INSTRUCTION_FILE_CHAIN`;core 的 `loadProjectMemory`
6
+ * 契约逐字「core … NEVER hardcodes a filename」),所以「这一 run 读的是哪一份、上一 run 读的是哪一份」也只有本仓
7
+ * 答得出。core 的 run 内变更车道(`steering_injected`,`source: "instructions_change"`)只跟**本 run 自己声明的**
8
+ * 来源比,进程重启(= 升级)后没有任何东西记得上一 run 注入的是哪一份 —— 于是两文件并存的仓升级后静默换源
9
+ * (车MN 收车档 §1.5,判据 G6)。
10
+ *
11
+ * ## 基线 = 会话的服务端元数据
12
+ * 基线是「同一 session 上一次 run 选中的链成员相对路径」(不是指纹:指纹密钥每进程随机,跨进程不可比)。它住在会话店的
13
+ * 服务端元数据面 —— SQL 两孪生是 `session_meta.instruction_source` 一列,local 文件店是一只 sidecar —— 所以重启 /
14
+ * 跨副本都读得到、随会话删除一起消失,且不进模型上下文、不进 wire 会话面。不接这张面的店(core 的内存 dev 店、
15
+ * `run-local` 的 core 文件店)⇒ 席缺席 ⇒ 一帧不发(诚实缺席:那两形的会话本来就活不过进程,或没有会话流可投)。
16
+ *
17
+ * ## 铸点(唯一)
18
+ * {@link announceInstructionSource} 是本码**唯一**的铸点(`@contract instructions.source_changed.single_mint`)。
19
+ * 调用点只有一处:`makeLoadProjectMemory` 在每次**走了指令链**的 prepare 载入完成之后(core 每个 run 恰调一次 `loadProjectMemory`)
20
+ * ⇒ 一 run 至多一帧。走链之前就判「这次没有项目可读」的 prepare(无手场景、cwd 不是本机绝对目录)是**不适用**,不是
21
+ * 「来源 = 无」—— 不进本铸点,基线保持上一次真读的那份(O6,7.101.0 合并复审:无手孪生 Runner 与主 Runner 共用同一只
22
+ * loader 与会话店,把不适用折成 `null` 会让「默认 → 无手 → 默认」同 session 两跳无端发两帧)。分工照 `task-mcp.ts` `mintInjectionDropNotices` 的先例:本模块铸通告,`emit` 由装配方给
23
+ * (部署的 `onNotice` 转发体 —— 先日志后分流,白名单 / 归属 / 消毒 / 隔离四件判据一个不绕)。
24
+ *
25
+ * ## 不做的事
26
+ * 不区分「升级换源」与「用户改了文件」(跨进程指纹不可比;载荷只说前后两个来源)。detail 没有 `runId`:铸点在 core 的
27
+ * `loadProjectMemory` 席内,该席的 ctx 不带 runId(core 7.30.1 键集 = cwd / handsEnabled / isSubagent / agentName /
28
+ * sessionId / phase)—— 与 `mcp.injection_dropped`「装配期铸、无 runId」同形,run 归属由投递腿承载(这条 run 的 SSE /
29
+ * 这条 run 的账本)。向 core 要 ctx 加 runId 已登记为未闭环项。
30
+ */
31
+ import type { EngineNotice } from "@sema-agent/core";
32
+ import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
33
+ export declare const INSTRUCTION_SOURCE_CHANGED: ServerNoticeCode;
34
+ /** 基线读数。`recorded: false` = 这个会话还没有任何 run 记过基线(首 run ⇒ 只写不发);`source: null` = 记过,且那一次
35
+ * 没有项目指令文件被选中。两态必须可判别:把「没记过」读成「记过没有」会让新会话首 run 发一帧 `null → CLAUDE.md`。 */
36
+ export type InstructionSourceBaseline = {
37
+ readonly recorded: false;
38
+ } | {
39
+ readonly recorded: true;
40
+ readonly source: string | null;
41
+ };
42
+ /** 基线属主口(会话店的服务端元数据面)。 */
43
+ export interface InstructionSourceStore {
44
+ readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
45
+ /**
46
+ * 覆写基线 —— **CAS**:只在店里此刻的基线仍等于 `expected`(调用方读到的那一份)时写,否则什么都不做(不抛)。
47
+ * 为什么是 CAS(codex che-NM r1 [high]):铸点对基线读写有期限,超期的写会**晚到**;不带期望值的覆写会让一个
48
+ * 基于过期读数的旧 run 盖掉更新的 run 写下的基线,下一 run 于是比错。会话行不存在 ⇒ 同样不写。
49
+ * 写**没有提交**必须抛(SQL 的写本来就如此;local sidecar 回滚内存再抛)—— 「内存已前进而盘没跟上」会让同来源的
50
+ * 后续 run 在相等比较上短路、再也不重写,重启后比的是一份过期的盘(codex che-NM r1 [medium])。
51
+ */
52
+ recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
53
+ }
54
+ /** 一只装配好的席:基线店 + 通告出口。 */
55
+ export interface InstructionSourceSeat {
56
+ readonly store: InstructionSourceStore;
57
+ readonly emit: (notice: EngineNotice) => void;
58
+ }
59
+ /**
60
+ * 基线的**存储编码**(SQL 列与 local sidecar 同一只):「没有项目指令文件」存成空串,链成员存它的相对路径;
61
+ * 「没记过」是 SQL `NULL` / sidecar 里缺键。空串不会与任何文件名撞(没有叫空名的文件),所以不需要保留词。
62
+ */
63
+ export declare function encodeInstructionSource(source: string | null): string;
64
+ /** 两份基线读数是否同一(CAS 的判等;三店与测试夹具同用这一只)。 */
65
+ export declare function sameInstructionSourceBaseline(a: InstructionSourceBaseline, b: InstructionSourceBaseline): boolean;
66
+ /** {@link encodeInstructionSource} 的逆。非串非空的值 = 这一格被本仓以外的东西写坏了 ⇒ 抛(调用方按基线读不出处置)。 */
67
+ export declare function decodeInstructionSource(raw: unknown): InstructionSourceBaseline;
68
+ /**
69
+ * 会话店是否提供基线面 —— **两个方法都在**才算(只有一半的店让读写不同源,比整张缺席更坏)。结构判而不是按类名判
70
+ * (包装店 `CachingSessionStore` 逐方法转发,判据看的是能力不是名字)。
71
+ */
72
+ export declare function instructionSourceStoreOf(sessionStore: unknown): InstructionSourceStore | undefined;
73
+ /** 装配点的唯一口(server boot 与 run-local 两处同走):店没有基线面或没有通告出口 ⇒ 席缺席(一帧不发)。 */
74
+ export declare function createInstructionSourceSeat(sessionStore: unknown, emit: ((notice: EngineNotice) => void) | undefined): InstructionSourceSeat | undefined;
75
+ /**
76
+ * 基线每一次读 / 写的期限(codex che-NM r1 [high])。铸点跑在 prepare 里、loader 等它交出答案才返回,而两支 SQL 孪生的
77
+ * 查询默认没有超时(MySQL 无、PG `statement_timeout` 缺省关)—— 一次卡住的行锁 / 连接就会把整个 prepare 挂住,而修前
78
+ * loader 根本不碰库。两层同一个数:① 这里放手(超期 ⇒ 按读 / 写失败处置,留痕放行);② 两支 SQL 孪生的基线查询带**每查询
79
+ * 超时**(codex che-NM r2 [high];S-9 `write-behind-counter.ts` 先例:mysql2 `{sql, timeout}` / pg `query_timeout`,pg-pool
80
+ * 超时即销毁那条连接)—— 只放手不截断,卡住的查询会一直占着池里的连接。超期后仍在飞的写由 CAS 挡住,不会盖掉更新的基线。
81
+ * 数值与 core 给 `probeInstructionSources` 的 2s 天花板同量级:一行 PK 读写在健康库上是毫秒级。
82
+ */
83
+ export declare const BASELINE_IO_DEADLINE_MS = 2000;
84
+ /**
85
+ * **唯一铸点**:本次 prepare 选中的来源 `current`(链成员相对路径;没有 = `null`)与基线比。
86
+ * · 没有基线 ⇒ 只写不发(新会话首 run);
87
+ * · 相同 ⇒ 什么都不做(不写);
88
+ * · 不同 ⇒ 铸一帧、再以读到的基线为期望值 CAS 覆写。
89
+ * 基线读 / 写各有 {@link BASELINE_IO_DEADLINE_MS} 的期限。
90
+ *
91
+ * 失败方向(F 类,`server.instruction-source.announce-degraded`,detail 带 `op=read|write|emit`):通告是**披露**面
92
+ * 不是执法面,所以基线读 / 写 / 出口失败一律留痕后放行,**绝不**抛回调用方 —— 抛回去会让 core 把这次 prepare 的项目记忆整块丢掉(loader 抛 ⇒ 不注入),
93
+ * 为一条通告赔上模型要读的指令本身。读失败 ⇒ 本 run 不比也不写;写失败 ⇒ 本 run 的帧照发(它对本 run 为真),代价是
94
+ * 下一 run 可能再说一遍同一次换源;出口抛(转发体今天结构上不抛,守的是「本函数不抛」这条契约)⇒ 本 run 缺这一帧。
95
+ */
96
+ export declare function announceInstructionSource(seat: InstructionSourceSeat, sessionId: string, current: string | null): Promise<void>;
97
+ //# sourceMappingURL=instruction-source-notice.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { recordFailOpen } from "./observability/fail-open.js";
2
+ export const INSTRUCTION_SOURCE_CHANGED = "instructions.source_changed";
3
+ export function encodeInstructionSource(source) {
4
+ return source ?? "";
5
+ }
6
+ export function sameInstructionSourceBaseline(a, b) {
7
+ if (!a.recorded || !b.recorded)
8
+ return a.recorded === b.recorded;
9
+ return a.source === b.source;
10
+ }
11
+ export function decodeInstructionSource(raw) {
12
+ if (raw === null || raw === undefined)
13
+ return { recorded: false };
14
+ if (typeof raw !== "string")
15
+ throw new TypeError(`instruction_source holds a ${typeof raw}, not a string`);
16
+ return { recorded: true, source: raw === "" ? null : raw };
17
+ }
18
+ export function instructionSourceStoreOf(sessionStore) {
19
+ const s = sessionStore;
20
+ if (typeof s?.readInstructionSource !== "function" || typeof s.recordInstructionSource !== "function")
21
+ return undefined;
22
+ return { readInstructionSource: s.readInstructionSource.bind(s), recordInstructionSource: s.recordInstructionSource.bind(s) };
23
+ }
24
+ export function createInstructionSourceSeat(sessionStore, emit) {
25
+ const store = instructionSourceStoreOf(sessionStore);
26
+ return store && emit ? { store, emit } : undefined;
27
+ }
28
+ const describeSource = (source) => source ?? "no project instruction file";
29
+ export const BASELINE_IO_DEADLINE_MS = 2_000;
30
+ class BaselineDeadlineExceeded extends Error {
31
+ }
32
+ function withinDeadline(op) {
33
+ const running = Promise.resolve().then(op);
34
+ running.catch(() => undefined);
35
+ let timer;
36
+ const deadline = new Promise((_, reject) => {
37
+ timer = setTimeout(() => reject(new BaselineDeadlineExceeded(`no answer within ${BASELINE_IO_DEADLINE_MS}ms`)), BASELINE_IO_DEADLINE_MS);
38
+ timer.unref?.();
39
+ });
40
+ return Promise.race([running, deadline]).finally(() => clearTimeout(timer));
41
+ }
42
+ export async function announceInstructionSource(seat, sessionId, current) {
43
+ let baseline;
44
+ try {
45
+ baseline = await withinDeadline(() => seat.store.readInstructionSource(sessionId));
46
+ }
47
+ catch (e) {
48
+ degraded("read", sessionId, e);
49
+ return;
50
+ }
51
+ if (baseline.recorded && baseline.source === current)
52
+ return;
53
+ if (baseline.recorded) {
54
+ const previous = baseline.source;
55
+ try {
56
+ seat.emit({
57
+ code: INSTRUCTION_SOURCE_CHANGED,
58
+ message: `Project instructions for this run come from ${describeSource(current)}; the previous run of this session read ${describeSource(previous)}.`,
59
+ detail: { previous, current, sessionId },
60
+ sessionId,
61
+ });
62
+ }
63
+ catch (e) {
64
+ degraded("emit", sessionId, e);
65
+ }
66
+ }
67
+ try {
68
+ await withinDeadline(() => seat.store.recordInstructionSource(sessionId, baseline, current));
69
+ }
70
+ catch (e) {
71
+ degraded("write", sessionId, e);
72
+ }
73
+ }
74
+ function degraded(op, sessionId, e) {
75
+ const why = e instanceof BaselineDeadlineExceeded ? `timeout=${BASELINE_IO_DEADLINE_MS}ms` : `err=${e instanceof Error ? e.message : String(e)}`;
76
+ recordFailOpen("server.instruction-source.announce-degraded", `op=${op} session=${sessionId} ${why}`);
77
+ }
78
+ //# sourceMappingURL=instruction-source-notice.js.map
@@ -0,0 +1,14 @@
1
+ import type { EngineNotice } from "@sema-agent/core";
2
+ import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
3
+ export declare const MEMORY_LAYER_LOCKED_LEGACY: ServerNoticeCode;
4
+ /** 探测读数:锁住的层名(排序)+ 看不全的目录条数。挂载面不存在(新数据根)⇒ 两者皆空。 */
5
+ export interface LockedLegacyLayerProbe {
6
+ readonly layers: readonly string[];
7
+ readonly unreadable: number;
8
+ }
9
+ export declare function probeLockedLegacyMemoryLayers(memoryDir: string): LockedLegacyLayerProbe;
10
+ /** 锁住的层 ⇒ 一条通告(纯数据);没有 ⇒ `undefined`。detail 两键:`layers`(层名)与 `remedy`(同一句 `chmod -R u+w <memoryDir>`)。 */
11
+ export declare function buildMemoryLayerLockedLegacyNotice(memoryDir: string, layers: readonly string[]): EngineNotice | undefined;
12
+ /** boot 期的唯一调用口:探测一次、有锁住的层就经 `emit` 发一条;看不全的子树走 F 类留痕。不抛(不拒启)。 */
13
+ export declare function announceLockedLegacyMemoryLayers(memoryDir: string, emit: (notice: EngineNotice) => void): void;
14
+ //# sourceMappingURL=memory-layer-legacy-lock.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { lstatSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { recordFailOpen } from "./observability/fail-open.js";
4
+ export const MEMORY_LAYER_LOCKED_LEGACY = "memory.layer_locked_legacy";
5
+ const OWNER_WRITE = 0o200;
6
+ function shellQuoted(s) {
7
+ return `'${s.replace(/'/g, "'\\''")}'`;
8
+ }
9
+ const isMissing = (err) => err?.code === "ENOENT";
10
+ function treeHasLockedDir(dir, gaps) {
11
+ let st;
12
+ try {
13
+ st = lstatSync(dir);
14
+ }
15
+ catch (err) {
16
+ if (!isMissing(err))
17
+ gaps.n += 1;
18
+ return false;
19
+ }
20
+ if (!st.isDirectory())
21
+ return false;
22
+ if ((st.mode & OWNER_WRITE) === 0)
23
+ return true;
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ }
28
+ catch (err) {
29
+ if (!isMissing(err))
30
+ gaps.n += 1;
31
+ return false;
32
+ }
33
+ for (const e of entries) {
34
+ if (e.isDirectory() && treeHasLockedDir(join(dir, e.name), gaps))
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ export function probeLockedLegacyMemoryLayers(memoryDir) {
40
+ const gaps = { n: 0 };
41
+ let top;
42
+ try {
43
+ top = readdirSync(memoryDir, { withFileTypes: true });
44
+ }
45
+ catch (err) {
46
+ return { layers: [], unreadable: isMissing(err) ? 0 : 1 };
47
+ }
48
+ const layers = top.filter((e) => e.isDirectory() && treeHasLockedDir(join(memoryDir, e.name), gaps)).map((e) => e.name);
49
+ return { layers: layers.sort(), unreadable: gaps.n };
50
+ }
51
+ export function buildMemoryLayerLockedLegacyNotice(memoryDir, layers) {
52
+ if (layers.length === 0)
53
+ return undefined;
54
+ const remedy = `chmod -R u+w ${shellQuoted(memoryDir)}`;
55
+ const one = layers.length === 1;
56
+ return {
57
+ code: MEMORY_LAYER_LOCKED_LEGACY,
58
+ message: `memory layer${one ? "" : "s"} ${layers.map((l) => JSON.stringify(l)).join(", ")} ${one ? "still carries" : "still carry"} the read-only permission bits ` +
59
+ "an engine from 7.30.x or earlier (a server replica at 7.100.0 or earlier) sets on every layer it mounts read-only — deleting an " +
60
+ "entry there (memory erase / origin clear) fails for a server that does not run as root: the request answers 500, its journal " +
61
+ `stays on disk and this replica's next start fails. Run \`${remedy}\` once no such engine still runs on this store (an older ` +
62
+ "replica sets the bits again whenever it mounts a layer read-only). Starting anyway.",
63
+ detail: { layers: [...layers], remedy },
64
+ };
65
+ }
66
+ export function announceLockedLegacyMemoryLayers(memoryDir, emit) {
67
+ const probe = probeLockedLegacyMemoryLayers(memoryDir);
68
+ if (probe.unreadable > 0)
69
+ recordFailOpen("server.memory.locked-layer-probe-partial", `unreadable=${probe.unreadable}`);
70
+ const notice = buildMemoryLayerLockedLegacyNotice(memoryDir, probe.layers);
71
+ if (notice !== undefined)
72
+ emit(notice);
73
+ }
74
+ //# sourceMappingURL=memory-layer-legacy-lock.js.map
@@ -1,6 +1,5 @@
1
1
  import { MemoryEngine, } from "@sema-agent/core";
2
2
  import { memoryMountRootOf } from "./memory-scope.js";
3
- import { memoryLayerWriteRefusal } from "./memory-layer-preflight.js";
4
3
  export function backendOwnsControlPlane(backend) {
5
4
  const b = backend;
6
5
  return typeof b?.controlPlaneRoot === "string" && b.controlPlaneRoot !== "";
@@ -41,7 +40,6 @@ function erasureEnvelopeRefusal(input, allowedScopes) {
41
40
  export function createMemoryComplianceFaces(store, opts) {
42
41
  if (!backendOwnsControlPlane(store.backend))
43
42
  return undefined;
44
- const mountRoot = memoryMountRootOf(store.backend, store.root);
45
43
  const engine = new MemoryEngine({
46
44
  backend: store.backend,
47
45
  memoryDir: memoryMountRootOf(store.backend, store.root),
@@ -59,14 +57,6 @@ export function createMemoryComplianceFaces(store, opts) {
59
57
  const refusal = erasureEnvelopeRefusal(input, opts?.allowedScopes);
60
58
  if (refusal !== undefined)
61
59
  return Promise.reject(refusal);
62
- try {
63
- const locked = memoryLayerWriteRefusal(mountRoot);
64
- if (locked !== undefined)
65
- return Promise.reject(locked);
66
- }
67
- catch (err) {
68
- return Promise.reject(err);
69
- }
70
60
  return engine.eraseMemoryEntries(input);
71
61
  },
72
62
  };
@@ -74,7 +64,6 @@ export function createMemoryComplianceFaces(store, opts) {
74
64
  export function createMemoryOriginFaces(store, opts) {
75
65
  if (!backendOwnsControlPlane(store.backend))
76
66
  return undefined;
77
- const mountRoot = memoryMountRootOf(store.backend, store.root);
78
67
  const engine = new MemoryEngine({
79
68
  backend: store.backend,
80
69
  memoryDir: memoryMountRootOf(store.backend, store.root),
@@ -84,12 +73,7 @@ export function createMemoryOriginFaces(store, opts) {
84
73
  return {
85
74
  listExternal: (scopes) => engine.listExternalOriginEntries(scopes),
86
75
  listClearances: () => engine.listOriginClearances(),
87
- clear: async (entryId, input) => {
88
- const locked = memoryLayerWriteRefusal(mountRoot);
89
- if (locked !== undefined)
90
- throw locked;
91
- return engine.clearEntryOrigin(entryId, input);
92
- },
76
+ clear: (entryId, input) => engine.clearEntryOrigin(entryId, input),
93
77
  };
94
78
  }
95
79
  export function createSessionMemoryStatusFace(store, opts) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * core 7.31.1 #1094 —— **`memory.target_outside_store`** 在本仓的唯一读法属主(纯叶:零 http / 零 boot 依赖)。
3
+ *
4
+ * ## 这个码说的是什么(core `docs/wire-consumption/7.31.1.md` 逐 face 表,tag v7.31.1 亲读)
5
+ * 记忆 store 里的一个目录经软链通到了 store **外**(或 store 根在 backend 打开 / 引擎首次挂载之后被改指)⇒ 引擎对落点在外的
6
+ * 删 / 写 / 重放 / 自身写一律**不做**,抛普通 `Error` + `.code`(文案以码领头,点名动作、路径、解析到哪)。core 的读法原话:
7
+ * operator 要处置的拒(去掉那只目录链),**不是**可重试的 IO 故障,也**不是**对调用方的裁决;链去掉之前重发恒拒。
8
+ *
9
+ * ## 两条到达车道,两个读者(本模块各给一只,别处不许再写第二份判据)
10
+ * ① **抛出面**(抹除 / 出处 / 外源列表 / 清标 / bundle 导出导入 —— 凡触 store 树的 backend 面):`.code` 就是码
11
+ * ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,一处字面量)。
12
+ * ② **报告面**(harvest `incident`):core 契约写明 report 车道上码**只在文本里** —— `sidecar_corrupt` 的 detail 恰两种形
13
+ * `memory commit refused: memory.target_outside_store: …` / `memory control plane refused: memory.target_outside_store: …`
14
+ * (core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字)⇒ {@link targetOutsideStoreInIncident} 只认**这两个 lead 紧跟码**
15
+ * 的位置:lead 与码都是 core 铸的字面,路径与引用数据全在码之后。🔴 不做全文子串找(codex 一审 C2:一条真控制面损坏若在
16
+ * 它引用的坏数据里恰好带着这串字,子串找会把它标成目录链 —— 两者运维动作相反)。不读散文、不猜语义;结构化码位
17
+ * (`HarvestReport.incident` / `.warnings` 带码)是向 core 索取的欠账。
18
+ * ⚠️ 同一拒的**还原类**(缺失文件还原 / 只读层还原 / `MEMORY.md` 写)只进 `HarvestReport.warnings`(可以 `ok:true`、无 incident),
19
+ * 那几行的前缀是**条目路径**(模型可写)—— 本仓今天不读 warnings 车道(既有面,不是本批回归),也不在模型可写的文本上
20
+ * 找码;这一半候 core 的结构化码位(登记在案,codex 一审 C3)。
21
+ *
22
+ * ## 不是这个码的三形(core 同表「different surface」,本仓**不**自拼码,如实走各自既有臂)
23
+ * · journal 重放目标目录成链 ⇒ `ControlPlaneCorruptError`(无 `.code`,KL-1094-b)⇒ 路由 typed 500 `internal.memory_control_plane_corrupt`;
24
+ * · 引擎原地写撞硬链(nlink>1)⇒ 文本 `has N names (a hard link)`、无码(KL-1094-g / #1105)⇒ 走该写点既有的失败车道(harvest
25
+ * warnings / 挂载失败),抛出面够不着;
26
+ * · 解析故障(`EACCES` / `ELOOP` / `ENOTDIR`)⇒ 以 errno 本身抛 ⇒ 路由未分类 500 + 运维 warn(`String(err)` 带 core 补救句)。
27
+ */
28
+ /** core 的码(`src/core/memory-engine/layout.ts` `TARGET_OUTSIDE_STORE`;core 不在包根导出它,本常量是它在本仓的唯一写点)。 */
29
+ export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
30
+ /** 抛出面:`err` 是这一拒 ⇒ 原样交回(已窄化成 `Error`,调用方要它的 `message`);否则 `undefined`。形 = core 的 `Error` + `.code`。 */
31
+ export declare function targetOutsideStoreOf(err: unknown): Error | undefined;
32
+ /** 报告面:harvest `incident` 是这一拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成链)。 */
33
+ export declare function targetOutsideStoreInIncident(incident: {
34
+ readonly kind: string;
35
+ readonly detail: string;
36
+ }): typeof TARGET_OUTSIDE_STORE | undefined;
37
+ //# sourceMappingURL=memory-store-refusal.d.ts.map
@@ -0,0 +1,14 @@
1
+ export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
2
+ export function targetOutsideStoreOf(err) {
3
+ if (!(err instanceof Error))
4
+ return undefined;
5
+ const { code } = err;
6
+ return code === TARGET_OUTSIDE_STORE ? err : undefined;
7
+ }
8
+ const INCIDENT_PREFIXES = [`memory commit refused: ${TARGET_OUTSIDE_STORE}: `, `memory control plane refused: ${TARGET_OUTSIDE_STORE}: `];
9
+ export function targetOutsideStoreInIncident(incident) {
10
+ if (incident.kind !== "sidecar_corrupt")
11
+ return undefined;
12
+ return INCIDENT_PREFIXES.some((p) => incident.detail.startsWith(p)) ? TARGET_OUTSIDE_STORE : undefined;
13
+ }
14
+ //# sourceMappingURL=memory-store-refusal.js.map
@@ -0,0 +1,130 @@
1
+ /** 不可用的成因 —— **闭集**(加一个成因 ⇒ {@link MODEL_ENTRY_REFUSAL_CODES} 与 {@link refusalEventOf} 编译红)。 */
2
+ export type ModelEntryRefusalReason = "no_brain" | "entry_invalid";
3
+ /** 机读码(拼法随 `config.*` 家族,按**规则**命名):出现在拒因句的方括号里、warn 字段里、`modelRefusals` 里。 */
4
+ export declare const MODEL_ENTRY_REFUSAL_CODES: {
5
+ readonly no_brain: "config.model_no_brain";
6
+ readonly entry_invalid: "config.model_entry_invalid";
7
+ };
8
+ export type ModelEntryRefusalCode = (typeof MODEL_ENTRY_REFUSAL_CODES)[ModelEntryRefusalReason];
9
+ /** 「这张表没有缺省模型」的机读码(S-705;boot 拒启与热路径模型面组拒同一个码)。 */
10
+ export declare const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
11
+ /** 🔴 S-712「被拒的名字被档位 / 别名增广铸到了别的条目上(或读不出被丢条目的名字,无从证明没有)」的机读码
12
+ * (不变量 `refused-names-not-minted`;热路径 / boot 首次 apply / dry-run 同一个码)。 */
13
+ export declare const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
14
+ /**
15
+ * 一条不可用条目的**事实**(纯数据;`config.modelRefusals[<name>]` 的值)。
16
+ * 🔴 不含任何值:不含 baseUrl、不含钥匙、不含坏字段的原值 —— 只有成因、码、上游 id(显式选择按 id 形也要命中)、
17
+ * 坏字段的**键名**,与一句给运维的出路。
18
+ */
19
+ export interface ModelEntryRefusal {
20
+ readonly reason: ModelEntryRefusalReason;
21
+ readonly code: ModelEntryRefusalCode;
22
+ /** 条目的上游 `id`(壳常按 id 形回读 mainLoopModel —— 按 id 选中同样要拿到拒因,不许落「未知模型」)。 */
23
+ readonly id: string;
24
+ /** 条目写的 provider 词(`no_brain` 的主语;`entry_invalid` 时照录,词可能本身就是坏的那一处)。 */
25
+ readonly provider: string;
26
+ /** `entry_invalid`:不合形的字段键路径(`contextWindow` / `cost.input` / `api` …);`no_brain` 恒空。 */
27
+ readonly fields: readonly string[];
28
+ /** 一句话:为什么不可用 + 出路(不含值)。 */
29
+ readonly detail: string;
30
+ }
31
+ /** 判据要知道的**部署事实**(不在条目里)。 */
32
+ export interface CatalogEntryVerdictCtx {
33
+ /** anthropic 路由装上了没有(= `config.anthropic` 在场,`createBrain` 同判)。 */
34
+ readonly anthropicRouteMounted: boolean;
35
+ }
36
+ /** 条目最小读面(`CenterModel` 与本地投影都结构满足;形判走 settings-schema,不信这张型)。 */
37
+ export interface CatalogEntryLike {
38
+ readonly name: string;
39
+ readonly id: string;
40
+ readonly provider: string;
41
+ }
42
+ /**
43
+ * **逐条判据**:`undefined` = 本部署能完整服务它。判序即语义:先形(不合形的条目谈不上走哪条腿),再腿。
44
+ *
45
+ * 形判的属主是 settings-schema 的 `ModelEntry`(两条读腿用的就是它)—— 本仓不手抄任何一条字段规则;这里只是
46
+ * 在第二道边界(绕过读腿的输入)上**再问一次同一个属主**,并把答案变成一条带码的拒因。只取 `success` 与 issue
47
+ * 坐标,不取它的输出(输出会补缺省、剥扩展键,那是读腿的事)。
48
+ */
49
+ export declare function judgeCatalogEntry(m: CatalogEntryLike, ctx: CatalogEntryVerdictCtx): ModelEntryRefusal | undefined;
50
+ /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及身份证明不了的被丢条目(坐标)。 */
51
+ export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
52
+ readonly usable: readonly M[];
53
+ readonly refused: Readonly<Record<string, ModelEntryRefusal>>;
54
+ /** S-712:读腿逐条丢掉、而身份证明不了的条目坐标(`models.3`;载体上不带 `name` 的那些 —— 读腿只在原文档对不上本次解析时
55
+ * 这样交出,见 `read-warnings.ts` `DroppedModelIdentity`)。非空 ⇒ 不变量 `refused-names-not-minted` 无从成立 ⇒ 模型面组拒。 */
56
+ readonly unreadable: readonly string[];
57
+ }
58
+ /**
59
+ * 批量形(模型面 stage 与 dry-run 共用这一只 —— 报数与真 apply 永不撕裂)。
60
+ * S-712:`dropped` = 读腿交出的被丢条目身份(`EffectiveConfig.models.droppedIdentities`)—— 它们与逐条判据判出的 `entry_invalid`
61
+ * 是**同一件事**(条目不合 `ModelEntry` 形),只是在第一道边界就被丢了;带名字的铸墓碑,不带名字的(读腿证明不了身份)进
62
+ * `unreadable`。没有字符串名的坏行读腿已按停用处置、根本不进载体(7.101.0 验修轮 V-2:没有名字就没有可保留的名字)。
63
+ * 入参按 `unknown` 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改),形不对的一律按证明不了处置(fail-closed)。
64
+ * 与一条可用 / 已判不可用条目同名时不覆盖(可用条目仍可用;同名撞车由不变量 `refused-names-not-minted` 响亮拒)。
65
+ */
66
+ export declare function judgeCatalogEntries<M extends CatalogEntryLike>(enabled: readonly M[], ctx: CatalogEntryVerdictCtx, dropped?: unknown): CatalogEntriesVerdict<M>;
67
+ /** 成因 → 它那一条 warn 的事件名(**穷举**)。两个名字是票面点名的既有/新事件,运维的告警规则按名挂。 */
68
+ export declare function refusalEventOf(reason: ModelEntryRefusalReason): "sema_registry_model_no_brain" | "sema_registry_model_entry_invalid";
69
+ /** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。 */
70
+ export declare function refusalWarnFields(name: string, r: ModelEntryRefusal): Record<string, unknown>;
71
+ /**
72
+ * 一次**显式选择**命中了不可用条目时的拒因句(HTTP `request.model_unavailable` 的 `error`、run-local 的 stderr、
73
+ * hook 选模型的失败文案 —— 各处同一句)。`field` = 调用方的通道名(`model` / `compactionModel` / `agents[0].model` /
74
+ * `@-mention`),`requested` = 调用方发来的原形(name 或 id),已由调用方截断。
75
+ */
76
+ export declare function modelUnavailableMessage(field: string, requested: string, name: string, r: ModelEntryRefusal): string;
77
+ /** 缺省模型不变量读的切面(`ServiceConfig` 与 `CrossInvariantView` 都结构满足)。 */
78
+ export interface DefaultModelView {
79
+ readonly models: Readonly<Record<string, unknown>>;
80
+ readonly modelRefusals: Readonly<Record<string, ModelEntryRefusal>>;
81
+ }
82
+ /**
83
+ * 这张表是从哪条腿来的(闭集 —— 拒因句的出路按腿分:加腿不补 {@link defaultModelRefusal} 的臂 = 编译红)。
84
+ * · `env` —— 进程此刻服务的表**没有目录在管**(没配目录 / 目录迟到且无 LKG / 目录的模型面这一组被拒):`MODEL_ID` 是出路之一;
85
+ * · `catalog` —— 判的是一份**目录候选**(模型面 stage):目录一旦生效就整张换掉 env 表,`MODEL_ID` 救不了这份候选 ——
86
+ * 出路只在目录里(修条目 / 换绑)。opus 腿 ④(7.101.0 合并复审):此前两腿同一句,目录腿教「设 MODEL_ID 即可」是假话。
87
+ */
88
+ export type ModelTableLeg = "env" | "catalog";
89
+ /**
90
+ * 🔴 S-705 —— **这张模型表有没有一只缺省模型**(`models.default`)。`undefined` = 有。
91
+ *
92
+ * 规则只有一句:**进程服务的那张表必须有 `default`**。env 腿(`MODEL_ID`)与目录腿(缺省解析)是它的两个来源,
93
+ * 任一在场即过;目录一旦生效就整张换掉 env 表,所以判的永远是「生效之后的那张表」—— 这正是它必须在目录应用
94
+ * **之后**判的理由(此前 `parseModelDomain` 在目录应用之前就对 `MODEL_ID` 空串拒启,把只配了目录的用户无谓拒掉)。
95
+ *
96
+ * 拒因一句:码 + 这条腿上**真实**的出路 + 若目录在场但每条都不可用,点名它们与各自的码。env 腿句首保留 `MODEL_ID is required`
97
+ * 这个既有的可 grep 片段(壳与运维脚本认它);目录腿不提 `MODEL_ID`(见 {@link ModelTableLeg})。
98
+ */
99
+ export declare function defaultModelRefusal(view: DefaultModelView, leg: ModelTableLeg): string | undefined;
100
+ /** 一个被铸出的被拒名字:`name` = 被拒条目的名字,`target` = 增广把它指向的那条条目,`via` = 它是以什么身份被铸出的。 */
101
+ export interface MintedRefusedName {
102
+ readonly name: string;
103
+ readonly target: string;
104
+ /** `tier-word`(档位词)/ `cc-alias`(CC 别名 → 它的档位)/ `best`(头档别名)/ `catalog-entry`(一条可用条目与它同名)。 */
105
+ readonly via: string;
106
+ /** 被拒条目自己的拒因码。 */
107
+ readonly code: ModelEntryRefusalCode;
108
+ }
109
+ /** 不变量 `refused-names-not-minted` 读的切面(`ServiceConfig` 与 `CrossInvariantView` 都结构满足)。 */
110
+ export interface RefusedNameView {
111
+ readonly models: Readonly<Record<string, unknown>>;
112
+ readonly tiers: Readonly<Record<string, string>>;
113
+ readonly modelRefusals: Readonly<Record<string, ModelEntryRefusal>>;
114
+ }
115
+ /**
116
+ * 🔴 S-712 —— 不变量 `refused-names-not-minted` 的判据:**这张表回应的每一个名字(目录键 ∪ core `expandTiers` 增广出来的档位词 /
117
+ * CC 别名 / `best`)都不是一个被拒条目的名字**。返回撞上的那些(空 = 成立)。
118
+ *
119
+ * 为什么是「增广之后」:引擎在构造 / 换装时自己 `expandTiers(models, tiers)`,显式 `model:"sonnet"` 与角色绑定 `advisor→sonnet` 都在
120
+ * 增广目录上解析。一条条目被拒(不可用 / 被 schema 丢掉)之后它的名字空出来,同名的档位词 / CC 别名就不再被它 shadow,增广把
121
+ * 那个词铸到另一条条目上 —— 点它的请求被静默换了目的地、端点与账号(gpt-6 腿 F2 / F3,真复现)。判「增广之后」让引擎拿到的目录
122
+ * **永远没有**被拒名:显式点它落 `request.model_unavailable`(选择面让出名字 + 查拒因表),角色绑定落引擎对未知名的响亮拒。
123
+ * 可用条目与被拒条目同名(同一个名字发布了两条、其中一条坏的)同样算撞:哪一条才是运维的意思无从判,拒比猜安全。
124
+ */
125
+ export declare function mintedRefusedNames(view: RefusedNameView): MintedRefusedName[];
126
+ /** 不变量 `refused-names-not-minted` 的拒因句(`undefined` = 成立)。warn 与世代账 / 拒因同一句;逐对明细另走 {@link mintedRefusedNames}。 */
127
+ export declare function refusedNameMintedRefusal(view: RefusedNameView): string | undefined;
128
+ /** 身份证明不了的被丢条目的拒因句(同一不变量同一个码:证明不了的名字无从证明它不被增广铸出)。 */
129
+ export declare function unreadableDroppedRefusal(entries: readonly string[]): string;
130
+ //# sourceMappingURL=model-entry-refusal.d.ts.map