@dzhechkov/harness-core 0.3.107 → 0.3.108
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/dist/claim-check.d.ts +20 -0
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +77 -8
- package/dist/claim-check.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +103 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +220 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/publish.d.ts +28 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +48 -1
- package/dist/publish.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +13 -2
- package/dist/registry.js.map +1 -1
- package/dist/sign.d.ts +158 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +325 -0
- package/dist/sign.js.map +1 -0
- package/dist/skill-schema.d.ts +24 -0
- package/dist/skill-schema.d.ts.map +1 -0
- package/dist/skill-schema.js +42 -0
- package/dist/skill-schema.js.map +1 -0
- package/package.json +3 -3
- package/src/claim-check.ts +92 -8
- package/src/feature-adr-routing.ts +271 -0
- package/src/index.ts +6 -4
- package/src/publish.ts +73 -1
- package/src/registry.ts +9 -2
- package/src/sign.ts +421 -0
- package/src/skill-schema.ts +53 -0
package/dist/sign.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dz sign` / `dz verify` — Ed25519 tamper-evidence for a skill pack.
|
|
3
|
+
*
|
|
4
|
+
* SCOPE, stated once so a green check mark cannot overstate itself (recalled lesson, security,
|
|
5
|
+
* 2026-07-06): *"Verifying a signature against a public key EMBEDDED in the same signed object proves
|
|
6
|
+
* nothing about issuer identity. Trust must bind issuer -> a PINNED out-of-band key. Ed25519 gives
|
|
7
|
+
* provenance/tamper-evidence, never truthfulness."*
|
|
8
|
+
*
|
|
9
|
+
* So: `verifyManifest` takes the public key as a REQUIRED PARAMETER. There is no default, no fallback,
|
|
10
|
+
* and no lookup inside the pack. A `pubkey.pem` sitting in the pack is data, not a key.
|
|
11
|
+
*
|
|
12
|
+
* Zero dependencies: `node:crypto`, `node:fs`, `node:path`.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash, sign as cryptoSign, verify as cryptoVerify, createPrivateKey, createPublicKey } from 'node:crypto';
|
|
15
|
+
import { readFileSync, existsSync, readdirSync, openSync, fstatSync, closeSync, constants as fsConstants } from 'node:fs';
|
|
16
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
17
|
+
export const MANIFEST_NAME = '.dz-manifest.json';
|
|
18
|
+
export const SBOM_NAME = 'sbom.json';
|
|
19
|
+
export const MANIFEST_VERSION = 1;
|
|
20
|
+
/** sha256 of a file's bytes, hex. Follows symlinks — used only when building a manifest we control. */
|
|
21
|
+
export function hashFile(absPath) {
|
|
22
|
+
return createHash('sha256').update(readFileSync(absPath)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Round-2 review (codex exec): `lstat` then `readFile` is a TOCTOU window — the path can be swapped
|
|
26
|
+
* for a symlink between the two calls. Open ONCE with `O_NOFOLLOW`, `fstat` the descriptor, and read
|
|
27
|
+
* from that same descriptor. The bytes hashed are the bytes the stat described.
|
|
28
|
+
*
|
|
29
|
+
* Returns `null` when the path is not a regular file, or is a symlink.
|
|
30
|
+
*/
|
|
31
|
+
export function hashRegularFileNoFollow(absPath) {
|
|
32
|
+
let fd;
|
|
33
|
+
try {
|
|
34
|
+
fd = openSync(absPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
35
|
+
if (!fstatSync(fd).isFile())
|
|
36
|
+
return null;
|
|
37
|
+
return createHash('sha256').update(readFileSync(fd)).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null; // ELOOP on a symlink, ENOENT, EACCES — all fail closed
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (fd !== undefined)
|
|
44
|
+
closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cross-model security review (codex exec, 2026-07-10) — a SIGNED manifest is not a TRUSTED manifest.
|
|
49
|
+
* Its own contents are attacker-controlled until the signature checks out, and even then they must be
|
|
50
|
+
* structurally sane before they touch the filesystem.
|
|
51
|
+
*
|
|
52
|
+
* - `join(root, '../../etc/passwd')` escaped the pack (traversal).
|
|
53
|
+
* - a newline in a path makes the canonical `<hex> <path>\n` encoding ambiguous.
|
|
54
|
+
* - duplicate paths make canonical bytes depend on input order — the sort is not a total order.
|
|
55
|
+
* - a non-string `path` threw inside `hashFile` instead of failing closed.
|
|
56
|
+
*/
|
|
57
|
+
const SAFE_MANIFEST_PATH = /^[A-Za-z0-9._-][A-Za-z0-9._/-]*$/;
|
|
58
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
59
|
+
export function isSafeManifestPath(p) {
|
|
60
|
+
if (typeof p !== 'string' || p.length === 0 || p.length > 4096)
|
|
61
|
+
return false;
|
|
62
|
+
if (!SAFE_MANIFEST_PATH.test(p))
|
|
63
|
+
return false; // no control chars, no backslash, no leading /
|
|
64
|
+
if (p.split('/').some((seg) => seg === '..' || seg === '.' || seg === ''))
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/** Returns an error message, or `null` when the entry list is structurally sound. */
|
|
69
|
+
export function checkManifestEntries(files) {
|
|
70
|
+
if (!Array.isArray(files))
|
|
71
|
+
return 'manifest has no file list';
|
|
72
|
+
if (files.length === 0)
|
|
73
|
+
return 'manifest signs nothing';
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const f of files) {
|
|
76
|
+
if (!f || typeof f !== 'object')
|
|
77
|
+
return 'manifest entry is not an object';
|
|
78
|
+
const { path: pth, sha256 } = f;
|
|
79
|
+
if (!isSafeManifestPath(pth))
|
|
80
|
+
return 'manifest entry has an unsafe path: ' + JSON.stringify(pth);
|
|
81
|
+
if (typeof sha256 !== 'string' || !SHA256_HEX.test(sha256))
|
|
82
|
+
return 'manifest entry has a malformed sha256 for ' + pth;
|
|
83
|
+
// Round-2 review: an exact-string `seen` set misses collisions on case-insensitive filesystems
|
|
84
|
+
// (`Readme` vs `README`). Unicode normalization (`e\u0301.txt` vs `é.txt`) cannot arise: paths are
|
|
85
|
+
// ASCII-only by `SAFE_MANIFEST_PATH`, and a non-ASCII path is refused before it reaches here. An
|
|
86
|
+
// NFC check here would be unreachable code pretending to be a guard.
|
|
87
|
+
const fold = pth.toLowerCase();
|
|
88
|
+
if (seen.has(fold))
|
|
89
|
+
return 'manifest lists ' + pth + ' twice (case/unicode fold) — canonical bytes would depend on order';
|
|
90
|
+
seen.add(fold);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Every file under `root`, POSIX-relative, excluding the manifest and the SBOM themselves. */
|
|
95
|
+
export function listPackFiles(root) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const walk = (dir, rel) => {
|
|
98
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
99
|
+
if (e.name === MANIFEST_NAME || e.name === SBOM_NAME)
|
|
100
|
+
continue;
|
|
101
|
+
const abs = join(dir, e.name);
|
|
102
|
+
const r = rel ? rel + '/' + e.name : e.name;
|
|
103
|
+
if (e.isDirectory())
|
|
104
|
+
walk(abs, r);
|
|
105
|
+
else
|
|
106
|
+
out.push(r);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(root, '');
|
|
110
|
+
return out.sort();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The bytes that get signed (FR-7). Sorted by path, LF endings, no trailing whitespace, and no
|
|
114
|
+
* dependence on JSON key order — a signature must not depend on how a serialiser felt that day.
|
|
115
|
+
* Format is `sha256sum`-compatible: `<hex> <path>\n`.
|
|
116
|
+
*/
|
|
117
|
+
export function canonicalizeManifest(files) {
|
|
118
|
+
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
119
|
+
const body = sorted.map((f) => f.sha256 + ' ' + f.path + '\n').join('');
|
|
120
|
+
return Buffer.from(body, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The bytes actually signed. Cross-model review (2026-07-10) found `pack` and `version` were carried
|
|
124
|
+
* in the manifest but NOT covered by the signature: an attacker could relabel a signed pack. Nothing
|
|
125
|
+
* is signed yet, so the format can change today. It can never change once packs are in the wild.
|
|
126
|
+
*/
|
|
127
|
+
export function canonicalizeSigned(manifest) {
|
|
128
|
+
const header = 'dz-manifest\nversion ' + String(manifest.version) + '\npack ' + manifest.pack + '\n';
|
|
129
|
+
return Buffer.concat([Buffer.from(header, 'utf8'), canonicalizeManifest(manifest.files)]);
|
|
130
|
+
}
|
|
131
|
+
/** Build a manifest for `files` (paths relative to `root`, POSIX separators). */
|
|
132
|
+
export function buildManifest(root, pack, files) {
|
|
133
|
+
const entries = files.map((rel) => ({
|
|
134
|
+
path: rel.split(sep).join('/'),
|
|
135
|
+
sha256: hashFile(join(root, rel)),
|
|
136
|
+
}));
|
|
137
|
+
return { version: MANIFEST_VERSION, pack, files: entries };
|
|
138
|
+
}
|
|
139
|
+
export function signManifest(manifest, privateKeyPem) {
|
|
140
|
+
// Defence in depth (round-2 review): `pack` is interpolated into a newline-delimited signed header.
|
|
141
|
+
// `verifyManifest` already refuses an unsafe pack name, so an ambiguous header could never verify —
|
|
142
|
+
// but refusing to CREATE one is cheaper than reasoning about why it cannot be exploited.
|
|
143
|
+
if (!isSafeManifestPath(manifest.pack))
|
|
144
|
+
throw new Error('refusing to sign an unsafe pack name: ' + JSON.stringify(manifest.pack));
|
|
145
|
+
if (!Number.isInteger(manifest.version))
|
|
146
|
+
throw new Error('refusing to sign a non-integer manifest version');
|
|
147
|
+
const structural = checkManifestEntries(manifest.files);
|
|
148
|
+
if (structural)
|
|
149
|
+
throw new Error('refusing to sign a malformed manifest: ' + structural);
|
|
150
|
+
const key = createPrivateKey(privateKeyPem);
|
|
151
|
+
const sig = cryptoSign(null, canonicalizeSigned(manifest), key);
|
|
152
|
+
return { manifest, signature: sig.toString('base64') };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* FR-8 / risk R1: every degenerate input FAILS CLOSED. Absence must never read as success — that is the
|
|
156
|
+
* failure mode that turns a security tool into a lie.
|
|
157
|
+
*
|
|
158
|
+
* `pubKeyPem` is required. It comes from the repo, never from `root`.
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* Round-2 review killed the `expectedFiles` option: a caller could NARROW the added-file check and
|
|
162
|
+
* silently allow an unsigned `evil.js` to sit in the pack. A safety option that a caller may disable
|
|
163
|
+
* is not a safety option. The pack is always scanned.
|
|
164
|
+
*/
|
|
165
|
+
export function verifyManifest(root, signed, pubKeyPem) {
|
|
166
|
+
const fail = (path, reason) => ({ ok: false, failures: [{ path, reason }] });
|
|
167
|
+
if (!signed || typeof signed !== 'object')
|
|
168
|
+
return fail(MANIFEST_NAME, 'no manifest');
|
|
169
|
+
const { manifest, signature } = signed;
|
|
170
|
+
if (!manifest || typeof manifest !== 'object')
|
|
171
|
+
return fail(MANIFEST_NAME, 'manifest is missing');
|
|
172
|
+
if (typeof manifest.version !== 'number' || !Number.isInteger(manifest.version)) {
|
|
173
|
+
return fail(MANIFEST_NAME, 'manifest version is not an integer');
|
|
174
|
+
}
|
|
175
|
+
if (!isSafeManifestPath(manifest.pack)) {
|
|
176
|
+
return fail(MANIFEST_NAME, 'manifest pack name is unsafe: ' + JSON.stringify(manifest.pack));
|
|
177
|
+
}
|
|
178
|
+
const structural = checkManifestEntries(manifest.files);
|
|
179
|
+
if (structural)
|
|
180
|
+
return fail(MANIFEST_NAME, structural);
|
|
181
|
+
if (typeof signature !== 'string' || signature.length === 0) {
|
|
182
|
+
return fail(MANIFEST_NAME, 'manifest is unsigned');
|
|
183
|
+
}
|
|
184
|
+
if (typeof pubKeyPem !== 'string' || pubKeyPem.length === 0) {
|
|
185
|
+
return fail(MANIFEST_NAME, 'no public key supplied — refusing to verify');
|
|
186
|
+
}
|
|
187
|
+
// The signature first: if the manifest itself was rewritten, its file list is not evidence.
|
|
188
|
+
let sigOk = false;
|
|
189
|
+
try {
|
|
190
|
+
sigOk = cryptoVerify(null, canonicalizeSigned(manifest), createPublicKey(pubKeyPem), Buffer.from(signature, 'base64'));
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return fail(MANIFEST_NAME, 'signature is malformed');
|
|
194
|
+
}
|
|
195
|
+
if (!sigOk)
|
|
196
|
+
return fail(MANIFEST_NAME, 'signature does not verify against the pinned key');
|
|
197
|
+
const failures = [];
|
|
198
|
+
for (const entry of manifest.files) {
|
|
199
|
+
const abs = join(root, entry.path);
|
|
200
|
+
if (!existsSync(abs)) {
|
|
201
|
+
failures.push({ path: entry.path, reason: 'listed in the manifest but absent' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// One open, O_NOFOLLOW, fstat the descriptor, hash from it: no symlink follow, no TOCTOU window.
|
|
205
|
+
const digest = hashRegularFileNoFollow(abs);
|
|
206
|
+
if (digest === null) {
|
|
207
|
+
failures.push({ path: entry.path, reason: 'is a symlink or not a regular file — refusing to hash it' });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (digest !== entry.sha256) {
|
|
211
|
+
failures.push({ path: entry.path, reason: 'content does not match its signed hash' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Bidirectional, always: hashing only what the manifest lists lets an attacker ADD a file.
|
|
215
|
+
const present = listPackFiles(root);
|
|
216
|
+
const listed = new Set(manifest.files.map((f) => f.path));
|
|
217
|
+
for (const rel of present) {
|
|
218
|
+
const p = rel.split(sep).join('/');
|
|
219
|
+
if (!listed.has(p))
|
|
220
|
+
failures.push({ path: p, reason: 'present in the pack but not signed' });
|
|
221
|
+
}
|
|
222
|
+
return { ok: failures.length === 0, failures };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* FR-5, the one irreversible mistake. `.gitignore` covers `*.pem` and `*.key` and nothing else — a key
|
|
226
|
+
* written as `signing-key.json` would be committed without complaint. Refuse by location, not by name.
|
|
227
|
+
*/
|
|
228
|
+
export function isInsideTree(keyPath, repoRoot) {
|
|
229
|
+
const key = resolve(keyPath);
|
|
230
|
+
const root = resolve(repoRoot);
|
|
231
|
+
if (key === root)
|
|
232
|
+
return true;
|
|
233
|
+
const rel = relative(root, key);
|
|
234
|
+
// `relative` yields '' for the root itself, a '..'-prefixed path for anything outside, and an
|
|
235
|
+
// absolute path when the two share no root. Anything else is inside.
|
|
236
|
+
if (rel === '')
|
|
237
|
+
return true;
|
|
238
|
+
if (rel === '..' || rel.startsWith('..' + sep))
|
|
239
|
+
return false;
|
|
240
|
+
if (isAbsolute(rel))
|
|
241
|
+
return false;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
export function assertKeyOutsideTree(keyPath, repoRoot) {
|
|
245
|
+
if (isInsideTree(keyPath, repoRoot)) {
|
|
246
|
+
throw new Error('refusing to write a private key inside the repository working tree: ' +
|
|
247
|
+
resolve(keyPath) +
|
|
248
|
+
' (a leaked signing key cannot be reverted — it means a new key and re-signing every pack)');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** CycloneDX 1.5 JSON, hand-built — it is JSON against a schema, not a reason for a dependency. */
|
|
252
|
+
export function buildSbom(manifest) {
|
|
253
|
+
return {
|
|
254
|
+
bomFormat: 'CycloneDX',
|
|
255
|
+
specVersion: '1.5',
|
|
256
|
+
version: 1,
|
|
257
|
+
metadata: { component: { type: 'library', name: manifest.pack } },
|
|
258
|
+
components: manifest.files.map((f) => ({
|
|
259
|
+
type: 'file',
|
|
260
|
+
name: f.path,
|
|
261
|
+
hashes: [{ alg: 'SHA-256', content: f.sha256 }],
|
|
262
|
+
})),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function decidePublishGate(input) {
|
|
266
|
+
if (!input.trustRootPresent) {
|
|
267
|
+
if (input.requireSigning) {
|
|
268
|
+
return { action: 'block', reason: 'no trust root (keys/dz.pub) and --require-signing was passed' };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
action: 'publish-unsigned',
|
|
272
|
+
reason: 'no trust root committed (keys/dz.pub) — packs publish UNSIGNED and unverifiable',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (!input.manifestPresent) {
|
|
276
|
+
return { action: 'block', reason: 'trust root is present but the pack carries no signature manifest' };
|
|
277
|
+
}
|
|
278
|
+
if (!input.verifyOk) {
|
|
279
|
+
return { action: 'block', reason: 'the pack does not match its signed manifest' };
|
|
280
|
+
}
|
|
281
|
+
return { action: 'publish-verified', reason: 'manifest verified against the pinned key' };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Precedence, explicit and pure: `--pubkey` > repo `keys/dz.pub` > the packaged key.
|
|
285
|
+
* Never derived from the pack under verification.
|
|
286
|
+
*/
|
|
287
|
+
export function resolveTrustRoot(c) {
|
|
288
|
+
if (c.explicit)
|
|
289
|
+
return { source: 'explicit', path: c.explicit };
|
|
290
|
+
if (c.repo)
|
|
291
|
+
return { source: 'repo', path: c.repo };
|
|
292
|
+
if (c.packaged)
|
|
293
|
+
return { source: 'packaged', path: c.packaged };
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The whole feature, as a table:
|
|
298
|
+
*
|
|
299
|
+
* | verdict | requireSigning off | on |
|
|
300
|
+
* |----------------|--------------------|------|
|
|
301
|
+
* | verified | ok | ok |
|
|
302
|
+
* | unsigned | report | fail |
|
|
303
|
+
* | tampered | FAIL | FAIL |
|
|
304
|
+
* | no-trust-root | report | fail |
|
|
305
|
+
*
|
|
306
|
+
* `tampered` is fatal in both columns; `no-trust-root` is never success. That is the load-bearing
|
|
307
|
+
* property. Today every pack is `unsigned` or `no-trust-root` — the leg is wired, not armed.
|
|
308
|
+
*/
|
|
309
|
+
export function decideVerifyPolicy(verdict, requireSigning) {
|
|
310
|
+
if (verdict === 'tampered') {
|
|
311
|
+
return { action: 'fail', reason: 'pack does not match its signed manifest' };
|
|
312
|
+
}
|
|
313
|
+
if (verdict === 'verified') {
|
|
314
|
+
return { action: 'ok', reason: 'verified against the pinned key' };
|
|
315
|
+
}
|
|
316
|
+
if (verdict === 'no-trust-root') {
|
|
317
|
+
return requireSigning
|
|
318
|
+
? { action: 'fail', reason: 'no trust root available and --require-signing was passed' }
|
|
319
|
+
: { action: 'report', reason: 'no trust root available — nothing could be verified' };
|
|
320
|
+
}
|
|
321
|
+
return requireSigning
|
|
322
|
+
? { action: 'fail', reason: 'pack is unsigned and --require-signing was passed' }
|
|
323
|
+
: { action: 'report', reason: 'pack is unsigned — not verified' };
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=sign.js.map
|
package/dist/sign.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-source trustTier (ADR-001, trusttier-single-source).
|
|
3
|
+
*
|
|
4
|
+
* A skill's `schemas/output.json` used to hardcode `properties.trustTier.const` — a second copy of the
|
|
5
|
+
* `trust_tier` in its `SKILL.md` frontmatter. Nothing read the schema copy; the frontmatter is the
|
|
6
|
+
* single source (`registry.ts` reads it). Two copies of a value drift by construction, and this one
|
|
7
|
+
* had. This transform removes the value, leaving only a shape constraint.
|
|
8
|
+
*/
|
|
9
|
+
export interface TrustTierStripResult {
|
|
10
|
+
/** The schema object, mutated in place if a const was present. */
|
|
11
|
+
readonly schema: Record<string, unknown>;
|
|
12
|
+
/** True iff a `const` was found under `properties.trustTier` and replaced. */
|
|
13
|
+
readonly changed: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Replace `properties.trustTier.const: N` with `{ minimum: 1, maximum: 3 }`, preserving every sibling
|
|
17
|
+
* key (`type`, `description`, …). Idempotent: a schema that already uses a range, has no `const`, or has
|
|
18
|
+
* no `trustTier`, is returned unchanged with `changed: false`.
|
|
19
|
+
*
|
|
20
|
+
* Pure w.r.t. inputs it does not own: it mutates the passed object (the caller owns it) and returns it,
|
|
21
|
+
* so it is trivially testable without a filesystem.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stripTrustTierConst(schema: Record<string, unknown>): TrustTierStripResult;
|
|
24
|
+
//# sourceMappingURL=skill-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-schema.d.ts","sourceRoot":"","sources":["../src/skill-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,oBAAoB,CA4BzF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-source trustTier (ADR-001, trusttier-single-source).
|
|
3
|
+
*
|
|
4
|
+
* A skill's `schemas/output.json` used to hardcode `properties.trustTier.const` — a second copy of the
|
|
5
|
+
* `trust_tier` in its `SKILL.md` frontmatter. Nothing read the schema copy; the frontmatter is the
|
|
6
|
+
* single source (`registry.ts` reads it). Two copies of a value drift by construction, and this one
|
|
7
|
+
* had. This transform removes the value, leaving only a shape constraint.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Replace `properties.trustTier.const: N` with `{ minimum: 1, maximum: 3 }`, preserving every sibling
|
|
11
|
+
* key (`type`, `description`, …). Idempotent: a schema that already uses a range, has no `const`, or has
|
|
12
|
+
* no `trustTier`, is returned unchanged with `changed: false`.
|
|
13
|
+
*
|
|
14
|
+
* Pure w.r.t. inputs it does not own: it mutates the passed object (the caller owns it) and returns it,
|
|
15
|
+
* so it is trivially testable without a filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export function stripTrustTierConst(schema) {
|
|
18
|
+
const props = schema.properties;
|
|
19
|
+
if (!props || typeof props !== 'object')
|
|
20
|
+
return { schema, changed: false };
|
|
21
|
+
const tt = props.trustTier;
|
|
22
|
+
// Cross-model review: `'const' in tt` also matches an INHERITED const on the prototype, and
|
|
23
|
+
// `!== undefined` cannot tell 'key absent' from 'key present, value undefined'. Use own-property
|
|
24
|
+
// checks throughout so a crafted object cannot trick the transform.
|
|
25
|
+
if (!tt || typeof tt !== 'object' || !Object.hasOwn(tt, 'const'))
|
|
26
|
+
return { schema, changed: false };
|
|
27
|
+
// Preserve type/description; drop the value; add the range. Order kept sane for a clean diff:
|
|
28
|
+
// type (if any) → minimum → maximum → description (if any) → any other siblings.
|
|
29
|
+
const { const: _dropped, type, description, ...rest } = tt;
|
|
30
|
+
const rebuilt = {};
|
|
31
|
+
rebuilt.type = Object.hasOwn(tt, 'type') ? type : 'integer';
|
|
32
|
+
rebuilt.minimum = 1;
|
|
33
|
+
rebuilt.maximum = 3;
|
|
34
|
+
if (Object.hasOwn(tt, 'description'))
|
|
35
|
+
rebuilt.description = description;
|
|
36
|
+
// `rest` already excludes const/type/description via destructuring; Object.entries is own-keys only.
|
|
37
|
+
for (const [k, v] of Object.entries(rest))
|
|
38
|
+
rebuilt[k] = v;
|
|
39
|
+
props.trustTier = rebuilt;
|
|
40
|
+
return { schema, changed: true };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=skill-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-schema.js","sourceRoot":"","sources":["../src/skill-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA+B;IACjE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAiD,CAAC;IACvE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAE3E,MAAM,EAAE,GAAG,KAAK,CAAC,SAAgD,CAAC;IAClE,4FAA4F;IAC5F,iGAAiG;IACjG,oEAAoE;IACpE,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEpG,8FAA8F;IAC9F,iFAAiF;IACjF,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,EAI7B,CAAC;IAE5B,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;IACpB,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;IACpB,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC;QAAE,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;IACxE,qGAAqG;IACrG,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAE1D,KAAK,CAAC,SAAS,GAAG,OAAO,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.108",
|
|
4
4
|
"description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"@dzhechkov/adapter-openclaude": "^0.1.0",
|
|
31
31
|
"@dzhechkov/adapter-opencode": "^0.2.0",
|
|
32
32
|
"yaml": "^2.0.0",
|
|
33
|
-
"@dzhechkov/adapter-agents-md": "0.1.1",
|
|
34
33
|
"@dzhechkov/adapter-copilot": "0.1.1",
|
|
34
|
+
"@dzhechkov/adapter-agents-md": "0.1.1",
|
|
35
35
|
"@dzhechkov/adapter-cursor": "0.1.1",
|
|
36
|
-
"@dzhechkov/adapter-gemini": "0.1.1",
|
|
37
36
|
"@dzhechkov/adapter-windsurf": "0.1.1",
|
|
37
|
+
"@dzhechkov/adapter-gemini": "0.1.1",
|
|
38
38
|
"@dzhechkov/core": "0.2.14",
|
|
39
39
|
"@dzhechkov/memory": "0.2.9"
|
|
40
40
|
},
|
package/src/claim-check.ts
CHANGED
|
@@ -44,19 +44,19 @@ const METRIC_TERMS = [
|
|
|
44
44
|
'packages', 'presets', 'downloads', 'benchmark', 'speedup', 'latency',
|
|
45
45
|
];
|
|
46
46
|
|
|
47
|
-
// Short/ambiguous metric tokens (ADR-263 F11): '
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// reference, and the line (after scrubbing) carries a number — "mAP 62.3" is
|
|
51
|
-
// a claim, "F-numbers map to findings" is not.
|
|
52
|
-
// 'map' additionally must not be a `.map` file suffix or a hyphenated
|
|
53
|
-
// compound ("map-free", "map-reduce") — mAP the metric never appears as either.
|
|
47
|
+
// Short/ambiguous metric tokens (ADR-263 F11): 'f1'/'o1' collide with finding/option labels.
|
|
48
|
+
// They only count as metric mentions when word-bounded, and the line (after scrubbing) carries a
|
|
49
|
+
// number — "auc 0.9" is a claim, "F-numbers map to findings" is not.
|
|
54
50
|
// `\d+ tests` is dz's single most-repeated headline claim ("2136 tests") and was slipping through:
|
|
55
51
|
// the substring term is 'tests passed', so a bare count never fired. Anchor it to a PRECEDING number
|
|
56
52
|
// rather than adding a bare 'test' term — otherwise every `usage.test.ts:42` path reference would
|
|
57
53
|
// register as a metric mention.
|
|
54
|
+
// NOTE: 'map' is NOT here. A line-wide "has a number" gate fired on the English word `map` next to any
|
|
55
|
+
// incidental digit — a `FR-2` label, a `v2`, an `item 2` (MEASURED — features/claimcheck-map-fp).
|
|
56
|
+
// mAP the metric is written with its score ADJACENT, so it gets a scoring-context regex (MAP_METRIC_RE),
|
|
57
|
+
// exactly as `recall` does (RECALL_METRIC_RE), instead of the loose line-wide gate.
|
|
58
58
|
const METRIC_TERMS_SHORT = [
|
|
59
|
-
|
|
59
|
+
/\bf1\b/, /\bauc\b/, /\biou\b/,
|
|
60
60
|
/\b\d[\d,._]*\s+tests?\b/,
|
|
61
61
|
];
|
|
62
62
|
// Finding/option labels (F1, O2, …) count as labels unless the token sits in a
|
|
@@ -93,6 +93,9 @@ function mentionsMetricTerm(lower: string, scrubbed: string): boolean {
|
|
|
93
93
|
// `recall` only in a scoring context (see RECALL_METRIC_RE). `precision` on the line is enough:
|
|
94
94
|
// "precision 0.9 / recall 0.8" is the canonical ML pair.
|
|
95
95
|
if (RECALL_METRIC_RE.test(scrubbed)) return true;
|
|
96
|
+
// mAP: a metric only with a score adjacent (see MAP_METRIC_RE). The regex embeds its own number, so
|
|
97
|
+
// it is checked before the loose line-wide HAS_NUMBER gate — the same shape as recall above.
|
|
98
|
+
if (MAP_METRIC_RE.test(scrubbed)) return true;
|
|
96
99
|
if (!HAS_NUMBER_RE.test(scrubbed)) return false;
|
|
97
100
|
return METRIC_TERMS_SHORT.some((re) => re.test(scrubbed));
|
|
98
101
|
}
|
|
@@ -158,6 +161,30 @@ const HEADING_RE = /^\s{0,3}#{1,6}\s/;
|
|
|
158
161
|
*/
|
|
159
162
|
const RECALL_METRIC_RE = /\brecall\s*(?:@\s*\d|rate\b|score\b|of\s+[\d.]|[:=]\s*[\d.])/i;
|
|
160
163
|
|
|
164
|
+
/**
|
|
165
|
+
* mAP (mean Average Precision) is both a metric AND the English word `map`. Matching it via the
|
|
166
|
+
* line-wide "has a number" gate fired on every line where `map` co-occurred with an unrelated digit —
|
|
167
|
+
* a `FR-2` label, a `v2`, an `item 2` (MEASURED — reproducer matrix in features/claimcheck-map-fp).
|
|
168
|
+
* Like `recall`, it counts as a metric only in a SCORING CONTEXT: a score sits ADJACENT to the token.
|
|
169
|
+
*
|
|
170
|
+
* Fires on: `mAP 62.3`, `mAP: 0.62`, `mAP=62`, `mAP@0.5`, `62.3 mAP`, `62% mAP`.
|
|
171
|
+
* Does not fire on: `the map imports` + `FR-2`, `.map` file, `map-reduce`, `a map of 3 zones`.
|
|
172
|
+
*
|
|
173
|
+
* A real mAP score is a DECIMAL or a PERCENT (`0.62`, `62.3`, `62%`) — never a bare integer and never a
|
|
174
|
+
* lone dot. Requiring that (not merely "a digit or dot", which cross-model review showed fires on
|
|
175
|
+
* `map: 3 zones`, `map @ 5 locations`, and even `map: .env`) is what separates the metric from prose:
|
|
176
|
+
* `map 3 items`, `a map of 3 zones`, `top 5 map layers` all stay prose. An optional `@`/`:`/`=` may sit
|
|
177
|
+
* between the token and its score. `(?<![.\w])` excludes `.map`; `(?!-)` excludes `map-reduce`/`map-free`.
|
|
178
|
+
*
|
|
179
|
+
* Known, accepted limitation (cross-model review): exotic notations `mAP50 62.3`, `mAP@[.5:.95]` are NOT
|
|
180
|
+
* matched — under-detection of rare forms, not a false positive. dz's own claims use `mAP 62.3`.
|
|
181
|
+
*/
|
|
182
|
+
const MAP_SCORE = String.raw`(?:\d+\.\d+|\.\d+|\d+\s*%)`;
|
|
183
|
+
const MAP_METRIC_RE = new RegExp(
|
|
184
|
+
String.raw`(?<![.\w])map\b(?!-)\s*(?:[@:=]\s*)?${MAP_SCORE}|${MAP_SCORE}\s+(?<![.\w])map\b(?!-)`,
|
|
185
|
+
'i',
|
|
186
|
+
);
|
|
187
|
+
|
|
161
188
|
/**
|
|
162
189
|
* A shell reproducer is STRUCTURAL, never a word. `(MEASURED — reproducer)` is self-certifying and
|
|
163
190
|
* must not pass; a backticked span whose first token is a command this repo actually measures with is
|
|
@@ -297,3 +324,60 @@ export function summarize(result: ClaimCheckResult): string {
|
|
|
297
324
|
const high = result.findings.filter((f) => f.severity === 'high').length;
|
|
298
325
|
return `claim-check: ${result.findings.length} finding(s) (${high} high) — accuracy claims need MEASURED/CLAIMED tags + a reproducer.`;
|
|
299
326
|
}
|
|
327
|
+
|
|
328
|
+
// ── Voluntary text-vet policy (ADR-001, mcp-claim-check-text) ────────────────
|
|
329
|
+
//
|
|
330
|
+
// `claimCheck('')` returns {ok:true} — correct for a file (no claims), wrong for a VOLUNTARY vet: an
|
|
331
|
+
// agent that asks "are my claims OK?" with nothing and hears "yes" is reassured about nothing. This
|
|
332
|
+
// pure policy decides error-vs-run BEFORE the engine is called, so a wrapper (the MCP tool) can fail
|
|
333
|
+
// closed without touching `claimCheck` itself.
|
|
334
|
+
|
|
335
|
+
export type FailOn = 'high' | 'medium' | 'none';
|
|
336
|
+
|
|
337
|
+
export interface ClaimTextDecision {
|
|
338
|
+
readonly kind: 'error' | 'run';
|
|
339
|
+
readonly reason?: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Empty / whitespace-only / non-string ⇒ error. A real paragraph ⇒ run. */
|
|
343
|
+
export function decideClaimCheckText(text: unknown): ClaimTextDecision {
|
|
344
|
+
if (typeof text !== 'string') {
|
|
345
|
+
return { kind: 'error', reason: 'text must be a string, got ' + typeof text };
|
|
346
|
+
}
|
|
347
|
+
// Cross-model review: trim() leaves zero-width and format characters (U+200B ZWSP, U+200D ZWJ,
|
|
348
|
+
// U+FEFF BOM, other Cf), so a string of invisibles would pass as `run` and vet nothing. Strip all
|
|
349
|
+
// whitespace AND Unicode format/control characters before the emptiness test.
|
|
350
|
+
const visible = text.replace(/[\s\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|\p{Cf}|\p{Cc}/gu, '');
|
|
351
|
+
if (visible.length === 0) {
|
|
352
|
+
return { kind: 'error', reason: 'text is empty or only invisible characters — nothing to vet' };
|
|
353
|
+
}
|
|
354
|
+
return { kind: 'run' };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Severity counts, so a caller can gate without re-walking the findings. */
|
|
358
|
+
export function severityCounts(result: ClaimCheckResult): { high: number; medium: number } {
|
|
359
|
+
let high = 0;
|
|
360
|
+
let medium = 0;
|
|
361
|
+
for (const f of result.findings) {
|
|
362
|
+
if (f.severity === 'high') high++;
|
|
363
|
+
else if (f.severity === 'medium') medium++;
|
|
364
|
+
}
|
|
365
|
+
return { high, medium };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Whether a run RESULT trips the caller's threshold. Reporting only — never throws, never converts the
|
|
370
|
+
* fail-closed empty case (which is handled earlier by `decideClaimCheckText`) into a pass.
|
|
371
|
+
* 'high' ⇒ gated iff any high finding
|
|
372
|
+
* 'medium' ⇒ gated iff any high OR medium finding
|
|
373
|
+
* 'none' ⇒ never gated
|
|
374
|
+
*/
|
|
375
|
+
export function isGated(result: ClaimCheckResult, failOn: FailOn): boolean {
|
|
376
|
+
const { high, medium } = severityCounts(result);
|
|
377
|
+
if (failOn === 'none') return false;
|
|
378
|
+
if (failOn === 'high') return high > 0;
|
|
379
|
+
if (failOn === 'medium') return high > 0 || medium > 0;
|
|
380
|
+
// Cross-model review: an unknown failOn (only reachable via a direct call, since the Zod enum guards
|
|
381
|
+
// the tool) must not silently behave like 'medium'. Fail SAFE: gate on any finding.
|
|
382
|
+
return high > 0 || medium > 0;
|
|
383
|
+
}
|