@lmzhen/dsh-evolution-approval 0.32.3 → 0.32.5

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.
package/README.md CHANGED
@@ -1,73 +1,19 @@
1
1
  # @lmzhen/dsh-evolution-approval
2
2
 
3
- Stage/pending approval service for self-evolution writes. DSH native approval is
4
- one-shot; this service adds the Hermes staged queue: `request()` stores background writes,
5
- `approve()` replays them through a runner, `reject()` discards them. The model-facing write tools
6
- register their own runners (`tool-memory` via `registerRunner('memory', …)`, `tool-skill-manage`
7
- via `registerRunner('skill', …)`), so staged writes are replayable only when the corresponding
8
- tool package is composed. The pending window is also reachable from the browser half over four
9
- loopback routes (§HTTP surface), so the settings panel can list, decide and PREVIEW staged writes
10
- without the console command.
3
+ > 审批窗口:让记忆/技能写入等你批准。
11
4
 
12
- Tests:
5
+ ## 你会看到
13
6
 
14
- ```sh
15
- node node_modules/vitest/vitest.mjs run packages/evolution-approval/tests # overlay: packages/evolution/evolution-approval/tests
16
- ```
7
+ `/evolution pending`、`approve`、`reject`,以及设置页待批卡。
17
8
 
18
- ## Model surface
9
+ ## 什么时候会用到
19
10
 
20
- - **Model-visible:** nothing of its own: a staged write's effect is what the model notices.
21
- - **Prompt prefix / KV cache:** unchanged by this package: family-level rules single-sourced in `packages/README.md` §"Model-visible prompt prefix and the KV cache".
22
- - **Mount it?** yes — the `evolution-approval` row, in `evolution-host`/`evolution-all`/one-click `evolution-preset` (`enabled: false` there).
11
+ 你打开审批后,每次可暂存的写入。
23
12
 
24
- ## Configuration
13
+ ## 默认 / 前提
25
14
 
26
- - `enabled` (default `false`): master switch (off = ungated).
27
- - `stageForeground` (default `true`): stage foreground writes too.
15
+ 默认关闭;打开后生效。
28
16
 
29
- ## HTTP surface
17
+ ## 边界
30
18
 
31
- Four loopback routes over the same service methods the console command uses, so the two faces cannot drift:
32
-
33
- | route | what it does |
34
- |---|---|
35
- | `GET /api/dsh-evolution/approval/pending?status=` | the staged rows the settings card renders (id, kind, summary, staged-at, age) — never the staged `args` |
36
- | `POST …/approve`, `POST …/reject` | forward to `approve(id)` / `reject(id)`; a refusal is HTTP 200 carrying the service's own sentence |
37
- | `GET …/preview?id=` | what that write WOULD store, resolved read-only |
38
-
39
- The fence is the family's shared loopback check (`evolution-core`'s `http-routes.ts`, one home for every
40
- route surface), and every "cannot answer" path is a JSON body with a `code` and a `message` rather than an
41
- empty response.
42
-
43
- **Previews** come from the package that OWNS the replay: `registerRunner(kind, runner, preview?)` takes an
44
- optional read-only companion that resolves the same `args` into `{path, before, after}`, and the route turns
45
- that into the same diff facts the history panel's diff route uses (`textDiffFacts`). A kind without a
46
- preview, or a resolver that cannot answer (a stale anchor, an operation with no byte-level change), answers
47
- `{available: false, reason}` — the card prints that sentence instead of an empty diff.
48
-
49
- ## Known limitations
50
-
51
- - **Freshness re-validation is per KIND:** a skill write carries `staged_from_sha256`, so its replay (and its
52
- preview) refuses a target that changed after staging; kinds that carry no anchor replay the `args` snapshot
53
- as-is. `/evolution pending --detail` shows what will be replayed.
54
- - **A preview is a read, not a reservation:** it resolves the bytes on disk right now, so a target that
55
- changes between the preview and the approve is refused by the replay's own anchor check rather than by the
56
- preview that was shown.
57
- - **Previews exist only where a resolver is registered** (`skill` today; `tool-skill-manage` supplies it). Every
58
- other kind answers `{available: false, reason}`.
59
- - `approve()` dedupes inside one process and providers resolve the record atomically, but the replay runner executes BEFORE that resolution: two OS processes approving one id can both write while one wins the audit transition. Run approvals from one writer process, or make runners idempotent.
60
- - **Approve + reject on one id** are not serialized inside a process: a reject can resolve a still-executing record while the runner completes and the write still lands: verify the write state, or reject only when no approve is in flight.
61
-
62
- - **The HTTP face is loopback-only by design.** The fence is the family's shared loopback check, authoritative on the socket (`X-Forwarded-For` is never trusted), so a deployment that opens the host to a LAN — where the platform's own `/api` answers normally — gets **403** on all four routes from any non-loopback client. Reach the host over loopback (a local browser, or an SSH tunnel). Whether the family should follow the platform's `trustedHosts` instead is an open decision (v46 `决策 3`; the mechanism lives in `evolution-core`'s `http-routes.ts`).
63
-
64
- **Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
65
-
66
- ## Notes and history
67
-
68
- - **The HTTP face + preview seam (G3, 2026-09-30).** The routes exist so a reader can act on the window
69
- without the console; the preview is registered BESIDE the runner because a preview derived anywhere else
70
- would drift from what the replay writes (the write path owns its own normalization).
71
-
72
- - **Concurrent approve + reject on the same id (F-204).** Inside one process the dedupe keys are `approve:<id>` / `reject:<id>`, so the two paths are not serialized against each other. When an approve runner is slow, a reject resolves the still-executing record to `rejected` **without holding a claim**; the runner may then complete and the write can still land while the audit history reads `rejected`: the write effect, not the audit verdict, is what actually persists (`写效果以实际为准`). `reject` on an executing record reports this and asks you to verify the write state manually. Only reject a write after confirming no approve is in flight, or verify the write effect manually afterwards.
73
- - **No freshness re-validation for kinds WITHOUT an anchor (F-328, corrected).** The staged record stores the `args` snapshot captured at request time and replays exactly those args. Kinds that carry no anchor — **`memory` today** — replay that snapshot as-is, so a target that changed after staging is not re-checked. A skill write is NOT in that class: it carries `staged_from_sha256`, so its replay and its preview refuse a changed target (the per-kind rule above). The pending surface (`/evolution pending --detail`) exposes the staged `args` so you can review what will actually be replayed before approving.
19
+ 批准/拒绝是人工决定;`release` 不是批准。
package/lib/index.js CHANGED
@@ -408,7 +408,9 @@ var EvolutionApproval = class extends Service {
408
408
  for (const record of await this.state().listPending("pending")) {
409
409
  const stagedAt = Date.parse(record.createdAt);
410
410
  if (!Number.isFinite(stagedAt) || stagedAt > cutoff) continue;
411
- if ((await this.state().tryResolvePending(record.id, "rejected")).applied) expired.push(record.id);
411
+ const claimId = this.newClaimId();
412
+ if (!await this.state().claimPending(record.id, claimId)) continue;
413
+ if ((await this.state().tryResolvePending(record.id, "rejected", claimId)).applied) expired.push(record.id);
412
414
  }
413
415
  if (expired.length > 0) this.ctx.logger.warn(`evolution-approval: ${expired.length} staged write(s) passed pendingTtlMs=${ttl} and were closed as rejected WITHOUT executing: ${expired.join(", ")} — stage them again if they are still wanted`);
414
416
  return expired;
@@ -418,6 +420,7 @@ var EvolutionApproval = class extends Service {
418
420
  return await this.state().listPending(status);
419
421
  }
420
422
  async approve(id, audit) {
423
+ await this.expireStale();
421
424
  return await this.dedupe(`approve:${id}`, () => this.doApprove(id, audit));
422
425
  }
423
426
  /** S2-P2-22 (0.3.80): return an ORPHANED executing record to the pending
@@ -641,11 +644,6 @@ var EvolutionApproval = class extends Service {
641
644
  message: `已批准写入「${id}」已重放,但了结记录返回空记录:请核验该写入。`
642
645
  };
643
646
  await this.recordDecision(resolution.record, audit);
644
- this.ctx.emit("evolution/approval-decided", {
645
- record: resolution.record,
646
- action: "approve",
647
- ok: true
648
- });
649
647
  return {
650
648
  ok: true,
651
649
  message: `已批准 ${record.kind} 写入「${id}」(${record.summary})。`
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import { Context, Service } from '@deepseek-ai/cordis';
20
20
  import type { Volatile } from '@deepseek-ai/cordis';
21
- import { type WriteConfirmMode } from '@lmzhen/dsh-evolution-core';
22
21
  import z from '@deepseek-ai/schemastery';
23
22
  import type { PendingKind, PendingRecord, PendingStatus } from '@lmzhen/dsh-evolution-state-storage';
24
23
  import type { DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
@@ -157,15 +156,6 @@ declare module '@deepseek-ai/cordis' {
157
156
  evolutionApproval: EvolutionApproval;
158
157
  }
159
158
  interface Events {
160
- /**
161
- * Emitted after an approval decision has been durably recorded.
162
- * @mode emit
163
- */
164
- 'evolution/approval-decided'(payload: {
165
- readonly record: PendingRecord;
166
- readonly action: 'approve' | 'reject';
167
- readonly ok: boolean;
168
- }): void;
169
159
  }
170
160
  }
171
161
  export interface Config {
@@ -184,12 +174,12 @@ export interface Config {
184
174
  */
185
175
  pendingTtlMs?: Volatile<number>;
186
176
  /**
187
- * U-6: what the confirmation before an APPROVE does. The same three words the skill-write gate uses
188
- * (`WriteConfirmMode`, whose home is `evolution-core/src/write-authority.ts` - moved there with the
189
- * authority that carries it; the re-export from `write-gates.ts` was removed in G1/1.8) - and now the
190
- * IMPORT below is what makes that true: without it the docblock claimed an import the file did not have.
177
+ * U-6: what the confirmation before an APPROVE does: auto (execute immediately) or ask (wait for a
178
+ * human answer). Legacy `timeout` is accepted only so an old value can migrate to `ask`; it is not
179
+ * exposed and is owned by ControlPlane when that lands.
191
180
  */
192
- approvalConfirm?: Volatile<WriteConfirmMode>;
181
+ /** Legacy `timeout` is accepted only to migrate to `ask`; it is no longer exposed or implemented. */
182
+ approvalConfirm?: Volatile<'auto' | 'ask' | 'timeout'>;
193
183
  }
194
184
  export declare class EvolutionApproval extends Service {
195
185
  static inject: string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-approval",
3
3
  "description": "Stage/pending approval service for Hermes-style self-evolution writes (community build)",
4
- "version": "0.32.3",
4
+ "version": "0.32.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,18 +27,18 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.5",
30
- "@lmzhen/dsh-evolution-core": "^0.32.3"
30
+ "@lmzhen/dsh-evolution-core": "^0.32.5"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.5",
34
- "@lmzhen/dsh-evolution-core": "^0.32.3",
35
- "@lmzhen/dsh-evolution-state": "^0.32.3",
36
- "@lmzhen/dsh-evolution-state-storage": "^0.32.3"
34
+ "@lmzhen/dsh-evolution-core": "^0.32.5",
35
+ "@lmzhen/dsh-evolution-state": "^0.32.5",
36
+ "@lmzhen/dsh-evolution-state-storage": "^0.32.5"
37
37
  },
38
38
  "devDependencies": {
39
- "@lmzhen/dsh-evolution-core": "^0.32.3",
40
- "@lmzhen/dsh-evolution-state": "^0.32.3",
41
- "@lmzhen/dsh-evolution-state-storage": "^0.32.3"
39
+ "@lmzhen/dsh-evolution-core": "^0.32.5",
40
+ "@lmzhen/dsh-evolution-state": "^0.32.5",
41
+ "@lmzhen/dsh-evolution-state-storage": "^0.32.5"
42
42
  },
43
43
  "engines": {
44
44
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"