@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 +35 -0
- package/lib/index.js +161 -0
- package/lib/invariant.js +8 -0
- package/lib/types/index.d.ts +74 -0
- package/lib/types/invariant.d.ts +5 -0
- package/lib/types/pending-store.d.ts +23 -0
- package/package.json +51 -0
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 };
|
package/lib/invariant.js
ADDED
|
@@ -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,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
|
+
}
|