@lmzhen/dsh-evolution-approval 0.1.0-rc.1

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 ADDED
@@ -0,0 +1,35 @@
1
+ # @deepseek-ai/dsh-evolution-approval
2
+
3
+ Stage/pending approval service for Hermes-style self-evolution writes.
4
+
5
+ DSH native approval is one-shot; this service adds the Hermes staged queue:
6
+ `request()` stores background writes, `approve()` replays them through a runner,
7
+ and `reject()` discards them. The core evolution plugin registers memory/skill
8
+ runners when both plugins are composed.
9
+
10
+ Run the companion invariant and tests:
11
+
12
+ ```sh
13
+ node node_modules/vitest/vitest.mjs run packages/evolution/evolution-approval/tests
14
+ ```
15
+
16
+ ## Model Experience
17
+
18
+ ### Indirect model surface
19
+
20
+ #### What the model sees
21
+
22
+ `@deepseek-ai/dsh-evolution-approval` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
23
+
24
+ #### Token effect
25
+
26
+ Zero direct token effect from this package; consumers add any model-visible tokens.
27
+
28
+ #### KV Cache effect
29
+
30
+ Independent of request-prefix construction. This package does not alter the assembled prompt or tool list.
31
+
32
+ ## Known Limitations and Deferred Work
33
+
34
+
35
+ - `approve()` deduplicates concurrent approvals inside one process, and state providers resolve the pending record atomically. However, the replay runner executes **before** that atomic resolution, so two OS processes approving the same id can each perform the write once while only one process wins the audit transition. Run approvals from a single writer process, or make replay runners idempotent when multi-process approval is required.
package/lib/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { randomUUID } from "node:crypto";
3
+ import z from "@deepseek-ai/schemastery";
4
+ //#region lib/types/index.js
5
+ /**
6
+ * Stage/pending write approval for self-evolution mutations.
7
+ *
8
+ * DSH's native approval seam is one-shot only. This service adds the
9
+ * Hermes-style staged queue: background review/curator writes are stored in
10
+ * `ctx.evolutionState`, and a human approves or rejects them later. A runner
11
+ * registry replays the exact mutation without passing through the gate a
12
+ * second time. Resolved records are KEPT as audit history.
13
+ *
14
+ * @module @lmzhen/dsh-evolution-approval
15
+ */
16
+ var EvolutionApproval = class extends Service {
17
+ static inject = ["evolutionState"];
18
+ static Config = z.object({
19
+ enabled: z.boolean().default(false),
20
+ stageForeground: z.boolean().default(true)
21
+ });
22
+ enabled;
23
+ stageForeground;
24
+ runners = /* @__PURE__ */ new Map();
25
+ inFlight = /* @__PURE__ */ new Map();
26
+ constructor(ctx, config = {}) {
27
+ super(ctx, "evolutionApproval");
28
+ this.enabled = config.enabled ?? false;
29
+ this.stageForeground = config.stageForeground ?? true;
30
+ }
31
+ state() {
32
+ return this.ctx.evolutionState;
33
+ }
34
+ /** Fail-closed capability adapters need to distinguish "allowed" from "enabled". */
35
+ get isEnabled() {
36
+ return this.enabled;
37
+ }
38
+ registerRunner(kind, runner) {
39
+ this.runners.set(kind, runner);
40
+ return () => {
41
+ if (this.runners.get(kind) === runner) this.runners.delete(kind);
42
+ };
43
+ }
44
+ /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
45
+ async run(kind, args) {
46
+ const runner = this.runners.get(kind);
47
+ if (!runner) return {
48
+ ok: false,
49
+ message: `No replay runner registered for kind "${kind}".`
50
+ };
51
+ return await runner(args);
52
+ }
53
+ /** Evaluate one mutation. Returns allow, or stores the write and returns staged. */
54
+ async request(input) {
55
+ if (!this.enabled) return {
56
+ action: "allow",
57
+ message: "Approval disabled."
58
+ };
59
+ if (input.origin === "background_review" || this.stageForeground) {
60
+ const record = {
61
+ id: randomUUID(),
62
+ kind: input.kind,
63
+ summary: input.summary,
64
+ args: input.args,
65
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
66
+ status: "pending"
67
+ };
68
+ await this.state().savePending(record);
69
+ return {
70
+ action: "staged",
71
+ pendingId: record.id,
72
+ message: `Write staged for approval. Review with /evolution pending and approve ${record.id}.`
73
+ };
74
+ }
75
+ return {
76
+ action: "allow",
77
+ message: "Foreground write allowed."
78
+ };
79
+ }
80
+ async list(status = "pending") {
81
+ return await this.state().listPending(status);
82
+ }
83
+ async approve(id) {
84
+ return await this.dedupe(id, () => this.doApprove(id));
85
+ }
86
+ async reject(id) {
87
+ return await this.dedupe(id, async () => {
88
+ const resolution = await this.state().tryResolvePending(id, "rejected");
89
+ if (!resolution.applied || !resolution.record) return {
90
+ ok: false,
91
+ message: `Pending write "${id}" is not pending (already resolved or missing).`
92
+ };
93
+ return {
94
+ ok: true,
95
+ message: `Rejected ${resolution.record.kind} write "${id}".`
96
+ };
97
+ });
98
+ }
99
+ dedupe(id, task) {
100
+ const existing = this.inFlight.get(id);
101
+ if (existing) return existing;
102
+ const run = task().finally(() => {
103
+ this.inFlight.delete(id);
104
+ });
105
+ this.inFlight.set(id, run);
106
+ return run;
107
+ }
108
+ async doApprove(id) {
109
+ const claimId = randomUUID();
110
+ const record = await this.state().claimPending(id, claimId);
111
+ if (!record) return {
112
+ ok: false,
113
+ message: `Pending write "${id}" is already being resolved by another writer.`
114
+ };
115
+ const runner = this.runners.get(record.kind);
116
+ if (!runner) {
117
+ if (record.kind === "capability") {
118
+ if (!(await this.state().tryResolvePending(id, "approved")).applied) return {
119
+ ok: false,
120
+ message: `Pending write "${id}" was already resolved.`
121
+ };
122
+ return {
123
+ ok: true,
124
+ message: "Capability approved for manual activation in Creator mode (no code was executed)."
125
+ };
126
+ }
127
+ await this.state().releasePendingClaim(id, claimId);
128
+ return {
129
+ ok: false,
130
+ message: `No replay runner registered for kind "${record.kind}".`
131
+ };
132
+ }
133
+ try {
134
+ const result = await runner(record.args);
135
+ if (!result.ok) {
136
+ await this.state().releasePendingClaim(id, claimId);
137
+ return {
138
+ ok: false,
139
+ message: result.message
140
+ };
141
+ }
142
+ } catch (error) {
143
+ await this.state().releasePendingClaim(id, claimId);
144
+ this.ctx.logger.warn(error);
145
+ return {
146
+ ok: false,
147
+ message: "Replay runner failed; the pending write remains pending."
148
+ };
149
+ }
150
+ if (!(await this.state().tryResolvePending(id, "approved")).applied) return {
151
+ ok: false,
152
+ message: `Pending write "${id}" was already resolved by another writer.`
153
+ };
154
+ return {
155
+ ok: true,
156
+ message: `Approved ${record.kind}`
157
+ };
158
+ }
159
+ };
160
+ //#endregion
161
+ export { EvolutionApproval, EvolutionApproval as default };
@@ -0,0 +1,8 @@
1
+ //#region lib/types/invariant.js
2
+ const PACKAGE_NAME = "@deepseek-ai/dsh-evolution-approval";
3
+ const name = "evolution-approval-invariant";
4
+ const inject = ["invariants"];
5
+ const install = () => {};
6
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
7
+ //#endregion
8
+ export { apply, inject, name };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Stage/pending write approval for self-evolution mutations.
3
+ *
4
+ * DSH's native approval seam is one-shot only. This service adds the
5
+ * Hermes-style staged queue: background review/curator writes are stored in
6
+ * `ctx.evolutionState`, and a human approves or rejects them later. A runner
7
+ * registry replays the exact mutation without passing through the gate a
8
+ * second time. Resolved records are KEPT as audit history.
9
+ *
10
+ * @module @deepseek-ai/dsh-evolution-approval
11
+ */
12
+ import { Context, Service } from '@deepseek-ai/cordis';
13
+ import type Schema from '@deepseek-ai/schemastery';
14
+ import type { PendingKind, PendingRecord, PendingStatus } from '@deepseek-ai/dsh-evolution-state-storage';
15
+ export type { PendingKind, PendingRecord, PendingStatus };
16
+ export type WriteRunner = (args: unknown) => Promise<{
17
+ ok: boolean;
18
+ message: string;
19
+ }>;
20
+ export interface ApprovalRequest {
21
+ kind: PendingKind;
22
+ summary: string;
23
+ args: unknown;
24
+ origin: 'foreground' | 'background_review';
25
+ }
26
+ export interface ApprovalDecision {
27
+ action: 'allow' | 'staged';
28
+ pendingId?: string;
29
+ message: string;
30
+ }
31
+ declare module '@deepseek-ai/cordis' {
32
+ interface Context {
33
+ evolutionApproval: EvolutionApproval;
34
+ }
35
+ }
36
+ export interface Config {
37
+ /** Master switch. Default false matches Hermes write_approval default. */
38
+ enabled?: boolean;
39
+ /** Require approval for foreground writes as well. */
40
+ stageForeground?: boolean;
41
+ }
42
+ export declare class EvolutionApproval extends Service {
43
+ static inject: string[];
44
+ static Config: Schema<Config>;
45
+ private readonly enabled;
46
+ private readonly stageForeground;
47
+ private readonly runners;
48
+ private readonly inFlight;
49
+ constructor(ctx: Context, config?: Config);
50
+ private state;
51
+ /** Fail-closed capability adapters need to distinguish "allowed" from "enabled". */
52
+ get isEnabled(): boolean;
53
+ registerRunner(kind: PendingKind, runner: WriteRunner): () => void;
54
+ /** Trusted plan-executor entry point: replay a write through the registered runner exactly once. */
55
+ run(kind: PendingKind, args: unknown): Promise<{
56
+ ok: boolean;
57
+ message: string;
58
+ }>;
59
+ /** Evaluate one mutation. Returns allow, or stores the write and returns staged. */
60
+ request(input: ApprovalRequest): Promise<ApprovalDecision>;
61
+ list(status?: PendingStatus): Promise<PendingRecord[]>;
62
+ approve(id: string): Promise<{
63
+ ok: boolean;
64
+ message: string;
65
+ }>;
66
+ reject(id: string): Promise<{
67
+ ok: boolean;
68
+ message: string;
69
+ }>;
70
+ private dedupe;
71
+ private doApprove;
72
+ }
73
+ export default EvolutionApproval;
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "evolution-approval-invariant";
3
+ export declare const inject: string[];
4
+ export declare const apply: (ctx: Context) => Promise<() => void>;
5
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,23 @@
1
+ export type PendingKind = 'memory' | 'skill' | 'skill_batch';
2
+ export interface PendingRecord {
3
+ id: string;
4
+ kind: PendingKind;
5
+ summary: string;
6
+ args: unknown;
7
+ createdAt: string;
8
+ status: 'pending' | 'approved' | 'rejected';
9
+ resolvedAt?: string;
10
+ }
11
+ export declare class PendingStore {
12
+ private records;
13
+ private readonly path;
14
+ constructor(env?: NodeJS.ProcessEnv);
15
+ private loadSync;
16
+ private save;
17
+ list(status?: 'pending' | 'approved' | 'rejected'): PendingRecord[];
18
+ stage(kind: PendingKind, summary: string, args: unknown): Promise<PendingRecord>;
19
+ resolve(id: string, status: 'approved' | 'rejected'): Promise<PendingRecord | null>;
20
+ remove(id: string): Promise<void>;
21
+ clear(): Promise<void>;
22
+ }
23
+ //# sourceMappingURL=pending-store.d.ts.map
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@lmzhen/dsh-evolution-approval",
3
+ "description": "Stage/pending approval service for Hermes-style self-evolution writes (community build)",
4
+ "version": "0.1.0-rc.1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lmzhen/dsh-evolution.git",
11
+ "directory": "packages/dsh-evolution-approval"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types/**/*.d.ts",
31
+ "lib/types/invariant.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
39
+ "@deepseek-ai/cordis": "^4.0.1",
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.1",
41
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.1"
42
+ },
43
+ "devDependencies": {
44
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.1.0-rc.1",
46
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.1",
47
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.1",
48
+ "@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.1",
49
+ "@lmzhen/dsh-evolution-state-json": "^0.1.0-rc.1"
50
+ }
51
+ }