@lmzhen/dsh-tool-skill-manage 0.11.3 → 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 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`, asks the operator once before it writes and is
38
- refused with `E-317` when the answer is not the confirm label; a `delete` carrying `absorbed_into` is the
39
- merge protocol and does not ask. This gate is UX, not a security door: it is admission-only (a
40
- replayed record already carries the human release that staged it), and an unmounted question
41
- service, a caller that is not the registry's exact live root agent, or a failing ask all PROCEED
42
- with one warning — the operator's own session stays the authority that asked for the write.
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
- if (await confirm({
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
- })) return null;
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 !== void 0 ? { signal: 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 {
@@ -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.11.3",
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.11.3",
31
- "@lmzhen/dsh-evolution-core": "^0.11.3"
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.11.3",
39
- "@lmzhen/dsh-skill-usage": "^0.11.3"
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.11.3",
47
- "@lmzhen/dsh-evolution-io": "^0.11.3",
48
- "@lmzhen/dsh-skill-usage": "^0.11.3"
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
  }