@sema-agent/server 7.96.0 → 7.97.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 (65) hide show
  1. package/USAGE.md +4 -1
  2. package/dist/approval.d.ts +28 -0
  3. package/dist/approval.js +24 -0
  4. package/dist/boot/budget-tracing.d.ts +1 -0
  5. package/dist/boot/budget-tracing.js +3 -2
  6. package/dist/boot/config-center.js +2 -5
  7. package/dist/boot/coordinators.js +2 -2
  8. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
  9. package/dist/boot/deferred-sandbox-path-env.js +3 -1
  10. package/dist/boot/memory-boundary.d.ts +17 -2
  11. package/dist/boot/memory-boundary.js +11 -0
  12. package/dist/boot/reapers.d.ts +3 -1
  13. package/dist/boot/reapers.js +2 -2
  14. package/dist/boot/resolve-spec.js +6 -6
  15. package/dist/boot/runner-deps.d.ts +2 -1
  16. package/dist/boot/session-faces.d.ts +4 -1
  17. package/dist/boot/session-faces.js +5 -5
  18. package/dist/boot/stage-04-stores.d.ts +1 -0
  19. package/dist/boot/stage-04-stores.js +2 -2
  20. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  21. package/dist/boot/stage-06-runners.d.ts +1 -0
  22. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  23. package/dist/boot/stage-07-capability-layer.js +10 -14
  24. package/dist/boot/stage-08-reapers.d.ts +2 -3
  25. package/dist/boot/stage-08-reapers.js +2 -2
  26. package/dist/boot/stage-09-leader.d.ts +1 -0
  27. package/dist/boot/stage-09-leader.js +2 -2
  28. package/dist/boot/stage-10-http-server.d.ts +1 -0
  29. package/dist/boot/stores.js +5 -11
  30. package/dist/capabilities/execution-lane.d.ts +5 -7
  31. package/dist/capabilities/execution-lane.js +2 -1
  32. package/dist/capabilities/memory-notice.d.ts +30 -12
  33. package/dist/capabilities/memory-notice.js +8 -1
  34. package/dist/capabilities/skills.d.ts +14 -0
  35. package/dist/capabilities/skills.js +4 -0
  36. package/dist/config-catalog.js +1 -0
  37. package/dist/config-types.d.ts +8 -1
  38. package/dist/config.js +4 -3
  39. package/dist/env-facts.js +2 -4
  40. package/dist/execution-lane-caps.d.ts +36 -6
  41. package/dist/execution-lane-caps.js +4 -0
  42. package/dist/file-history-capture.d.ts +65 -0
  43. package/dist/file-history-capture.js +21 -0
  44. package/dist/hosted-posture.d.ts +1 -1
  45. package/dist/http/resume-legs.d.ts +14 -4
  46. package/dist/http/resume-legs.js +26 -17
  47. package/dist/http/route-ctx.d.ts +35 -9
  48. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  49. package/dist/http/routes/approvals-assistant.js +16 -6
  50. package/dist/http/routes/capabilities.js +2 -0
  51. package/dist/http/routes/runs.js +2 -2
  52. package/dist/http/routes/session-sync.js +6 -1
  53. package/dist/http/server.d.ts +4 -1
  54. package/dist/http/server.js +1 -1
  55. package/dist/http/verify-rounds.d.ts +1 -1
  56. package/dist/memory-scope.d.ts +6 -2
  57. package/dist/memory-scope.js +3 -2
  58. package/dist/observability/fail-open.d.ts +4 -0
  59. package/dist/observability/fail-open.js +4 -0
  60. package/dist/run-local.js +4 -5
  61. package/dist/session-sync.d.ts +4 -1
  62. package/dist/session-sync.js +2 -2
  63. package/dist/task-cwd.d.ts +9 -3
  64. package/dist/task-cwd.js +3 -1
  65. package/package.json +1 -1
@@ -32,7 +32,7 @@ import { assertSchemaMatchesCode } from "../plugins/schema-consistency.js";
32
32
  import { assertCheckpointColumnSchema } from "../plugins/checkpoint-store-sql.js";
33
33
  import { assertFileHistorySchema } from "../plugins/file-history-store-sql.js";
34
34
  import { resolveStreamApprovalGate } from "../tool-approval.js";
35
- import { buildMemoryWriteBoundaryAudit, effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
35
+ import { buildMemoryBoundaryLaneFacts, buildMemoryWriteBoundaryAudit, effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
36
36
  import { buildMemoryPosture } from "../memory-posture.js";
37
37
  import { establishSqlEnginePosture } from "../sql-engine-posture.js";
38
38
  import { buildDisplaySafeUrl } from "../trace/redact.js";
@@ -194,25 +194,19 @@ export async function openStores(ctx) {
194
194
  }
195
195
  }
196
196
  {
197
- const lane = config.remoteExec?.provider;
198
- const hostSemanticsLane = lane === undefined || lane === "host";
199
- const coreRemoteExecutionEnv = config.remoteExec !== undefined;
200
- const containmentRoots = lane === undefined ? [process.cwd()] : [];
197
+ const laneFacts = buildMemoryBoundaryLaneFacts(config, process.cwd());
201
198
  const memoryMountRoot = memoryEngine ? memoryMountRootOf(memoryEngine.backend, memoryEngine.root) : undefined;
202
199
  const audit = buildMemoryWriteBoundaryAudit({
203
200
  memoryRoot: memoryMountRoot,
204
- lane: lane ?? "in-process",
205
- hostSemanticsLane,
206
- coreRemoteExecutionEnv,
201
+ ...laneFacts,
207
202
  declaredCapable: effectiveMemoryPersistenceCapable(config),
208
- containmentRoots,
209
203
  engineBackend: config.memoryEngineBackend,
210
204
  });
211
205
  for (const w of audit.warnings)
212
- logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root: memoryMountRoot ?? null });
206
+ logger.warn(w.tag, { detail: w.detail, lane: laneFacts.lane, root: memoryMountRoot ?? null });
213
207
  if (config.memoryPersistenceCapable === undefined && effectiveMemoryPersistenceCapable(config) === true) {
214
208
  logger.info?.("memory_persistence_capable_auto_declared", {
215
- lane: lane ?? "in-process",
209
+ lane: laneFacts.lane,
216
210
  detail: "REMOTE_EXEC=host: file tools run on this machine's filesystem, so the assembler declares memoryPersistenceCapable=true (the core-side remote-env withhold would otherwise silently drop the memory WRITE instruction on this lane). Override with MEMORY_PERSISTENCE_CAPABLE=true|false.",
217
211
  });
218
212
  }
@@ -1,18 +1,16 @@
1
- import type { RemoteExecProvider } from "../execution-lane-caps.js";
1
+ import { type ExecutionLane, type RemoteExecProvider } from "../execution-lane-caps.js";
2
2
  export interface ExecutionLaneCapability {
3
- /** 本部署的执行车道词(闭集 = {@link RemoteExecProvider};`REMOTE_EXEC` 未设 ⇒ `"host"`,见头注)。 */
4
- readonly provider: RemoteExecProvider;
3
+ /** 本部署的执行车道词(闭集 = {@link ExecutionLane} 八词;`REMOTE_EXEC` 未设 ⇒ `"in-process"`,见头注 S-602 段)。 */
4
+ readonly provider: ExecutionLane;
5
5
  /** 工具跑的那台机器就是本进程这台吗(= server 盘上的绝对路径对模型的工具有没有意义)。
6
6
  * 属主 = `task-cwd.ts` 的 {@link toolsRunOnThisHost},与 `skills[].baseDir` 的铸点同一只函数。 */
7
7
  readonly toolsOnThisHost: boolean;
8
8
  }
9
9
  /**
10
10
  * 部署席 → 消费端窄投影。形照 `projectWebSearchCapability` / `projectSqlEngineCapability`:
11
- * **只 import 型**,不碰 config.ts,于是本模块是叶。
11
+ * 不碰 config.ts(只从车道表模块取词与归一读口,那是叶),于是本模块是叶。
12
12
  *
13
- * 🔴 **唯一**写缺席折词的地方。`"host"` 这个字面量不入 `api-notice-text-freeze`:它是
14
- * {@link RemoteExecProvider} 的成员,配置联合里那条臂改名 ⇒ 本行当场 tsc 红(S-382 对三个 provider 词
15
- * 免入表的同一条理由 —— 会静默漂的只有**本仓自铸**的词,这里一个都没有)。
13
+ * 🔴 本处**不写**任何缺席折词(S-602 门 D):缺席归一只在 {@link executionLaneOf} 一处。
16
14
  */
17
15
  export declare function projectExecutionLaneCapability(config: {
18
16
  readonly remoteExec?: {
@@ -1,7 +1,8 @@
1
1
  import { toolsRunOnThisHost } from "../task-cwd.js";
2
+ import { executionLaneOf } from "../execution-lane-caps.js";
2
3
  export function projectExecutionLaneCapability(config) {
3
4
  return {
4
- provider: config.remoteExec?.provider ?? "host",
5
+ provider: executionLaneOf(config.remoteExec?.provider),
5
6
  toolsOnThisHost: toolsRunOnThisHost(config),
6
7
  };
7
8
  }
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { type PromptProvider, type ToolCatalogEntry } from "@sema-agent/core";
22
22
  import type { ScenarioHands } from "./hands-lane.js";
23
+ import { type ExecutionLane } from "../execution-lane-caps.js";
23
24
  /**
24
25
  * 「已知落盘路」的判据,作用在 core **静态工具目录**的一行上(S-503 / [ref])—— 与 core 自己的
25
26
  * `rosterCanPersist` 推断逐条同形,取的是同一批目录属性,不是它结论的抄件:
@@ -77,24 +78,41 @@ export interface MemoryFaceFacts {
77
78
  /** 折完之后的最终 `TaskSpec.excludeTools`(roster 真卸载,不是延迟披露)。**全部**已知落盘路都被排掉
78
79
  * ⇒ 手带在场也等于没有写通道 —— core 的 `rosterCanPersist` 也是按排除后的 roster 算的,这里跟上。 */
79
80
  excludeTools: readonly string[] | undefined;
80
- /** 本部署的执行环境在 core 眼里算不算 **remote**(`isRemoteExecutionEnv`)。判别位 = `REMOTE_EXEC` 有没有
81
- * 设 —— 注意 `host` 也算:core 的判别是**鸭子类型**(`workspaceHandle`/`execStream`/`suspendVM`/…),
82
- * 而我方的 `remote-env-host.ts` 这几件全实现,亲验坐实。
83
- * 为什么这一位必须进判别式(codex 复审 round2 [high],已核真):core 的 `rosterCanPersist` 在
84
- * `handsEnabled && isRemoteExecutionEnv` 时把 Write/Edit/NotebookEdit/Bash **全部不算**落盘路(沙箱盘
85
- * 每任务即焚,够不着 host 记忆根)。而 server 在任何 remote 车道上默认让记忆引擎 dark —— 两件事合起来
86
- * 正是**最常见的云部署形**:引擎不在、手带在,却一份持久面都没有。漏了这一位,那台机器就继续给
87
- * 「已经记住了」的幻觉式回执 —— 恰恰是 [ref] 立案要消灭的形态。 */
88
- remoteExecutionEnv: boolean;
81
+ /**
82
+ * 本部署的手带**挂没挂**(= core 拿没拿到执行环境;能力表 `coreRemoteExecutionEnv` 位,经
83
+ * {@link buildNoticeLaneFacts} 取)。`false` = `in-process`:core 落 `StubExecutionEnv`,手带整族不 mount
84
+ * ⇒ **一条落盘路都没有**,不管名册上写着什么名字。
85
+ * 🔴 S-602(clay 裁 [ref] A):改前这里只有一位 `remoteExecutionEnv`(判别位 = `REMOTE_EXEC` 设没设),
86
+ * 于是 in-process 读成「不是 remote ⇒ 手就在本机 fs 上 ⇒ 已知落盘路真的在」—— 而那条 lane 根本没有手,
87
+ * 用户说「记住 X」拿到的是无处落盘的幻觉回执([ref] 要消灭的原形)。一位拆成两位,各答各的问题。
88
+ */
89
+ handsWired: boolean;
90
+ /**
91
+ * 手带的文件平面**不在**本 worker(能力表 `hostFilePlane` 答否;经 {@link buildNoticeLaneFacts} 取)——
92
+ * 沙箱 / 远端 / 设备车道。为什么这一位必须进判别式(codex 复审 round2 [high],已核真):core 的
93
+ * `rosterCanPersist` 在沙箱盘上把 Write/Edit/NotebookEdit/Bash **全部不算**落盘路(每任务即焚,够不着
94
+ * host 记忆根)。而 server 在任何 remote 车道上默认让记忆引擎 dark —— 两件事合起来正是**最常见的云部署形**:
95
+ * 引擎不在、手带在,却一份持久面都没有。
96
+ * ⚠️ `host` 车道这一位是 `false`(文件平面就是本机);core 自己仍把 host 判 remote,那一面由装配层的
97
+ * `effectiveMemoryPersistenceCapable`(host 腿自动声明 capable)在臂② 接住,不归本位。
98
+ */
99
+ filePlaneRemote: boolean;
89
100
  }
101
+ /**
102
+ * S-602 —— 判别式的两位**车道事实**,由能力表派生([ref]:不手抄投影)。resolve-spec 的装配点只调它,
103
+ * 不在调用点手写 `config.remoteExec !== undefined`(门 D R4)。
104
+ */
105
+ export declare function buildNoticeLaneFacts(lane: ExecutionLane): Pick<MemoryFaceFacts, "handsWired" | "filePlaneRemote">;
90
106
  /**
91
107
  * 该不该把 `NO_PERSISTENT_MEMORY_NOTICE` 组进本次任务的 system prompt。
92
108
  *
93
109
  * 优先序(每一条都有独立理由,不是同一条判据的三种写法):
94
110
  * ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
95
- * ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署事实压过我们的推断,
96
- * 与 core 对 `memoryPersistenceCapable` 的处置同向;
97
- * ③ 否则推断:有已知落盘路(手带在场、不是只读、不在 remote 车道、且没被卸载决定卸光)⇒ 否;没有 ⇒ 是。
111
+ * ② 部署显式声明(或 host 腿装配层自动声明)⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署
112
+ * 事实压过我们的推断,与 core 对 `memoryPersistenceCapable` 的处置同向;
113
+ * ③ 否则推断:有已知落盘路(手带挂上、场景不摘手、不是只读、文件平面在本机、且没被卸载决定卸光)⇒ 否;没有 ⇒ 是。
114
+ * 🔴 S-602 只改臂③ 里「车道」那一段(`handsWired` / `filePlaneRemote`),①② 的优先序一字不动 —— 「in-process ⇒
115
+ * 披露」只在这两臂**之后**成立(挂了记忆面、或声明了 capable 的 in-process 部署照旧不披露)。
98
116
  */
99
117
  export declare function shouldDiscloseNoPersistentMemory(facts: MemoryFaceFacts): boolean;
100
118
  /**
@@ -1,5 +1,6 @@
1
1
  import { describeToolCatalog, HANDS_BAND_TAGS, NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
2
2
  import { hasConstitutionAnchors } from "../task-settings.js";
3
+ import { executionLaneCaps } from "../execution-lane-caps.js";
3
4
  export function persistencePathToolNamesOf(entries) {
4
5
  const names = new Set();
5
6
  for (const e of entries) {
@@ -21,6 +22,10 @@ export function persistencePathToolNamesOf(entries) {
21
22
  }
22
23
  export const KNOWN_PERSISTENCE_TOOL_NAMES = persistencePathToolNamesOf(describeToolCatalog());
23
24
  export const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
25
+ export function buildNoticeLaneFacts(lane) {
26
+ const caps = executionLaneCaps(lane);
27
+ return { handsWired: caps.coreRemoteExecutionEnv === "supported", filePlaneRemote: caps.hostFilePlane !== "supported" };
28
+ }
24
29
  function isUnmounted(facts, name) {
25
30
  return facts.excludeAllTools || (facts.excludeTools?.includes(name) ?? false);
26
31
  }
@@ -34,7 +39,9 @@ export function shouldDiscloseNoPersistentMemory(facts) {
34
39
  return !facts.declaredCapable;
35
40
  if (facts.hands === "none" || facts.handsReadOnly)
36
41
  return true;
37
- if (facts.remoteExecutionEnv)
42
+ if (!facts.handsWired)
43
+ return true;
44
+ if (facts.filePlaneRemote)
38
45
  return true;
39
46
  return !anyPersistenceToolSurvives(facts);
40
47
  }
@@ -1,4 +1,5 @@
1
1
  import type { SkillSpec } from "@sema-agent/core";
2
+ import type { RemoteExecProvider } from "../execution-lane-caps.js";
2
3
  /**
3
4
  * Built-in skills baked into the image, loaded ONCE at startup (no per-task disk IO). A skill is a
4
5
  * Markdown capability (frontmatter + body). core uses **progressive disclosure** (core ≥1.98.0):
@@ -57,6 +58,19 @@ export interface LoadSkillsOptions {
57
58
  * `label` = 错误文本里的对象名(center-plugins 用它约束 manifest 本体,报错不该指成 skill entry)。 */
58
59
  export declare function assertConfined(p: string, rootReal: string, label?: string): void;
59
60
  export declare function loadSkills(dir: string, opts?: LoadSkillsOptions): LoadedSkill[];
61
+ /**
62
+ * S-602 —— **出厂技能**的装载腿(`SKILLS_DIR` 自扫)。stage-07 与 run-local 两处装配点**都调这一只**:改前两处
63
+ * 各写一遍 `loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) })`,测试只能
64
+ * 直传布尔去证 `loadSkills`(`baked-skills.test.ts`),证不到「config → 装配腿 → baseDir」这一段;收成一只
65
+ * 之后,联合格从 config 走到 `baseDir`(`test/capabilities-http.test.ts` S-602 段)证的就是两处装配点本人。
66
+ * 判据属主不变:`task-cwd.ts` 的 {@link toolsRunOnThisHost}(S-602 起 in-process ⇒ 假 ⇒ 目录形技能不带 `baseDir`)。
67
+ */
68
+ export declare function loadDeploymentSkills(config: {
69
+ readonly skillsDir: string;
70
+ readonly remoteExec?: {
71
+ readonly provider: RemoteExecProvider;
72
+ } | undefined;
73
+ }): LoadedSkill[];
60
74
  /** Skills applicable to a scenario: those tagged with it, plus untagged (global) ones.
61
75
  * [ref]:alias 窗撤销,单键查询——退役旧名标签不再命中(迁移面在 7.44 发车帖/MIGRATION 记载)。 */
62
76
  export declare function skillsForScenario(loaded: LoadedSkill[], scenario: string): SkillSpec[];
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { join, resolve, sep } from "node:path";
3
+ import { toolsRunOnThisHost } from "../task-cwd.js";
3
4
  export function assertConfined(p, rootReal, label = "skill entry") {
4
5
  let real;
5
6
  try {
@@ -66,6 +67,9 @@ export function loadSkills(dir, opts) {
66
67
  }
67
68
  return out;
68
69
  }
70
+ export function loadDeploymentSkills(config) {
71
+ return loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
72
+ }
69
73
  function collectAttachments(skillDir, rel, confineRoot) {
70
74
  const files = [];
71
75
  const here = rel === "" ? skillDir : join(skillDir, rel);
@@ -145,6 +145,7 @@ export const CONFIG_CATALOG = [
145
145
  r("ENGINE_LINGER_MS", "limitsHttp", "number", "父还活着时自退前三合取须持续成立的静默窗(ms;[1000,24h] 拒启带界)", { staticDefault: "60000", configKey: "engineLingerMs" }),
146
146
  r("ENGINE_ORPHAN_LINGER_MS", "limitsHttp", "number", "父进程已不在(孤儿)时的静默窗(ms;[1000,24h] 拒启带界;SEMA_PARENT_PID 缺席则不生效)", { staticDefault: "2000", configKey: "engineOrphanLingerMs" }),
147
147
  r("EXPERIMENTAL_OBSERVER_AGENTS", "orchestration", "boolean", "observer 开闸线(实验面 ship-dark;多租户只认 center caps,env 被忽略)", { staticDefault: "false", configKey: "experimentalObserverAgents" }),
148
+ r("FILE_HISTORY_CAPTURE", "store", "boolean", "文件历史**捕获**总开关(三条车道同义)。交换:关 = 不再捕获任何改动文件的首次快照,同时没有 rewind / restoreFiles / rewindFilesTo / 2c session-sync(对应能力位与 `fileHistoryCapture` 同源翻 off)。关掉 ≠ 删掉:已捕获的历史不被批量删除、也不再被读用,仍按会话删除与留存规则清理 —— 要清存量走 DELETE 会话或留存旋钮", { staticDefault: "true", danger: DATA, configKey: "fileHistoryCapture" }),
148
149
  r("FILE_HISTORY_RETENTION_KEEP", "store", "number", "per-file rewind 历史的保留上限(与 core FileStorageBackend 同域键形,三条车道一套语义)", { danger: DATA, derivedDefaultNote: "缺席 ⇒ core 缺省 100" }),
149
150
  r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", "fleet 面向 center 公告的本 worker 可达地址(与 center 腿同在 ⇒ 启 fleet client;坏形拒启)"),
150
151
  r("FORK_ENABLED", "orchestration", "boolean", "CC /fork(core Fork 工具)装配开关", { derivedDefaultNote: "posture 三态:缺席 = 单用户 ⇒ on / 多租户 ⇒ off(per-principal entitlement 另判)", configKey: "forkEnabled" }),
@@ -956,6 +956,13 @@ export interface ServiceConfigFlat {
956
956
  fileHistory?: {
957
957
  retention?: FileHistoryRetentionPolicy;
958
958
  };
959
+ /** S-589:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
960
+ * (local / mysql / pg)同一语义:`false` ⇒ 装配点不把历史店交给 core(`RunnerDeps.fileHistoryStore` 缺席 ⇒
961
+ * 首次触碰捕获结构上不发生),同时没有 rewind / restore / `rewindFilesTo` / 2c session-sync(那几位能力位与
962
+ * 能力词 `fileHistoryCapture:"off"` 同源翻关)。**只管写入,不管擦除**:已捕获的历史照旧随会话删除 / 留存
963
+ * 规则清理,翻旋钮本身不做批量删除。判定的唯一属主是 `file-history-capture.ts` 的 `assembleFileHistory`。
964
+ * 刻意**不**并进上面的 `fileHistory` 键:那一格是 core 店构造选项的同域键形,原样透传给 core。 */
965
+ fileHistoryCapture: boolean;
959
966
  /** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面,
960
967
  * 消费在 capabilities/center-plugins.ts)。 */
961
968
  pluginsAllowHosts: string[];
@@ -1905,7 +1912,7 @@ export interface ServiceConfigFlat {
1905
1912
  configLocalDir?: string;
1906
1913
  }
1907
1914
  /** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
1908
- export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "storeLockWaitMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "sendUserFile">;
1915
+ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "storeLockWaitMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "fileHistoryCapture" | "sendUserFile">;
1909
1916
  /** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
1910
1917
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
1911
1918
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
package/dist/config.js CHANGED
@@ -18,7 +18,7 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
18
18
  import {} from "./posture-source.js";
19
19
  import { hostedPrincipalRefusal } from "./hosted-posture.js";
20
20
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
- import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
21
+ import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
22
22
  import { AUTONOMY_MODES } from "./runtime-governance.js";
23
23
  import { modelProviderRefusal } from "./model-provider.js";
24
24
  import { modelRouteUrlRefusal } from "./model-route-url.js";
@@ -197,7 +197,7 @@ let EXEC_LANE_NOTICE;
197
197
  function buildExecLaneNotice(remoteExec) {
198
198
  const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
199
199
  return {
200
- lane: remoteExec?.provider ?? "in-process",
200
+ lane: executionLaneOf(remoteExec?.provider),
201
201
  source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
202
202
  };
203
203
  }
@@ -776,6 +776,7 @@ function parseStoreDomain(ctx) {
776
776
  snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
777
777
  snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
778
778
  fileHistory: parseFileHistoryRetention(),
779
+ fileHistoryCapture: boolEnv("FILE_HISTORY_CAPTURE", true),
779
780
  sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
780
781
  ? {
781
782
  endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
@@ -2038,7 +2039,7 @@ function parseA2aServeSkills(raw) {
2038
2039
  const STORE_GROUP_KEYS = [
2039
2040
  "sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
2040
2041
  "runStoreStrictHydrate",
2041
- "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "sendUserFile",
2042
+ "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "fileHistoryCapture", "sendUserFile",
2042
2043
  ];
2043
2044
  const MODEL_PLANE_GROUP_KEYS = [
2044
2045
  "gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
package/dist/env-facts.js CHANGED
@@ -158,11 +158,9 @@ export async function sweepStaleScratchpads(localDataRoot, opts) {
158
158
  return removed;
159
159
  }
160
160
  export function egressForRemoteExec(remoteExec) {
161
- if (!remoteExec?.provider)
162
- return undefined;
163
- if (remoteExec.provider === "e2b")
161
+ if (remoteExec?.provider === "e2b")
164
162
  return remoteExec.allowInternetAccess === false ? "none" : "full";
165
- if (remoteExec.provider === "k8s")
163
+ if (remoteExec?.provider === "k8s")
166
164
  return "full";
167
165
  return undefined;
168
166
  }
@@ -45,12 +45,18 @@ export interface ExecutionLaneCaps {
45
45
  /** 记忆持久面的**自动**表态(部署可用 `MEMORY_PERSISTENCE_CAPABLE` 显式覆盖,那是旋钮不是车道位)。
46
46
  * 判别式:`boot/memory-boundary.ts` 的 `effectiveMemoryPersistenceCapable`。 */
47
47
  readonly memoryPersistenceCapable: LaneCapability;
48
- /** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)。判别式:`boot/resolve-spec.ts` 的
49
- * `!isSandboxPathAdjudicationLane(...)` 与 `boot/stores.ts` 的 `lane === undefined || lane === "host"`
50
- * ——**两处各写了一份**,对账测试同时咬住两份(它们分叉过就是记忆边界判错盘)。 */
48
+ /** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)。🔴 S-602 起**消费点读本位**,不再各写判别式
49
+ * (改前 `resolve-spec` / `stores` / `memory-scope` / `deferred-sandbox-path-env` 各写一份
50
+ * `lane === undefined || lane === "host"` 的同义形,分叉过就是记忆边界判错盘)。读本位的真装配点:
51
+ * `boot/deferred-sandbox-path-env.ts` 的 `isSandboxPathAdjudicationLane`(resolve-spec 写门 / parked 赎回腿 /
52
+ * 执行环境装配三处的唯一属主)、`memory-scope.ts` 的 `memoryEngineRemoteLanePosture`、
53
+ * `boot/memory-boundary.ts` 的 `buildMemoryBoundaryLaneFacts`(stores)、`capabilities/memory-notice.ts` 的
54
+ * `buildNoticeLaneFacts`(resolve-spec)。⚠️ `in-process` 在本位上是 YES(hooks/进程内读确实在本机盘上),
55
+ * 但它**没有手** —— 问「手在不在本机盘上」要读 {@link handsOnWorkerFilePlane},不是只读本位。 */
51
56
  readonly hostFilePlane: LaneCapability;
52
- /** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型;判别位 = `config.remoteExec` 在不在场,
53
- * **含 host**)。5.26.0 起它决定 `# Memory` 写指令撤不撤。 */
57
+ /** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型,**含 host**)= core 拿没拿到执行环境 = 手带挂没挂。
58
+ * 5.26.0 起它决定 `# Memory` 写指令撤不撤。🔴 S-602 起消费点读本位(改前各写 `config.remoteExec !== undefined`);
59
+ * 对账测试拿 core 自己的 `isRemoteExecutionEnv` 问**真 adapter 实例**来核本位的值。 */
54
60
  readonly coreRemoteExecutionEnv: LaneCapability;
55
61
  /**
56
62
  * S-354 / core 7.20.0([ref]):这条车道的适配器**声明**了 `capabilities.canonicalPathAuthoritative`
@@ -103,13 +109,37 @@ export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
103
109
  */
104
110
  export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
105
111
  /**
106
- * `config.remoteExec?.provider` → 车道词。
112
+ * `config.remoteExec?.provider` → 车道词。**全仓唯一**的缺席归一读口(S-602 门 D:`?? "in-process"` 只许住在这里,
113
+ * `?? "host"` 与 `lane === undefined || lane === "host"` 同义折零容忍)。
107
114
  *
108
115
  * · `undefined`(REMOTE_EXEC 未设)⇒ `"in-process"`,那是真车道不是缺席;
109
116
  * · 认不出的词 ⇒ `undefined`,**调用方必须 fail-closed**(禁静默按某个默认车道处理)。生产链上这一臂
110
117
  * 不可达:`loadConfig` 的 [ref] 拒启门先咬未知词;它存在是为了消费点自卫时有个响亮出口。
118
+ *
119
+ * 两个重载 = 类型收窄([ref] §2.2 A):喂**已装配的** config 形(`RemoteExecProvider | undefined`)⇒
120
+ * 返回型里没有 `undefined`,调用点不必为一条不可达的臂写兜底(兜底写多了就是下一处折词);喂开放字符串
121
+ * (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**编译期**的:
122
+ * `RemoteExecProvider ⊂ ExecutionLane`(`ExecutionLane` 由它派生),而 {@link EXECUTION_LANE_WORDS} 是
123
+ * `Record<ExecutionLane, true>` —— 少一个词编译红,所以对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真。
111
124
  */
125
+ export declare function executionLaneOf(provider: RemoteExecProvider | undefined): ExecutionLane;
112
126
  export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;
127
+ /**
128
+ * S-602 —— 「这条车道的**手**就在本 worker 的盘上吗」。**两位合取,都读表**(不手抄):
129
+ * · `coreRemoteExecutionEnv` —— core 拿到了执行环境 = 手带真挂上(`in-process` 答 NO:core 落 `StubExecutionEnv`);
130
+ * · `hostFilePlane` —— 那套手的文件平面就是本 worker 的盘(沙箱 / 远端 / 设备车道答 NO)。
131
+ * 今天只有 `host` 两位同 YES。
132
+ *
133
+ * 🔴 **只读 `hostFilePlane` 是错的**:`in-process` 那一位是 YES(hooks / 进程内读确实在本机盘上),而它
134
+ * **没有手**。clay 裁 [ref] ③ 的字面写「判据读 `hostFilePlane`」,意图是同一句的「无手不挂」—— 照字面
135
+ * 实现会让无手部署**继续**挂 SendUserFile,与意图相反;本函数按意图实现(收车档 che-MA 写明)。
136
+ *
137
+ * 三个消费点问的都是这一句,**同一只函数**,不许各写一份 `=== "host"`:
138
+ * ① `task-cwd.ts` 的 `toolsRunOnThisHost` —— wire `executionLane.toolsOnThisHost` + 出厂技能 `baseDir`;
139
+ * ② `boot/coordinators.ts` 的 SendUserFile 本机腿(经 ①;[ref] ③「无手不挂」);
140
+ * ③ `approval.ts` 的 `buildNoResponderBootWarns` —— 写门的 ask 只在「有手写本机盘」时才会发生。
141
+ */
142
+ export declare function handsOnWorkerFilePlane(lane: ExecutionLane): boolean;
113
143
  /** 把一位能力在**具体部署**上落成布尔:租户面是唯一入参(旋钮由消费点自己再合取)。
114
144
  * 穷举 switch,无 default 臂 —— {@link LaneCapability} 加词而此处不加臂 = 编译红。 */
115
145
  export declare function laneCapabilityHolds(cap: LaneCapability, deployment: {
@@ -162,6 +162,10 @@ export function executionLaneOf(provider) {
162
162
  return "in-process";
163
163
  return isExecutionLane(provider) ? provider : undefined;
164
164
  }
165
+ export function handsOnWorkerFilePlane(lane) {
166
+ const caps = executionLaneCaps(lane);
167
+ return caps.hostFilePlane === "supported" && caps.coreRemoteExecutionEnv === "supported";
168
+ }
165
169
  export function laneCapabilityHolds(cap, deployment) {
166
170
  switch (cap) {
167
171
  case "supported":
@@ -0,0 +1,65 @@
1
+ /**
2
+ * S-589 —— 文件历史的**装配单点**与能力词 `fileHistoryCapture` 的闭集(叶模块:只 type-import store-backend)。
3
+ *
4
+ * ## 病(修前)
5
+ * `boot/budget-tracing.ts` 只要后端有文件历史店就无条件交给 core(`RunnerDeps.fileHistoryStore`)。core 的首次触碰
6
+ * 捕获是**进程级**的:接了店就恒捕获,没有任何单会话 / 单 run 的杠杆;全仓相关旋钮只有保留条数
7
+ * `FILE_HISTORY_RETENTION_KEEP`。于是用户在客户端关掉「文件检查点」关掉的只是提示,文件字节照旧进
8
+ * `file_history_*` —— 隐私 / 留存轴上的用户面语义谎言。
9
+ *
10
+ * ## 一处判定,两张脸(按动词类别切分,不是特例)
11
+ * 「这台部署有没有文件历史店」只在 {@link assembleFileHistory} 判一次,部署旋钮 `FILE_HISTORY_CAPTURE` 在这一处合取:
12
+ * · **捕获脸** `store` = 后端 ∧ 旋钮。喂 core 的首次触碰捕获(`RunnerDeps.fileHistoryStore`)、rewind 族能力位
13
+ * (`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync`)与本模块的能力词、2c sync 路由族。
14
+ * · **擦除脸** `erasure` = 后端在即在,类型上**只有删除类动词**。喂 E21 在线删除(`boot/session-faces.ts`)与
15
+ * 孤儿 blob GC(`boot/reapers.ts`)。
16
+ * 规则一句话:**旋钮只管写入,从不管擦除**。关掉捕获的部署照样要能删掉已经捕获的文件字节 —— 否则翻旋钮之后
17
+ * `DELETE /v1/sessions/:id` 会回成功却把存量文件内容留在库里,而留存车道(`plugins/retention-store-sql.ts` 的
18
+ * 原生 SQL 删六张 `file_history_*` 表,本来就不看店在不在)与在线删除从此不再等价(E21 两个执行体分家)。
19
+ * ⚠️ 关掉 ≠ 删掉:翻旋钮**不**做任何批量删除。已捕获的历史照既有的会话删除 / 留存规则清理;在旋钮关着的
20
+ * 部署上它也不会再被读用(rewind / restore / sync 都随捕获脸一起缺席)。
21
+ *
22
+ * ## 能力词为什么是闭集两词、没有 `on-optional`
23
+ * cli 要的是「`/config` 那个开关该不该出现」。present-iff 布尔在今天的**每一台**部署上都只能回答「关不掉」
24
+ * (core 的捕获是进程级的)⇒ 出一格三词闭集的前两词:`off`(这台部署根本不捕获,也没有 rewind)与
25
+ * `on-always`(捕获中,且无法按会话关闭)。第三词 `on-optional`(「本会话可以不捕获」)要等 core 收一个
26
+ * 单会话 / 单 run 的不捕获位,**在那之前刻意留空**:预先声明一个结构上不可达的词,等于给客户端一个永远等不到的分支。
27
+ */
28
+ import type { ServiceFileHistoryStore, StoreBackend } from "./plugins/store-backend.js";
29
+ /** 能力词闭集的单点属主(`Record` 锁步:联合加词而这里不补 ⇒ 编译红;词表数组由它派生,不手抄)。 */
30
+ declare const FILE_HISTORY_CAPTURE_WORD_SET: {
31
+ readonly off: true;
32
+ readonly "on-always": true;
33
+ };
34
+ /** `GET /v1/capabilities` 的 `fileHistoryCapture` 词。 */
35
+ export type FileHistoryCapture = keyof typeof FILE_HISTORY_CAPTURE_WORD_SET;
36
+ /** 闭集的全部词(契约文档门 / 测试据此对表)。 */
37
+ export declare const FILE_HISTORY_CAPTURE_WORDS: readonly FileHistoryCapture[];
38
+ /** 擦除脸 —— 物理店上**只有删除类**的那几个动词(类型收窄 = 结构上拿它捕获不了、导出不了、还原不了)。
39
+ * `reap` 是 core 契约必填(local File 店没有整 scope 的 `deleteBySession`,E21 在那条车道上退回 `reap(scope, [])`);
40
+ * `deleteBySession` / `sweepOrphanBlobs` 是 SQL 双生的服务端额外动词。 */
41
+ export type FileHistoryErasureFace = Pick<ServiceFileHistoryStore, "reap" | "deleteBySession" | "sweepOrphanBlobs">;
42
+ /** 装配结果:两张脸。 */
43
+ export interface FileHistoryAssembly {
44
+ /** 捕获脸:在场 ⟺ 后端在 ∧ `FILE_HISTORY_CAPTURE`。 */
45
+ readonly store: ServiceFileHistoryStore | undefined;
46
+ /** 擦除脸:在场 ⟺ 后端在(与旋钮无关)。 */
47
+ readonly erasure: FileHistoryErasureFace | undefined;
48
+ }
49
+ /**
50
+ * **唯一**的装配判定。`capture` = 部署旋钮 `FILE_HISTORY_CAPTURE`(`config.fileHistoryCapture`)。
51
+ *
52
+ * 形照 `execution-lane-caps.ts` 的穷举 switch(无 default 臂):意图词加一个(将来的 `on-optional`)而这里不加臂
53
+ * ⇒ 编译红 —— 加臂的那一刻就是决定「这一词下捕获脸接不接、core 那一位单会话不捕获位怎么递」的地方。
54
+ */
55
+ export declare function assembleFileHistory(backend: StoreBackend | undefined, capture: boolean): FileHistoryAssembly;
56
+ /**
57
+ * 能力词 = 捕获脸在不在(读**装配结果**,不照旋钮字面回声:后端本来就没有这只店的部署同样是 `off`)。
58
+ *
59
+ * 与 `rewindFiles` 读的是**同一个判定**(`deps.fileHistoryStore` 在场),今天两位同构。分叉条件(写在这里免得
60
+ * 将来有人把两位当成一回事):core 收了单会话不捕获位之后,本词在捕获脸在场时会变成 `on-optional`,而
61
+ * `rewindFiles` 仍是 true —— 它回答的是「这台部署能不能还原」,本词回答的是「捕获关不关得掉」。
62
+ */
63
+ export declare function fileHistoryCaptureOf(captureFace: object | undefined): FileHistoryCapture;
64
+ export {};
65
+ //# sourceMappingURL=file-history-capture.d.ts.map
@@ -0,0 +1,21 @@
1
+ const FILE_HISTORY_CAPTURE_WORD_SET = {
2
+ off: true,
3
+ "on-always": true,
4
+ };
5
+ export const FILE_HISTORY_CAPTURE_WORDS = Object.keys(FILE_HISTORY_CAPTURE_WORD_SET);
6
+ export function assembleFileHistory(backend, capture) {
7
+ if (backend === undefined)
8
+ return { store: undefined, erasure: undefined };
9
+ const physical = backend.fileHistory();
10
+ const intent = capture ? "on-always" : "off";
11
+ switch (intent) {
12
+ case "off":
13
+ return { store: undefined, erasure: physical };
14
+ case "on-always":
15
+ return { store: physical, erasure: physical };
16
+ }
17
+ }
18
+ export function fileHistoryCaptureOf(captureFace) {
19
+ return captureFace === undefined ? "off" : "on-always";
20
+ }
21
+ //# sourceMappingURL=file-history-capture.js.map
@@ -151,7 +151,7 @@ export type TaskHostPostureScope = HostedPostureInput & Readonly<Pick<ServiceCon
151
151
  *
152
152
  * 🔴 **为什么这一条不拒**(初版给了 422,当批撤回):这个键**零授权** —— 引擎只把它渲给模型,没有任何
153
153
  * 裁决腿读它(`http/wire-types.ts` 的字段注逐字,来源是 core 的 `ApproverPosture` 头注)。而续跑腿
154
- * **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;`resumePlanReview` 上更贵:
154
+ * **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;plan_review 驱动段(`drivePlanReview`)上更贵:
155
155
  * nonce 先烧、拒在其后才抛,运维重签再撞同一个拒 = 无限循环。本仓对这一类的成文结论逐字是
156
156
  * 「**砖死 resume 比降级糟**」(`boot/resolve-spec.ts` 顶部那段列了三处先例)。
157
157
  * ⇒ 把本批最硬的处置给族里**危害最小**的成员,方向是错的。
@@ -17,7 +17,7 @@ import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
18
  import { type HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
- import type { RouteCtxBase, DriveResumeArgs } from "./route-ctx.js";
20
+ import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
22
22
  import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
23
23
  import type { createRequestGuards } from "./request-guards.js";
@@ -67,13 +67,23 @@ export declare function createResumeLegs(ctx: {
67
67
  status: number;
68
68
  body: object;
69
69
  }>;
70
- resumePlanReview: (sessionId: string, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, log?: {
70
+ preparePlanReview: (sessionId: string, log?: {
71
71
  taskId?: string;
72
72
  principalPresent?: boolean;
73
- }, acceptEarly?: boolean, decider?: {
73
+ }, decider?: {
74
74
  principal?: string;
75
75
  explicitOperator: boolean;
76
- }, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
76
+ }) => Promise<{
77
+ readonly ok: false;
78
+ readonly answer: {
79
+ status: number;
80
+ body: object;
81
+ };
82
+ } | {
83
+ readonly ok: true;
84
+ readonly prepared: PreparedPlanReview;
85
+ }>;
86
+ drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
77
87
  status: number;
78
88
  body: object;
79
89
  }>;