@lmzhen/dsh-evolution-approval 0.1.0-rc.7 → 0.1.0-rc.71

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
@@ -36,11 +36,22 @@ var EvolutionApproval = class extends Service {
36
36
  return this.enabled;
37
37
  }
38
38
  registerRunner(kind, runner) {
39
+ if (this.runners.has(kind)) throw new Error(`approval runner for "${kind}" is already registered`);
39
40
  this.runners.set(kind, runner);
40
41
  return () => {
41
42
  if (this.runners.get(kind) === runner) this.runners.delete(kind);
42
43
  };
43
44
  }
45
+ /**
46
+ * Whether a replay runner is registered for `kind` (rc.42 audit P1-9
47
+ * pre-check surface): callers that can execute a write directly (the review
48
+ * pipeline) use it to avoid staging a pending record that no runner could
49
+ * ever replay. `capability` records are answerable without a runner by
50
+ * design, so they are exempt from the staging pre-check.
51
+ */
52
+ hasRunner(kind) {
53
+ return this.runners.has(kind);
54
+ }
44
55
  /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
45
56
  async run(kind, args) {
46
57
  const runner = this.runners.get(kind);
@@ -52,15 +63,21 @@ var EvolutionApproval = class extends Service {
52
63
  }
53
64
  /** Evaluate one mutation. Returns allow, or stores the write and returns staged. */
54
65
  async request(input) {
66
+ const summary = normalizeSummary(input);
55
67
  if (!this.enabled) return {
56
68
  action: "allow",
57
69
  message: "Approval disabled."
58
70
  };
71
+ if (input.sessionPolicy === "never") return {
72
+ action: "allow",
73
+ message: "Session approval policy is \"never\"; write allowed without staging."
74
+ };
59
75
  if (input.origin === "background_review" || this.stageForeground) {
76
+ if (input.kind !== "capability" && !this.runners.has(input.kind)) this.ctx.logger.warn(`evolution-approval: staging "${input.kind}" write with no replay runner registered - it will not be approvable`);
60
77
  const record = {
61
78
  id: randomUUID(),
62
79
  kind: input.kind,
63
- summary: input.summary,
80
+ summary,
64
81
  args: input.args,
65
82
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
66
83
  status: "pending"
@@ -81,10 +98,15 @@ var EvolutionApproval = class extends Service {
81
98
  return await this.state().listPending(status);
82
99
  }
83
100
  async approve(id) {
84
- return await this.dedupe(id, () => this.doApprove(id));
101
+ return await this.dedupe(`approve:${id}`, () => this.doApprove(id));
85
102
  }
86
103
  async reject(id) {
87
- return await this.dedupe(id, async () => {
104
+ return await this.dedupe(`reject:${id}`, async () => {
105
+ const claimId = randomUUID();
106
+ if (!await this.state().claimPending(id, claimId)) return {
107
+ ok: false,
108
+ message: `Pending write "${id}" is already being resolved by another writer.`
109
+ };
88
110
  const resolution = await this.state().tryResolvePending(id, "rejected");
89
111
  if (!resolution.applied || !resolution.record) return {
90
112
  ok: false,
@@ -157,5 +179,15 @@ var EvolutionApproval = class extends Service {
157
179
  };
158
180
  }
159
181
  };
182
+ /** Compact, batch-aware approval summary so pending lists stay scannable. */
183
+ function normalizeSummary(input) {
184
+ const trimmed = input.summary.length > 120 ? `${input.summary.slice(0, 117)}...` : input.summary;
185
+ if (input.kind === "memory") {
186
+ const candidate = input.args;
187
+ if (Array.isArray(candidate?.operations) && candidate.operations.length > 1) return `memory ${candidate.target ?? "memory"} batch of ${candidate.operations.length} operations`;
188
+ }
189
+ if (input.kind === "skill" && /^skill delete /.test(trimmed)) return `${trimmed} (warning: archive)`;
190
+ return trimmed;
191
+ }
160
192
  //#endregion
161
193
  export { EvolutionApproval, EvolutionApproval as default };
@@ -22,6 +22,15 @@ export interface ApprovalRequest {
22
22
  summary: string;
23
23
  args: unknown;
24
24
  origin: 'foreground' | 'background_review';
25
+ /**
26
+ * The requesting session's effective approval policy (platform vocabulary:
27
+ * 'ask' | 'never' — see `dsh-user-approval`). When 'never', the session has
28
+ * declared the deterministic unattended stance (CI, cron, automated runs),
29
+ * so the write is allowed instead of staging an unanswerable pending record
30
+ * (claw alignment: "skip approval for non-interactive contexts"). Absent
31
+ * (no session / no approval service) keeps the previous behavior.
32
+ */
33
+ sessionPolicy?: 'ask' | 'never';
25
34
  }
26
35
  export interface ApprovalDecision {
27
36
  action: 'allow' | 'staged';
@@ -51,6 +60,14 @@ export declare class EvolutionApproval extends Service {
51
60
  /** Fail-closed capability adapters need to distinguish "allowed" from "enabled". */
52
61
  get isEnabled(): boolean;
53
62
  registerRunner(kind: PendingKind, runner: WriteRunner): () => void;
63
+ /**
64
+ * Whether a replay runner is registered for `kind` (rc.42 audit P1-9
65
+ * pre-check surface): callers that can execute a write directly (the review
66
+ * pipeline) use it to avoid staging a pending record that no runner could
67
+ * ever replay. `capability` records are answerable without a runner by
68
+ * design, so they are exempt from the staging pre-check.
69
+ */
70
+ hasRunner(kind: PendingKind): boolean;
54
71
  /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
55
72
  run(kind: PendingKind, args: unknown): Promise<{
56
73
  ok: boolean;
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.1.0-rc.7",
4
+ "version": "0.1.0-rc.71",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -35,17 +35,17 @@
35
35
  "@deepseek-ai/schemastery": "^3.18.1"
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
38
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.7",
41
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.7"
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.71",
41
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.71"
42
42
  },
43
43
  "devDependencies": {
44
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.7",
46
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.7",
47
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.7",
48
- "@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.7",
49
- "@lmzhen/dsh-evolution-state-json": "^0.1.0-rc.7"
44
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.71",
46
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.71",
47
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.71",
48
+ "@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.71",
49
+ "@lmzhen/dsh-evolution-state-json": "^0.1.0-rc.71"
50
50
  }
51
51
  }