@lmzhen/dsh-evolution-core 0.26.26 → 0.27.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.
Files changed (107) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +66 -850
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.js +346 -0
  25. package/lib/types/diagnostics.d.ts +26 -0
  26. package/lib/types/diagnostics.js +23 -0
  27. package/lib/types/document-body.js +91 -0
  28. package/lib/types/drift-signals.js +319 -0
  29. package/lib/types/env.js +25 -0
  30. package/lib/types/errors.d.ts +4 -1
  31. package/lib/types/errors.js +59 -0
  32. package/lib/types/events.js +16 -0
  33. package/lib/types/evidence.js +65 -0
  34. package/lib/types/evolution-events.js +479 -0
  35. package/lib/types/fold-untrusted.js +44 -0
  36. package/lib/types/frontmatter.js +623 -0
  37. package/lib/types/fuzzy-match.js +135 -0
  38. package/lib/types/gates.js +51 -0
  39. package/lib/types/guards.js +27 -0
  40. package/lib/types/health-baseline.js +63 -0
  41. package/lib/types/http-routes.d.ts +67 -35
  42. package/lib/types/http-routes.js +135 -0
  43. package/lib/types/index.d.ts +7 -2
  44. package/lib/types/index.js +101 -0
  45. package/lib/types/instance-scope.js +69 -0
  46. package/lib/types/io.js +675 -0
  47. package/lib/types/learn-prompt.js +42 -0
  48. package/lib/types/limits.js +63 -0
  49. package/lib/types/live.js +107 -0
  50. package/lib/types/long-runs.js +96 -0
  51. package/lib/types/memory-store.d.ts +0 -16
  52. package/lib/types/memory-store.js +606 -0
  53. package/lib/types/mutations.js +95 -0
  54. package/lib/types/namespace-migration.js +156 -0
  55. package/lib/types/numeric.js +47 -0
  56. package/lib/types/opt-in.js +101 -0
  57. package/lib/types/package-identity.js +47 -0
  58. package/lib/types/params.js +328 -0
  59. package/lib/types/payload.js +36 -0
  60. package/lib/types/platform-line.generated.d.ts +10 -0
  61. package/lib/types/platform-line.generated.js +12 -0
  62. package/lib/types/platform-services.d.ts +2 -2
  63. package/lib/types/platform-services.js +52 -0
  64. package/lib/types/probe.js +122 -0
  65. package/lib/types/prompts.js +409 -0
  66. package/lib/types/quality.js +308 -0
  67. package/lib/types/reachability.js +122 -0
  68. package/lib/types/redact.js +257 -0
  69. package/lib/types/reference-rewrite.js +144 -0
  70. package/lib/types/reports.js +110 -0
  71. package/lib/types/restrictor.d.ts +11 -0
  72. package/lib/types/restrictor.js +48 -0
  73. package/lib/types/review-channel.js +59 -0
  74. package/lib/types/run-registry.js +345 -0
  75. package/lib/types/scope.js +7 -0
  76. package/lib/types/serial.js +17 -0
  77. package/lib/types/session-fold.d.ts +15 -0
  78. package/lib/types/session-fold.js +28 -0
  79. package/lib/types/session-projection.js +295 -0
  80. package/lib/types/signals.js +209 -0
  81. package/lib/types/skill-discovery.js +66 -0
  82. package/lib/types/skill-health.js +134 -0
  83. package/lib/types/skill-history.js +745 -0
  84. package/lib/types/skill-lint.js +112 -0
  85. package/lib/types/skill-reads.js +67 -0
  86. package/lib/types/skill-row-cells.js +123 -0
  87. package/lib/types/skill-store.js +3528 -0
  88. package/lib/types/state-store.js +60 -0
  89. package/lib/types/store-ledger.js +117 -0
  90. package/lib/types/subagent/channel.d.ts +4 -0
  91. package/lib/types/subagent/channel.js +131 -0
  92. package/lib/types/subagent/index.d.ts +4 -0
  93. package/lib/types/subagent/index.js +5 -0
  94. package/lib/types/subagent/readiness.d.ts +6 -0
  95. package/lib/types/subagent/readiness.js +35 -0
  96. package/lib/types/subagent/types.d.ts +58 -0
  97. package/lib/types/subagent/types.js +2 -0
  98. package/lib/types/threats.js +338 -0
  99. package/lib/types/tool-dispatch.d.ts +5 -0
  100. package/lib/types/tool-dispatch.js +535 -0
  101. package/lib/types/tree-write.js +40 -0
  102. package/lib/types/unit-members.js +121 -0
  103. package/lib/types/usage.js +474 -0
  104. package/lib/types/write-authority.js +23 -0
  105. package/lib/types/write-gates.js +344 -0
  106. package/lib/types/write-inventory.js +158 -0
  107. package/package.json +39 -11
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Credential-shaped text redaction for cross-boundary model inputs (011 §8).
3
+ *
4
+ * The maintenance facts block and probe output — like the review snapshot
5
+ * before them — leave the owning session's context, so secret-shaped text is
6
+ * masked before it is sent. Best-effort and conservative: targets well-known
7
+ * secret shapes and inline assignment patterns, never wholesale content.
8
+ * Migrated from `evolution-review` so both channels share one implementation.
9
+ */
10
+ const SECRET_PATTERNS = [
11
+ // V8-12 (0.3.47): `\b` left boundary — `sk-` alone matched inside
12
+ // `task-…`/`risk-…` words. V9-10 (0.3.51): the "other patterns carry their
13
+ // own anchors" claim was overstated — AKIA/ghp_/glpat-/xox do carry a
14
+ // fixed prefix, but the JWT (`eyJ…`) and bearer rows have no left boundary
15
+ // at all (a `…xeyJ…` substring would redact mid-word). Deliberate: JWT
16
+ // body shapes never legitimately appear inside another word, and the
17
+ // bearer row's initial-cap `Bearer` form makes mid-word hits effectively
18
+ // nonexistent. Revisit only if a real false positive shows up.
19
+ ['openai-style key', /\bsk-[A-Za-z0-9_-]{16,}/g],
20
+ ['aws access key', /AKIA[0-9A-Z]{16}/g],
21
+ ['github token', /gh[pousr]_[A-Za-z0-9]{20,}/g],
22
+ ['gitlab token', /glpat-[A-Za-z0-9_-]{16,}/g],
23
+ ['slack token', /xox[baprs]-[A-Za-z0-9-]{10,}/g],
24
+ ['jwt', /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
25
+ // v22 (SEC-4): provider token shapes the original set missed (each verified
26
+ // to pass redactSecrets verbatim before this change).
27
+ ['npm token', /npm_[A-Za-z0-9]{20,}/g],
28
+ ['stripe key', /[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g],
29
+ ['github fine-grained token', /github_pat_[A-Za-z0-9_]{20,}/g],
30
+ ['google api key', /AIza[0-9A-Za-z_-]{30,}/g],
31
+ // F-335 (0.3.23): case-insensitive (`bearer`), and `\s+` so a tab or run of
32
+ // spaces between `Bearer` and the token is still masked (`Bearer\t...`).
33
+ ['bearer credential', /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi],
34
+ // R3 (round-2 audit): HTTP Basic — `Authorization: Basic <base64>` crossed
35
+ // every model boundary: `Basic` was in no value-shape row and
36
+ // `authorization` in no keyword set, so the base64 user:password blob
37
+ // shipped verbatim while the README claimed credential masking.
38
+ ['basic credential', /\bBasic[\s]+[a-z0-9._~+/=]{16,}/gi],
39
+ ];
40
+ // F-335 (0.3.23): the older `\b(?:token|...)\b` missed connected keys such as
41
+ // `auth_token=`, `client_secret=`, `access_token=`. The v18 audit (A2-5)
42
+ // found the replacement's value class too narrow: a value containing
43
+ // `@ ! # $ %` or spaces did not match at all and leaked verbatim. The pattern
44
+ // now matches the key + separator, then ANY quoted or unquoted value up to
45
+ // the line end, and masks the value wholesale.
46
+ //
47
+ // The unquoted fallback must NOT exclude the quote characters: a value whose
48
+ // opening quote has no closing quote on this line (a truncated log line, a
49
+ // multi-line JSON value, an escaped quote inside the value) otherwise matched
50
+ // no branch at all, and the regex fell back to "value = one space" — masking
51
+ // the separator and leaking the secret verbatim. The quoted branches still win
52
+ // for well-formed values because they are tried first.
53
+ //
54
+ // A2-6 (v18): the old `(?:\b|[\w-]+[_\-])` prefix was O(n²) on a long word
55
+ // with no separator (100k chars measured ~23-26s, synchronously). P2-2 (v37):
56
+ // the `{0,64}` bound added then is GONE — a 65-char key prefix crossed
57
+ // verbatim. Linearity now rests on the mandatory left anchor `(^|[^\w-])`:
58
+ // only a key run behind `^`/a non-word char is attempted, and once per run.
59
+ // P2-7/P2-8 (v19): the value is the WHOLE rest of the line — one branch, no
60
+ // quoted/unquoted alternation. Two defects forced this: the separator's `[\s]*`
61
+ // matched a newline, so `api_key:` with no same-line value swallowed the NEXT
62
+ // line; and the quoted branch short-circuited at the first inner quote, leaving
63
+ // `token="a"b"c" tail` as `token=<redacted>b"c" tail`. Over-masking a line is
64
+ // acceptable for a redactor; leaking or eating an unrelated line is not.
65
+ // Capture groups: 1 = leading boundary (kept), 2 = connected prefix (kept),
66
+ // 3 = key (kept), 4 = separator (kept), 5 = value (masked).
67
+ // V27 G0.5 (CB-1): group 4 tolerates a CLOSING QUOTE after the key. In JSON —
68
+ // the most common carrier of credentials in tool results and session text —
69
+ // the key is quoted (`"password": "hunter2"`), and the quote between the key
70
+ // and the `:` made the separator miss entirely, so the whole pattern failed and
71
+ // the value crossed the boundary verbatim. Value-shape patterns only masked
72
+ // well-known token prefixes; an arbitrary passphrase like `hunter2` had no
73
+ // layer left to catch it.
74
+ const INLINE_ASSIGNMENT_PATTERN = new RegExp('(^|[^\\w-])([\\w-]*[_\\-])?((?:token|api[_-]?key|secret|password|passwd|authorization)' +
75
+ '(?:[_\\-][\\w-]*)?)\\b(["\\\']?[\\t ]*[:=][\\t ]*)' +
76
+ '([^\\r\\n]+)', 'gi');
77
+ // v22 (SEC-4): a scheme://user:password@host URL — only the password segment
78
+ // is masked; the group-preserving replace keeps the scheme/user readable so a
79
+ // connection string stays diagnosable.
80
+ // P2-28 (v39): the scheme run is capped at 64 characters — unbounded, a
81
+ // scheme-free 60k blob cost 2.1-4.4s per call (the engine re-walks the run at
82
+ // every offset, synchronously, on the outbound redaction path); 25ms now.
83
+ const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,63}:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
84
+ // v28 G5.1 (REDACT-02): a PEM private key block is masked WHOLE (header to
85
+ // footer). threats.ts blocks the same shape on the WRITE path
86
+ // (`private_key_block`, SEC-3) — but the review/maintenance OUTBOUND channels
87
+ // pass through redact only, never the threat scan, so a session that pasted a
88
+ // key used to ship it verbatim to the second model context. The header
89
+ // alternation mirrors threats.ts's pattern (keep the two in sync).
90
+ // The `\\s` doubles are required: these are STRING literals feeding
91
+ // `new RegExp`, so each backslash must survive JS string-escape processing.
92
+ const PEM_HEAD = '-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----';
93
+ const PEM_TAIL = '-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----';
94
+ const PEM_PRIVATE_KEY_PATTERN = new RegExp(`${PEM_HEAD}[\\s\\S]*?${PEM_TAIL}`, 'g');
95
+ // v28 G5.1 (REDACT-01): the block-style form the inline pattern cannot reach —
96
+ // its separator `[\t ]*[:=]` and value class are same-line-only, so YAML /
97
+ // docker-compose / kubectl-secret style
98
+ // api_key:
99
+ // wJalrXUtnFEMI…
100
+ // crossed verbatim. This predicate matches a line that is ONLY a credential
101
+ // key plus its mapping separator; the following INDENTED line is its value.
102
+ // v31 REDACT-03: the key carries the same connected-prefix group as the
103
+ // inline pattern (`AWS_SECRET_ACCESS_KEY`, `CLIENT_SECRET`, `DB_PASSWORD` —
104
+ // the inline pattern's connected-prefix form, unbounded since P2-2 (v37)).
105
+ // Without it, every prefixed block key
106
+ // missed the pass AND the AWS residual sweep (the underscore blocks every
107
+ // `\b`), so the secret shipped verbatim (executed-verified leak).
108
+ // v31 REDACT-03: the key carries the same connected-prefix group as the
109
+ // inline pattern (`AWS_SECRET_ACCESS_KEY`, `CLIENT_SECRET`, `DB_PASSWORD` —
110
+ // the inline pattern's connected-prefix form, unbounded since P2-2 (v37)).
111
+ // Without it, every prefixed block key
112
+ // missed the pass AND the AWS residual sweep (the underscore blocks every
113
+ // `\b`), so the secret shipped verbatim (executed-verified leak).
114
+ // OPT-03 (2026-09): the trailing `\s*$` already absorbed a `\r` on the KEY
115
+ // line, but the VALUE-line pattern below still anchored `[ \t]*$` — on CRLF
116
+ // text the key line matched, the value line silently `continue`d, and the
117
+ // secret crossed verbatim. Both anchors now tolerate an optional `\r` so the
118
+ // line-paired pass behaves identically on LF and CRLF input (Windows-authored
119
+ // .env / compose / kubectl YAML is the realistic carrier).
120
+ const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]*)?)\s*:(?:\r)?$/i;
121
+ // S0.3 (v37 P0-2): the two patterns above cannot express a camelCase hump —
122
+ // they are case-INSENSITIVE, so [A-Z] would match lowercase too. This pair
123
+ // decides by a case-insensitive KEYWORD test that rejects a lowercase
124
+ // continuation: clientSecret= / accessToken= / SecretAccessKey= are masked,
125
+ // while tokenizer= / secretary: / passwordless: stay untouched.
126
+ const CREDENTIAL_KEY_RE = /(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]asswd|[Aa]pi[_-]?[Kk]ey)(?![a-z])/;
127
+ // P2-2 (v37): the candidate key carried the same defect class in `{1,80}` form —
128
+ // a longer camelCase key (this pass is the ONLY layer that can reach one) was
129
+ // skipped wholesale. The cap is gone; the scan stays linear (see A2-6 above).
130
+ // Review B-P1 (2026-09-16): the former fourth group was a CONSUMING
131
+ // `([^\r\n]+)` value. Every match then cost O(distance to end of line), so one
132
+ // long single-line input (minified JSON / a dense assignment chain) went
133
+ // quadratic in the line length — 2.1s for 160k chars and 7.0s for 320k against
134
+ // ~0.1ms for the replace form this loop replaced. The zero-width lookahead
135
+ // keeps the old "a value must exist" requirement (an empty `token=` still does
136
+ // not match) and the loop locates the value end by hand, which stays linear.
137
+ const CANDIDATE_ASSIGNMENT_PATTERN = /(^|[^\w-])([\w-]+)(["']?[\t ]*[:=][\t ]*)(?=[^\r\n])/g;
138
+ /**
139
+ * Mask credential-shaped text before it crosses a session boundary.
140
+ * @param text - the text about to be sent to a model outside this session.
141
+ * @returns the text with matched secrets replaced by `<redacted>`.
142
+ */
143
+ export function redactSecrets(text) {
144
+ // 0.3.16 (E-1): the generic replacer once keyed on `p1 === undefined` to
145
+ // distinguish "no capture group" — but for a capture-group-free regex the
146
+ // second callback argument is the match OFFSET (a number), so the output
147
+ // carried the offset (e.g. 'use 4<redacted> tomorrow'). The plain patterns
148
+ // now take a literal replacement; only the patterns with a real capture
149
+ // group (the inline-assignment label prefix, the URL scheme/user) keep
150
+ // their parts.
151
+ let out = text;
152
+ // v28 G5.1: PEM blocks first — the value-shape passes below only mask the
153
+ // well-known token prefixes, and a key body carries no marker to anchor on.
154
+ out = out.replace(PEM_PRIVATE_KEY_PATTERN, '<redacted-private-key>');
155
+ for (const [, pattern] of SECRET_PATTERNS) {
156
+ out = out.replace(pattern, '<redacted>');
157
+ }
158
+ out = out.replace(URL_CREDENTIALS_PATTERN, (_match, lead) => `${lead ?? ''}<redacted>@`);
159
+ out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ''}${prefix ?? ''}${key ?? ''}${separator ?? ''}<redacted>`);
160
+ // S0.3: camelCase keys ride this second, case-aware pass (see CREDENTIAL_KEY_RE).
161
+ // PLAN S1.2 (2026-09-16): exec loop instead of String.replace. The replace
162
+ // callback returned a rejected (non-credential-key) match untouched, but the
163
+ // greedy value group had already consumed the rest of the line, so every
164
+ // LATER key on that line was never scanned and leaked
165
+ // (`{"name": "x", "clientSecret": "supersecret123"}` — the candidate pass is
166
+ // the ONLY layer that reaches a camelCase key, see the comment on the
167
+ // pattern above). A rejected match now advances only past lead+key+separator
168
+ // so the line remainder — including later credential keys — stays scannable.
169
+ // Termination: key is >= 1 char and the separator always carries `[:=]`, so
170
+ // lastIndex strictly grows every round. Trade-off: the resume point sits
171
+ // inside the rejected value, so a value-shaped `k=v` tail can be re-matched
172
+ // too — same over-mask-don't-leak policy as P2-7 (a rejected value's
173
+ // non-credential `a:b` shapes, e.g. `http://…`, still pass: their "key"
174
+ // fails the same predicate). A fresh `g` instance per call keeps the shared
175
+ // const's lastIndex state out of the loop.
176
+ const candidatePattern = new RegExp(CANDIDATE_ASSIGNMENT_PATTERN.source, 'g');
177
+ let candidateOut = '';
178
+ let consumed = 0;
179
+ for (let m = candidatePattern.exec(out); m !== null; m = candidatePattern.exec(out)) {
180
+ const lead = m[1] ?? '';
181
+ const key = m[2] ?? '';
182
+ const separator = m[3] ?? '';
183
+ if (!CREDENTIAL_KEY_RE.test(key))
184
+ continue;
185
+ // Same output shape as the previous replace callback: the value (the rest
186
+ // of the line) is masked wholesale. Its END is located by hand instead of
187
+ // by a consuming group, so neither the accepted nor the rejected path ever
188
+ // re-scans the line remainder (review B-P1, see the pattern note). A
189
+ // rejection resumes exactly where it used to — just past the separator,
190
+ // inside the value — which keeps the documented over-mask-don't-leak
191
+ // trade-off unchanged.
192
+ let valueEnd = m.index + m[0].length;
193
+ while (valueEnd < out.length && out[valueEnd] !== '\n' && out[valueEnd] !== '\r')
194
+ valueEnd += 1;
195
+ candidateOut += out.slice(consumed, m.index) + lead + key + separator + '<redacted>';
196
+ consumed = valueEnd;
197
+ candidatePattern.lastIndex = valueEnd;
198
+ }
199
+ out = candidateOut + out.slice(consumed);
200
+ // Line-paired passes on the split lines. Order matters: the block-style pass
201
+ // runs first so the `<redacted>` it plants still enables the AWS residual
202
+ // pass below (a 40-char secret on the block value line is already gone, but
203
+ // a same-line AWS pair id+secret benefits from both).
204
+ const lines = out.split('\n');
205
+ for (let i = 0; i < lines.length - 1; i++) {
206
+ const line = lines[i];
207
+ if (line === undefined)
208
+ continue;
209
+ // S0.3: a bare camelCase key line (`clientSecret:` / `SecretAccessKey:`) pairs
210
+ // with its indented value through the same case-aware predicate as the inline pass.
211
+ const camelKey = /^([\w-]+)\s*:(?:\r)?$/.exec(line)?.[1];
212
+ if (!(BLOCK_KEY_ONLY_LINE.test(line) || (camelKey !== undefined && CREDENTIAL_KEY_RE.test(camelKey))))
213
+ continue;
214
+ // PLAN S2.3 (2026-09-16): the value is the first NON-EMPTY line after the
215
+ // bare key. The pass used to look at `lines[i + 1]` ONLY, so one blank
216
+ // line between the key and its value (a wrapped YAML entry, a paste with
217
+ // a stray newline — `api_key:\n\n wJalr…`) abandoned the pairing and the
218
+ // secret crossed verbatim. Blank lines are skipped; the first non-empty
219
+ // line decides: indented → it is the value (mask, with the existing
220
+ // already-`<redacted>` skip and the CRLF-tolerant anchors); dedented → it
221
+ // is the NEXT frontmatter key, not a value (stop, never mask it).
222
+ let valueLine = -1;
223
+ for (let j = i + 1; j < lines.length; j++) {
224
+ const candidate = lines[j] ?? '';
225
+ if (candidate.trim() === '')
226
+ continue;
227
+ valueLine = j;
228
+ break;
229
+ }
230
+ if (valueLine < 0)
231
+ continue;
232
+ const next = lines[valueLine] ?? '';
233
+ // Over-masking an indented line under a credential key is acceptable for a
234
+ // redactor (same policy as P2-7); an unindented line is NOT the value.
235
+ // OPT-03: `(?:\r)?$` — see BLOCK_KEY_ONLY_LINE above; without it a CRLF
236
+ // value line never matched and the secret crossed unredacted.
237
+ const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)(?:\r)?$/.exec(next) ?? [];
238
+ if (indent === undefined || value === undefined)
239
+ continue;
240
+ if (value.includes('<redacted>'))
241
+ continue;
242
+ lines[valueLine] = `${indent}<redacted>${tail ?? ''}`;
243
+ }
244
+ out = lines.join('\n');
245
+ // v22 (SEC-4): an AWS SECRET access key is a bare 40-char base64ish run —
246
+ // far too common in legitimate text to mask unconditionally. It is masked
247
+ // only on a line that already shows a redaction or an aws/secret keyword
248
+ // (the paired access key id was just masked there), closing the
249
+ // id-redacted-but-secret-residual reassembly gap.
250
+ out = out.split('\n').map((line) => {
251
+ if (!/<redacted>/.test(line) && !/\baws\b|\bAKIA\b|\bsecret\b/i.test(line))
252
+ return line;
253
+ return line.replace(/(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, '<redacted>');
254
+ }).join('\n');
255
+ return out;
256
+ }
257
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Reference re-homing plans (0.5.0 V1, design §16.7).
3
+ *
4
+ * A consolidation moves one skill's body under ANOTHER skill root, where every
5
+ * support-dir reference resolves against that other root (design §2.1). This
6
+ * module plans what has to happen to the support files and to the body's
7
+ * references for the move to leave no dangling link. Pure: the caller owns IO,
8
+ * decides the destination names, and decides whether the plan is applied (V2)
9
+ * or only reported (V1). Cross-skill rewriting is out of scope by design (§14) —
10
+ * a plan covers ONE body.
11
+ */
12
+ import { resolveCitations } from "./citations.js";
13
+ /**
14
+ * Provisional re-homing rule. The naming style for re-homed files is still an
15
+ * open decision (design §16.3 B5); this rule only fires on a COLLISION, where
16
+ * something has to give: a path that the target already occupies gets the source
17
+ * name as a prefix (`references/a.md` -> `references/<source>-a.md`). Nested
18
+ * destinations are deliberately avoided: the support listing is one level deep,
19
+ * so a file moved into a subdirectory would drop out of the listing entirely.
20
+ * @param sourceFiles - the moving skill's support files.
21
+ * @param targetFiles - the destination skill's support files.
22
+ * @param sourceName - the moving skill's name, used as the collision prefix.
23
+ * @returns the moves plus the paths that collided (for the report).
24
+ */
25
+ export function planRehoming(sourceFiles, targetFiles, sourceName) {
26
+ const taken = new Set(targetFiles);
27
+ const collisions = [];
28
+ const moves = [];
29
+ for (const path of sourceFiles) {
30
+ if (!taken.has(path)) {
31
+ moves.push({ from: path, to: path });
32
+ continue;
33
+ }
34
+ collisions.push(path);
35
+ const cut = path.lastIndexOf('/');
36
+ const dir = cut < 0 ? '' : path.slice(0, cut + 1);
37
+ const name = path.slice(cut + 1);
38
+ let candidate = `${dir}${sourceName}-${name}`;
39
+ let counter = 2;
40
+ while (taken.has(candidate)) {
41
+ candidate = `${dir}${sourceName}-${counter}-${name}`;
42
+ counter += 1;
43
+ }
44
+ taken.add(candidate);
45
+ moves.push({ from: path, to: candidate });
46
+ }
47
+ return { moves, collisions };
48
+ }
49
+ /**
50
+ * Plan the reference rewrites for one body that is moving into another root.
51
+ * @param input - the body, the MOVING skill's file list, the re-homing moves and
52
+ * the destination's file list (with the moved paths already included).
53
+ * @returns the edits, the cited files without a destination, and any target that
54
+ * the destination file list cannot prove (applying such a plan would dangle).
55
+ */
56
+ export function planReferenceRewrite(input) {
57
+ const report = resolveCitations({ content: input.content, file: 'SKILL.md', files: input.files });
58
+ const byFrom = new Map(input.moves.map(move => [move.from, move.to]));
59
+ const available = new Set(input.targetFiles);
60
+ const edits = [];
61
+ const unresolved = [];
62
+ const residual = new Set();
63
+ const used = new Set();
64
+ for (const ref of report.refs) {
65
+ if (ref.kind !== 'citation' || ref.target === null)
66
+ continue;
67
+ const move = byFrom.get(ref.target);
68
+ const to = move ?? ref.target;
69
+ if (move !== undefined) {
70
+ used.add(ref.target);
71
+ if (move !== ref.target)
72
+ edits.push({ line: ref.line, from: ref.target, to: move, raw: ref.raw });
73
+ }
74
+ else if (!report.dangling.some(entry => entry.target === ref.target)) {
75
+ // Already resolvable in the MOVING skill: it only stays valid if the
76
+ // destination happens to carry the same path, which the caller proves by
77
+ // listing it under `targetFiles`.
78
+ unresolved.push(ref.target);
79
+ }
80
+ if (!available.has(to))
81
+ residual.add(to);
82
+ }
83
+ return {
84
+ edits,
85
+ unresolved: [...new Set(unresolved)],
86
+ residualDangling: [...residual],
87
+ moves: input.moves.filter(move => used.has(move.from)),
88
+ };
89
+ }
90
+ /**
91
+ * Apply a plan's edits to the body (V2). Line-wise and literal: line N gets its
92
+ * FIRST occurrence of the recorded token replaced, everything else is copied
93
+ * byte for byte — no regex, so a token that happens to contain regex
94
+ * metacharacters cannot rewrite more than it matched.
95
+ * @param content - the body the plan was computed against.
96
+ * @param edits - the plan's edits.
97
+ * @returns the rewritten body (identical to the input when there is nothing to do).
98
+ */
99
+ export function applyReferenceRewrite(content, edits) {
100
+ if (edits.length === 0)
101
+ return content;
102
+ const byLine = new Map();
103
+ for (const edit of edits) {
104
+ const list = byLine.get(edit.line);
105
+ if (list === undefined)
106
+ byLine.set(edit.line, [edit]);
107
+ else
108
+ list.push(edit);
109
+ }
110
+ const lines = content.split('\n');
111
+ for (const [lineNo, lineEdits] of byLine) {
112
+ const index = lineNo - 1;
113
+ const original = lines[index];
114
+ if (original === undefined)
115
+ continue;
116
+ let next = original;
117
+ for (const edit of lineEdits) {
118
+ const at = next.indexOf(edit.raw);
119
+ if (at < 0)
120
+ continue;
121
+ next = next.slice(0, at) + edit.to + next.slice(at + edit.raw.length);
122
+ }
123
+ lines[index] = next;
124
+ }
125
+ return lines.join('\n');
126
+ }
127
+ /**
128
+ * One-line, model-facing summary of a plan, for the consolidation refusal and
129
+ * for the V2 apply path's audit text.
130
+ * @param plan - the plan to describe.
131
+ * @returns a compact sentence naming counts, the moves and any residual risk.
132
+ */
133
+ export function describeReferenceRewrite(plan) {
134
+ const renames = plan.moves.filter(move => move.from !== move.to);
135
+ const parts = [
136
+ `re-home ${plan.moves.length} file(s)`,
137
+ renames.length === 0 ? '' : `renamed: ${renames.map(move => `${move.from}->${move.to}`).join(', ')}`,
138
+ `rewrite ${plan.edits.length} reference(s)`,
139
+ `residual dangling ${plan.residualDangling.length}`,
140
+ plan.unresolved.length === 0 ? '' : `no destination: ${plan.unresolved.slice(0, 5).join(', ')}`,
141
+ ].filter(part => part.length > 0);
142
+ return `plan: ${parts.join('; ')}`;
143
+ }
144
+ //# sourceMappingURL=reference-rewrite.js.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 0.19.0 (S2): the family's ONE report-retention sweep.
3
+ *
4
+ * Reports accumulate by design (every run writes one), so each kind needs a
5
+ * window or the reports home becomes an unbounded directory. The logic lived
6
+ * inside the curator only; the maintenance run reports added in 0.19.0 needed
7
+ * the same thing, and a second copy of "list, order, delete beyond the window"
8
+ * is exactly the kind of near-duplicate this family treats as a defect.
9
+ *
10
+ * Three behaviours are the curator's, kept verbatim because they are the reason
11
+ * the sweep is safe to run at all:
12
+ *
13
+ * - An UNLISTABLE directory is not "no reports" — nothing is deleted and the
14
+ * caller is told (the same three-state rule the probe files use).
15
+ * - A report with no usable timestamp is KEPT (never delete what cannot be
16
+ * ordered) and the fact is said out loud once.
17
+ * - The order is TOTAL: declared time, then mtime, then name. A tie falling
18
+ * through to readdir order evicted a different set per platform (CI-caught).
19
+ * @module
20
+ */
21
+ import { join } from 'node:path';
22
+ import { isPresent, isUnknown, probeList, probeMtime, reportTime } from "./probe.js";
23
+ // `evolutionHome` is owned by state-store.ts (its ONLY definition) — importing it
24
+ // from env.ts yields an undefined binding that only throws at call time.
25
+ import { evolutionHome } from "./state-store.js";
26
+ /**
27
+ * The lock target a reports sweep holds for one home.
28
+ *
29
+ * `transactIo` takes the IO write lock on THIS path (minting `<path>.lock` with
30
+ * the io protocol's `pid:token` body) and holds it for the whole task; nothing
31
+ * ever writes the target, because the sweep's exclusion is the point. It lives
32
+ * inside `<home>/reports` next to the directory it protects (the sweeps' own name
33
+ * filters ignore it, and the lock file is removed on release).
34
+ *
35
+ * Moved here from evolution-curator in 0.19.0 (S2): the maintenance reports'
36
+ * sweep needs the SAME exclusion, and a second lock would protect nothing.
37
+ * @returns the lock target path.
38
+ */
39
+ export function reportsSweepLockTarget() {
40
+ return join(evolutionHome(), 'reports', '.retention');
41
+ }
42
+ /**
43
+ * Delete the reports beyond each bucket's window, each with its `.md` digest.
44
+ *
45
+ * Best-effort by construction: pruning never fails the run that wrote a report.
46
+ * @param options - io seam, directory, windows, warn channel and owner name.
47
+ * @returns nothing; deletions and skips are reported through `warn`.
48
+ */
49
+ export async function sweepReports(options) {
50
+ const warn = options.warn ?? (() => { });
51
+ const listed = await probeList(options.io, options.dir);
52
+ if (isUnknown(listed)) {
53
+ warn(`${options.owner}: report retention skipped — the reports directory could not be listed (${listed.reason})`);
54
+ return;
55
+ }
56
+ const entries = isPresent(listed) ? listed.value : [];
57
+ // Longest prefix first: `curator-error-` must win over `curator-`.
58
+ const buckets = [...options.buckets].sort((a, b) => b.prefix.length - a.prefix.length);
59
+ const dated = new Map(buckets.map(bucket => [bucket.prefix, []]));
60
+ let unorderableWarned = false;
61
+ for (const name of entries) {
62
+ if (!name.endsWith('.json'))
63
+ continue;
64
+ const bucket = buckets.find(item => name.startsWith(item.prefix));
65
+ if (!bucket)
66
+ continue;
67
+ try {
68
+ const path = join(options.dir, name);
69
+ const raw = await options.io.readText(path);
70
+ if (raw === null)
71
+ continue;
72
+ // The ordering time comes from ONE place (`reportTime`): the declared
73
+ // `startedAt`, then the declared `at`, then the optional mtime probe.
74
+ const startedAt = await reportTime(options.io, path, JSON.parse(raw));
75
+ const probed = await probeMtime(options.io, path);
76
+ const mtime = isPresent(probed) ? probed.value : null;
77
+ if (startedAt !== null)
78
+ dated.get(bucket.prefix)?.push({ name, startedAt, mtime });
79
+ else if (!unorderableWarned) {
80
+ unorderableWarned = true;
81
+ warn(`${options.owner}: report "${name}" carries no usable timestamp and this backend has no mtime probe - it is kept outside the retention window`);
82
+ }
83
+ }
84
+ catch {
85
+ // Unclassifiable report: keep it — never delete what we cannot order.
86
+ }
87
+ }
88
+ // TOTAL order (v46 review, CI-caught): declared time, then mtime, then name.
89
+ const newestFirst = (a, b) => b.startedAt - a.startedAt || (b.mtime ?? 0) - (a.mtime ?? 0) || b.name.localeCompare(a.name);
90
+ for (const bucket of buckets) {
91
+ const rows = dated.get(bucket.prefix) ?? [];
92
+ rows.sort(newestFirst);
93
+ for (const old of rows.slice(bucket.keep)) {
94
+ const stem = old.name.replace(/\.json$/, '');
95
+ try {
96
+ await options.io.remove(join(options.dir, old.name));
97
+ }
98
+ catch {
99
+ // Best-effort pruning.
100
+ }
101
+ try {
102
+ await options.io.remove(join(options.dir, `${stem}.md`));
103
+ }
104
+ catch {
105
+ // The digest may already be gone (or never existed); keep going.
106
+ }
107
+ }
108
+ }
109
+ }
110
+ //# sourceMappingURL=reports.js.map
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "evolution-restrictor";
3
+ export declare const inject: readonly ["agentPresets"];
4
+ export interface Config {
5
+ readonly allow?: readonly string[];
6
+ readonly deny?: readonly string[];
7
+ /** Restricted preset id this row belongs to; only agents composed with it are restricted. */
8
+ readonly presetId?: string;
9
+ }
10
+ export declare function apply(ctx: Context, config?: Config): void;
11
+ //# sourceMappingURL=restrictor.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { errorText } from "./errors.js";
2
+ export const name = 'evolution-restrictor';
3
+ export const inject = ['agentPresets'];
4
+ function normalize(names) {
5
+ return [...new Set((names ?? []).map(value => value.trim()).filter(value => value !== ''))].sort();
6
+ }
7
+ function failure(detail) {
8
+ return new Error(errorText('e-324-restricted-preset-unavailable', { a1: 'evolution restrictor ' + detail }));
9
+ }
10
+ export function apply(ctx, config = {}) {
11
+ const allow = normalize(config.allow);
12
+ const deny = normalize(config.deny);
13
+ if (allow.length === 0 && deny.length === 0)
14
+ throw failure('needs a non-empty allow or deny set');
15
+ const filter = {
16
+ ...(allow.length > 0 ? { allow } : {}),
17
+ ...(deny.length > 0 ? { deny } : {}),
18
+ };
19
+ const presetId = typeof config.presetId === 'string' && config.presetId.trim() !== '' ? config.presetId : undefined;
20
+ if (presetId === undefined)
21
+ throw failure('requires the restricted presetId in its row config');
22
+ const on = ctx.on;
23
+ if (typeof on !== 'function')
24
+ throw failure('requires a context that owns on()');
25
+ const presets = ctx.agentPresets;
26
+ if (presets === undefined || typeof presets.composedPreset !== 'function') {
27
+ throw failure('requires the agentPresets service (inject: agentPresets)');
28
+ }
29
+ // The listener lives in the preset MOUNT scope: scope routing delivers every descendant
30
+ // child's events here (a listener owned by an enclosing scope sees descendant events), and
31
+ // `composedPreset` narrows it to the agents actually composed with this restricted preset.
32
+ on.call(ctx, 'agent/created', (payload) => {
33
+ const agent = payload.agent;
34
+ if (agent === undefined)
35
+ return;
36
+ if (presets.composedPreset(agent.ctx) !== presetId)
37
+ return;
38
+ const tools = agent.ctx.tools;
39
+ if (tools === undefined || typeof tools.restrict !== 'function') {
40
+ throw failure('child scope exposes no tools.restrict()');
41
+ }
42
+ const effect = agent.ctx.effect;
43
+ if (effect === undefined)
44
+ throw failure('child scope owns no effect() to release the restriction');
45
+ effect.call(agent.ctx, () => tools.restrict(filter), 'evolution-restrictor:child');
46
+ });
47
+ }
48
+ //# sourceMappingURL=restrictor.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Family-internal mark for the review channel's INJECT delivery (v37 S2.2).
3
+ *
4
+ * With the default `reviewMode: 'inject'` the review prompt runs in the PARENT
5
+ * session, so the parent model's own `skill_manage` calls carry no origin the
6
+ * platform could attribute: `.pinned` and the `.hermes-managed` authorship mark
7
+ * were skipped for exactly the autonomous writes they exist for. The delivery
8
+ * marks the session here and the write path reads the mark.
9
+ *
10
+ * Window: after the review prompt, until the next REAL user message (a
11
+ * `user/message` with `source.kind === 'user'` clears it; plugin-sourced notices
12
+ * never do). Process-wide and keyed by session on purpose — the same
13
+ * "restart is a fresh conversation boundary" discipline as the review plugin's
14
+ * own per-session counters. This module is the ONE owner of the marker.
15
+ * @module @lmzhen/dsh-evolution-core/review-channel
16
+ */
17
+ /** Session ids currently executing a review prompt, oldest delivery first. */
18
+ const markedSessions = new Set();
19
+ /**
20
+ * Mark one session as running the review channel's prompt; idempotent, and
21
+ * keyed by session so a mark can never leak into another session.
22
+ * @param sessionId - the session the review prompt was delivered to.
23
+ */
24
+ export function markReviewChannel(sessionId) {
25
+ markedSessions.delete(sessionId);
26
+ markedSessions.add(sessionId);
27
+ }
28
+ /**
29
+ * Clear the mark: the session's next prompt is human input again.
30
+ * @param sessionId - the session whose mark is dropped.
31
+ */
32
+ export function clearReviewChannel(sessionId) {
33
+ markedSessions.delete(sessionId);
34
+ }
35
+ /**
36
+ * Whether this session's current prompt came from the review channel. An
37
+ * execution without a session is never the review channel.
38
+ * @param sessionId - the executing session id, when the caller has one.
39
+ * @returns true only for a marked session.
40
+ */
41
+ export function isReviewChannelSession(sessionId) {
42
+ return sessionId !== undefined && markedSessions.has(sessionId);
43
+ }
44
+ /**
45
+ * Drop marks whose session is gone — a dead session cannot execute a write.
46
+ * @param isAlive - liveness probe for one session id.
47
+ * @returns the number of removed marks.
48
+ */
49
+ export function sweepReviewChannelSessions(isAlive) {
50
+ let removed = 0;
51
+ for (const sessionId of [...markedSessions]) {
52
+ if (isAlive(sessionId))
53
+ continue;
54
+ markedSessions.delete(sessionId);
55
+ removed += 1;
56
+ }
57
+ return removed;
58
+ }
59
+ //# sourceMappingURL=review-channel.js.map