@sema-agent/server 7.12.0 → 7.14.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 (74) hide show
  1. package/USAGE.md +51 -2
  2. package/dist/adoption/plan.d.ts +38 -4
  3. package/dist/adoption/plan.js +72 -0
  4. package/dist/adoption/quiesce.d.ts +70 -0
  5. package/dist/adoption/quiesce.js +148 -0
  6. package/dist/adoption/runner.js +63 -5
  7. package/dist/adoption/sql.d.ts +15 -0
  8. package/dist/adoption/sql.js +18 -0
  9. package/dist/adoption/wire.d.ts +7 -1
  10. package/dist/adoption/wire.js +6 -0
  11. package/dist/approval-card.d.ts +5 -0
  12. package/dist/approval-card.js +22 -0
  13. package/dist/boot/coordinators.js +2 -1
  14. package/dist/boot/memory-boundary.d.ts +84 -0
  15. package/dist/boot/memory-boundary.js +110 -0
  16. package/dist/boot/permission-rules-audit.js +29 -1
  17. package/dist/boot/reapers.d.ts +15 -0
  18. package/dist/boot/reapers.js +101 -44
  19. package/dist/boot/resolve-spec.js +31 -0
  20. package/dist/boot/runner-deps.d.ts +16 -2
  21. package/dist/boot/runner-deps.js +24 -4
  22. package/dist/boot/stores.js +93 -3
  23. package/dist/capabilities/memory-notice.d.ts +83 -0
  24. package/dist/capabilities/memory-notice.js +90 -0
  25. package/dist/config-types.d.ts +100 -11
  26. package/dist/config.d.ts +1 -1
  27. package/dist/config.js +111 -1
  28. package/dist/governance-ask-marks.js +2 -1
  29. package/dist/http/active-run-conflict.d.ts +33 -8
  30. package/dist/http/active-run-conflict.js +37 -2
  31. package/dist/http/routes/adoption.js +25 -2
  32. package/dist/http/routes/approvals-assistant.js +33 -3
  33. package/dist/http/routes/capabilities.js +35 -5
  34. package/dist/http/routes/images.js +18 -0
  35. package/dist/http/routes/runs.js +21 -5
  36. package/dist/http/routes/tasks.js +18 -6
  37. package/dist/http/routes/trace-usage.js +43 -14
  38. package/dist/http/server.d.ts +35 -9
  39. package/dist/http/server.js +111 -17
  40. package/dist/http/wire-types.d.ts +6 -1
  41. package/dist/main.js +46 -6
  42. package/dist/observability/fail-open.d.ts +4 -0
  43. package/dist/observability/fail-open.js +4 -0
  44. package/dist/plugins/adoption-log-sql.d.ts +40 -0
  45. package/dist/plugins/adoption-log-sql.js +69 -2
  46. package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
  47. package/dist/plugins/approval-ask-store-sql.js +2 -1
  48. package/dist/plugins/file-run-store.d.ts +85 -1
  49. package/dist/plugins/file-run-store.js +450 -17
  50. package/dist/plugins/memory-embedder.d.ts +44 -0
  51. package/dist/plugins/memory-embedder.js +173 -0
  52. package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
  53. package/dist/plugins/permission-rule-store-sql.js +60 -2
  54. package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
  55. package/dist/plugins/shared-memory-store-sql.js +55 -18
  56. package/dist/plugins/sql-driver.d.ts +19 -0
  57. package/dist/plugins/sql-driver.js +12 -0
  58. package/dist/plugins/store-backend.d.ts +3 -1
  59. package/dist/plugins/store-backend.js +24 -1
  60. package/dist/plugins/tidb-pool.js +11 -4
  61. package/dist/plugins/tool-result-store-sql.d.ts +35 -2
  62. package/dist/plugins/tool-result-store-sql.js +127 -11
  63. package/dist/plugins/web-search.d.ts +3 -1
  64. package/dist/plugins/web-search.js +3 -1
  65. package/dist/rules-consent.d.ts +33 -4
  66. package/dist/rules-consent.js +43 -2
  67. package/dist/run-local.js +6 -2
  68. package/dist/runtime-governance.d.ts +33 -0
  69. package/dist/runtime-governance.js +32 -0
  70. package/dist/security.js +3 -1
  71. package/dist/tool-approval.d.ts +32 -0
  72. package/dist/tool-approval.js +39 -0
  73. package/dist/trace/core-keyset-guard.d.ts +1 -1
  74. package/package.json +3 -3
@@ -90,6 +90,24 @@ export function buildBlobWriteSql(dialect, leg) {
90
90
  // —— 收编那时已经报 adopted。推进 rev 才让那些「站在过去」的写者按 OCC 的本意输掉并重试。
91
91
  return `UPDATE ${leg.table} SET ${blob.column} = ${ph(dialect, 1)}, ${blob.revColumn} = ${blob.revColumn} + 1 WHERE ${where} AND ${blob.revColumn} = ${ph(dialect, blob.pk.length + 2)}`;
92
92
  }
93
+ /**
94
+ * 规则桶的**整桶改键**(A-010.16)。参数序:`[newOwnerKey, toPrincipal, oldOwnerKey]`。
95
+ *
96
+ * `owner_key = sha256("principal:" + principal)` —— 身份在**键的 hash 原像**里,与 session_policy
97
+ * 同族。差别在于这把键**不含逐行变量**(session_policy 的原像里还有 session_id),所以不必逐行扫出来
98
+ * 重算:一对 (旧键, 新键) 算一次,一条等值 UPDATE 走完全表。
99
+ *
100
+ * `principal` 列跟着同改:它是 owner_key 的**明文原像列**(店按前者寻址、`GET /v1/rules` 的回执与运维
101
+ * 的 `WHERE principal = …` 排查按后者读)。只改一列,库里就有两份互相矛盾的身份说法。
102
+ *
103
+ * 幂等性来自谓词:重跑时旧桶键已经不在库里 ⇒ 零行匹配 ⇒ 零行变化。
104
+ * `owner_kind = 'local-owner'` 的那只桶天然不受影响 —— 它的 owner_key 是 `sha256("local-owner")`,
105
+ * 与任何 principal 派生的键都不相等,所以**匹配不到**(不需要额外的 owner_kind 谓词来护住它)。
106
+ */
107
+ export function buildRuleOwnerRekeySql(dialect, leg) {
108
+ return (`UPDATE ${leg.table} SET owner_key = ${ph(dialect, 1)}, principal = ${ph(dialect, 2)}` +
109
+ ` WHERE owner_key = ${ph(dialect, 3)}`);
110
+ }
93
111
  /** session_policy 重键腿的扫描:拿到 from 侧每一行的 `(policy_key, session_id)`。参数序:`[fromPrincipal]`。 */
94
112
  export function buildSessionPolicyScanSql(dialect) {
95
113
  return `SELECT policy_key, session_id FROM session_policy WHERE principal = ${ph(dialect, 1)}`;
@@ -68,7 +68,7 @@ export type AdoptionConfigEntry = z.infer<typeof AdoptionConfigEntrySchema>;
68
68
  * 🔴 这是**冻结的闭集**(下游按成员名写正向断言)。改名/删员 = wire 破坏;新增 = 新行为面,要先过三问。
69
69
  * 判据形见 183 §10.5:「按设计不迁」与「漏了没迁」在黑盒上必须可分,所以它是**正向声明**而不是沉默。
70
70
  */
71
- export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by"];
71
+ export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by", "permission-rule-approval-history", "permission-rule-import-ticket"];
72
72
  export type NotMigratedFace = (typeof NOT_MIGRATED_FACES)[number];
73
73
  export declare const AdoptionNotMigratedSchema: z.ZodObject<{
74
74
  face: z.ZodEnum<{
@@ -78,6 +78,8 @@ export declare const AdoptionNotMigratedSchema: z.ZodObject<{
78
78
  "approval-exemption-grantor": "approval-exemption-grantor";
79
79
  "approval-ask-decision-actor": "approval-ask-decision-actor";
80
80
  "image-bake-requested-by": "image-bake-requested-by";
81
+ "permission-rule-approval-history": "permission-rule-approval-history";
82
+ "permission-rule-import-ticket": "permission-rule-import-ticket";
81
83
  }>;
82
84
  ruling: z.ZodEnum<{
83
85
  D1: "D1";
@@ -124,6 +126,8 @@ export declare const AdoptionReportSchema: z.ZodObject<{
124
126
  "approval-exemption-grantor": "approval-exemption-grantor";
125
127
  "approval-ask-decision-actor": "approval-ask-decision-actor";
126
128
  "image-bake-requested-by": "image-bake-requested-by";
129
+ "permission-rule-approval-history": "permission-rule-approval-history";
130
+ "permission-rule-import-ticket": "permission-rule-import-ticket";
127
131
  }>;
128
132
  ruling: z.ZodEnum<{
129
133
  D1: "D1";
@@ -183,6 +187,8 @@ export declare const AdoptionReceiptSchema: z.ZodObject<{
183
187
  "approval-exemption-grantor": "approval-exemption-grantor";
184
188
  "approval-ask-decision-actor": "approval-ask-decision-actor";
185
189
  "image-bake-requested-by": "image-bake-requested-by";
190
+ "permission-rule-approval-history": "permission-rule-approval-history";
191
+ "permission-rule-import-ticket": "permission-rule-import-ticket";
186
192
  }>;
187
193
  ruling: z.ZodEnum<{
188
194
  D1: "D1";
@@ -64,6 +64,12 @@ export const NOT_MIGRATED_FACES = [
64
64
  "approval-exemption-grantor",
65
65
  "approval-ask-decision-actor",
66
66
  "image-bake-requested-by",
67
+ // A-010.22(台账 P1)—— 规则店族此前对回执**零表态**:`permission_rule` 那只**活桶**现在真迁了
68
+ // (`permission_rule#owner_key` 腿),但同族另外两张表按 D2/D1 刻意不迁,而「刻意不迁」与「漏了没迁」
69
+ // 在黑盒上必须可分(§10.5)。少了这两员,运维读到的回执是:`notMigratedByDesign` 里没有规则店的影子、
70
+ // `residualSourceRows` 又把它们数成 0 —— 一份「什么都没落下」的**假声明**。
71
+ "permission-rule-approval-history",
72
+ "permission-rule-import-ticket",
67
73
  ];
68
74
  export const AdoptionNotMigratedSchema = z
69
75
  .object({
@@ -50,6 +50,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
50
50
  requiresRealApproval: z.ZodBoolean;
51
51
  }, z.core.$strict>;
52
52
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
53
+ persistedRuleShadowed: z.ZodOptional<z.ZodString>;
53
54
  ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
55
  rule: z.ZodString;
55
56
  match: z.ZodEnum<{
@@ -89,6 +90,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
89
90
  requiresRealApproval: z.ZodBoolean;
90
91
  }, z.core.$strict>;
91
92
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
93
+ persistedRuleShadowed: z.ZodOptional<z.ZodString>;
92
94
  ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
93
95
  rule: z.ZodString;
94
96
  match: z.ZodEnum<{
@@ -132,6 +134,9 @@ export interface ApprovalCardSource {
132
134
  /** [2942]/[2943]:治理来源标 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口),
133
135
  * 于是 live 帧 / `card_json` / 重放帧三面同源,不是三处各判一遍。 */
134
136
  governanceForced?: true;
137
+ /** #144(core 5.25.0):被越级的持久规则原文 —— **已 redactSecrets**(发帧点做,本模块只 clip)。
138
+ * 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 / `card_json` / 重放帧三面同源。 */
139
+ persistedRuleShadowed?: string;
135
140
  /** #154 车二:引擎铸的规则候选(**只在规则店装配时**由发帧点填;语义与在场性契约见
136
141
  * `ApprovalCardSchema.ruleSuggestions`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
137
142
  * 三面同源。 */
@@ -79,6 +79,24 @@ export const ApprovalCardSchema = z
79
79
  * park(fail-safe),回滚窗结束即自愈。
80
80
  */
81
81
  governanceForced: z.literal(true).optional(),
82
+ /**
83
+ * #144(core 5.25.0 [3438]/[3443],**ADDITIVE**):这次 ask **命中了**一条用户的持久 allow 规则,
84
+ * 但规则**清不掉**它 —— 值 = 那条被越级的规则**原文**(语义、消音边界与「缺席 ≠ 没有规则」的
85
+ * 硬条款逐字见 `tool-approval.ts` 的 `ToolApprovalFrame.persistedRuleShadowed`)。
86
+ *
87
+ * 放在**卡的顶层**、与 `governanceForced` 并列而不是塞进 `risk`:`risk` 是引擎对这次**操作**的风险
88
+ * 判定,这一格讲的是「你那条规则怎么了」——两件事。与 `governanceForced` 也**刻意分列**:那个说
89
+ * 「门是运维下的」,这个说「有一条你的规则在场但被越级」,两者可以同时在场也可以各自单独在场
90
+ * (governance 是不可消音的三个来源之一,doctrine `always` 与工具自带 egress/irreversible mark 是另两个)。
91
+ *
92
+ * 值域 = **用户内容族**(规则原文是人写的文本,core 侧已过 `inlineUntrusted` 200 cap):
93
+ * 写侧在**上游**(`tool-approval.ts` 发帧点)过 `redactSecrets` 再交进来,本层只截长(`clip`)。
94
+ * UNTRUSTED-for-display,同 `message`/`args` 待遇。
95
+ *
96
+ * 回滚窗代价与 `governanceForced` **逐字同族**(见上一段那条 ⚠️):`.strict()` 下旧二进制读不动带本键的
97
+ * 新行 ⇒ 重放跳过 + 幂等重入走 park(fail-safe),`schemaVersion` 不动。
98
+ */
99
+ persistedRuleShadowed: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
82
100
  /**
83
101
  * #154 车二(core 5.18.0 design/179):引擎为这次 ask 铸的**规则候选**(`Bash(git status)` 精确 /
84
102
  * `Bash(git status:*)` 词界前缀)—— 呈卡面据它渲「不再询问」。
@@ -183,6 +201,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
183
201
  const toolCallId = clip(source.toolCallId, MAX_IDENT);
184
202
  const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
185
203
  const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
204
+ const shadowedRule = clip(source.persistedRuleShadowed, MAX_RULE_TEXT_CHARS);
186
205
  return {
187
206
  toolName: clip(source.toolName, MAX_IDENT) ?? "",
188
207
  message: clip(source.message, MAX_MESSAGE) ?? "",
@@ -195,6 +214,9 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
195
214
  },
196
215
  // [2942]/[2943]:只在为真时投影(`=== true` 严判:非布尔真值不得把一张普通卡染成治理卡)。
197
216
  ...(source.governanceForced === true ? { governanceForced: true } : {}),
217
+ // #144:被越级的规则原文。**空串不投**(core 的四个 mint 点只在真有命中规则时带这个键 ⇒ 空串不是
218
+ // 「有一条空规则」而是坏值;投一个空串会让壳渲一行「你的规则 ⟨⟩ 仍在」)。截长同 `message` 待遇。
219
+ ...(shadowedRule !== undefined && shadowedRule !== "" ? { persistedRuleShadowed: shadowedRule } : {}),
198
220
  // #154 车二:候选**逐字**投影(空数组 ⇒ 不投键 —— 「没有候选」与「本部署不供候选」在卡面上都读作
199
221
  // 「本卡无此选项」,铸一个空数组只会让消费端多一条无意义的分支)。拷贝一份:卡不与调用方共享可变引用。
200
222
  ...(source.ruleSuggestions !== undefined && source.ruleSuggestions.length > 0
@@ -24,7 +24,8 @@ export function createLiveCoordinators(ctx) {
24
24
  // seam (1.290 sync-ask leg). Present ⇒ a policy `ask` on a LIVE streaming leg becomes a `tool_approval` frame the
25
25
  // shell renders as the CC three-choice card, answered via POST /v1/tool-approvals/:id/respond.
26
26
  // [1535] 正名(旧文「background/workflow ⇒ headless auto-deny stands」已过时):宿主 sync 腿现同时装
27
- // `spec.onAsk = boundAsk(ctx)`(server.ts 装配点)——core 继承链把闭包冻给委派子代,**bg/嵌套子代的
27
+ // `spec.onAsk = boundAsk(ctx)`(装配点在 `http/routes/tasks.ts`,design/158 A9 从 server.ts 拆出后
28
+ // 的全树唯一装配点)——core 继承链把闭包冻给委派子代,**bg/嵌套子代的
28
29
  // ask 浮到宿主 live 流**(带 sourceTaskId);真正无宿主流的腿(durable-submit/headless resume)才留
29
30
  // 「unavailable → durable park;无 park 设施 core fail-closed deny」。durable 部署 G1 语义不变。
30
31
  // [1.294 G1] durable 姿势必须在 runnerDeps 字面量之前可判(onAsk 的「在场性」本身就是 core suspendAsk 的
@@ -0,0 +1,84 @@
1
+ /**
2
+ * #214 记忆边界不变式(板 [3492]② 立案,[3521] 裁1 与 core #151 同窗)—— 启动期把「结构性写不进的记忆
3
+ * 部署形」说出来。
4
+ *
5
+ * 病根(test 仓 [3479] 实测,[3490]② 定位):core 的记忆工具面是 `memory_search`/`memory_get` 两只**只读**
6
+ * 工具;**写**记忆没有专用工具 —— 模型用 fs 工具往记忆根写文件,harvest 再从那里收编。于是整条链的成立
7
+ * 条件是「记忆根落在该次任务的 fs 授权边界之内」。server 把根交给 core(`RunnerDeps.memoryEngineDir`)
8
+ * 之后,没有任何一处保证这一点,也没有任何一处说过「这台部署的记忆写面够不着」。默认根是
9
+ * `~/.ai-agent`(或 `AGENT_DATA_DIR`),默认围栏是任务 cwd —— 两者天然不相交:用户说「记住 X」,模型照
10
+ * 提示词去 Write,拿回一个 `path_not_in_root`,而回执早就发出去了。
11
+ *
12
+ * 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount / operator 自己把根塞进
13
+ * `additionalDirectories`,都是真实可行的部署形,而我方在 boot 期看不全。看得全的是「按我们自己接的线,
14
+ * 这条链**结构上**走不通」——那句必须说出来。围栏根算不出来时**不说话**(判不了就闭嘴,别把猜测当告警)。
15
+ *
16
+ * ⚠️ 这不是 fail-open 的托词:真正的 fail-closed 归属面在别处(多租户记忆整体 dark、remote lane 默认 dark)。
17
+ * 本模块判的是「已经决定要开记忆」之后的**可达性**,而可达性的证据一半在部署环境里,不在进程里。
18
+ *
19
+ * ── 指路:声明 `false` 之后引擎那边发生什么(core 5.27.0,[3612] F2;#231 提货批)────────────────────
20
+ * 本模块两条 warn 的收尾句都在劝 operator 表态(`MEMORY_PERSISTENCE_CAPABLE=true|false`)。表 `false`
21
+ * 之后的语义自 core 5.27.0 起**不止是披露**:该会话成为**受限会话** —— materialize/search/harvest 按
22
+ * 已提交账(ledger + shadow)供给,磁盘上无事务背书的分歧既不收编也不供给,而是留盘 + 响亮点名
23
+ * (`restricted_divergence`,`HarvestRejectionCode` 新成员;server 侧把它计进
24
+ * `memory_harvest_rejections_total{code}` + 一条 warn,见 `boot/runner-deps.ts` 的 harvest 报告站点),
25
+ * 等下一次非受限会话走正常门收编。
26
+ * ⚠️ **这一层只在 file 记忆引擎上**(受限视图 = `FileMemoryEngineBackend` 的实装,`materialize` 取它是
27
+ * `?? this.backend` 的可选 seam):`MEMORY_ENGINE_BACKEND=pg|tidb` 的两条腿读侧走库、盘上只是每任务投影,
28
+ * 没有「读磁盘即收编」的通道,`false` 在它们上仍买到披露/写门/零收编 harvest 三件。
29
+ * ⚠️ **口径别混**:受限 = **会话级 verdict 单键**(declared-false 那一个键);`writeScope:null` 的只读
30
+ * **平面**(请求键 `memoryWrite:false` 的 per-run 暂停 / org 层默认只读 / 双根非写面)**保持 adopt-on-read
31
+ * 原样**。受限是会话的**声明**,永远不是平面的**结构**。
32
+ *
33
+ * ── 本仓的 `adoptionRestricted` 消费位:**没有**(记档,免下次重找)──────────────────────────────────
34
+ * core 5.27.0 同批 additive 两件:`MemoryEngine.materialize` 第三参 `opts?.adoptionRestricted` 与
35
+ * `MemorySessionHandle.adoptionRestricted` 读面。本仓**两件都不碰**,而且是结构性的:记忆引擎由 core 的
36
+ * runner 自己驱动(我方只交 `RunnerDeps.memoryBackend` / `memoryEngineDir`),`grep -rn "\.materialize("
37
+ * src test` 零命中 ⇒ 没有调用点可以传第三参,也没有 handle 可读。受限判据因此**只能**从
38
+ * `TaskSpec.memoryPersistenceCapable` 一条路进去(`boot/resolve-spec.ts` 直通),我方显式**不铸**受限会话、
39
+ * 也**不投**这个读面(硬造一个消费点 = 造给自己看的 wire 键)。哪天真要在诊断面露它,先有消费方需求。
40
+ */
41
+ /** boot 期可知的全部事实。每一位都必须由调用点从真配置/真装配结果取,禁在本模块内重算(重算=第二真源)。 */
42
+ export interface MemoryWriteBoundaryFacts {
43
+ /** 记忆引擎的根(`memoryEngine.root`)。`undefined` = 引擎未接 ⇒ 本判据整体不适用。 */
44
+ memoryRoot: string | undefined;
45
+ /** 执行车道名,只进文案(`config.remoteExec?.provider ?? "in-process"`)。 */
46
+ lane: string;
47
+ /** 该车道的文件工具是否跑在**本机** fs 上(in-process / `REMOTE_EXEC=host`)。false = 沙箱盘,与记忆根分平面。
48
+ * ⚠️ 这一位管的是**文件平面**,与下面的 `coreRemoteExecutionEnv` 是**两根轴**,别合并(codex round2
49
+ * [medium] 就是把它们当成一根轴的后果):`host` 车道的文件平面就是本机(这一位 true),但 core 眼里它
50
+ * 仍是 remote(那一位也 true)。 */
51
+ hostSemanticsLane: boolean;
52
+ /** core 的 `isRemoteExecutionEnv` 对本部署执行环境的判决 —— 判别位 = `REMOTE_EXEC` 有没有设(**含
53
+ * `host`**:core 的判别是鸭子类型,`remote-env-host.ts` 把那几件全实现了,亲验坐实)。
54
+ * 5.26.0 的 r13/r14 收窄按的是**这一位**,不是文件平面:凡 core 判 remote,`# Memory` 写指令一律撤,
55
+ * 除非声明 `memoryPersistenceCapable: true`。 */
56
+ coreRemoteExecutionEnv: boolean;
57
+ /** 部署声明 `MEMORY_PERSISTENCE_CAPABLE`(三态)。两极都让本模块闭嘴,但理由不同 —— 见各臂注释。 */
58
+ declaredCapable: boolean | undefined;
59
+ /** boot 期算得出的文件工具围栏根。空数组 = 算不出(每任务临时目录 / 未配 workspace)⇒ 不判。 */
60
+ containmentRoots: readonly string[];
61
+ }
62
+ /** 一条启动告警:`tag` 进日志事件名(可被运维按名过滤/告警),`detail` 是给人读的整句(含恢复路径)。 */
63
+ export interface MemoryWriteBoundaryWarning {
64
+ tag: "memory_root_outside_write_boundary" | "memory_remote_lane_write_instruction_dropped";
65
+ detail: string;
66
+ }
67
+ /**
68
+ * 判两件事,互斥(remote 上不叠 host 的围栏话 —— 围栏根本不在这台机器上,说了是噪音):
69
+ *
70
+ * ① **remote 车道**(只可能因为 `MEMORY_ENGINE_REMOTE_LANE=allow` 才在这里还有引擎):core 5.26.0 的
71
+ * r13/r14 收窄把 `# Memory` 写指令在 remote executionEnv 上撤了(沙箱手带够不着 host 记忆根),
72
+ * 官方恢复路径是声明 `memoryPersistenceCapable: true`。未声明 ⇒ 点名 warn,把恢复旋钮写进句子里。
73
+ * ② **host 车道**:记忆根不在任何一个已知围栏根内 ⇒ 模型被教着往一个 fs 工具够不到的路径写。
74
+ *
75
+ * 两极声明都让本模块闭嘴,但理由不同,别合并:
76
+ * · `true` = operator 自证有推断看不见的持久通道(共享挂载 / 自定义 writer),我们的结构判断本就不该
77
+ * 压过部署方对自己环境的声明;
78
+ * · `false` = operator 已经承认「本机存不下」,引擎据此改口披露只读 —— 再 warn 一次是把已知情的事
79
+ * 当新闻报。
80
+ */
81
+ export declare function buildMemoryWriteBoundaryAudit(facts: MemoryWriteBoundaryFacts): {
82
+ warnings: MemoryWriteBoundaryWarning[];
83
+ };
84
+ //# sourceMappingURL=memory-boundary.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * #214 记忆边界不变式(板 [3492]② 立案,[3521] 裁1 与 core #151 同窗)—— 启动期把「结构性写不进的记忆
3
+ * 部署形」说出来。
4
+ *
5
+ * 病根(test 仓 [3479] 实测,[3490]② 定位):core 的记忆工具面是 `memory_search`/`memory_get` 两只**只读**
6
+ * 工具;**写**记忆没有专用工具 —— 模型用 fs 工具往记忆根写文件,harvest 再从那里收编。于是整条链的成立
7
+ * 条件是「记忆根落在该次任务的 fs 授权边界之内」。server 把根交给 core(`RunnerDeps.memoryEngineDir`)
8
+ * 之后,没有任何一处保证这一点,也没有任何一处说过「这台部署的记忆写面够不着」。默认根是
9
+ * `~/.ai-agent`(或 `AGENT_DATA_DIR`),默认围栏是任务 cwd —— 两者天然不相交:用户说「记住 X」,模型照
10
+ * 提示词去 Write,拿回一个 `path_not_in_root`,而回执早就发出去了。
11
+ *
12
+ * 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount / operator 自己把根塞进
13
+ * `additionalDirectories`,都是真实可行的部署形,而我方在 boot 期看不全。看得全的是「按我们自己接的线,
14
+ * 这条链**结构上**走不通」——那句必须说出来。围栏根算不出来时**不说话**(判不了就闭嘴,别把猜测当告警)。
15
+ *
16
+ * ⚠️ 这不是 fail-open 的托词:真正的 fail-closed 归属面在别处(多租户记忆整体 dark、remote lane 默认 dark)。
17
+ * 本模块判的是「已经决定要开记忆」之后的**可达性**,而可达性的证据一半在部署环境里,不在进程里。
18
+ *
19
+ * ── 指路:声明 `false` 之后引擎那边发生什么(core 5.27.0,[3612] F2;#231 提货批)────────────────────
20
+ * 本模块两条 warn 的收尾句都在劝 operator 表态(`MEMORY_PERSISTENCE_CAPABLE=true|false`)。表 `false`
21
+ * 之后的语义自 core 5.27.0 起**不止是披露**:该会话成为**受限会话** —— materialize/search/harvest 按
22
+ * 已提交账(ledger + shadow)供给,磁盘上无事务背书的分歧既不收编也不供给,而是留盘 + 响亮点名
23
+ * (`restricted_divergence`,`HarvestRejectionCode` 新成员;server 侧把它计进
24
+ * `memory_harvest_rejections_total{code}` + 一条 warn,见 `boot/runner-deps.ts` 的 harvest 报告站点),
25
+ * 等下一次非受限会话走正常门收编。
26
+ * ⚠️ **这一层只在 file 记忆引擎上**(受限视图 = `FileMemoryEngineBackend` 的实装,`materialize` 取它是
27
+ * `?? this.backend` 的可选 seam):`MEMORY_ENGINE_BACKEND=pg|tidb` 的两条腿读侧走库、盘上只是每任务投影,
28
+ * 没有「读磁盘即收编」的通道,`false` 在它们上仍买到披露/写门/零收编 harvest 三件。
29
+ * ⚠️ **口径别混**:受限 = **会话级 verdict 单键**(declared-false 那一个键);`writeScope:null` 的只读
30
+ * **平面**(请求键 `memoryWrite:false` 的 per-run 暂停 / org 层默认只读 / 双根非写面)**保持 adopt-on-read
31
+ * 原样**。受限是会话的**声明**,永远不是平面的**结构**。
32
+ *
33
+ * ── 本仓的 `adoptionRestricted` 消费位:**没有**(记档,免下次重找)──────────────────────────────────
34
+ * core 5.27.0 同批 additive 两件:`MemoryEngine.materialize` 第三参 `opts?.adoptionRestricted` 与
35
+ * `MemorySessionHandle.adoptionRestricted` 读面。本仓**两件都不碰**,而且是结构性的:记忆引擎由 core 的
36
+ * runner 自己驱动(我方只交 `RunnerDeps.memoryBackend` / `memoryEngineDir`),`grep -rn "\.materialize("
37
+ * src test` 零命中 ⇒ 没有调用点可以传第三参,也没有 handle 可读。受限判据因此**只能**从
38
+ * `TaskSpec.memoryPersistenceCapable` 一条路进去(`boot/resolve-spec.ts` 直通),我方显式**不铸**受限会话、
39
+ * 也**不投**这个读面(硬造一个消费点 = 造给自己看的 wire 键)。哪天真要在诊断面露它,先有消费方需求。
40
+ */
41
+ import { resolve } from "node:path";
42
+ /** 词法包含判定:`child` 是否落在 `root` 之内(含相等)。纯词法,不碰 fs —— boot 期这些路径可能还没被
43
+ * 创建,而 `realpath` 会把「同一棵树的两种写法」这个我们**不想**放过的差别悄悄抹平。
44
+ * 先过 `resolve` 折掉 `.`/`..`/重复分隔符(codex 复审:`/srv/ws/../mem` 不该被读成落在 `/srv/ws` 里),
45
+ * 再按 `root + "/"` 前缀判,所以 `/srv/workspace-2` 不会被 `/srv/workspace` 吃掉。 */
46
+ function isInside(child, root) {
47
+ const c = resolve(child);
48
+ const r = resolve(root);
49
+ return c === r || c.startsWith(r === "/" ? "/" : `${r}/`);
50
+ }
51
+ /**
52
+ * 判两件事,互斥(remote 上不叠 host 的围栏话 —— 围栏根本不在这台机器上,说了是噪音):
53
+ *
54
+ * ① **remote 车道**(只可能因为 `MEMORY_ENGINE_REMOTE_LANE=allow` 才在这里还有引擎):core 5.26.0 的
55
+ * r13/r14 收窄把 `# Memory` 写指令在 remote executionEnv 上撤了(沙箱手带够不着 host 记忆根),
56
+ * 官方恢复路径是声明 `memoryPersistenceCapable: true`。未声明 ⇒ 点名 warn,把恢复旋钮写进句子里。
57
+ * ② **host 车道**:记忆根不在任何一个已知围栏根内 ⇒ 模型被教着往一个 fs 工具够不到的路径写。
58
+ *
59
+ * 两极声明都让本模块闭嘴,但理由不同,别合并:
60
+ * · `true` = operator 自证有推断看不见的持久通道(共享挂载 / 自定义 writer),我们的结构判断本就不该
61
+ * 压过部署方对自己环境的声明;
62
+ * · `false` = operator 已经承认「本机存不下」,引擎据此改口披露只读 —— 再 warn 一次是把已知情的事
63
+ * 当新闻报。
64
+ */
65
+ export function buildMemoryWriteBoundaryAudit(facts) {
66
+ const { memoryRoot, lane, hostSemanticsLane, coreRemoteExecutionEnv, declaredCapable, containmentRoots } = facts;
67
+ if (memoryRoot === undefined)
68
+ return { warnings: [] }; // 引擎未接
69
+ if (declaredCapable !== undefined)
70
+ return { warnings: [] }; // 部署已表态(两极,理由见头注)
71
+ // ① 写指令被撤 —— 判据是 **core 的 remote 判决**,不是文件平面(codex round2 [medium],已核真:
72
+ // `REMOTE_EXEC=host` 的文件平面是本机,但 core 照样判它 remote 并撤掉写指令。按文件平面分腿会让
73
+ // 这一形整个静默 —— 恰恰是最容易被忽略的那台机器:记忆看着是开的,写指令没了,没人说过一句)。
74
+ if (coreRemoteExecutionEnv) {
75
+ const plane = hostSemanticsLane
76
+ ? `the model's file tools DO run on this worker's filesystem, but core classifies this execution environment as ` +
77
+ `remote (it exposes the remote-env surface), so the instruction is withheld anyway`
78
+ : `the model's file tools write the SANDBOX filesystem while the memory root (${memoryRoot}) lives on this worker`;
79
+ return {
80
+ warnings: [
81
+ {
82
+ tag: "memory_remote_lane_write_instruction_dropped",
83
+ detail: `the memory engine is wired over the "${lane}" execution lane and ${plane}. ` +
84
+ `Since core 5.26.0 the "# Memory" WRITE instruction is withheld on a remote execution environment, so this ` +
85
+ `deployment reads memory but no longer teaches the model how to save into it. If this lane genuinely reaches ` +
86
+ `the memory root, declare it: MEMORY_PERSISTENCE_CAPABLE=true restores the write instruction. If it does not, ` +
87
+ `MEMORY_PERSISTENCE_CAPABLE=false makes the read-only state explicit to the model instead of leaving it silent.`,
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ if (containmentRoots.length === 0)
93
+ return { warnings: [] }; // 围栏根算不出 ⇒ 不判(见头注)
94
+ if (containmentRoots.some((root) => isInside(memoryRoot, root)))
95
+ return { warnings: [] };
96
+ return {
97
+ warnings: [
98
+ {
99
+ tag: "memory_root_outside_write_boundary",
100
+ detail: `the memory engine root (${memoryRoot}) is outside every file-tool containment root known at boot ` +
101
+ `(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into that root — ` +
102
+ `so a "remember this" turn will be answered with a save the file tools structurally refuse (path_not_in_root). ` +
103
+ `Fix it in one of three ways: point MEMORY_ENGINE_DIR at a path under the workspace, hand the root to tasks via ` +
104
+ `additionalDirectories, or declare MEMORY_PERSISTENCE_CAPABLE=false so the model is told up front that this ` +
105
+ `deployment cannot save instead of receipting a write that never lands.`,
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ //# sourceMappingURL=memory-boundary.js.map
@@ -44,12 +44,40 @@ export async function auditDormantPermissionRules(ctx) {
44
44
  if (buckets > 0 && !ctx.explicit) {
45
45
  // ②③ 都不成立才说话。文案给的是**一条可行动的因果**:看到少问了 ⇒ 这里有 N 只既有桶 ⇒ 用
46
46
  // `GET /v1/rules` 看、用 `DELETE /v1/rules` 收,或者把旋钮显式关掉。
47
+ //
48
+ // 🔴 **local-owner 桶的现状必须同拍说清**(#203 收官件 / #205 件5,core 5.24.0 提货批):
49
+ // `countBuckets` 数的是**所有**桶,其中可能有一只**身份缺席**的 local-owner 桶(File 后端 =
50
+ // `permission-rules/local-owner.json`,SQL 后端 = owner_key `local-owner` 的行)。而 `GET`/`DELETE
51
+ // /v1/rules` 两口是 **principal 寻址**的(无 principal 一律 401,契约上刻意的边界)—— 对那只桶,
52
+ // 上面那句「inspect with GET / revoke with DELETE」是**兑不出来的承诺**。
53
+ //
54
+ // ⚠️ 文案只说**兑得出来**的话(codex 对抗复审 R1-[medium],验真后改):第一稿在这里写的是
55
+ // 「adopt it into a principal first」——**本仓没有那条路**。收编入口的来源词表逐字只有
56
+ // `{kind:"principal"}`(`adoption/wire.ts` 的 `AdoptionSourceSchema`,`.strict()` 拒 local-owner),
57
+ // core 的 `adoptFilePermissionRuleStore` 在 src/ 下零调用点,SQL 侧连对应原语都没有。指一条走不通
58
+ // 的路比不指更坏:运维会去试,试不通才发现自己被误导。
59
+ // 今天**真能做的**只有两件:①`PERMISSION_RULES_ENABLED=false` 把整条车道关掉(上面已给);
60
+ // ②运维直接动存储。
61
+ // 🔴 第二条的**谓词必须是真的**(codex 对抗复审 R2-[medium],验真后改):第一稿写的是
62
+ // 「owner_key='local-owner'」——那条 SQL **一行都删不到**。`buildRuleOwnerKey`
63
+ // (`plugins/permission-rule-store-sql.ts`)把 owner_key 算成 `sha256("local-owner")` 的十六进制,
64
+ // 字面判别式存在**另一列** `owner_kind` 上。给一条匹配零行的删除语句比不给更坏:它会静默成功,
65
+ // 运维以为收回了权限而规则还在放行。真谓词 = `permission_rule` 表的 `owner_kind='local-owner'`。
66
+ // File 后端那侧是固定文件名,照给。
67
+ // 店层与车道层其实已接通(core 5.24.0 的 `removePersistedRule` 收 `RuleOwner`,本仓
68
+ // `rules-consent.ts` 的 `listRules`/`removeRule` 已能寻址该桶,格见
69
+ // `test/rules-consent-local-owner.test.ts`),缺的只是一个 HTTP 开口 —— 开不开、开在哪条门
70
+ // (operator 面 / run-local 面)属产品面裁定,不在本车擅开。
47
71
  ctx.logger.info("permission_rules_activated_by_default", {
48
72
  buckets,
49
73
  knob: "PERMISSION_RULES_ENABLED",
50
74
  explicit: false,
51
75
  note: `${buckets} pre-existing permission-rule bucket(s) went live with this release's ON-by-default flip — ` +
52
- "inspect with GET /v1/rules, revoke with DELETE /v1/rules, or set PERMISSION_RULES_ENABLED=false to keep the lane off",
76
+ "inspect with GET /v1/rules, revoke with DELETE /v1/rules, or set PERMISSION_RULES_ENABLED=false to keep the lane off. " +
77
+ "NOTE: an identity-less local-owner bucket (if this count includes one) is NOT reachable through those two " +
78
+ "principal-addressed endpoints, and this build exposes NO self-service path for it — either turn the knob off, " +
79
+ "or remove the bucket at the storage layer (file backend: the permission-rules/local-owner.json file; " +
80
+ "SQL backend: the permission_rule row WHERE owner_kind='local-owner')",
53
81
  });
54
82
  }
55
83
  return { buckets };
@@ -51,6 +51,12 @@ export interface ThrottledReaperCatch {
51
51
  * survives across ticks — a fresh instance per tick would never accumulate past 1.
52
52
  */
53
53
  export declare function createThrottledReaperCatch(name: string, logger: Logger, threshold?: number): ThrottledReaperCatch;
54
+ /** A-010.17 —— 维护 tick 真正消费的那**一手**(窄口;理由见 `ReapersCtx.permissionRuleStores`)。
55
+ * 可选成员:File 车道没有这一面(追加日志的压实是另一件事,如实登记在 `rules-consent.ts` 的
56
+ * `PermissionRuleStoreBundle.reapExpired` 注里),缺席 ⇒ 本腿零调用。 */
57
+ export interface PermissionRuleRetentionSweeper {
58
+ reapExpired?(nowMs: number): Promise<number>;
59
+ }
54
60
  export interface ReapersCtx {
55
61
  config: ServiceConfig;
56
62
  logger: Logger;
@@ -80,6 +86,15 @@ export interface ReapersCtx {
80
86
  * `TOOL_APPROVAL_ENABLED=false` 的部署恒 undefined ⇒ 收敛器照常收敛,只是不发通知帧(壳侧靠
81
87
  * 重连 preamble 对账,见 approval-card.ts 的 `ApprovalRevokeFrame` 顶注)。 */
82
88
  toolApproval: ToolApprovalCoordinator | undefined;
89
+ /** A-010.17:规则店的**保留期口**。`PERMISSION_RULES_ENABLED=false` 或后端没实装 ⇒ undefined
90
+ * ⇒ 本腿根本不注册(零扫描),与它的同族旋钮腿同姿势。
91
+ *
92
+ * 🔴 类型刻意窄到 {@link PermissionRuleRetentionSweeper} 而不是整个 `PermissionRuleStoreBundle`:
93
+ * 维护 tick 对那只束的其余能力(provider / approvals / tickets)一无所知也不该知道 —— 窄口让测试
94
+ * 能用**两行**字面量驱动真实现的同一段代码,而不必为了满足一个大接口去铸 `as unknown as` 宽松断言
95
+ * (`boot/permission-rules-audit.ts` 的 `DormantRuleCounter` 是同一条判据的先例)。
96
+ * 生产传的仍是整只束(结构上满足这个窄口)。 */
97
+ permissionRuleStores: PermissionRuleRetentionSweeper | undefined;
83
98
  /** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */
84
99
  getRunDenySweep: () => ((now: number) => Promise<void>) | undefined;
85
100
  }