@lmzhen/dsh-evolution-core 0.3.65 → 0.3.66
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 +214 -56
- package/lib/types/io.d.ts +31 -6
- package/lib/types/skill-store.d.ts +11 -0
- package/lib/types/threats.d.ts +9 -9
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -79,6 +79,18 @@ const RENAME_RETRY_MAX_ATTEMPTS = 6;
|
|
|
79
79
|
* rethrows with a pointer at the usual causes instead of a bare errno.
|
|
80
80
|
* `fn` is the rename primitive, injectable for deterministic tests.
|
|
81
81
|
*
|
|
82
|
+
* v22 (LOCK-5) documented platform limit: POSIX rename-over-existing is
|
|
83
|
+
* atomic for concurrent READERS, but Windows MoveFileExW(REPLACE_EXISTING)
|
|
84
|
+
* can make the target briefly invisible (ENOENT) while the replacement is in
|
|
85
|
+
* flight. The retry budget above covers the WRITE side only — a LOCKLESS
|
|
86
|
+
* reader (`readText`/`readJson` on a path it does not hold the lock for) can
|
|
87
|
+
* observe that window and see "missing" for a file that was just committed.
|
|
88
|
+
* Verified harmless today: every lockless-read consumer treats the transient
|
|
89
|
+
* miss as self-healing state (no consumer persists a read of null into a
|
|
90
|
+
* write), and distinguishing that ENOENT from a genuinely absent file in
|
|
91
|
+
* `readText` would tax every ordinary missing-file probe. Revisit only if a
|
|
92
|
+
* consumer appears that must never transiently miss.
|
|
93
|
+
*
|
|
82
94
|
* @param tmp - the source path to rename.
|
|
83
95
|
* @param target - the destination path.
|
|
84
96
|
* @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
|
|
@@ -189,8 +201,18 @@ function isProcessAlive(pid) {
|
|
|
189
201
|
const LOCK_SUFFIX = ".lock";
|
|
190
202
|
/** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
|
|
191
203
|
* torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
|
|
192
|
-
* the pid part is intact, which is what the takeover probe needs.
|
|
193
|
-
|
|
204
|
+
* the pid part is intact, which is what the takeover probe needs. The capture
|
|
205
|
+
* group feeds `parseLockBody` (v20 A-1: skill-store's sweepers consume this
|
|
206
|
+
* helper instead of re-inlining a third regex copy — F-17 single-source). */
|
|
207
|
+
const LOCK_BODY_RE = /^(\d+):[0-9a-f]*$/;
|
|
208
|
+
/** Parse a writer-lock body into its holder pid. `null` when the body does
|
|
209
|
+
* not have the `pid:token` shape at all (e.g. a user support file named
|
|
210
|
+
* `*.lock`) — callers leave such files alone. A shape-matching body always
|
|
211
|
+
* yields a number (possibly `0`, which `isProcessAlive` treats as dead). */
|
|
212
|
+
function parseLockBody(body) {
|
|
213
|
+
const match = LOCK_BODY_RE.exec(body.trim());
|
|
214
|
+
return match === null ? null : Number(match[1]);
|
|
215
|
+
}
|
|
194
216
|
/**
|
|
195
217
|
* Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
|
|
196
218
|
* (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
|
|
@@ -309,7 +331,18 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
309
331
|
} catch {
|
|
310
332
|
continue;
|
|
311
333
|
}
|
|
312
|
-
const
|
|
334
|
+
const verifyRead = await readFile(lock, "utf8").then((body) => ({
|
|
335
|
+
ok: true,
|
|
336
|
+
body
|
|
337
|
+
}), () => ({
|
|
338
|
+
ok: false,
|
|
339
|
+
body: ""
|
|
340
|
+
}));
|
|
341
|
+
if (!verifyRead.ok) {
|
|
342
|
+
await rm(ticket, { force: true }).catch(() => {});
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const verify = verifyRead.body;
|
|
313
346
|
if (verify === holderContent) {
|
|
314
347
|
const verifyHolder = Number(verify.split(":")[0] ?? "");
|
|
315
348
|
if (!(Number.isInteger(verifyHolder) && verifyHolder > 0 && isAlive(verifyHolder))) await rm(lock, { force: true }).catch(() => {});
|
|
@@ -580,23 +613,40 @@ async function mutateUsage(root, io, task, options = {}) {
|
|
|
580
613
|
await transactIo(io, usageFile(root), async (current) => {
|
|
581
614
|
let shapePreserved = false;
|
|
582
615
|
let recovered = null;
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
616
|
+
const quarantineCopy = async () => {
|
|
617
|
+
const corruptPath = `${usageFile(root)}.corrupt`;
|
|
618
|
+
return io.writeText(corruptPath, current).then(() => true, () => false);
|
|
619
|
+
};
|
|
620
|
+
if (current !== null) {
|
|
621
|
+
let parsed = null;
|
|
622
|
+
try {
|
|
623
|
+
const probe = JSON.parse(current);
|
|
624
|
+
if (probe !== null && typeof probe === "object" && !Array.isArray(probe)) parsed = probe;
|
|
625
|
+
} catch {
|
|
626
|
+
parsed = null;
|
|
627
|
+
}
|
|
628
|
+
if (parsed === null) {
|
|
629
|
+
if (!await quarantineCopy()) {
|
|
630
|
+
options.onQuarantine?.(`usage sidecar ${usageFile(root)} is unreadable; the .corrupt copy could not be written — refusing this write so the original bytes stay recoverable (telemetry is frozen until the file is recovered manually)`);
|
|
631
|
+
return current;
|
|
632
|
+
}
|
|
633
|
+
options.onQuarantine?.(`usage sidecar ${usageFile(root)} is unreadable (unparsable JSON or wrong top-level shape); the original bytes were copied to ${usageFile(root)}.corrupt and the sidecar restarts empty`);
|
|
634
|
+
} else {
|
|
635
|
+
const record = parsed;
|
|
588
636
|
const isMalformed = (value) => value === null || typeof value !== "object" || Array.isArray(value);
|
|
589
|
-
if (typeof record.version === "number" && record.version > 1)
|
|
590
|
-
|
|
637
|
+
if (typeof record.version === "number" && record.version > 1) {
|
|
638
|
+
options.onQuarantine?.(`usage sidecar ${usageFile(root)} carries schema version ${String(record.version)} (> this runtime) — writes stay frozen and the bytes preserved until the runtime is upgraded`);
|
|
639
|
+
shapePreserved = true;
|
|
640
|
+
} else if (Object.values(record).some(isMalformed)) {
|
|
591
641
|
const bad = Object.keys(record).filter((key) => isMalformed(record[key]));
|
|
592
|
-
|
|
593
|
-
|
|
642
|
+
if (!await quarantineCopy()) {
|
|
643
|
+
options.onQuarantine?.(`usage sidecar ${usageFile(root)} carried ${bad.length} malformed entr${bad.length === 1 ? "y" : "ies"} (${bad.slice(0, 5).join(", ")}); the .corrupt copy could not be written — refusing this write so the original bytes stay recoverable`);
|
|
644
|
+
return current;
|
|
645
|
+
}
|
|
594
646
|
recovered = JSON.stringify(Object.fromEntries(Object.entries(record).filter(([, value]) => !isMalformed(value))));
|
|
595
|
-
options.onQuarantine?.(`usage sidecar ${usageFile(root)} carried ${bad.length} malformed entr${bad.length === 1 ? "y" : "ies"} (${bad.slice(0, 5).join(", ")}); the original bytes were copied to ${
|
|
647
|
+
options.onQuarantine?.(`usage sidecar ${usageFile(root)} carried ${bad.length} malformed entr${bad.length === 1 ? "y" : "ies"} (${bad.slice(0, 5).join(", ")}); the original bytes were copied to ${usageFile(root)}.corrupt and the remaining entries continue to be served`);
|
|
596
648
|
}
|
|
597
649
|
}
|
|
598
|
-
} catch {
|
|
599
|
-
return current;
|
|
600
650
|
}
|
|
601
651
|
if (shapePreserved) return current;
|
|
602
652
|
const map = parseUsage(recovered ?? current);
|
|
@@ -1315,7 +1365,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1315
1365
|
}
|
|
1316
1366
|
const events = await rotateIfDue(io, path, v1EventRecords(parsedBody), rotateAt);
|
|
1317
1367
|
let maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
1318
|
-
|
|
1368
|
+
for (const name of await listEventArchives(io, path)) maxSeq = Math.max(maxSeq, Number.parseInt(name.slice(7, name.length - 5), 10));
|
|
1319
1369
|
const record = {
|
|
1320
1370
|
...event,
|
|
1321
1371
|
seq: maxSeq + 1,
|
|
@@ -2072,19 +2122,19 @@ const PATTERNS = [
|
|
|
2072
2122
|
label: "send_to_url",
|
|
2073
2123
|
category: "exfiltration",
|
|
2074
2124
|
scope: "strict",
|
|
2075
|
-
regex: /(?:send|post|upload|transmit)\s+[
|
|
2125
|
+
regex: /(?:send|post|upload|transmit)\s+[\s\S]{0,512}?\s+(?:to|at)\s+https?:\/\//i
|
|
2076
2126
|
},
|
|
2077
2127
|
{
|
|
2078
2128
|
label: "exfil_curl",
|
|
2079
2129
|
category: "exfiltration",
|
|
2080
2130
|
scope: "all",
|
|
2081
|
-
regex: /\bcurl\s+[
|
|
2131
|
+
regex: /\bcurl\s+[\s\S]{0,512}?\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i
|
|
2082
2132
|
},
|
|
2083
2133
|
{
|
|
2084
2134
|
label: "exfil_wget",
|
|
2085
2135
|
category: "exfiltration",
|
|
2086
2136
|
scope: "all",
|
|
2087
|
-
regex: /\bwget\s+[
|
|
2137
|
+
regex: /\bwget\s+[\s\S]{0,512}?\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i
|
|
2088
2138
|
},
|
|
2089
2139
|
{
|
|
2090
2140
|
label: "read_secrets",
|
|
@@ -2150,7 +2200,37 @@ const PATTERNS = [
|
|
|
2150
2200
|
label: "private_key_block",
|
|
2151
2201
|
category: "hardcoded_secrets",
|
|
2152
2202
|
scope: "all",
|
|
2153
|
-
regex: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY
|
|
2203
|
+
regex: /-----BEGIN\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\s+)?PRIVATE\s+KEY(?:\s+BLOCK)?-----/
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
label: "prompt_injection_ignore_zh",
|
|
2207
|
+
category: "prompt_injection",
|
|
2208
|
+
scope: "all",
|
|
2209
|
+
regex: /(?:忽略|无视|抛开)[\s\S]{0,8}(?:之前|以上|上面|先前|以前)?[\s\S]{0,8}(?:所有|全部|任何)?[\s\S]{0,4}(?:指令|规则|设定|约束)/
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
label: "deception_hide_zh",
|
|
2213
|
+
category: "deception",
|
|
2214
|
+
scope: "all",
|
|
2215
|
+
regex: /(?:不要|别|勿)(?:告诉|告知|透露)(?:给)?[\s\S]{0,6}(?:用户|任何人|主人|开发者)/
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
label: "system_prompt_leak_zh",
|
|
2219
|
+
category: "deception",
|
|
2220
|
+
scope: "context",
|
|
2221
|
+
regex: /(?:泄露|输出|打印|透露|导出)[\s\S]{0,10}系统提示|系统提示[\s\S]{0,10}(?:泄露|透露|发送|导出)/
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
label: "context_exfil_zh",
|
|
2225
|
+
category: "exfiltration",
|
|
2226
|
+
scope: "strict",
|
|
2227
|
+
regex: /(?:对话记录|聊天记录|全部上下文|完整上下文)[\s\S]{0,30}(?:发送|上传|传输|外传|泄露)[\s\S]{0,30}(?:到|至|给)/
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
label: "secret_exfil_zh",
|
|
2231
|
+
category: "exfiltration",
|
|
2232
|
+
scope: "strict",
|
|
2233
|
+
regex: /(?:密钥|凭据|口令|密码|环境变量)[\s\S]{0,30}(?:发送|上传|传输|外传|泄露)[\s\S]{0,30}(?:到|至)\s*(?:https?:\/\/|[\w.-]+\.(?:com|net|org|io|cn|dev|xyz|ru)\b)/
|
|
2154
2234
|
}
|
|
2155
2235
|
];
|
|
2156
2236
|
const ZERO_WIDTH_CHARS = new RegExp(`[\\u034f\\u200b\\u2060\\u2061\\u2062\\u2063\\u2064\\u206a-\\u206f\\ufeff\\u{e0000}-\\u{e007f}]`, "u");
|
|
@@ -2199,11 +2279,13 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
|
|
|
2199
2279
|
scope: "all"
|
|
2200
2280
|
});
|
|
2201
2281
|
const normalized = text.normalize("NFKC");
|
|
2282
|
+
const SPACE_SPLITTERS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/gu;
|
|
2283
|
+
const patternTexts = [normalized.replace(SPACE_SPLITTERS, " "), normalized.replace(SPACE_SPLITTERS, "")];
|
|
2202
2284
|
const windows = [];
|
|
2203
|
-
if (
|
|
2285
|
+
for (const patternText of patternTexts) if (patternText.length <= windowSize) windows.push(patternText);
|
|
2204
2286
|
else {
|
|
2205
2287
|
const step = Math.max(Math.floor(windowSize / 2), 1);
|
|
2206
|
-
for (let start = 0; start <
|
|
2288
|
+
for (let start = 0; start < patternText.length; start += step) windows.push(patternText.slice(start, start + windowSize));
|
|
2207
2289
|
}
|
|
2208
2290
|
const seen = /* @__PURE__ */ new Set();
|
|
2209
2291
|
for (const window of windows) for (const pattern of PATTERNS) {
|
|
@@ -2236,27 +2318,26 @@ function scanMemoryThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS
|
|
|
2236
2318
|
const { blocked, findings } = evaluateThreat(text, "strict", maxScanChars, options);
|
|
2237
2319
|
if (!blocked) return null;
|
|
2238
2320
|
const pattern = findings.find((f) => f.category !== "unicode_obfuscation");
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return `Blocked by security scan: invisible or potentially malicious Unicode detected.${exemptionHint}`;
|
|
2321
|
+
if (pattern) return `Blocked by security scan (${pattern.label}). Rephrase without instruction-like language.${THREAT_EXEMPTION_HINT}`;
|
|
2322
|
+
return `Blocked by security scan: invisible or potentially malicious Unicode detected.${THREAT_EXEMPTION_HINT}`;
|
|
2242
2323
|
}
|
|
2243
2324
|
/** User-facing block message for skill content writes. */
|
|
2244
2325
|
function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
|
|
2245
2326
|
const { blocked, findings } = evaluateThreat(text, "strict", maxScanChars, options);
|
|
2246
2327
|
if (!blocked) return null;
|
|
2247
|
-
return `Blocked by security scan (${findings[0]?.label ?? "unknown"}). This content appears to contain potentially malicious instructions
|
|
2328
|
+
return `Blocked by security scan (${findings[0]?.label ?? "unknown"}). This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
|
|
2248
2329
|
}
|
|
2249
2330
|
/**
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2257
|
-
* message verbatim.
|
|
2331
|
+
* WD2 (0.3.56): the shared tail of every user-facing threat block — names the
|
|
2332
|
+
* deployable self-heal path so the model (or operator) can allowlist a
|
|
2333
|
+
* known-benign label. v20 (B-2) single source: the scan builders embed this
|
|
2334
|
+
* constant verbatim; do NOT re-word it per call site (the three former copies
|
|
2335
|
+
* — memory inline, content inline, and the dead `THREAT_EXEMPT_HINT` export,
|
|
2336
|
+
* whose docblock still claimed a store-side append that memory-store/skill-store
|
|
2337
|
+
* had already removed — had drifted apart). The evolution-threat guard channel
|
|
2338
|
+
* returns the scan message verbatim, so the hint rides along there too.
|
|
2258
2339
|
*/
|
|
2259
|
-
const
|
|
2340
|
+
const THREAT_EXEMPTION_HINT = " A deployment that needs a specific label can exempt it via threatExemptLabels (see the README env/dial reference).";
|
|
2260
2341
|
//#endregion
|
|
2261
2342
|
//#region lib/types/memory-store.js
|
|
2262
2343
|
/**
|
|
@@ -2955,7 +3036,7 @@ function injectCatalogDescriptionCap(composition) {
|
|
|
2955
3036
|
const next = lines[j] ?? "";
|
|
2956
3037
|
if (next.trim() === "") break;
|
|
2957
3038
|
if (!/^\s/.test(next)) break;
|
|
2958
|
-
if (
|
|
3039
|
+
if (/^ {2}config:(\s|$)/.test(next)) hasConfig = true;
|
|
2959
3040
|
end = j;
|
|
2960
3041
|
}
|
|
2961
3042
|
if (hasConfig) continue;
|
|
@@ -3156,9 +3237,14 @@ const SECRET_PATTERNS = [
|
|
|
3156
3237
|
["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
|
|
3157
3238
|
["slack token", /xox[baprs]-[A-Za-z0-9-]{10,}/g],
|
|
3158
3239
|
["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
|
|
3240
|
+
["npm token", /npm_[A-Za-z0-9]{20,}/g],
|
|
3241
|
+
["stripe key", /[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g],
|
|
3242
|
+
["github fine-grained token", /github_pat_[A-Za-z0-9_]{20,}/g],
|
|
3243
|
+
["google api key", /AIza[0-9A-Za-z_-]{30,}/g],
|
|
3159
3244
|
["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi]
|
|
3160
3245
|
];
|
|
3161
3246
|
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]{0,64}[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]{0,64})?)\\b([\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
|
|
3247
|
+
const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
|
|
3162
3248
|
/**
|
|
3163
3249
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
3164
3250
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -3167,7 +3253,12 @@ const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]
|
|
|
3167
3253
|
function redactSecrets(text) {
|
|
3168
3254
|
let out = text;
|
|
3169
3255
|
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
3256
|
+
out = out.replace(URL_CREDENTIALS_PATTERN, (_match, lead) => `${lead ?? ""}<redacted>@`);
|
|
3170
3257
|
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
|
|
3258
|
+
out = out.split("\n").map((line) => {
|
|
3259
|
+
if (!/<redacted>/.test(line) && !/\baws\b|\bAKIA\b|\bsecret\b/i.test(line)) return line;
|
|
3260
|
+
return line.replace(/(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, "<redacted>");
|
|
3261
|
+
}).join("\n");
|
|
3171
3262
|
return out;
|
|
3172
3263
|
}
|
|
3173
3264
|
//#endregion
|
|
@@ -3616,6 +3707,8 @@ const MARKER_LOCK_NAMES = [
|
|
|
3616
3707
|
`.pinned${LOCK_SUFFIX}`,
|
|
3617
3708
|
`.hermes-managed${LOCK_SUFFIX}`
|
|
3618
3709
|
];
|
|
3710
|
+
/** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
|
|
3711
|
+
const ARCHIVE_RETENTION_DAYS = 365;
|
|
3619
3712
|
function markerPath(dir, marker) {
|
|
3620
3713
|
return join(dir, markerEntryName(marker));
|
|
3621
3714
|
}
|
|
@@ -4435,13 +4528,22 @@ var SkillLibrary = class {
|
|
|
4435
4528
|
ok: false,
|
|
4436
4529
|
message: `Skill "${normalized}" not found.`
|
|
4437
4530
|
};
|
|
4438
|
-
if (pinned)
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4531
|
+
if (pinned) {
|
|
4532
|
+
try {
|
|
4533
|
+
await this.io.writeText(marker, "");
|
|
4534
|
+
} catch (error) {
|
|
4535
|
+
if (!isCommittedOnly(error)) throw error;
|
|
4536
|
+
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
4537
|
+
}
|
|
4538
|
+
if (!await this.io.exists(join(dir, "SKILL.md"))) {
|
|
4539
|
+
await this.io.remove(marker).catch(() => {});
|
|
4540
|
+
if ((await this.io.list(dir).catch(() => [])).length === 0) await this.io.remove(dir).catch(() => {});
|
|
4541
|
+
return {
|
|
4542
|
+
ok: false,
|
|
4543
|
+
message: `Skill "${normalized}" was archived concurrently while pinning; the partial marker was removed — retry after the mover settles.`
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
4546
|
+
} else await this.io.remove(marker);
|
|
4445
4547
|
await this.audit(normalized, pinned ? "pin" : "unpin", null, null, pinned ? "pinned" : "unpinned");
|
|
4446
4548
|
return {
|
|
4447
4549
|
ok: true,
|
|
@@ -4487,6 +4589,10 @@ var SkillLibrary = class {
|
|
|
4487
4589
|
ok: false,
|
|
4488
4590
|
message: `Skill "${normalized}" already exists.`
|
|
4489
4591
|
};
|
|
4592
|
+
for (const entry of await this.io.list(this.root).catch(() => [])) if (typeof entry === "string" && entry !== normalized && entry.toLowerCase() === normalized.toLowerCase()) return {
|
|
4593
|
+
ok: false,
|
|
4594
|
+
message: `Skill "${normalized}" collides with the existing case-variant directory "${entry}" (skill names are lowercase-only); rename one of them.`
|
|
4595
|
+
};
|
|
4490
4596
|
const protection = await this.writeProtection(normalized, origin);
|
|
4491
4597
|
if (protection) return {
|
|
4492
4598
|
ok: false,
|
|
@@ -4530,7 +4636,17 @@ var SkillLibrary = class {
|
|
|
4530
4636
|
ok: false,
|
|
4531
4637
|
message: `Skill "${normalized}" already exists.`
|
|
4532
4638
|
};
|
|
4533
|
-
if (origin !== "foreground")
|
|
4639
|
+
if (origin !== "foreground") {
|
|
4640
|
+
await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
4641
|
+
if (!await this.io.exists(createPath)) {
|
|
4642
|
+
await this.io.remove(markerPath(dir, "hermes-managed")).catch(() => {});
|
|
4643
|
+
if ((await this.io.list(dir).catch(() => [])).length === 0) await this.io.remove(dir).catch(() => {});
|
|
4644
|
+
return {
|
|
4645
|
+
ok: false,
|
|
4646
|
+
message: `Skill "${normalized}" was archived concurrently while being created; the partial marker was removed — retry once the mover settles.`
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4534
4650
|
await this.audit(normalized, "create", null, onDisk, "created");
|
|
4535
4651
|
this.notifyMutation({
|
|
4536
4652
|
action: "create",
|
|
@@ -4863,9 +4979,7 @@ var SkillLibrary = class {
|
|
|
4863
4979
|
* stolen by the sweep. A dead-pid residue would otherwise permanently
|
|
4864
4980
|
* refuse archive/restore. */
|
|
4865
4981
|
async deleteStrandedLocks(dir) {
|
|
4866
|
-
await this.sweepLockIfStranded(join(dir,
|
|
4867
|
-
await this.sweepLockIfStranded(join(dir, ".pinned.lock"));
|
|
4868
|
-
await this.sweepLockIfStranded(join(dir, ".hermes-managed.lock"));
|
|
4982
|
+
for (const markerLock of MARKER_LOCK_NAMES) await this.sweepLockIfStranded(join(dir, markerLock));
|
|
4869
4983
|
for (const supportDir of SUPPORT_DIRS) {
|
|
4870
4984
|
let entries = [];
|
|
4871
4985
|
try {
|
|
@@ -4882,9 +4996,8 @@ var SkillLibrary = class {
|
|
|
4882
4996
|
async sweepLockIfStranded(lockPath) {
|
|
4883
4997
|
const body = await this.io.readText(lockPath).catch(() => null);
|
|
4884
4998
|
if (body === null) return;
|
|
4885
|
-
const
|
|
4886
|
-
if (
|
|
4887
|
-
const pid = Number(match[1]);
|
|
4999
|
+
const pid = parseLockBody(body);
|
|
5000
|
+
if (pid === null) return;
|
|
4888
5001
|
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) return;
|
|
4889
5002
|
await this.io.remove(lockPath).catch(() => {});
|
|
4890
5003
|
}
|
|
@@ -4908,9 +5021,8 @@ var SkillLibrary = class {
|
|
|
4908
5021
|
throw new Error(`snapshot restore refused: cannot verify ${label} (${error instanceof Error ? error.message : String(error)}); a live writer may hold it`);
|
|
4909
5022
|
}
|
|
4910
5023
|
if (body === null) return;
|
|
4911
|
-
const
|
|
4912
|
-
if (
|
|
4913
|
-
const pid = Number(match[1]);
|
|
5024
|
+
const pid = parseLockBody(body);
|
|
5025
|
+
if (pid === null) return;
|
|
4914
5026
|
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
|
|
4915
5027
|
await this.io.remove(lockPath).catch(() => {});
|
|
4916
5028
|
}
|
|
@@ -5329,7 +5441,17 @@ var SkillLibrary = class {
|
|
|
5329
5441
|
try {
|
|
5330
5442
|
for (const entry of landing) {
|
|
5331
5443
|
try {
|
|
5332
|
-
|
|
5444
|
+
if (this.transact) {
|
|
5445
|
+
const drift = { seen: false };
|
|
5446
|
+
await this.transact(this.io, entry.target, (current) => {
|
|
5447
|
+
if (current !== entry.previous) {
|
|
5448
|
+
drift.seen = true;
|
|
5449
|
+
return current;
|
|
5450
|
+
}
|
|
5451
|
+
return entry.content;
|
|
5452
|
+
});
|
|
5453
|
+
if (drift.seen) throw new Error(`concurrent modification detected: ${entry.target} changed after the plan was computed (a concurrent writer won the race); no further writes were performed`);
|
|
5454
|
+
} else await this.io.writeText(entry.target, entry.content);
|
|
5333
5455
|
} catch (error) {
|
|
5334
5456
|
if (error?.committed !== true) throw error;
|
|
5335
5457
|
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
@@ -5544,12 +5666,41 @@ var SkillLibrary = class {
|
|
|
5544
5666
|
};
|
|
5545
5667
|
}
|
|
5546
5668
|
/**
|
|
5669
|
+
* v23 (ML-1): `.archive` retention. Archived skills are recoverable history,
|
|
5670
|
+
* but nothing bounded their growth — the curator auto-archives idle skills,
|
|
5671
|
+
* consolidation and manual deletes take the same path, and every snapshot
|
|
5672
|
+
* copies the whole `.archive` (keep-5 retention amplifies it ×6). Entries
|
|
5673
|
+
* older than this many days are pruned at snapshot time. Generous by
|
|
5674
|
+
* design: a year-old auto-archive is effectively dead recoverability.
|
|
5675
|
+
* Backends without the mtime probe skip pruning (no false deletes on
|
|
5676
|
+
* unknown age).
|
|
5677
|
+
*/
|
|
5678
|
+
async pruneExpiredArchives() {
|
|
5679
|
+
const archiveRoot = join(this.root, ".archive");
|
|
5680
|
+
if (!this.io.mtime) return;
|
|
5681
|
+
let entries = [];
|
|
5682
|
+
try {
|
|
5683
|
+
entries = await this.io.list(archiveRoot);
|
|
5684
|
+
} catch {
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
const cutoff = Date.now() - ARCHIVE_RETENTION_DAYS * 864e5;
|
|
5688
|
+
for (const entry of entries) {
|
|
5689
|
+
const entryPath = join(archiveRoot, entry);
|
|
5690
|
+
const mtime = await this.io.mtime(entryPath).catch(() => null);
|
|
5691
|
+
if (mtime === null || mtime > cutoff) continue;
|
|
5692
|
+
await this.io.remove(entryPath).catch(() => {});
|
|
5693
|
+
console.warn(`skill-store: pruned archived skill "${entry}" (older than ${ARCHIVE_RETENTION_DAYS} days; recoverable from snapshots until they rotate)`);
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
/**
|
|
5547
5697
|
* Snapshot the recoverable skills state: active tree, usage/suppression
|
|
5548
5698
|
* sidecars, `.archive/` and caller-supplied extras. `extras` are opaque
|
|
5549
5699
|
* side files the Snapshot owner cares about (curator state); they are
|
|
5550
5700
|
* listed in the manifest and only those names are ever read back.
|
|
5551
5701
|
*/
|
|
5552
5702
|
async snapshotAll(reason = "pre-mutation", extras = []) {
|
|
5703
|
+
await this.pruneExpiredArchives();
|
|
5553
5704
|
const backupRoot = join(this.root, ".backups");
|
|
5554
5705
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5555
5706
|
let dest = join(backupRoot, `skills-${stamp}`);
|
|
@@ -5631,7 +5782,14 @@ var SkillLibrary = class {
|
|
|
5631
5782
|
for (const name of entries.sort().reverse()) {
|
|
5632
5783
|
if (!name.startsWith("skills-")) continue;
|
|
5633
5784
|
const manifest = await this.readSnapshotManifest(join(backupRoot, name));
|
|
5634
|
-
if (manifest === null)
|
|
5785
|
+
if (manifest === null) {
|
|
5786
|
+
out.push({
|
|
5787
|
+
path: join(backupRoot, name),
|
|
5788
|
+
createdAt: "",
|
|
5789
|
+
reason: "unreadable or missing manifest (orphan snapshot)"
|
|
5790
|
+
});
|
|
5791
|
+
continue;
|
|
5792
|
+
}
|
|
5635
5793
|
out.push({
|
|
5636
5794
|
path: join(backupRoot, name),
|
|
5637
5795
|
createdAt: manifest.createdAt,
|
|
@@ -5765,4 +5923,4 @@ var SkillLibrary = class {
|
|
|
5765
5923
|
}
|
|
5766
5924
|
};
|
|
5767
5925
|
//#endregion
|
|
5768
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary,
|
|
5926
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
package/lib/types/io.d.ts
CHANGED
|
@@ -45,11 +45,17 @@ export interface EvolutionIoLike {
|
|
|
45
45
|
* milliseconds since epoch, or `null` when unknown (unsupported backend,
|
|
46
46
|
* missing path, stat failure). Intended as a cheap invalidation stamp for a
|
|
47
47
|
* cached directory listing; a backend without it keeps event-driven
|
|
48
|
-
* invalidation only.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* invalidation only. v20 correction: the former "NO in-tree consumer" note
|
|
49
|
+
* (V9-07, 0.3.51) went stale — there are now FOUR in-tree consumers, and a
|
|
50
|
+
* custom backend that omits `mtime` degrades them silently (every call
|
|
51
|
+
* site is optional-call + null-fallback, so omission stays legal):
|
|
52
|
+
* - evolution-skill-catalog: root-mtime stamp on the summaries cache —
|
|
53
|
+
* the second, out-of-band invalidation signal next to the
|
|
54
|
+
* `evolution/skill-mutated` / `evolution/skills-refresh` events;
|
|
55
|
+
* - evolution-curator: run-report recency ordering (2 call sites);
|
|
56
|
+
* - evolution-commands: `.bak` freshness probe in the preset installer.
|
|
57
|
+
* With `mtime` absent, catalog invalidation degrades to purely
|
|
58
|
+
* event-driven. Register new consumers here (the seam contract).
|
|
53
59
|
*/
|
|
54
60
|
mtime?(this: void, path: string): Promise<number | null>;
|
|
55
61
|
}
|
|
@@ -84,6 +90,18 @@ export declare const pendingSelfCleanup: Map<string, string>;
|
|
|
84
90
|
* rethrows with a pointer at the usual causes instead of a bare errno.
|
|
85
91
|
* `fn` is the rename primitive, injectable for deterministic tests.
|
|
86
92
|
*
|
|
93
|
+
* v22 (LOCK-5) documented platform limit: POSIX rename-over-existing is
|
|
94
|
+
* atomic for concurrent READERS, but Windows MoveFileExW(REPLACE_EXISTING)
|
|
95
|
+
* can make the target briefly invisible (ENOENT) while the replacement is in
|
|
96
|
+
* flight. The retry budget above covers the WRITE side only — a LOCKLESS
|
|
97
|
+
* reader (`readText`/`readJson` on a path it does not hold the lock for) can
|
|
98
|
+
* observe that window and see "missing" for a file that was just committed.
|
|
99
|
+
* Verified harmless today: every lockless-read consumer treats the transient
|
|
100
|
+
* miss as self-healing state (no consumer persists a read of null into a
|
|
101
|
+
* write), and distinguishing that ENOENT from a genuinely absent file in
|
|
102
|
+
* `readText` would tax every ordinary missing-file probe. Revisit only if a
|
|
103
|
+
* consumer appears that must never transiently miss.
|
|
104
|
+
*
|
|
87
105
|
* @param tmp - the source path to rename.
|
|
88
106
|
* @param target - the destination path.
|
|
89
107
|
* @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
|
|
@@ -119,8 +137,15 @@ export declare function isProcessAlive(pid: number): boolean;
|
|
|
119
137
|
export declare const LOCK_SUFFIX = ".lock";
|
|
120
138
|
/** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
|
|
121
139
|
* torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
|
|
122
|
-
* the pid part is intact, which is what the takeover probe needs.
|
|
140
|
+
* the pid part is intact, which is what the takeover probe needs. The capture
|
|
141
|
+
* group feeds `parseLockBody` (v20 A-1: skill-store's sweepers consume this
|
|
142
|
+
* helper instead of re-inlining a third regex copy — F-17 single-source). */
|
|
123
143
|
export declare const LOCK_BODY_RE: RegExp;
|
|
144
|
+
/** Parse a writer-lock body into its holder pid. `null` when the body does
|
|
145
|
+
* not have the `pid:token` shape at all (e.g. a user support file named
|
|
146
|
+
* `*.lock`) — callers leave such files alone. A shape-matching body always
|
|
147
|
+
* yields a number (possibly `0`, which `isProcessAlive` treats as dead). */
|
|
148
|
+
export declare function parseLockBody(body: string): number | null;
|
|
124
149
|
/**
|
|
125
150
|
* Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
|
|
126
151
|
* (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
|
|
@@ -474,6 +474,17 @@ export declare class SkillLibrary {
|
|
|
474
474
|
private writeSupportFileCore;
|
|
475
475
|
removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
476
476
|
private removeSupportFileCore;
|
|
477
|
+
/**
|
|
478
|
+
* v23 (ML-1): `.archive` retention. Archived skills are recoverable history,
|
|
479
|
+
* but nothing bounded their growth — the curator auto-archives idle skills,
|
|
480
|
+
* consolidation and manual deletes take the same path, and every snapshot
|
|
481
|
+
* copies the whole `.archive` (keep-5 retention amplifies it ×6). Entries
|
|
482
|
+
* older than this many days are pruned at snapshot time. Generous by
|
|
483
|
+
* design: a year-old auto-archive is effectively dead recoverability.
|
|
484
|
+
* Backends without the mtime probe skip pruning (no false deletes on
|
|
485
|
+
* unknown age).
|
|
486
|
+
*/
|
|
487
|
+
private pruneExpiredArchives;
|
|
477
488
|
/**
|
|
478
489
|
* Snapshot the recoverable skills state: active tree, usage/suppression
|
|
479
490
|
* sidecars, `.archive/` and caller-supplied extras. `extras` are opaque
|
package/lib/types/threats.d.ts
CHANGED
|
@@ -61,14 +61,14 @@ export declare function scanMemoryThreats(text: string, maxScanChars?: number, o
|
|
|
61
61
|
/** User-facing block message for skill content writes. */
|
|
62
62
|
export declare function scanContentThreats(text: string, maxScanChars?: number, options?: ScanOptions): string | null;
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* message verbatim.
|
|
64
|
+
* WD2 (0.3.56): the shared tail of every user-facing threat block — names the
|
|
65
|
+
* deployable self-heal path so the model (or operator) can allowlist a
|
|
66
|
+
* known-benign label. v20 (B-2) single source: the scan builders embed this
|
|
67
|
+
* constant verbatim; do NOT re-word it per call site (the three former copies
|
|
68
|
+
* — memory inline, content inline, and the dead `THREAT_EXEMPT_HINT` export,
|
|
69
|
+
* whose docblock still claimed a store-side append that memory-store/skill-store
|
|
70
|
+
* had already removed — had drifted apart). The evolution-threat guard channel
|
|
71
|
+
* returns the scan message verbatim, so the hint rides along there too.
|
|
72
72
|
*/
|
|
73
|
-
export declare const
|
|
73
|
+
export declare const THREAT_EXEMPTION_HINT = " A deployment that needs a specific label can exempt it via threatExemptLabels (see the README env/dial reference).";
|
|
74
74
|
//# sourceMappingURL=threats.d.ts.map
|
package/package.json
CHANGED