@lmzhen/dsh-tool-skill-manage 0.11.2 → 0.12.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/README.md +19 -6
- package/lib/index.js +74 -9
- package/lib/types/index.d.ts +10 -0
- package/lib/types/write-gates.d.ts +20 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -34,12 +34,25 @@ write whose target the session never read is refused with `E-318`, and only a re
|
|
|
34
34
|
counts. A session log the tool cannot read proceeds with one warning rather than blocking every
|
|
35
35
|
autonomous write.
|
|
36
36
|
|
|
37
|
-
A foreground `create`, or a bare foreground `delete`,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
A foreground `create`, or a bare foreground `delete`, is the ONE write this tool puts to a human
|
|
38
|
+
before writing; a `delete` carrying `absorbed_into` is the merge protocol and does not ask. WHAT
|
|
39
|
+
that confirmation does is the `skillWriteConfirm` parameter (`/evolution params`, and the
|
|
40
|
+
技能写入规则 card):
|
|
41
|
+
|
|
42
|
+
| Mode | What happens |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `auto` (**the default**) | the write proceeds with no prompt — an unattended run (a background pass, a scheduled review, a headless session) must not park a tool call on a question nobody will answer |
|
|
45
|
+
| `ask` | the question waits for as long as it takes; the answer is `Create`/`Delete` to proceed, `Cancel` (or a dismissed card) to refuse with `E-317` |
|
|
46
|
+
| `timeout` | the question is asked with a deadline of `skillWriteConfirmTimeoutSeconds` (default 120 s); an unanswered prompt cancels the write with `E-319`, which names the three knobs |
|
|
47
|
+
|
|
48
|
+
The deadline is enforced by the gate itself, not only by the abort signal it hands the question
|
|
49
|
+
service: a prompt that outlives it returns anyway, and the timer is unref'd and cleared, so an
|
|
50
|
+
answered prompt leaves neither a parked call nor a live timer behind. This gate is UX, not a
|
|
51
|
+
security door: it is admission-only (a replayed record already carries the human release that staged
|
|
52
|
+
it), and — in the two modes that ask at all — an unmounted question service, a caller that is not
|
|
53
|
+
the registry's exact live root agent, or a failing ask all PROCEED with one warning. The shipped
|
|
54
|
+
`auto` mode asks nothing, so it never reaches those fallbacks; the operator's own session stays the
|
|
55
|
+
authority that asked for the write.
|
|
43
56
|
|
|
44
57
|
### Approval seam
|
|
45
58
|
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,16 @@ import { DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_REFE
|
|
|
32
32
|
const CONFIRM_QUESTION_ID = "evolution-skill-write";
|
|
33
33
|
/** The cancel label every confirm question offers. */
|
|
34
34
|
const CANCEL_LABEL = "Cancel";
|
|
35
|
+
/** What a `timeout`-mode deadline settles with: comparable by identity against the seam's
|
|
36
|
+
* boolean, so the two outcomes stay distinguishable without a mutable flag (which a linter
|
|
37
|
+
* cannot follow through a timer callback, and a reader cannot either). */
|
|
38
|
+
const CONFIRM_TIMED_OUT = "confirm-timed-out";
|
|
39
|
+
/** The mode a deployment that configures nothing gets. */
|
|
40
|
+
const DEFAULT_WRITE_CONFIRM_MODE = "auto";
|
|
41
|
+
/** A usable deadline: the configured value when it is a positive finite number, else the default. */
|
|
42
|
+
function timeoutSecondsOf(configured) {
|
|
43
|
+
return configured !== void 0 && Number.isFinite(configured) && configured >= 1 ? Math.floor(configured) : 120;
|
|
44
|
+
}
|
|
35
45
|
/** The scalar fields the tool schema types as strings; the replay channel has no schema.
|
|
36
46
|
* `action` is one of them: a non-string action used to fall through to the library as "Unknown
|
|
37
47
|
* action" while the read gate re-defaulted it to `patch` and refused with E-318 (review
|
|
@@ -120,16 +130,28 @@ const GATES = [
|
|
|
120
130
|
{
|
|
121
131
|
id: "human-confirm",
|
|
122
132
|
appliesTo: ["admission"],
|
|
123
|
-
run: async ({ view, origin, confirm, warn }) => {
|
|
133
|
+
run: async ({ view, origin, confirm, warn, confirmMode, confirmTimeoutSeconds }) => {
|
|
124
134
|
const action = view.action;
|
|
125
135
|
if (action === void 0 || origin !== "foreground" || view.name === "") return null;
|
|
126
136
|
if (!(action === "create" || action === "delete" && view.absorbedInto === void 0)) return null;
|
|
137
|
+
const mode = confirmMode ?? "auto";
|
|
138
|
+
if (mode === "auto") return null;
|
|
127
139
|
if (confirm === void 0) {
|
|
128
140
|
warn("skill_manage: no confirmation channel is mounted, so the write proceeds unconfirmed.");
|
|
129
141
|
return null;
|
|
130
142
|
}
|
|
131
143
|
const confirmLabel = action === "create" ? "Create" : "Delete";
|
|
132
|
-
|
|
144
|
+
const seconds = timeoutSecondsOf(confirmTimeoutSeconds);
|
|
145
|
+
const cancel = new AbortController();
|
|
146
|
+
let timer;
|
|
147
|
+
const deadline = mode === "timeout" ? new Promise((resolve) => {
|
|
148
|
+
timer = setTimeout(() => {
|
|
149
|
+
cancel.abort();
|
|
150
|
+
resolve(CONFIRM_TIMED_OUT);
|
|
151
|
+
}, seconds * 1e3);
|
|
152
|
+
timer.unref();
|
|
153
|
+
}) : void 0;
|
|
154
|
+
const asked = confirm({
|
|
133
155
|
action,
|
|
134
156
|
name: view.name,
|
|
135
157
|
confirmLabel,
|
|
@@ -138,8 +160,21 @@ const GATES = [
|
|
|
138
160
|
header: "Confirm",
|
|
139
161
|
question: action === "create" ? `Create skill "${view.name}"? A new skill directory is written into the family tree.` : `Delete skill "${view.name}"? It is archived under .archive and leaves the catalog.`,
|
|
140
162
|
options: [{ label: confirmLabel }, { label: CANCEL_LABEL }]
|
|
141
|
-
}
|
|
142
|
-
|
|
163
|
+
},
|
|
164
|
+
...mode === "timeout" ? { signal: cancel.signal } : {}
|
|
165
|
+
});
|
|
166
|
+
let outcome;
|
|
167
|
+
try {
|
|
168
|
+
outcome = deadline === void 0 ? await asked : await Promise.race([asked, deadline]);
|
|
169
|
+
} finally {
|
|
170
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
if (outcome === true) return null;
|
|
173
|
+
if (outcome === CONFIRM_TIMED_OUT || cancel.signal.aborted) return errorText("e-319-skill-write-confirm-timed-out", {
|
|
174
|
+
a1: view.name,
|
|
175
|
+
a2: action === "create" ? "created" : "deleted",
|
|
176
|
+
a3: String(seconds)
|
|
177
|
+
});
|
|
143
178
|
return errorText("e-317-skill-write-not-confirmed", {
|
|
144
179
|
a1: view.name,
|
|
145
180
|
a2: action === "create" ? "created" : "deleted"
|
|
@@ -214,7 +249,13 @@ const Config = z.object({
|
|
|
214
249
|
descriptionStrict: z.boolean().default(false),
|
|
215
250
|
threatExemptLabels: z.array(z.string()).default([]),
|
|
216
251
|
strictCrossSource: z.boolean().default(false),
|
|
217
|
-
skillVersionKeep: z.number().min(1).default(DEFAULT_SKILL_LIMITS.versionKeep ?? DEFAULT_SKILL_VERSION_KEEP)
|
|
252
|
+
skillVersionKeep: z.number().min(1).default(DEFAULT_SKILL_LIMITS.versionKeep ?? DEFAULT_SKILL_VERSION_KEEP),
|
|
253
|
+
skillWriteConfirm: z.union([
|
|
254
|
+
z.const("auto"),
|
|
255
|
+
z.const("ask"),
|
|
256
|
+
z.const("timeout")
|
|
257
|
+
]).default(DEFAULT_WRITE_CONFIRM_MODE),
|
|
258
|
+
skillWriteConfirmTimeoutSeconds: z.number().min(1).default(120)
|
|
218
259
|
});
|
|
219
260
|
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
220
261
|
* constants and the row schema, so an empty document resolves to today's behaviour. */
|
|
@@ -226,7 +267,13 @@ const SKILLS_SETTINGS_SCHEMA = z.object({
|
|
|
226
267
|
descriptionStrict: z.boolean().default(false),
|
|
227
268
|
strictCrossSource: z.boolean().default(false),
|
|
228
269
|
citationPolicy: z.union([z.const("verify"), z.const("refuse")]).default(DEFAULT_CITATION_POLICY),
|
|
229
|
-
supportFileCharPolicy: z.union([z.const("report"), z.const("enforce")]).default(DEFAULT_SUPPORT_FILE_CHAR_POLICY)
|
|
270
|
+
supportFileCharPolicy: z.union([z.const("report"), z.const("enforce")]).default(DEFAULT_SUPPORT_FILE_CHAR_POLICY),
|
|
271
|
+
skillWriteConfirm: z.union([
|
|
272
|
+
z.const("auto"),
|
|
273
|
+
z.const("ask"),
|
|
274
|
+
z.const("timeout")
|
|
275
|
+
]).default(DEFAULT_WRITE_CONFIRM_MODE),
|
|
276
|
+
skillWriteConfirmTimeoutSeconds: z.number().min(1).default(120)
|
|
230
277
|
});
|
|
231
278
|
/** The four caps a user may only tighten, in schema order (the sentry reads it). */
|
|
232
279
|
const SKILL_SETTINGS_CAPS = [
|
|
@@ -245,6 +292,18 @@ const SKILL_SETTINGS_CAPS = [
|
|
|
245
292
|
function validateSkillSettings(value, ceilings) {
|
|
246
293
|
for (const key of SKILL_SETTINGS_CAPS) if (value[key] > ceilings[key]) throw new Error(`${key} may only be tightened: ${value[key]} exceeds the deployment value ${ceilings[key]}`);
|
|
247
294
|
}
|
|
295
|
+
/** One abort signal that carries BOTH cancellations: the call's own (the operator cancelled the
|
|
296
|
+
* turn) and the gate's deadline (`skillWriteConfirm: 'timeout'`). `undefined` when neither exists —
|
|
297
|
+
* the platform's `ask()` then waits without a signal, which is the `ask` mode.
|
|
298
|
+
* @param call - this call's cancellation signal, when the exec context carries one.
|
|
299
|
+
* @param deadline - the gate's own deadline signal, when the mode imposes one.
|
|
300
|
+
* @returns the request field to spread, or undefined when neither signal exists.
|
|
301
|
+
*/
|
|
302
|
+
function combinedSignal(call, deadline) {
|
|
303
|
+
if (call === void 0) return deadline === void 0 ? void 0 : { signal: deadline };
|
|
304
|
+
if (deadline === void 0) return { signal: call };
|
|
305
|
+
return { signal: AbortSignal.any([call, deadline]) };
|
|
306
|
+
}
|
|
248
307
|
/** The platform error code a question-service failure carries, when it carries one. */
|
|
249
308
|
function questionErrorCode(error) {
|
|
250
309
|
const code = error?.code;
|
|
@@ -348,7 +407,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
348
407
|
options: request.question.options.map((option) => ({ label: option.label }))
|
|
349
408
|
}],
|
|
350
409
|
agent,
|
|
351
|
-
...exec.signal
|
|
410
|
+
...combinedSignal(exec.signal, request.signal) ?? {}
|
|
352
411
|
});
|
|
353
412
|
const declared = exec.agent;
|
|
354
413
|
if (declared !== void 0) try {
|
|
@@ -396,7 +455,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
396
455
|
descriptionStrict: rawConfig.descriptionStrict ?? false,
|
|
397
456
|
strictCrossSource: rawConfig.strictCrossSource ?? false,
|
|
398
457
|
citationPolicy: libraryLimits.citationPolicy ?? DEFAULT_CITATION_POLICY,
|
|
399
|
-
supportFileCharPolicy: libraryLimits.supportFileCharPolicy ?? DEFAULT_SUPPORT_FILE_CHAR_POLICY
|
|
458
|
+
supportFileCharPolicy: libraryLimits.supportFileCharPolicy ?? DEFAULT_SUPPORT_FILE_CHAR_POLICY,
|
|
459
|
+
skillWriteConfirm: rawConfig.skillWriteConfirm ?? "auto",
|
|
460
|
+
skillWriteConfirmTimeoutSeconds: rawConfig.skillWriteConfirmTimeoutSeconds ?? 120
|
|
400
461
|
};
|
|
401
462
|
const section = {};
|
|
402
463
|
const settings = () => {
|
|
@@ -412,7 +473,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
412
473
|
descriptionStrict: pick("descriptionStrict"),
|
|
413
474
|
strictCrossSource: pick("strictCrossSource"),
|
|
414
475
|
citationPolicy: overridden("citationPolicy") ?? stages.citationPolicy ?? settingsBase.citationPolicy,
|
|
415
|
-
supportFileCharPolicy: overridden("supportFileCharPolicy") ?? stages.supportFileCharPolicy ?? settingsBase.supportFileCharPolicy
|
|
476
|
+
supportFileCharPolicy: overridden("supportFileCharPolicy") ?? stages.supportFileCharPolicy ?? settingsBase.supportFileCharPolicy,
|
|
477
|
+
skillWriteConfirm: pick("skillWriteConfirm"),
|
|
478
|
+
skillWriteConfirmTimeoutSeconds: pick("skillWriteConfirmTimeoutSeconds")
|
|
416
479
|
};
|
|
417
480
|
};
|
|
418
481
|
const applyLimits = () => {
|
|
@@ -748,6 +811,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
748
811
|
protectedNames: protectedSkillNamesOf(),
|
|
749
812
|
readNames: sessionReadSkillNames(exec.agent?.session),
|
|
750
813
|
confirm: async (request) => confirmSkillWrite(request, exec),
|
|
814
|
+
confirmMode: settings().skillWriteConfirm,
|
|
815
|
+
confirmTimeoutSeconds: settings().skillWriteConfirmTimeoutSeconds,
|
|
751
816
|
warn: warnWriteGateOnce
|
|
752
817
|
});
|
|
753
818
|
if (refusal !== null) return {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { Context } from '@deepseek-ai/cordis';
|
|
21
21
|
import z from '@deepseek-ai/schemastery';
|
|
22
22
|
import type { CitationPolicy, SupportFileCharPolicy } from '@lmzhen/dsh-evolution-core';
|
|
23
|
+
import { type WriteConfirmMode } from './write-gates.ts';
|
|
23
24
|
export declare const name = "tool-skill-manage";
|
|
24
25
|
export declare const inject: string[];
|
|
25
26
|
export interface Config {
|
|
@@ -50,6 +51,11 @@ export interface Config {
|
|
|
50
51
|
* like the four caps above — the settings layer deliberately has no card for it (retention is
|
|
51
52
|
* storage policy, not an authoring knob). */
|
|
52
53
|
skillVersionKeep?: number;
|
|
54
|
+
/** What the ONE confirmation before a create or a bare delete does (registry `skillWriteConfirm`).
|
|
55
|
+
* Default `auto`: an unattended run must not park a tool call on a question nobody will answer. */
|
|
56
|
+
skillWriteConfirm?: WriteConfirmMode;
|
|
57
|
+
/** Seconds a `timeout`-mode prompt waits for an answer before the write is cancelled. */
|
|
58
|
+
skillWriteConfirmTimeoutSeconds?: number;
|
|
53
59
|
}
|
|
54
60
|
export declare const Config: z<Config>;
|
|
55
61
|
/** Write behaviour a user may change (G3/S3.4). Field names are the CANONICAL
|
|
@@ -73,6 +79,10 @@ export interface SkillSettings {
|
|
|
73
79
|
citationPolicy: CitationPolicy;
|
|
74
80
|
/** Warn about an oversize support file, or refuse the write. */
|
|
75
81
|
supportFileCharPolicy: SupportFileCharPolicy;
|
|
82
|
+
/** What the confirmation before a create or a bare delete does (0.12.0). */
|
|
83
|
+
skillWriteConfirm: WriteConfirmMode;
|
|
84
|
+
/** Seconds a `timeout`-mode confirmation waits before the write is cancelled. */
|
|
85
|
+
skillWriteConfirmTimeoutSeconds: number;
|
|
76
86
|
}
|
|
77
87
|
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
78
88
|
* constants and the row schema, so an empty document resolves to today's behaviour. */
|
|
@@ -25,6 +25,19 @@
|
|
|
25
25
|
import { type WriteOrigin } from '@lmzhen/dsh-evolution-core';
|
|
26
26
|
/** The point whose verdict is being taken. */
|
|
27
27
|
export type WriteGatePoint = 'admission' | 'execution';
|
|
28
|
+
/**
|
|
29
|
+
* How the ONE confirmation behaves (registry `skillWriteConfirm`).
|
|
30
|
+
*
|
|
31
|
+
* `auto` writes without asking — the default, because an unattended run (a background pass, a
|
|
32
|
+
* scheduled review, a headless session) must not park a tool call on a question nobody will answer.
|
|
33
|
+
* `ask` waits for as long as it takes, which is the deployment that wants the gate in the loop.
|
|
34
|
+
* `timeout` asks and cancels the write on its own deadline.
|
|
35
|
+
*/
|
|
36
|
+
export type WriteConfirmMode = 'auto' | 'ask' | 'timeout';
|
|
37
|
+
/** The mode a deployment that configures nothing gets. */
|
|
38
|
+
export declare const DEFAULT_WRITE_CONFIRM_MODE: WriteConfirmMode;
|
|
39
|
+
/** Seconds a `timeout`-mode prompt waits before the write is cancelled. */
|
|
40
|
+
export declare const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
|
|
28
41
|
/** One confirm question, as the gate writes it and the seam asks it. */
|
|
29
42
|
export interface WriteConfirmRequest {
|
|
30
43
|
/** The action being confirmed (`create` or `delete`). */
|
|
@@ -42,6 +55,9 @@ export interface WriteConfirmRequest {
|
|
|
42
55
|
};
|
|
43
56
|
/** The option label that means "proceed". */
|
|
44
57
|
readonly confirmLabel: string;
|
|
58
|
+
/** A deadline the gate imposes on itself (`timeout` mode); the seam combines it with the call's
|
|
59
|
+
* own cancellation. An abort is a dismissal, never a consent. */
|
|
60
|
+
readonly signal?: AbortSignal;
|
|
45
61
|
}
|
|
46
62
|
/**
|
|
47
63
|
* Ask the human to confirm one irreversible skill write.
|
|
@@ -68,6 +84,10 @@ export interface WriteGateContext {
|
|
|
68
84
|
readonly readNames: ReadonlySet<string> | undefined;
|
|
69
85
|
/** The human confirm seam — read only by the gates that apply to `'admission'`. */
|
|
70
86
|
readonly confirm: WriteConfirm | undefined;
|
|
87
|
+
/** The registry's `skillWriteConfirm`: `auto` (the default) skips the question entirely. */
|
|
88
|
+
readonly confirmMode?: WriteConfirmMode;
|
|
89
|
+
/** The registry's `skillWriteConfirmTimeoutSeconds`, read only in `timeout` mode. */
|
|
90
|
+
readonly confirmTimeoutSeconds?: number;
|
|
71
91
|
/** Report a degraded gate; must not throw. The implementation decides how often it speaks — the
|
|
72
92
|
* shipped seam latches once per PROCESS, because the conditions it reports (no question service,
|
|
73
93
|
* an unreadable session log) belong to the deployment, not to one write. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-tool-skill-manage",
|
|
3
3
|
"description": "Model-facing skill_manage tool (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,24 +27,24 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.12.0",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.12.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
35
35
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
36
36
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
37
37
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
38
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
39
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
38
|
+
"@lmzhen/dsh-evolution-io": "^0.12.0",
|
|
39
|
+
"@lmzhen/dsh-skill-usage": "^0.12.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
|
|
43
43
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
44
44
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
45
45
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
46
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
47
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
48
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
46
|
+
"@lmzhen/dsh-evolution-core": "^0.12.0",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.12.0",
|
|
48
|
+
"@lmzhen/dsh-skill-usage": "^0.12.0"
|
|
49
49
|
}
|
|
50
50
|
}
|