@lmzhen/dsh-evolution-approval 0.32.5 → 0.32.6

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,19 +1,27 @@
1
1
  # @lmzhen/dsh-evolution-approval
2
2
 
3
- > 审批窗口:让记忆/技能写入等你批准。
3
+ Staged approval service for memory and skill writes. It records a proposed mutation, then replays the registered runner only after an operator approves it.
4
4
 
5
- ## 你会看到
5
+ ## Operator surface
6
6
 
7
- `/evolution pending`、`approve`、`reject`,以及设置页待批卡。
7
+ Use `/evolution pending`, `approve <id>`, and `reject <id>`. The settings UI can show the pending queue and, when the owning tool registered a preview, a before/after diff without executing the write.
8
8
 
9
- ## 什么时候会用到
9
+ `/evolution release <id>` is different from approval. It returns an orphaned `executing` record to `pending` after a crashed approval. A live foreign process or an in-flight local decision blocks release.
10
10
 
11
- 你打开审批后,每次可暂存的写入。
11
+ ## Defaults
12
12
 
13
- ## 默认 / 前提
13
+ | Setting | Default | Effect |
14
+ |---|---|---|
15
+ | `enabled` | `false` | Writes proceed without this staged queue. |
16
+ | `stageForeground` | `true` | Once enabled, foreground writes also stage unless the effective session policy is `never`. |
17
+ | `pendingTtlMs` | `0` | Pending records never expire automatically. |
18
+ | `approvalConfirm` | `auto` | Approving does not add a second confirmation by default. |
14
19
 
15
- 默认关闭;打开后生效。
20
+ A positive `pendingTtlMs` closes expired records as `rejected` and never executes them. Expiry is evaluated on queue reads and decisions, not by a background timer.
16
21
 
17
- ## 边界
22
+ ## Boundaries
18
23
 
19
- 批准/拒绝是人工决定;`release` 不是批准。
24
+ - Background-review writes require a registered replay runner. Staging a record without one is not a usable approval flow.
25
+ - Curator and merge writes have their own controls; they do not pass through this ordinary memory/skill approval queue.
26
+ - Before using `release`, verify whether the original runner already landed the write and what effect it had. Approving the returned record replays that operation, which may be non-idempotent.
27
+ - Resolved history is bounded by the state provider. The JSON provider archives older resolved records; the domain provider does not create that JSON archive sidecar.
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { clampOnce, elapsedSince, registerOwnerSettings, textDiffFacts, unwrapLive } from "@lmzhen/dsh-evolution-core";
1
2
  import { Service } from "@deepseek-ai/cordis";
2
3
  import { randomUUID } from "node:crypto";
3
- import { clampOnce, elapsedSince, textDiffFacts, unwrapLive } from "@lmzhen/dsh-evolution-core";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { PENDING_STATUSES } from "@lmzhen/dsh-evolution-state-storage";
6
6
  import { HOST_ROUTES, json, methodNotAllowed, readJsonObject, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
@@ -31,6 +31,26 @@ const APPROVAL_ROUTES = HOST_ROUTES.approval;
31
31
  * reported the same way rather than escaping as an empty 500, because the operator's next step
32
32
  * ("reject it", "retry") is only actionable if the reason is visible.
33
33
  */
34
+ function structuredOf(record) {
35
+ const args = record.args;
36
+ if (record.kind === "memory") {
37
+ const operations = Array.isArray(args?.operations) ? args.operations : [];
38
+ const first = operations[0];
39
+ return {
40
+ verb: typeof first?.action === "string" ? first.action : typeof args?.action === "string" ? args.action : "update",
41
+ target: typeof args?.target === "string" ? args.target : "memory",
42
+ operationCount: operations.length === 0 ? 1 : operations.length,
43
+ actions: record.status === "executing" ? ["release"] : ["approve", "reject"]
44
+ };
45
+ }
46
+ const operation = args?.operation;
47
+ return {
48
+ verb: typeof operation?.action === "string" ? operation.action : "update",
49
+ target: typeof operation?.name === "string" ? operation.name : record.summary,
50
+ operationCount: 1,
51
+ actions: record.status === "executing" ? ["release"] : ["approve", "reject"]
52
+ };
53
+ }
34
54
  async function decide(run, id) {
35
55
  try {
36
56
  const result = await run(id);
@@ -85,7 +105,8 @@ function makeApprovalRoutes(approval, audit) {
85
105
  createdAt: record.createdAt,
86
106
  age: elapsedSince(record.createdAt, Date.now()),
87
107
  status: record.status,
88
- ...record.claimedBy === void 0 ? {} : { claimedBy: record.claimedBy }
108
+ ...record.claimedBy === void 0 ? {} : { claimedBy: record.claimedBy },
109
+ ...structuredOf(record)
89
110
  }))
90
111
  });
91
112
  } catch (error) {
@@ -165,6 +186,26 @@ function makeApprovalRoutes(approval, audit) {
165
186
  return await decide((current) => approval.approve(current, audit), id);
166
187
  }
167
188
  },
189
+ {
190
+ path: APPROVAL_ROUTES.release,
191
+ methods: ["POST"],
192
+ requestBody: "buffered",
193
+ fetch: async (request) => {
194
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
195
+ const id = idOf(await readJsonObject(request));
196
+ if (id === "") return json(400, {
197
+ ok: false,
198
+ code: "bad-request",
199
+ message: "缺少 id"
200
+ });
201
+ if (approval.release === void 0) return json(200, {
202
+ ok: false,
203
+ code: "approval-refused",
204
+ message: "这个部署没有提供 release"
205
+ });
206
+ return await decide((current) => approval.release(current), id);
207
+ }
208
+ },
168
209
  {
169
210
  path: APPROVAL_ROUTES.reject,
170
211
  methods: ["POST"],
@@ -235,7 +276,7 @@ function effectiveSessionPolicy(ctx, session) {
235
276
  if (!approval || session === void 0) return void 0;
236
277
  return approval.overrideOf(session) ?? approval.config?.policy ?? "ask";
237
278
  }
238
- var EvolutionApproval = class extends Service {
279
+ var EvolutionApproval = class EvolutionApproval extends Service {
239
280
  static inject = ["evolutionState"];
240
281
  static Config = z.object({
241
282
  enabled: z.boolean().default(false).volatile(),
@@ -265,6 +306,7 @@ var EvolutionApproval = class extends Service {
265
306
  inFlight = /* @__PURE__ */ new Map();
266
307
  constructor(ctx, config = {}) {
267
308
  super(ctx, "evolutionApproval");
309
+ registerOwnerSettings(this.ctx, "evolution-approval", EvolutionApproval.Config);
268
310
  this.enabledConfig = config.enabled;
269
311
  this.stageForegroundConfig = config.stageForeground ?? true;
270
312
  this.pendingTtlMsConfig = config.pendingTtlMs;
@@ -24,6 +24,7 @@ export declare const APPROVAL_ROUTES: {
24
24
  readonly preview: "/api/dsh-evolution/approval/preview";
25
25
  readonly approve: "/api/dsh-evolution/approval/approve";
26
26
  readonly reject: "/api/dsh-evolution/approval/reject";
27
+ readonly release: "/api/dsh-evolution/approval/release";
27
28
  };
28
29
  /** What one row of the pending list carries to the browser half. */
29
30
  export interface PendingRow {
@@ -36,6 +37,11 @@ export interface PendingRow {
36
37
  readonly status: PendingStatus;
37
38
  /** Present on a claimed-but-unfinished approve: the operator clears it by rejecting (doctor says so too). */
38
39
  readonly claimedBy?: string | undefined;
40
+ /** Structured summary derived from the staged args; the raw summary stays for details. */
41
+ readonly verb: string;
42
+ readonly target: string;
43
+ readonly operationCount: number;
44
+ readonly actions: readonly string[];
39
45
  }
40
46
  /**
41
47
  * The slice of the approval service these routes read.
@@ -54,6 +60,11 @@ export interface ApprovalFace {
54
60
  ok: boolean;
55
61
  message: string;
56
62
  }>;
63
+ /** Release an executing claim; absent on fixtures that only model approve/reject. */
64
+ release?(id: string): Promise<{
65
+ ok: boolean;
66
+ message: string;
67
+ }>;
57
68
  /** The preview the replayer of that kind registered, when it registered one. */
58
69
  previewOf(kind: PendingKind): WritePreview | undefined;
59
70
  }
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.5",
4
+ "version": "0.32.6",
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.5"
30
+ "@lmzhen/dsh-evolution-core": "^0.32.6"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.5",
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"
34
+ "@lmzhen/dsh-evolution-core": "^0.32.6",
35
+ "@lmzhen/dsh-evolution-state": "^0.32.6",
36
+ "@lmzhen/dsh-evolution-state-storage": "^0.32.6"
37
37
  },
38
38
  "devDependencies": {
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"
39
+ "@lmzhen/dsh-evolution-core": "^0.32.6",
40
+ "@lmzhen/dsh-evolution-state": "^0.32.6",
41
+ "@lmzhen/dsh-evolution-state-storage": "^0.32.6"
42
42
  },
43
43
  "engines": {
44
44
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"