@lmzhen/dsh-evolution-approval 0.21.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 +40 -12
- package/lib/types/index.d.ts +16 -9
- package/package.json +7 -7
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,
|
|
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
|
|
431
|
-
*
|
|
432
|
-
*
|
|
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 =
|
|
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 — `
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
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
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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 — `
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
-
"@lmzhen/dsh-evolution-state": "^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.
|
|
41
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.
|
|
42
|
-
"@lmzhen/dsh-evolution-state": "^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
|
}
|