@lmzhen/dsh-evolution-approval 0.3.16 → 0.3.17

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
@@ -52,8 +52,18 @@ var EvolutionApproval = class extends Service {
52
52
  hasRunner(kind) {
53
53
  return this.runners.has(kind);
54
54
  }
55
- /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
56
- async run(kind, args) {
55
+ /**
56
+ * Trusted plan-executor entry point: replay a write through the registered
57
+ * runner exactly once. 0.3.17 (S3.2, E-23): this is the BACKGROUND-REVIEW
58
+ * replay channel only — the caller must declare the intent (the review
59
+ * pipeline is the sole production consumer); a bare call is refused so the
60
+ * staging boundary cannot be bypassed by accident from another surface.
61
+ */
62
+ async run(kind, args, intent) {
63
+ if (intent?.interface !== "background_review") return {
64
+ ok: false,
65
+ message: "approval.run is the background-review replay channel; direct execution must use the tool path (staging is the only gate bypass for foreground)."
66
+ };
57
67
  const runner = this.runners.get(kind);
58
68
  if (!runner) return {
59
69
  ok: false,
@@ -68,7 +78,7 @@ var EvolutionApproval = class extends Service {
68
78
  action: "allow",
69
79
  message: "Approval disabled."
70
80
  };
71
- if (input.sessionPolicy === "never") return {
81
+ if ((this.deriveSessionPolicy(input.sessionId) ?? (this.ctx.get("approval") ? void 0 : input.sessionPolicy)) === "never") return {
72
82
  action: "allow",
73
83
  message: "Session approval policy is \"never\"; write allowed without staging."
74
84
  };
@@ -80,7 +90,9 @@ var EvolutionApproval = class extends Service {
80
90
  summary,
81
91
  args: input.args,
82
92
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
83
- status: "pending"
93
+ status: "pending",
94
+ origin: input.origin,
95
+ ...input.sessionId ? { sessionId: input.sessionId } : {}
84
96
  };
85
97
  await this.state().savePending(record);
86
98
  return {
@@ -103,21 +115,46 @@ var EvolutionApproval = class extends Service {
103
115
  async reject(id) {
104
116
  return await this.dedupe(`reject:${id}`, async () => {
105
117
  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
- };
118
+ if (!await this.state().claimPending(id, claimId)) {
119
+ if ((await this.state().listPending("executing")).find((item) => item.id === id)) return (await this.state().tryResolvePending(id, "rejected")).applied ? {
120
+ ok: true,
121
+ message: `Rejected executing write "${id}" (crashed approve cleaned up; verify the write state manually).`
122
+ } : {
123
+ ok: false,
124
+ message: `Pending write "${id}" could not be rejected: it resolved concurrently.`
125
+ };
126
+ return {
127
+ ok: false,
128
+ message: `Pending write "${id}" is already being resolved by another writer.`
129
+ };
130
+ }
110
131
  const resolution = await this.state().tryResolvePending(id, "rejected");
111
- if (!resolution.applied || !resolution.record) return {
112
- ok: false,
113
- message: `Pending write "${id}" is not pending (already resolved or missing).`
114
- };
132
+ if (!resolution.applied || !resolution.record) {
133
+ await this.state().releasePendingClaim(id, claimId);
134
+ return {
135
+ ok: false,
136
+ message: `Pending write "${id}" is not pending (already resolved or missing).`
137
+ };
138
+ }
115
139
  return {
116
140
  ok: true,
117
141
  message: `Rejected ${resolution.record.kind} write "${id}".`
118
142
  };
119
143
  });
120
144
  }
145
+ /**
146
+ * 0.3.17 (S3.1): platform-side session policy derivation — the platform's
147
+ * approval service (`overrideOf`) is the authority; a caller's self-reported
148
+ * sessionPolicy is a fallback only without it (E-22). The mount check is
149
+ * lazy: the platform service can start before or after this plugin.
150
+ */
151
+ deriveSessionPolicy(sessionId) {
152
+ if (!sessionId) return void 0;
153
+ const platformApproval = this.ctx.get("approval");
154
+ if (!platformApproval) return void 0;
155
+ const override = platformApproval.overrideOf?.(sessionId);
156
+ return override === "never" || override === "ask" ? override : void 0;
157
+ }
121
158
  dedupe(id, task) {
122
159
  const existing = this.inFlight.get(id);
123
160
  if (existing) return existing;
@@ -130,10 +167,16 @@ var EvolutionApproval = class extends Service {
130
167
  async doApprove(id) {
131
168
  const claimId = randomUUID();
132
169
  const record = await this.state().claimPending(id, claimId);
133
- if (!record) return {
134
- ok: false,
135
- message: `Pending write "${id}" is already being resolved by another writer.`
136
- };
170
+ if (!record) {
171
+ if ((await this.state().listPending("executing")).find((item) => item.id === id)) return {
172
+ ok: false,
173
+ message: `Pending write "${id}" is executing (a previous approve may have run before a crash). Verify its effect manually, then reject it — /evolution pending shows it as EXECUTING.`
174
+ };
175
+ return {
176
+ ok: false,
177
+ message: `Pending write "${id}" is already being resolved by another writer.`
178
+ };
179
+ }
137
180
  const runner = this.runners.get(record.kind);
138
181
  if (!runner) {
139
182
  if (record.kind === "capability") {
@@ -175,7 +218,7 @@ var EvolutionApproval = class extends Service {
175
218
  };
176
219
  return {
177
220
  ok: true,
178
- message: `Approved ${record.kind}`
221
+ message: `Approved ${record.kind} write "${id}" (${record.summary}).`
179
222
  };
180
223
  }
181
224
  };
@@ -31,6 +31,9 @@ export interface ApprovalRequest {
31
31
  * (no session / no approval service) keeps the previous behavior.
32
32
  */
33
33
  sessionPolicy?: 'ask' | 'never';
34
+ /** 0.3.17 (E-25): the requesting session id, kept on the record for
35
+ * audit attribution (staged AND resolved history). */
36
+ sessionId?: string;
34
37
  }
35
38
  export interface ApprovalDecision {
36
39
  action: 'allow' | 'staged';
@@ -68,8 +71,16 @@ export declare class EvolutionApproval extends Service {
68
71
  * design, so they are exempt from the staging pre-check.
69
72
  */
70
73
  hasRunner(kind: PendingKind): boolean;
71
- /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
72
- run(kind: PendingKind, args: unknown): Promise<{
74
+ /**
75
+ * Trusted plan-executor entry point: replay a write through the registered
76
+ * runner exactly once. 0.3.17 (S3.2, E-23): this is the BACKGROUND-REVIEW
77
+ * replay channel only — the caller must declare the intent (the review
78
+ * pipeline is the sole production consumer); a bare call is refused so the
79
+ * staging boundary cannot be bypassed by accident from another surface.
80
+ */
81
+ run(kind: PendingKind, args: unknown, intent?: {
82
+ interface: 'background_review';
83
+ }): Promise<{
73
84
  ok: boolean;
74
85
  message: string;
75
86
  }>;
@@ -84,6 +95,13 @@ export declare class EvolutionApproval extends Service {
84
95
  ok: boolean;
85
96
  message: string;
86
97
  }>;
98
+ /**
99
+ * 0.3.17 (S3.1): platform-side session policy derivation — the platform's
100
+ * approval service (`overrideOf`) is the authority; a caller's self-reported
101
+ * sessionPolicy is a fallback only without it (E-22). The mount check is
102
+ * lazy: the platform service can start before or after this plugin.
103
+ */
104
+ private deriveSessionPolicy;
87
105
  private dedupe;
88
106
  private doApprove;
89
107
  }
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.3.16",
4
+ "version": "0.3.17",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,15 +37,15 @@
37
37
  "peerDependencies": {
38
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.3.16",
41
- "@lmzhen/dsh-evolution-state": "^0.3.16"
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.17",
41
+ "@lmzhen/dsh-evolution-state": "^0.3.17"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.3.16",
46
- "@lmzhen/dsh-evolution-state": "^0.3.16",
47
- "@lmzhen/dsh-evolution-io": "^0.3.16",
48
- "@lmzhen/dsh-evolution-io-node": "^0.3.16",
49
- "@lmzhen/dsh-evolution-state-json": "^0.3.16"
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.17",
46
+ "@lmzhen/dsh-evolution-state": "^0.3.17",
47
+ "@lmzhen/dsh-evolution-io": "^0.3.17",
48
+ "@lmzhen/dsh-evolution-io-node": "^0.3.17",
49
+ "@lmzhen/dsh-evolution-state-json": "^0.3.17"
50
50
  }
51
51
  }