@lmzhen/dsh-evolution-approval 0.22.0 → 0.23.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.
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { randomUUID } from "node:crypto";
3
- import { clampOnce, elapsedSince, isLoopbackRequest, isProcessAlive, parseLockBody, readJsonObject, textDiffFacts, writeJson } from "@lmzhen/dsh-evolution-core";
3
+ import { clampOnce, elapsedSince, isLoopbackRequest, readJsonObject, textDiffFacts, writeJson } 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
  //#region lib/types/routes.js
@@ -228,6 +228,30 @@ function makeApprovalRoutes(approval) {
228
228
  *
229
229
  * @module @lmzhen/dsh-evolution-approval
230
230
  */
231
+ /** A claim id carries its holder pid as `<pid>:<hex token>` (minted by `newClaimId()`). */
232
+ const CLAIM_ID_RE = /^(\d+):[0-9a-f]*$/;
233
+ /**
234
+ * @param claimId - an id from a record's `claimedBy` field.
235
+ * @returns the holder pid, or null when the id does not have the claim shape (pre-v43 bare UUID).
236
+ */
237
+ function claimHolderPid(claimId) {
238
+ const match = CLAIM_ID_RE.exec(claimId.trim());
239
+ return match === null ? null : Number(match[1]);
240
+ }
241
+ /**
242
+ * Whether a pid can still be running. `kill(pid, 0)` is the POSIX/Windows probe: it sends no
243
+ * signal and answers through the error — EPERM means the process EXISTS but is not ours.
244
+ * @param pid - the holder pid from a claim id (callers guard `> 0`; there is no pid 0).
245
+ * @returns true when the process exists.
246
+ */
247
+ function isProcessAlive(pid) {
248
+ try {
249
+ process.kill(pid, 0);
250
+ return true;
251
+ } catch (error) {
252
+ return error.code === "EPERM";
253
+ }
254
+ }
231
255
  /** The requesting session's effective policy (override ?? configured default);
232
256
  * undefined when the approval service is not mounted or no session is
233
257
  * available — callers keep their previous behavior. Single source (G4.8,
@@ -242,7 +266,12 @@ var EvolutionApproval = class extends Service {
242
266
  static Config = z.object({
243
267
  enabled: z.boolean().default(false),
244
268
  stageForeground: z.boolean().default(true),
245
- pendingTtlMs: z.number().min(0).default(0).volatile()
269
+ pendingTtlMs: z.number().min(0).default(0).volatile(),
270
+ approvalConfirm: z.union([
271
+ z.const("auto"),
272
+ z.const("ask"),
273
+ z.const("timeout")
274
+ ]).default("auto").volatile()
246
275
  });
247
276
  enabled;
248
277
  stageForegroundConfig;
@@ -427,9 +456,9 @@ var EvolutionApproval = class extends Service {
427
456
  * core/instance-scope.ts, i.e. PER PROCESS, so a second process over one home
428
457
  * is not excluded at all. Two checks stand in its place:
429
458
  * 1. `inFlight` — an approve/reject running in THIS service instance;
430
- * 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>`, the
431
- * same body shape the io write lock uses, so `parseLockBody` + the core
432
- * `isProcessAlive` probe answer whether that holder can still be running: a
459
+ * 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>` (this
460
+ * service's own grammar, see `claimHolderPid`), so that id plus a liveness
461
+ * probe answer whether the holder can still be running: a
433
462
  * LIVE FOREIGN pid REFUSES the release; our own pid is this process's
434
463
  * leftover (a live in-process approve is what `inFlight` covers); a dead pid
435
464
  * is a crash. The last two are releasable, which is the S2-P2-22 case.
@@ -461,7 +490,7 @@ var EvolutionApproval = class extends Service {
461
490
  ok: false,
462
491
  message: `Pending write "${id}" carries no claim to release (unexpected record shape) — reject it instead.`
463
492
  };
464
- const holderPid = parseLockBody(claimId);
493
+ const holderPid = claimHolderPid(claimId);
465
494
  if (holderPid !== null && holderPid > 0 && holderPid !== process.pid && isProcessAlive(holderPid)) return {
466
495
  ok: false,
467
496
  message: `Pending write "${id}" is claimed by pid ${holderPid}, which is ALIVE — another process may be running this approve. Do not release it: verify the write effect instead and let that run finish (a completed approve resolves its own record).`
@@ -531,12 +560,11 @@ var EvolutionApproval = class extends Service {
531
560
  if (override === "never" || override === "ask") return override;
532
561
  return platformApproval.config?.policy ?? "ask";
533
562
  }
534
- /** v43 FLOW2-1: a claim id that CARRIES its holder pid — `parseLockBody`
535
- * reads it back and `isProcessAlive` turns it into `release()`'s verifiable
536
- * orphan criterion. The `<pid>:<hex token>` body is the io write lock's own
537
- * shape, so one set of core helpers reads both; the token stays unique per
538
- * call, so two approves of one record can never share a claim (the token is a
539
- * dashed UUID stripped to hex because `LOCK_BODY_RE` accepts only hex). */
563
+ /** v43 FLOW2-1: a claim id that CARRIES its holder pid — `claimHolderPid` reads
564
+ * it back and the liveness probe turns it into `release()`'s verifiable orphan
565
+ * criterion. The token stays unique per call, so two approves of one record can
566
+ * never share a claim (a dashed UUID stripped to hex, which is what the
567
+ * claim-id grammar accepts). */
540
568
  newClaimId() {
541
569
  return `${process.pid}:${randomUUID().split("-").join("")}`;
542
570
  }
@@ -164,6 +164,12 @@ export interface Config {
164
164
  * reference and is read at USE time — a settings write changes the window without a reload.
165
165
  */
166
166
  pendingTtlMs?: Volatile<number>;
167
+ /**
168
+ * U-6: what the confirmation before an APPROVE does. The same three words `skillWriteConfirm` uses
169
+ * for a skill write (`tool-skill-manage/src/write-gates.ts:50`) — mirrored, not imported: the two
170
+ * packages do not depend on each other and this is a vocabulary, not behaviour.
171
+ */
172
+ approvalConfirm?: Volatile<'auto' | 'ask' | 'timeout'>;
167
173
  }
168
174
  export declare class EvolutionApproval extends Service {
169
175
  static inject: string[];
@@ -171,10 +177,12 @@ export declare class EvolutionApproval extends Service {
171
177
  enabled: z<boolean, boolean, "defined">;
172
178
  stageForeground: z<boolean, boolean, "defined">;
173
179
  pendingTtlMs: z<number, number, "volatile-defined">;
180
+ approvalConfirm: z<"ask" | "auto" | "timeout", "ask" | "auto" | "timeout", "volatile-defined">;
174
181
  }>>, Schemastery.ObjectT<NoInfer<{
175
182
  enabled: z<boolean, boolean, "defined">;
176
183
  stageForeground: z<boolean, boolean, "defined">;
177
184
  pendingTtlMs: z<number, number, "volatile-defined">;
185
+ approvalConfirm: z<"ask" | "auto" | "timeout", "ask" | "auto" | "timeout", "volatile-defined">;
178
186
  }>>, "plain">;
179
187
  private readonly enabled;
180
188
  private readonly stageForegroundConfig;
@@ -263,9 +271,9 @@ export declare class EvolutionApproval extends Service {
263
271
  * core/instance-scope.ts, i.e. PER PROCESS, so a second process over one home
264
272
  * is not excluded at all. Two checks stand in its place:
265
273
  * 1. `inFlight` — an approve/reject running in THIS service instance;
266
- * 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>`, the
267
- * same body shape the io write lock uses, so `parseLockBody` + the core
268
- * `isProcessAlive` probe answer whether that holder can still be running: a
274
+ * 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>` (this
275
+ * service's own grammar, see `claimHolderPid`), so that id plus a liveness
276
+ * probe answer whether the holder can still be running: a
269
277
  * LIVE FOREIGN pid REFUSES the release; our own pid is this process's
270
278
  * leftover (a live in-process approve is what `inFlight` covers); a dead pid
271
279
  * is a crash. The last two are releasable, which is the S2-P2-22 case.
@@ -294,12 +302,11 @@ export declare class EvolutionApproval extends Service {
294
302
  * check is lazy: the platform service can start before or after this plugin.
295
303
  */
296
304
  private deriveSessionPolicy;
297
- /** v43 FLOW2-1: a claim id that CARRIES its holder pid — `parseLockBody`
298
- * reads it back and `isProcessAlive` turns it into `release()`'s verifiable
299
- * orphan criterion. The `<pid>:<hex token>` body is the io write lock's own
300
- * shape, so one set of core helpers reads both; the token stays unique per
301
- * call, so two approves of one record can never share a claim (the token is a
302
- * dashed UUID stripped to hex because `LOCK_BODY_RE` accepts only hex). */
305
+ /** v43 FLOW2-1: a claim id that CARRIES its holder pid — `claimHolderPid` reads
306
+ * it back and the liveness probe turns it into `release()`'s verifiable orphan
307
+ * criterion. The token stays unique per call, so two approves of one record can
308
+ * never share a claim (a dashed UUID stripped to hex, which is what the
309
+ * claim-id grammar accepts). */
303
310
  private newClaimId;
304
311
  private dedupe;
305
312
  private doApprove;
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.22.0",
4
+ "version": "0.23.0",
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.4",
30
- "@lmzhen/dsh-evolution-core": "^0.22.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.23.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
35
- "@lmzhen/dsh-evolution-state-storage": "^0.22.0",
36
- "@lmzhen/dsh-evolution-state": "^0.22.0"
35
+ "@lmzhen/dsh-evolution-state-storage": "^0.23.0",
36
+ "@lmzhen/dsh-evolution-state": "^0.23.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
40
- "@lmzhen/dsh-evolution-core": "^0.22.0",
41
- "@lmzhen/dsh-evolution-state-storage": "^0.22.0",
42
- "@lmzhen/dsh-evolution-state": "^0.22.0"
40
+ "@lmzhen/dsh-evolution-core": "^0.23.0",
41
+ "@lmzhen/dsh-evolution-state-storage": "^0.23.0",
42
+ "@lmzhen/dsh-evolution-state": "^0.23.0"
43
43
  }
44
44
  }