@sema-agent/server 7.45.0 → 7.46.0-rc.2

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 (41) hide show
  1. package/USAGE.md +38 -0
  2. package/dist/approval-card.d.ts +106 -40
  3. package/dist/approval-card.js +45 -8
  4. package/dist/boot/coordinators.d.ts +1 -1
  5. package/dist/boot/memory-consolidation.d.ts +191 -0
  6. package/dist/boot/memory-consolidation.js +132 -0
  7. package/dist/boot/runner-deps.d.ts +1 -1
  8. package/dist/config-types.d.ts +48 -2
  9. package/dist/config-types.js +1 -0
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.js +53 -3
  12. package/dist/http/routes/capabilities.js +4 -0
  13. package/dist/http/routes/memory-compliance.d.ts +102 -0
  14. package/dist/http/routes/memory-compliance.js +113 -0
  15. package/dist/http/routes/memory-consolidation.d.ts +60 -0
  16. package/dist/http/routes/memory-consolidation.js +155 -0
  17. package/dist/http/routes/memory-origin.d.ts +124 -0
  18. package/dist/http/routes/memory-origin.js +193 -0
  19. package/dist/http/routes/rules.js +1 -1
  20. package/dist/http/routes/side-query.js +1 -0
  21. package/dist/http/server.d.ts +71 -2
  22. package/dist/http/server.js +27 -2
  23. package/dist/main.js +55 -2
  24. package/dist/memory-operator-faces.d.ts +211 -0
  25. package/dist/memory-operator-faces.js +76 -0
  26. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  27. package/dist/plugins/checkpoint-store-sql.js +31 -17
  28. package/dist/plugins/local-checkpoint-store.js +3 -3
  29. package/dist/plugins/permission-rule-store-file.d.ts +2 -2
  30. package/dist/plugins/permission-rule-store-file.js +41 -4
  31. package/dist/plugins/permission-rule-store-sql.d.ts +2 -2
  32. package/dist/plugins/permission-rule-store-sql.js +59 -18
  33. package/dist/plugins/store-backend.d.ts +1 -1
  34. package/dist/plugins/tidb-pool.js +3 -3
  35. package/dist/rules-consent.d.ts +30 -3
  36. package/dist/rules-consent.js +50 -7
  37. package/dist/task-cwd.d.ts +1 -1
  38. package/dist/tool-approval.d.ts +43 -10
  39. package/dist/tool-approval.js +105 -27
  40. package/dist/trace/core-keyset-guard.d.ts +2 -2
  41. package/package.json +3 -3
@@ -44,6 +44,9 @@ import { handleAdminDrain } from "./routes/admin-drain.js";
44
44
  import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js";
45
45
  import { handleMemoryPolicy } from "./routes/memory-policy.js";
46
46
  import { handleMemoryBundle } from "./routes/memory-bundle.js";
47
+ import { handleMemoryCompliance } from "./routes/memory-compliance.js";
48
+ import { handleMemoryOrigin } from "./routes/memory-origin.js";
49
+ import { handleMemoryConsolidation } from "./routes/memory-consolidation.js";
47
50
  import { handleSharedMemory } from "./routes/shared-memory.js";
48
51
  import { handleRules } from "./routes/rules.js";
49
52
  import { handleSessionsList } from "./routes/sessions-list.js";
@@ -143,6 +146,9 @@ const ROUTE_DOMAINS = [
143
146
  handleAdminConfigRefresh,
144
147
  handleMemoryPolicy,
145
148
  handleMemoryBundle,
149
+ handleMemoryCompliance,
150
+ handleMemoryOrigin,
151
+ handleMemoryConsolidation,
146
152
  handleRules,
147
153
  handleSharedMemory,
148
154
  handleSessionsList,
@@ -439,6 +445,12 @@ export function createHttpServer(rawDeps) {
439
445
  return;
440
446
  if (await handleMemoryBundle(req, res, url, ctx))
441
447
  return;
448
+ if (await handleMemoryCompliance(req, res, url, ctx))
449
+ return;
450
+ if (await handleMemoryOrigin(req, res, url, ctx))
451
+ return;
452
+ if (deps.memoryConsolidation && (await handleMemoryConsolidation(req, res, url, ctx)))
453
+ return;
442
454
  if (await handleRules(req, res, url, ctx))
443
455
  return;
444
456
  if (deps.sharedMemoryStore && deps.orgMemoryDirectory && (await handleSharedMemory(req, res, url, ctx)))
@@ -2092,7 +2104,7 @@ function checkpointRowRedeemableElsewhere(reason) {
2092
2104
  export function isMethodDispatchedSubmitPath(url) {
2093
2105
  return url === "/v1/a2a";
2094
2106
  }
2095
- function hasServiceAuth(config) {
2107
+ export function hasServiceAuth(config) {
2096
2108
  return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
2097
2109
  }
2098
2110
  export function isBillableSubmitPath(url) {
@@ -2102,6 +2114,7 @@ export function isBillableSubmitPath(url) {
2102
2114
  url === "/v1/runs" ||
2103
2115
  url === "/v1/leader" ||
2104
2116
  url === "/v1/a2a" ||
2117
+ url === "/v1/admin/memory/consolidation/run" ||
2105
2118
  ASSISTANT_RESUME_RE.test(url) ||
2106
2119
  ASSISTANT_PLAN_REVIEW_RE.test(url) ||
2107
2120
  RUN_SUBAGENT_RESUME_RE.test(url) ||
@@ -2114,10 +2127,17 @@ export function isCredentialGatedRewrite(method, url) {
2114
2127
  return (url === "/v1/adoption" ||
2115
2128
  url.startsWith("/v1/rules/cc-import/") ||
2116
2129
  url === "/v1/memory/import" ||
2117
- url === "/v1/memory/export");
2130
+ url === "/v1/memory/export" ||
2131
+ url === "/v1/memory/erase" ||
2132
+ url.startsWith("/v1/memory/origin/entries/"));
2118
2133
  }
2119
2134
  if (m === "DELETE")
2120
2135
  return url === "/v1/rules";
2136
+ if (m === "GET") {
2137
+ return (/^\/v1\/memory\/entries\/[^/]+\/provenance$/.test(url) ||
2138
+ url === "/v1/memory/origin/external" ||
2139
+ url === "/v1/memory/origin/clearances");
2140
+ }
2121
2141
  return false;
2122
2142
  }
2123
2143
  const ROUTE_LABEL_PATTERNS = [
@@ -2161,6 +2181,8 @@ const ROUTE_LABEL_PATTERNS = [
2161
2181
  [/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
2162
2182
  [/^\/v1\/ops\/retention\/holds\/[^/]*$/, "/v1/ops/retention/holds/:domain"],
2163
2183
  [/^\/v1\/memory\/sync\/[^/]+$/, "/v1/memory/sync/:scope"],
2184
+ [/^\/v1\/memory\/entries\/[^/]+\/provenance$/, "/v1/memory/entries/:entryId/provenance"],
2185
+ [/^\/v1\/memory\/origin\/entries\/[^/]+\/clear$/, "/v1/memory/origin/entries/:entryId/clear"],
2164
2186
  [/^\/v1\/usage\/(summary|series|breakdown)$/, "/v1/usage/$sub"],
2165
2187
  [/^\/v1\/shared-memory\/stores\/[^/]+\/documents$/, "/v1/shared-memory/stores/:store/documents"],
2166
2188
  [/^\/v1\/shared-memory\/stores\/[^/]+\/document$/, "/v1/shared-memory/stores/:store/document"],
@@ -2175,11 +2197,14 @@ const ROUTE_LABEL_LITERALS = new Set([
2175
2197
  "/v1/fleet/stream",
2176
2198
  "/v1/memory/export", "/v1/sendfile-links",
2177
2199
  "/v1/memory/import",
2200
+ "/v1/memory/erase",
2201
+ "/v1/memory/origin/external", "/v1/memory/origin/clearances",
2178
2202
  "/v1/shared-memory/stores",
2179
2203
  "/v1/diagnostics/wiring",
2180
2204
  "/v1/adoption",
2181
2205
  "/v1/admin/drain",
2182
2206
  "/v1/admin/config/refresh",
2207
+ "/v1/admin/memory/consolidation/run", "/v1/admin/memory/consolidation",
2183
2208
  "/v1/rules/cc-import/prepare", "/v1/rules/cc-import/redeem",
2184
2209
  "/v1/rules",
2185
2210
  "/.well-known/agent-card.json", "/v1/a2a",
package/dist/main.js CHANGED
@@ -28,9 +28,10 @@ import { installFailOpenRecorder } from "./observability/fail-open.js";
28
28
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
29
29
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
30
30
  import { RateLimiter } from "./observability/rate-limit.js";
31
- import { createHttpServer, explicitOperatorOk } from "./http/server.js";
31
+ import { createHttpServer, explicitOperatorOk, hasServiceAuth } from "./http/server.js";
32
32
  import { exportMemoryScope } from "./memory-export.js";
33
33
  import { createMemoryBundleFaces } from "./memory-bundle-engine.js";
34
+ import { backendOwnsControlPlane, createMemoryComplianceFaces, createMemoryOriginFaces } from "./memory-operator-faces.js";
34
35
  import { performMemorySync } from "./memory-sync.js";
35
36
  import { startOtlpExporter } from "./observability/otel-exporter.js";
36
37
  import { HEARTBEAT_MS, backgroundAgentOutput, taskHandleOutput, taskHandleStop } from "./runs.js";
@@ -46,13 +47,14 @@ import { createResolveSpec } from "./boot/resolve-spec.js";
46
47
  import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
47
48
  import { startReapers } from "./boot/reapers.js";
48
49
  import { assertRetentionLaneWirable, startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./boot/retention-lane.js";
50
+ import { assertConsolidationDriverWirable, buildConsolidationValveAudit, createMemoryConsolidationFaces, resolveConsolidationDriverSeat } from "./boot/memory-consolidation.js";
49
51
  import { startFleetReconciler } from "./fleet/fleet-reconciler.js";
50
52
  import { openStores } from "./boot/stores.js";
51
53
  import { runAdoptionBootScan } from "./boot/adoption.js";
52
54
  import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
53
55
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
54
56
  import { createLimitSync } from "./boot/limit-sync.js";
55
- import { createSideQueryLane } from "./boot/side-query-lane.js";
57
+ import { createSideQueryLane, createPerModelAuthSeat } from "./boot/side-query-lane.js";
56
58
  import { createWebFetchSummarizeLane } from "./boot/webfetch-summarize-lane.js";
57
59
  import { createRuleConsentLane } from "./rules-consent.js";
58
60
  import { createExecutionEnv } from "./boot/execution-env.js";
@@ -116,6 +118,16 @@ async function main() {
116
118
  await configCenter.applyLocalRemoteExec();
117
119
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
118
120
  const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine) : undefined;
121
+ const memoryComplianceFaces = memoryEngine
122
+ ? createMemoryComplianceFaces(memoryEngine, {
123
+ onIncident: (err) => logger.warn("memory_compliance_incident", { code: err.code ?? null, detail: err.message }),
124
+ })
125
+ : undefined;
126
+ const memoryOriginFaces = memoryEngine
127
+ ? createMemoryOriginFaces(memoryEngine, {
128
+ onIncident: (err) => logger.warn("memory_origin_incident", { code: err.code ?? null, detail: err.message }),
129
+ })
130
+ : undefined;
119
131
  const adoptionBoot = await runAdoptionBootScan({ backend, logger });
120
132
  if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
121
133
  logger.info("adoption_boot_scan", { ...adoptionBoot });
@@ -173,6 +185,44 @@ async function main() {
173
185
  ],
174
186
  locked: (config.lockedConfigKeys ?? []).includes("retentionPolicy"),
175
187
  });
188
+ assertConsolidationDriverWirable({
189
+ enabled: config.memoryConsolidationDriver.enabled,
190
+ memoryEngineWired: memoryEngine !== undefined,
191
+ controlPlaneOwned: backendOwnsControlPlane(memoryEngine?.backend),
192
+ memoryEngineBackend: config.memoryEngineBackend,
193
+ ...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
194
+ });
195
+ const memoryConsolidationFaces = config.memoryConsolidationDriver.enabled && memoryEngine
196
+ ? createMemoryConsolidationFaces(memoryEngine, {
197
+ seat: resolveConsolidationDriverSeat({
198
+ brain,
199
+ models: config.models,
200
+ tiers: config.tiers,
201
+ roles: config.roles,
202
+ getApiKeyAndHeaders: createPerModelAuthSeat(() => configCenter.getKeyResolver()),
203
+ }),
204
+ scopes: config.memoryConsolidationDriver.scopes,
205
+ ...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
206
+ onIncident: (err) => logger.warn("memory_consolidation_incident", { code: err.code ?? null, detail: err.message }),
207
+ onStopDetail: (audit) => logger.warn("memory_consolidation_stop_detail", audit),
208
+ })
209
+ : undefined;
210
+ for (const w of buildConsolidationValveAudit({
211
+ enabled: config.memoryConsolidationDriver.enabled,
212
+ operatorPrincipals: config.operatorPrincipals,
213
+ hasServiceAuth: hasServiceAuth(config),
214
+ allowUnauthedWrites: config.allowUnauthedWrites ?? false,
215
+ }).warnings) {
216
+ logger.warn(w.tag, { detail: w.detail });
217
+ }
218
+ if (memoryConsolidationFaces !== undefined) {
219
+ logger.info("memory_consolidation_driver_armed", {
220
+ seat: memoryConsolidationFaces.seat,
221
+ model: memoryConsolidationFaces.model,
222
+ scopes: memoryConsolidationFaces.scopes,
223
+ periodic: false,
224
+ });
225
+ }
176
226
  const governanceSeams = createGovernanceSeams({
177
227
  config,
178
228
  logger,
@@ -547,6 +597,9 @@ async function main() {
547
597
  : undefined,
548
598
  memoryBundleExport: memoryBundleFaces ? memoryBundleFaces.exportScopes : undefined,
549
599
  memoryBundleImport: memoryBundleFaces ? memoryBundleFaces.importBundle : undefined,
600
+ memoryCompliance: memoryComplianceFaces,
601
+ memoryOriginFace: memoryOriginFaces,
602
+ memoryConsolidation: memoryConsolidationFaces,
550
603
  orgMemoryDirectory: orgMemoryAdmission.directory,
551
604
  sharedMemoryStore: sharedMemoryStore ? sharedMemoryStore : undefined,
552
605
  sessionMirrorRuling: principalCaps
@@ -0,0 +1,211 @@
1
+ /**
2
+ * design/316 件③ —— `GET /v1/memory/entries/:entryId/provenance` · `POST /v1/memory/erase` 两口的
3
+ * **引擎接线**(core 5.57.0 `MemoryEngine.provenanceOf` / `eraseMemoryEntries`,design/178 v2-a/v2-b)。
4
+ *
5
+ * ## 为什么是一个模块而不是 main.ts 里直接 new
6
+ *
7
+ * 同 `memory-bundle-engine.ts` 的那句话:本仓**没有**一个长命的 `MemoryEngine` 实例 —— `boot/stores.ts`
8
+ * 装配出来的 `memoryEngine` 是 `{ backend, root }` 二元组,真引擎由 core 的 Runner **每任务现构**
9
+ * (按任务的 scope 平面决定 `memoryDir`)。HTTP 面若要一个引擎,必须自己构;构的判断写在这里,main.ts
10
+ * 只调用与转接。
11
+ *
12
+ * ## 控制面归属:为什么 `controlPlaneRoot` 缺席就整口不挂(F-9 裁)
13
+ *
14
+ * 这两条路径与 bundle 那两条**不同**:它们**真的读引擎控制面**。亲读 core 5.57.0 `engine.js`(不信 JSDoc,
15
+ * 读实现):`provenanceOf`(:628)读 `challengedEntryIds(this.controlDir)` / `lineageAccountOfEntry(this.controlDir, …)`;
16
+ * `eraseMemoryEntries` 的降级腿(:485)读 `lineageContributionsOfSession(this.controlDir, …)`,证据腿则整段跑在
17
+ * backend 自己的 `controlPlaneRoot` 上(托管链 `transfers.jsonl`、erasure anchor)。
18
+ *
19
+ * 而 `this.controlDir` 的来源是 core 构造器的这一支(engine.js:235-239 逐字):`backend.controlPlaneRoot`
20
+ * 在场就用它,**否则**回落到 `deriveControlPlaneDir(resolveMemoryEngineRoot(), memoryDir)` —— 一个从**本进程
21
+ * 的本地盘**派生出来的目录。本仓是 stateless replicas(CLAUDE.md 首段):回落形下,erase 的 write-ahead
22
+ * 托管行(崩溃窗内条目字节的唯一一份)会落到某个随机 pod 的本地盘上,pod 重建即失,而且操作面引擎与
23
+ * Runner 每任务现构的引擎会看到**两个不同的控制面**。
24
+ *
25
+ * ⇒ 裁:`controlPlaneRoot` 缺席 ⇒ 本工厂返回 **undefined**,两口整个不挂载,HTTP 面诚实 501。今天满足这一
26
+ * 条的只有 core 自带的 `FileMemoryEngineBackend`(它把控制面钉死在 config-root 一侧);本仓两只 SQL 记忆
27
+ * 孪生(`plugins/memory-engine-{pg,tidb}.ts`)没有这个面 ⇒ 在那些部署上两口不挂。SQL 控制面是 core 的候升件
28
+ * (design/316 §0b-4 咨询单),**绝不**在本层为它们现造一个「够用的」控制面 —— 那是把一个 core 拒绝生产的
29
+ * 半吊子治理面自己造出来,与 bundle 那条「a governance-less export is the laundering shape」同族。
30
+ *
31
+ * 🔴 这条判断是承重的,所以它有**机器钉**而不是只有这段注释:`test/memory-operator-faces.test.ts` 用一只
32
+ * 假 backend 断言 ①缺 `controlPlaneRoot` ⇒ undefined;②给一个**不存在**的 `memoryDir` 两口照样答得出来
33
+ * (⇒ 它没被读);③backend 缺审计/托管面时 core 自己答 `capability-absent` 三值,本层零修饰。
34
+ *
35
+ * ## 能力缺席在**构造期**判定(与 bundle 三件套逐字同形)
36
+ *
37
+ * 判据是一次**属性检查**、boot 期就判得出、而且是**永久**缺席(换后端才会变)——不是瞬时状态。照旧挂着
38
+ * 就成了「能力位说 yes、每一次调用都确定性失败」,而本仓「says yes ⟺ route works」是结构性承诺。
39
+ * (这条纪律是 #264 提货批被 codex 对抗复审驳倒后立的,见 memory-bundle-engine.ts 头注。)
40
+ *
41
+ * ⚠️ **刻意不把审计/托管/证据三个可选面编进构造判定**:`committedSnapshotOf` / `custodyOf` 缺席时 core
42
+ * 明写要诚实答 `binding:"unknown"` / `contentState:"capability-absent"` / `custody:"capability-absent"`
43
+ * (#196 absence-reports-not-silent-green,engine.d.ts:748 逐字),那是一个**答得出话**的部署;把它们编进
44
+ * 构造判定会让这种部署整口消失,把 core 的诚实答案换成一句 501 谎话。`eraseWithEvidence` 同理:缺它时
45
+ * core 自己响亮拒(`memory.erasure_evidence_unavailable`),那是**运行期**的诚实拒,与「换部署形态才行」
46
+ * 不是一回事(附录 A 两条分列)。
47
+ */
48
+ import { type EntryProvenanceAccount, type MemoryBackend, type MemoryEntryOrigin, type MemoryErasureAttestation, type OriginClearanceRow } from "@sema-agent/core";
49
+ /**
50
+ * 控制面归属判据 —— 三个记忆操作面(合规两口 / 外源标记三口 / consolidation 阀门的 D1 拒启门)共用的
51
+ * **同一只**谓词:backend 的面上带**非空** `controlPlaneRoot` 才算归属在案。空串与缺席同判「不归属」:
52
+ * core 对空串会 truthy-回退到本副本盘上派生控制面(engine 构造里的 `backendControl ? … : derive(…)`),
53
+ * 对无状态多副本部署这正是 D1 要拒的那台机器(折叠账随 pod 一起死)。抽成单点导出是为了让三处
54
+ * 「逐字同源」成为结构事实而不是手抄承诺(merge-rescan 746 C-1:main.ts 手抄版漏了空串臂)。
55
+ */
56
+ export declare function backendOwnsControlPlane(backend: unknown): boolean;
57
+ /** 两口 HTTP 面消费的窄能力面(`ServiceDeps.memoryCompliance` 的实参来源)。 */
58
+ export interface MemoryComplianceFaces {
59
+ /** 单条出处账。core 的失败姿态原样穿过:控制面损坏 ⇒ `ControlPlaneCorruptError`(fail-closed,
60
+ * 绝不在一个完整性未知的账上拼一个答案);能力缺席 ⇒ 三值 `capability-absent`,**不是**抛。 */
61
+ provenanceOf: (entryId: string) => Promise<EntryProvenanceAccount>;
62
+ /**
63
+ * 抹除。入参 `unknown`:调用方递交的是未校验的对象,三选一选择子的整体判决归 core
64
+ * (`erasureRequestInvalid` 是它的单一属主面)—— 在本层抄一遍就是第二真源。
65
+ *
66
+ * `opts.allowedScopes` = **授权信封座**(见 {@link erasureEnvelopeRefusal} 的头注)。
67
+ */
68
+ erase: (input: unknown, opts?: {
69
+ allowedScopes?: readonly string[];
70
+ }) => Promise<MemoryErasureAttestation>;
71
+ }
72
+ /**
73
+ * **选择子替身**的拦截谓词(codex 对抗复审 [critical],亲验复现后修 —— 复现器与结论逐条在
74
+ * `test/memory-compliance-http.test.ts` 的同名 describe 与 `test/memory-operator-faces.test.ts` 里)。
75
+ *
76
+ * ## 病(在 core 5.57.0 上实测,不是纸面推演)
77
+ *
78
+ * `{"select":{"scope":"user:harmless","__proto__":{"ids":["victim"]}}}` 这个体:
79
+ * · `JSON.parse` 把 `__proto__` 建成 select 的**自有数据属性**(JS 对象字面量里写不出这个形 ——
80
+ * 那里的 `__proto__:` 是设原型的语法糖,所以这条路只能由**原始 JSON 文本**走进来);
81
+ * · core 的 `captureErasureInput`(file-backend.js:130)用**赋值**把每个自有键抄进 `{}`,抄到
82
+ * `__proto__` 那一下改的是克隆体的**原型**,于是这个键从 `Object.keys` 里消失;
83
+ * · core 的 `erasureRequestInvalid` 数的是 `Object.keys` ⇒ 只看见一个 `scope` ⇒ **判合法**;
84
+ * · 而两条执行腿的分派都写作 `"ids" in select`(engine.js:477 降级腿、file-backend.js:2614 证据腿),
85
+ * `in` **查原型链** ⇒ 继承来的 `ids` 赢,删的是攻击者点名的那批 id;
86
+ * · 收执里的 `select` / `selectHash` 记的却是那个自有的 `scope`。
87
+ * ⇒ **删掉的东西与凭据上写的不是同一批**,而这一口的全部意义就是那份可归档的凭据;删除不可回滚。
88
+ *
89
+ * ## 修在哪一层(为什么不是「替上游打补丁」)
90
+ *
91
+ * core 的复制与分派确实该收紧(own-property 检查 / null 原型克隆),那是**上游工单**。本层这条谓词不
92
+ * 改上游语义、也不替它清洗数据:它做的是**我方边界该做的事** —— 一个会重写对象原型的 JSON 键**不是**
93
+ * 一个选择子键,我方不把这种体递交下去,而且**响亮拒**(不是静默抹掉那个键:安全轴上的静默修正正是
94
+ * 本仓禁止的形,而且悄悄改写调用方的请求会让它以为自己删的是别的东西)。
95
+ *
96
+ * 两个执法点共用**这一只**谓词:路由拿它出 400(wire 可见、有钉),seam 拿它 fail-closed 兜底(seam 是
97
+ * 可导出模块,不能靠「今天只有一个调用方」这句话来安全;走到那一臂 = 路由的门被绕过了 = 装配 bug,
98
+ * 500 fail-loud 是对的)。刻意不做「只在 seam 拦」:那样 wire 上就没有一句能测的诚实拒。
99
+ *
100
+ * 走法是**迭代**而非递归:体的深度只受体积上限约束,一个深嵌套的 JSON 能把递归版打爆栈(把一个
101
+ * 完整性缺陷换成一个可远程触发的崩溃不算修好)。
102
+ */
103
+ export declare function erasureInputSmugglesPrototypeKey(input: unknown): boolean;
104
+ /**
105
+ * 在**已装配的记忆后端**上建合规两面。命名照 CLAUDE.md 工厂律:返回的是带行为的活对象 ⇒ `create*`。
106
+ *
107
+ * `memoryDir` = `boot/stores.ts` 的 `memoryEngine.root`(该部署的记忆工作根)。见文件头 ② 号钉:这两条
108
+ * 路径不读它(erase 的派生索引清扫只在**真有**已删条目的绑定投影时才碰挂载树),传它是为了不发明第二个
109
+ * 「引擎根」的概念 —— 与 `createMemoryBundleFaces` 逐字同一条理由。
110
+ */
111
+ export declare function createMemoryComplianceFaces(store: {
112
+ backend: MemoryBackend;
113
+ root: string;
114
+ },
115
+ /**
116
+ * 引擎的 **advisory incident 座**(core `MemoryEngineOptions.onIncident`)。接它不是锦上添花:
117
+ * 抹除腿上有一条**只走这个座**的码 —— `memory.erasure_index_residue`(engine.js:616)。它说的是
118
+ * 「这次抹除的**索引残留披露**没能写进收执」:于是 operator 归档的那份凭据里少了一句「MEMORY.md
119
+ * 里可能还留着被删条目的名字」,而收执**看上去是干净的**。座不接 ⇒ 这句话在本部署上无声消失,正是
120
+ * 安全轴上禁止的那种静默(合规腿尤甚)。同座还带 `memory.announce_failed`(公告队列本身写不进去)/
121
+ * `memory.challenge_ledger_corrupt` / `memory.lineage_settle_failed`。
122
+ * ⚠️ core 的 d.ts 只列了两个码走这个座 —— **亲读实装**(engine.js 的四处 `sink(e)`)不止两个,
123
+ * `memory.erasure_index_residue` 正是它漏写的那条(宪法:不信 JSDoc,锚实现行)。机器钉在
124
+ * `test/memory-operator-faces.test.ts`,用一份 residuals 被冻住的收执把那条腿确定性打出来。
125
+ * 缺席合法(advisory-silent,与 bundle 三件套同形):宿主不接就只是没人听,引擎不会因此失败。
126
+ */
127
+ opts?: {
128
+ onIncident?: (err: Error & {
129
+ code?: string;
130
+ }) => void;
131
+ }): MemoryComplianceFaces | undefined;
132
+ /** 件② 三口 HTTP 面消费的窄能力面(`ServiceDeps.memoryOriginFace` 的实参来源)。 */
133
+ export interface MemoryOriginFaces {
134
+ /**
135
+ * §13-4① 视图面:点名 scope 里**每一条**带外源标记的条目 + 它的出处账。
136
+ *
137
+ * 🔴 `scopes` 由**调用方**给,本层不发明默认值:v1 本仓没有任何 scope 枚举读面(core 5.59.0 的
138
+ * `listMemoryScopes` 已到 File 腿、SQL 腿是 core 候升件 #437④,本仓尚未接线),而「空 scope 名单」
139
+ * 与「查全店」在这条路上是两件事 —— 猜一个默认名单会让审计的**不完备变成静默的**(空数组与
140
+ * 「这些 scope 干净」同形)。缺参时路由那侧 400 并指路,理由与文案同源于此。
141
+ */
142
+ listExternal: (scopes: readonly string[]) => Promise<Array<{
143
+ id: string;
144
+ slug: string;
145
+ scope: string;
146
+ origin: MemoryEntryOrigin;
147
+ provenance: EntryProvenanceAccount;
148
+ }>>;
149
+ /** §13-4② 读半场:清标审计账(**同步** —— core 的 `listOriginClearances` 就是一次严格 sidecar 读;
150
+ * 账本损坏 ⇒ `ControlPlaneCorruptError`,fail-closed,绝不回半张表)。 */
151
+ listClearances: () => OriginClearanceRow[];
152
+ /** §13-4② 写半场:审计化的 UN-MARK 阀门。拒因族是 core 的**闭集码**,本层零翻译原样上抛。 */
153
+ clear: (entryId: string, input: {
154
+ requestId: string;
155
+ reason: string;
156
+ }) => Promise<{
157
+ entryId: string;
158
+ clearanceId: string;
159
+ origin: MemoryEntryOrigin;
160
+ landedSlug: string;
161
+ }>;
162
+ }
163
+ /**
164
+ * design/316 件② —— 外源标记人面的**引擎接线**(core 5.59.0 `MemoryEngine.listExternalOriginEntries` /
165
+ * `listOriginClearances` / `clearEntryOrigin`,design/336 §13-4①②)。
166
+ *
167
+ * ## 控制面归属:判据与合规两口**逐字同一条**(§1 F-9 裁),但理由更硬
168
+ *
169
+ * 亲读 core 5.59.0(不信 JSDoc,读 `engine.js` 实装):
170
+ * · `listOriginClearances`(:3598)= `readOriginClearances(this.controlDir)`;
171
+ * · `clearEntryOrigin`(:3601)整条弧都压在 `this.controlDir` 上 —— 写前开行(`openOriginClearance`)、
172
+ * 墓碑落定登记(`markOriginClearanceTombstoned`)、终态事件(`settleOriginClearance`);
173
+ * · `listExternalOriginEntries`(:3582)每一行都要 `provenanceOf`,而它读 `challengedEntryIds` /
174
+ * `lineageAccountOfEntry`,同样是控制面。
175
+ * 三个动词没有一个能离开控制面。而 `clearEntryOrigin` 的写前行里躺着 `entryText` ——
176
+ * **被清标条目的完整正文**;core 的原话逐字是「A crash between the tombstone batch and the re-record
177
+ * batch leaves this as the only copy」(origin-clearance.d.ts:30)。也就是说:墓碑已落、重录未成的那个
178
+ * 崩溃窗内,这一行是那条记忆在世界上**唯一的一份**。
179
+ *
180
+ * ⇒ 在 `controlPlaneRoot` 缺席的部署上(core 会把控制面落到 `resolveMemoryEngineRoot()` 派生的**副本
181
+ * 本地盘**,engine.js:235-239),这唯一的一份会落在某个随机 pod 的本地盘上,pod 重建即**永久丢失**,
182
+ * 而且下一次 resume 调用大概率落到另一台副本、读不到那行 ⇒ 一条被清标的记忆连同它的托管字节一起消失。
183
+ * 本仓是 stateless replicas(CLAUDE.md 首段)。所以判在**构造期**:工厂返 undefined、三口整个不挂、
184
+ * HTTP 面诚实 501,与 `createMemoryComplianceFaces` 同形同码不同文案(要查的旋钮不是同一个)。
185
+ * SQL 控制面是 core 的候升件(design/316 §0b-4 / #437④),**绝不**在本层为它现造一个。
186
+ *
187
+ * ## 刻意不做的两件事
188
+ *
189
+ * ⚠️ **不把 `listScopes` 编进构造判定**:core 5.59.0 长出了 `listMemoryScopes`(#437③,File 腿在场、
190
+ * SQL 腿候升),但那是**发现链**的面,不是本族三口的执行前提 —— 把它编进来会让一个「scope 名由运维
191
+ * 自己拿着」的部署整口消失。发现链的不完备由 `GET …/external` 缺参时的 400 文案如实说出来。
192
+ * ⚠️ **不在 seam 里翻译拒因码**:core 的 origin-clear 族是**闭集**,翻译点只有一个(路由的 switch),
193
+ * 在这里再翻一遍就是第二真源。seam 原样抛,路由逐码字面量转发。
194
+ */
195
+ export declare function createMemoryOriginFaces(store: {
196
+ backend: MemoryBackend;
197
+ root: string;
198
+ },
199
+ /**
200
+ * 引擎的 advisory incident 座(core `MemoryEngineOptions.onIncident`)。
201
+ * 🔴 与合规面的同名座**理由不同,如实写**:亲读 core 5.59.0 的这三条动词路径(engine.js:3577-3760)
202
+ * 一个 `sink(...)` 都没有 —— 今天没有任何码**只**走这个座上来。接它是因为代价为零而失效形是静默的:
203
+ * core 哪天在这条路上加一条只走 sink 的披露(清标的族里最像的候选是账本尺寸/重建那两条),不接的
204
+ * 部署会让它无声消失。缺席合法(advisory-silent,与 bundle / 合规三件套同形)。
205
+ */
206
+ opts?: {
207
+ onIncident?: (err: Error & {
208
+ code?: string;
209
+ }) => void;
210
+ }): MemoryOriginFaces | undefined;
211
+ //# sourceMappingURL=memory-operator-faces.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { MemoryEngine, } from "@sema-agent/core";
2
+ export function backendOwnsControlPlane(backend) {
3
+ const b = backend;
4
+ return typeof b?.controlPlaneRoot === "string" && b.controlPlaneRoot !== "";
5
+ }
6
+ export function erasureInputSmugglesPrototypeKey(input) {
7
+ const stack = [input];
8
+ while (stack.length > 0) {
9
+ const node = stack.pop();
10
+ if (Array.isArray(node)) {
11
+ for (const v of node)
12
+ if (typeof v === "object" && v !== null)
13
+ stack.push(v);
14
+ continue;
15
+ }
16
+ if (typeof node !== "object" || node === null)
17
+ continue;
18
+ if (Object.prototype.hasOwnProperty.call(node, "__proto__"))
19
+ return true;
20
+ for (const v of Object.values(node))
21
+ if (typeof v === "object" && v !== null)
22
+ stack.push(v);
23
+ }
24
+ return false;
25
+ }
26
+ function erasureEnvelopeRefusal(input, allowedScopes) {
27
+ if (allowedScopes === undefined)
28
+ return undefined;
29
+ const select = typeof input === "object" && input !== null ? input.select : undefined;
30
+ const scope = typeof select === "object" && select !== null ? select.scope : undefined;
31
+ if (typeof scope === "string" && allowedScopes.includes(scope))
32
+ return undefined;
33
+ const e = new Error(typeof scope === "string"
34
+ ? `memory erasure refused: scope ${JSON.stringify(scope)} is outside this caller's erasure envelope`
35
+ : "memory erasure refused: this request's selector is not scope-addressable, and an envelope that cannot judge a selector must not admit it");
36
+ e.code = "memory.erasure_scope_forbidden";
37
+ return e;
38
+ }
39
+ export function createMemoryComplianceFaces(store, opts) {
40
+ if (!backendOwnsControlPlane(store.backend))
41
+ return undefined;
42
+ const engine = new MemoryEngine({
43
+ backend: store.backend,
44
+ memoryDir: store.root,
45
+ ...(opts?.onIncident !== undefined ? { onIncident: opts.onIncident } : {}),
46
+ });
47
+ return {
48
+ provenanceOf: (entryId) => engine.provenanceOf(entryId),
49
+ erase: (input, opts) => {
50
+ if (erasureInputSmugglesPrototypeKey(input)) {
51
+ const e = new Error("memory erasure refused: the request carries a key that rewrites an object's prototype — such a key is not a selector, and core's selector dispatch would read it in preference to the declared one");
52
+ e.code = "memory.erasure_selector_unsafe";
53
+ return Promise.reject(e);
54
+ }
55
+ const refusal = erasureEnvelopeRefusal(input, opts?.allowedScopes);
56
+ if (refusal !== undefined)
57
+ return Promise.reject(refusal);
58
+ return engine.eraseMemoryEntries(input);
59
+ },
60
+ };
61
+ }
62
+ export function createMemoryOriginFaces(store, opts) {
63
+ if (!backendOwnsControlPlane(store.backend))
64
+ return undefined;
65
+ const engine = new MemoryEngine({
66
+ backend: store.backend,
67
+ memoryDir: store.root,
68
+ ...(opts?.onIncident !== undefined ? { onIncident: opts.onIncident } : {}),
69
+ });
70
+ return {
71
+ listExternal: (scopes) => engine.listExternalOriginEntries(scopes),
72
+ listClearances: () => engine.listOriginClearances(),
73
+ clear: (entryId, input) => engine.clearEntryOrigin(entryId, input),
74
+ };
75
+ }
76
+ //# sourceMappingURL=memory-operator-faces.js.map
@@ -1,6 +1,7 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
4
+ import { type RuleOfferProjection } from "../approval-card.js";
4
5
  import { type SqlDriver } from "./sql-driver.js";
5
6
  /**
6
7
  * design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
@@ -63,11 +64,26 @@ export interface PendingCheckpoint {
63
64
  * (DESC) so the supervisor inbox triages high-risk asks first. `null` for gates with no descriptor. */
64
65
  riskDescriptor: RiskDescriptor | null;
65
66
  /**
66
- * [3683]-2 / [3684]②(**ADDITIVE**):这条 park 的**规则候选** —— core 在 mint 这道门时挂到
67
- * `pendingAction.ruleSuggestions` 上的那一份(core `checkpoint-store.d.ts` design/179 §4;它明写与
68
- * 同步腿的 `AskRequest.ruleSuggestions` 是**同一条契约**:顺序即展示序,core `exact` 形在场时排在
69
- * 最前,core 契约基数 ≤2 —— server 侧执法帽是 `MAX_RULE_SUGGESTIONS`=4 的容忍余量,两个数的分工见
70
- * 该常量顶注;**索引不是身份**:形不合的候选逐条丢,判形读 `match`)。
67
+ * [3683]-2 / [3684]② 起役;core 5.58.0(design/375) OFFER 判别联合形:这条 park 的「不再询问」
68
+ * offers —— core 在 mint 这道门时挂到 `pendingAction.ruleOffers` 上的那一份(core
69
+ * `checkpoint-store.d.ts` 明写与同步腿的 `AskRequest.ruleOffers` 是**同一条契约**:序即契约——
70
+ * whole-string exact `single` 在场时恒 index 0、`batch` 恒末位,core 契约基数 ≤2 —— server 侧执法帽
71
+ * `MAX_RULE_OFFERS`=4 的容忍余量,两个数的分工见该常量顶注)。
72
+ *
73
+ * 🔴 **本腿的下标不是 core 的 offer index,别把它当选择键**(5.58 复审 F-3,与契约文同批改准)。
74
+ * core 的契约原话是:丢掉不认识的成员时**必须保住每个留下来成员的原始 wire 下标**,做不到的消费端
75
+ * 「must suppress its persistence actions entirely (fail toward asking)」。本腿走的是**后者** ——
76
+ * {@link boundedRuleOffers} 对形不合的 offer 是**逐条丢弃并压紧**的(一条坏 offer 不该让另一条真 offer
77
+ * 消失,这条不变量本身有它自己的红格),压紧就意味着中间剔除会让后面的下标整体前移。之所以站得住,
78
+ * 是因为**本腿没有兑付口**:durable 回决体(`/v1/approvals/:id/decide`)不带规则位,选择键一次都没有
79
+ * 用到本腿的下标。⇒ **硬条款**:哪天 durable 腿真要开兑付位,先把「原始下标随行」显式加进本投影
80
+ * (或按 core 的另一半整格不投),而不是默默拿压紧后的下标当选择键。
81
+ * 与**同步腿**刻意分家:`buildRuleLaneMaterial` 那侧是**纯前缀截、零逐条丢弃**,所以活卡帧 /
82
+ * `card_json` / 重放帧上的下标**恒等于** core 的 offer index —— 兑付真发生在那条腿上。
83
+ *
84
+ * 🔴 **混舰队旧行(pre-5.58 的 `ruleSuggestions` 数组,无 `kind` 判别位)**:按新 Raw 形逐条不合
85
+ * ⇒ 整格落「无供给」——与 [5154] 升级单的舰队条款一致(旧建议面丢失只影响展示,ask 安全面照常;
86
+ * pending 审批短命,重触发即得新形卡)。**不做兼容读**(schema stale=重问,core 同款纪律)。
71
87
  *
72
88
  * 🔴 **无供给 ≠ 丢失**(本键存在的全部理由):同步腿的卡带这一格(`card_json` 里逐字在),而耐久腿
73
89
  * (park 行 → `GET /v1/approvals`)此前**根本没投**,于是同一张卡走 durable 路时「不再询问」那一格
@@ -80,26 +96,22 @@ export interface PendingCheckpoint {
80
96
  *
81
97
  * 🔴 **展示用,永不参与 resume**:core 侧同款定性(「never read by the resume path, and never a rule by
82
98
  * itself」)。本仓耐久腿今天也没有兑付口(`/v1/approvals/:id/decide` 的体不带规则位),所以它是**分诊
83
- * 材料**不是承诺 —— 消费端渲这一格时要按「同副本活卡腿才兑得动」读(同 `ApprovalCardSchema.ruleSuggestions`
99
+ * 材料**不是承诺 —— 消费端渲这一格时要按「同副本活卡腿才兑得动」读(同 `ApprovalCardSchema.ruleOffers`
84
100
  * 顶注那条射程说明)。
85
101
  */
86
- ruleSuggestions?: ReadonlyArray<{
87
- rule: string;
88
- match: "exact" | "prefix";
89
- command: string;
90
- }>;
102
+ ruleOffers?: readonly RuleOfferProjection[];
91
103
  }
92
104
  /**
93
- * `pendingAction.ruleSuggestions` 的**铸行侧**投影(mint 面,与 {@link boundedToolInput} 同位同待遇)。
105
+ * `pendingAction.ruleOffers` 的**铸行侧**投影(mint 面,与 {@link boundedToolInput} 同位同待遇)。
94
106
  *
95
107
  * 三件事,一件都不能少:
96
108
  * 1. **结构窄读**(宪法 [2704] 边界必 schema / 禁裸 as-cast):blob 里的这一格是 `unknown`(旧 core /
97
- * 坏行 / 未来加字段都可能)。形不合的**整条丢弃**(不是整只丢弃)——一条坏候选不该让另一条真候选消失。
98
- * 判据只看 `rule`/`match`/`command` 三键合形(`RuleSuggestionRawSchema` 是**默认 strip 形** `.strip()`,
109
+ * 坏行 / 未来加字段都可能)。形不合的**整条丢弃**(不是整只丢弃)——一条坏 offer 不该让另一条真 offer 消失。
110
+ * 判据按 `kind` 判别联合分臂合形(`RuleOfferRawSchema` 两臂各自 **strip 形** `.strip()`,
99
111
  * 多余键**剥掉**、且不拒收 —— 重扫二轮更正:旧注写 `.loose()`,而 zod 4 的 loose = passthrough,
100
112
  * 未知键会**留在** `parsed.data` 里,与「剥掉」相反)。上游 additive 加一键是 core 的常规做法,拿 strict 去窄读会让那一次加字段把整只
101
- * 候选静默清零 —— 「有供给」当场变「无供给」,与本键的存在理由正相反(顶注第三段)。
102
- * 2. **脱敏**:候选文本是从**命令原文**铸出来的(`Bash(curl -H "Authorization: Bearer …")` 这种 exact
113
+ * offer 静默清零 —— 「有供给」当场变「无供给」,与本键的存在理由正相反(顶注第三段)。
114
+ * 2. **脱敏**:offer 文本是从**命令原文**铸出来的(`Bash(curl -H "Authorization: Bearer …")` 这种 exact
103
115
  * 形拼得出秘密),而 `GET /v1/approvals` 是**跨租户可见**的运维队列。落列前过 `redactSecrets` ——
104
116
  * 与同行 `tool_input` 在 mint 就 `redactDeep` 是**同一条**纪律(#209 件4 把 `shadowedRule` 修在读面
105
117
  * 是因为那条路的行早已落库、改铸点救不了存量;本键是**新**列,没有存量行要救,修在铸点更彻底:
@@ -107,12 +119,18 @@ export interface PendingCheckpoint {
107
119
  * 3. **脱敏之后**才截长(codex 对抗复审 [medium],验真后修):`redactSecrets` 会**变长**,先按限长校验
108
120
  * 原文再脱敏会产出超限文本,而 SQL 腿有**回读**(列 → 本函数再跑一遍)、LOCAL 腿没有 ⇒ 同一份素材
109
121
  * 两个后端给出不同卡面。顺序反过来之后本函数对自己的输出**幂等**,两条腿同形。完整论证见
110
- * `RuleSuggestionRawSchema` 顶注。
111
- * 4. **限基数**:core 契约今天 ≤2,这里按 {@link MAX_RULE_SUGGESTIONS}(=4,server 侧容忍帽,两个数的
112
- * 分工逐字见该常量顶注)截 —— 一条被改坏/未来放宽的上游不该把运维队列的一行撑爆。
113
- * 🔴 **扣预算在合形之后**(合并码重扫):旧序先 `slice` 再判形,于是 ≥MAX 条坏候选能把唯一的真候选
114
- * 挤出预算,读数是 `undefined` —— 与上面第 1 条自证的不变量(坏的丢、好的照投)正相反。扫描窗
115
- * {@link MAX_RULE_SUGGESTION_SCAN} 仍是有界的:一条被改坏成上万条的行不该让每 3s 一拍的读路径去逐条判形。
122
+ * `RuleOfferRawSchema` 顶注。
123
+ * 4. **限基数**:core 契约今天 ≤2,这里按 {@link MAX_RULE_OFFERS}(=4,server 侧容忍帽,两个数的
124
+ * 分工逐字见该常量顶注)截;`batch.rules` 成员另按 {@link MAX_RULE_OFFER_BATCH_MEMBERS}(=8)截 ——
125
+ * 一条被改坏/未来放宽的上游不该把运维队列的一行撑爆。两个截都是**前缀截**(保序)
126
+ * ⚠️ **但本函数整体不保原始下标**:第 1 条的「坏的逐条丢」会压紧数组 中间剔除后下标前移。
127
+ * 本腿因此是**纯展示腿**,下标不得当选择键 —— 硬条款与它站得住的理由逐字见
128
+ * {@link PendingCheckpoint.ruleOffers} 顶注(同步腿相反:那侧零逐条丢弃,下标恒等于 core 的 offer index)。
129
+ * 🔴 **成员帽扣在判形之前**(5.58 复审 F-2):见 {@link preboundBatchMembers} —— offer 条数那一维由
130
+ * {@link MAX_RULE_OFFER_SCAN} 管,成员那一维在那之前是**无界**的。
131
+ * 🔴 **扣预算在合形之后**(合并码重扫):旧序先 `slice` 再判形,于是 ≥MAX 条坏 offer 能把唯一的真
132
+ * offer 挤出预算,读数是 `undefined` —— 与上面第 1 条自证的不变量(坏的丢、好的照投)正相反。扫描窗
133
+ * {@link MAX_RULE_OFFER_SCAN} 仍是有界的:一条被改坏成上万条的行不该让每 3s 一拍的读路径去逐条判形。
116
134
  *
117
135
  * 🔴 `opts.countRedactions`:**两条腿都照常脱敏,只有回读腿不计数**(合并码重扫;codex 复审第一轮
118
136
  * 指出「回读不脱敏」会把纵深防御一起关掉,验真后按这条收)。`redactSecrets` 对自己的输出是**值幂等、
@@ -123,13 +141,9 @@ export interface PendingCheckpoint {
123
141
  *
124
142
  * 返回 `undefined` = **无供给**(键不落列):core 没给、给了空数组、或整只形不合。
125
143
  */
126
- export declare function boundedRuleSuggestions(raw: unknown, opts?: {
144
+ export declare function boundedRuleOffers(raw: unknown, opts?: {
127
145
  countRedactions?: boolean;
128
- }): Array<{
129
- rule: string;
130
- match: "exact" | "prefix";
131
- command: string;
132
- }> | undefined;
146
+ }): RuleOfferProjection[] | undefined;
133
147
  /** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
134
148
  * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
135
149
  * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */