@dzhechkov/harness-cli 0.3.210 → 0.3.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +281 -12
- package/dist/cli.js.map +1 -1
- package/keys/README.md +8 -0
- package/package.json +4 -3
- package/src/cli.ts +296 -12
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ point any tool at (a skill is just Markdown). Dedicated targets are on the [road
|
|
|
132
132
|
|
|
133
133
|
## User Journey — from install to mastery
|
|
134
134
|
|
|
135
|
-
All
|
|
135
|
+
All 43 commands mapped to a real workflow:
|
|
136
136
|
|
|
137
137
|
```
|
|
138
138
|
DISCOVER → INSTALL → USE → CREATE → MAINTAIN → SHARE
|
|
@@ -280,6 +280,81 @@ npx @dzhechkov/keysarium init # 7-phase research + comman
|
|
|
280
280
|
- Need a **themed set** that works together → `--preset`
|
|
281
281
|
- Need a **full pipeline** with commands and governance → `npx`
|
|
282
282
|
|
|
283
|
+
### `dz sign` / `dz verify-pack` — cryptographic tamper-evidence
|
|
284
|
+
|
|
285
|
+
Ed25519 over a file manifest, plus a CycloneDX SBOM. Zero dependencies (`node:crypto`).
|
|
286
|
+
|
|
287
|
+
**What it answers:** *are the bytes I am looking at the bytes the holder of the pinned key signed?*
|
|
288
|
+
**What it does NOT answer:** whether those bytes are any good. A signature gives provenance and
|
|
289
|
+
tamper-evidence, never truthfulness.
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Sign a pack. The private key MUST live outside the repo — dz refuses otherwise.
|
|
293
|
+
dz sign --pack packages/@dzhechkov/skills-qe --key ~/.dz/keys/dz.key
|
|
294
|
+
|
|
295
|
+
# Verify. The public key comes from the REPO (keys/dz.pub), never from the pack itself:
|
|
296
|
+
# whoever replaced the pack would have replaced a key shipped inside it.
|
|
297
|
+
dz verify-pack --pack packages/@dzhechkov/skills-qe # exit 0 = unmodified
|
|
298
|
+
dz verify-pack --pack ./downloaded-pack --pubkey keys/dz.pub # explicit trust root
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A single flipped byte, a deleted file, or an **added** file all fail verification and the offending
|
|
302
|
+
path is named. Every degenerate input (no manifest, empty file list, empty signature, no public key)
|
|
303
|
+
fails **closed** — absence never reads as success.
|
|
304
|
+
|
|
305
|
+
`dz publish` runs the check before publishing anything. With `keys/dz.pub` committed, an unsigned or
|
|
306
|
+
mismatching pack **blocks the release**. Until then, packs publish unsigned and `dz publish` says so on
|
|
307
|
+
every run; `--require-signing` turns that into a refusal today.
|
|
308
|
+
|
|
309
|
+
**From Claude Code, in plain language:**
|
|
310
|
+
> "Sign the QE skill pack with my key at ~/.dz/keys/dz.key, then verify it."
|
|
311
|
+
> "Check whether the pack I just downloaded matches what we published."
|
|
312
|
+
> "Publish, but refuse if anything is unsigned."
|
|
313
|
+
|
|
314
|
+
### Signature checks in `dz doctor` and `dz upgrade`
|
|
315
|
+
|
|
316
|
+
`dz doctor` verifies every installed `skills-*` pack against a **pinned** Ed25519 key, and `dz upgrade`
|
|
317
|
+
verifies what it just installed. A pack that does not match its signed manifest is **fatal**:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
dz doctor # ... signatures: 3 verified, 0 unsigned, 0 TAMPERED, 0 unverifiable
|
|
321
|
+
dz doctor --require-signing # an unsigned pack becomes fatal too
|
|
322
|
+
dz upgrade # a TAMPERED pack aborts the upgrade
|
|
323
|
+
dz doctor --pubkey ./my.pub # verify against a key you pinned yourself
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Key precedence: `--pubkey` > the repository's `keys/dz.pub` > the key shipped inside `harness-cli`.
|
|
327
|
+
The key **never** comes from the pack being verified — whoever replaced the pack would have replaced a
|
|
328
|
+
key that travelled inside it.
|
|
329
|
+
|
|
330
|
+
**Honest limits.** The packaged key lives in the verifier and vouches for *other* packs; a compromised
|
|
331
|
+
`harness-cli` is outside the threat model, because you have already run its code. And a signature proves
|
|
332
|
+
the bytes are unmodified — never that the skill is any good. Today no key is committed, so every pack
|
|
333
|
+
reports `unverifiable` and nothing fails.
|
|
334
|
+
|
|
335
|
+
**From Claude Code, in plain language:**
|
|
336
|
+
> "Run doctor and tell me if any installed pack was modified."
|
|
337
|
+
> "Upgrade, but abort if anything fails its signature."
|
|
338
|
+
|
|
339
|
+
### npm provenance on release
|
|
340
|
+
|
|
341
|
+
`dz publish` appends `--provenance` **only** where an OIDC token can be minted — `GITHUB_ACTIONS=true`
|
|
342
|
+
plus both `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, which GitHub exports under
|
|
343
|
+
`permissions: id-token: write`.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
dz publish # auto: provenance in CI, silent no-op locally
|
|
347
|
+
dz publish --provenance # force. FAILS before the batch if the environment cannot mint a token
|
|
348
|
+
dz publish --no-provenance # escape hatch for a registry outage; prints why it was used
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Provenance proves *which workflow, at which commit, built this tarball* — and there is no private key for
|
|
352
|
+
anyone to leak. It does **not** prove the code is good, and an attacker who can push to `main` and trigger
|
|
353
|
+
the release job gets a perfectly attested malicious package.
|
|
354
|
+
|
|
355
|
+
A ready-to-install workflow is at `features/publish-provenance/07_code_changes/publish.yml`; copy it to
|
|
356
|
+
`.github/workflows/` and add an `NPM_TOKEN` secret.
|
|
357
|
+
|
|
283
358
|
### Skill Packs (22 packs · 173 skills)
|
|
284
359
|
|
|
285
360
|
Each pack is an npm package — click through for the **full per-skill documentation** (what each skill does + how to trigger it). Install a whole pack with `dz install <pkg>`, or pick skills with `dz init --select` / a `--preset`.
|
|
@@ -301,7 +376,7 @@ Each pack is an npm package — click through for the **full per-skill documenta
|
|
|
301
376
|
| [@dzhechkov/skills-pm](https://www.npmjs.com/package/@dzhechkov/skills-pm) | 18 | *imported (MIT)* — product-management toolkit: OST, RICE/ICE prioritization, product-strategy, pricing, OKRs, NSM/metrics/A-B/cohort, outcome-roadmap, stakeholder-map, sprint-plan, strategy-red-team, GTM/growth/beachhead, market-sizing; curated from phuryn/pm-skills (`dz init --preset pm`) ([ADR-0002](https://github.com/djd1m/dz-harness-hub/blob/main/docs/adr/0002-product-and-design-expansion.md)) |
|
|
302
377
|
| [@dzhechkov/skills-taste](https://www.npmjs.com/package/@dzhechkov/skills-taste) | 1 | *imported (MIT)* — `design-taste-frontend`: anti-slop landing/portfolio/redesign framework (dials + pre-flight + GSAP skeletons); complements frontend-design (`dz init --select design-taste-frontend`) ([ADR-0002](https://github.com/djd1m/dz-harness-hub/blob/main/docs/adr/0002-product-and-design-expansion.md)) |
|
|
303
378
|
| [@dzhechkov/skills-book-digitizer](https://www.npmjs.com/package/@dzhechkov/skills-book-digitizer) | 8 | *dz-original* — book → installable methodology pack: `digitize-book` (orchestrator) + ingest/extract/distill/pack/kb-index + `book-brain-register` (CP6 promote → cross-project brain) + `source-brain-ingest` (repo sibling). Verified provenance, IP-safe, resumable (`dz init --select digitize-book`) ([ADR-001](https://github.com/djd1m/dz-harness-hub/blob/main/features/book-knowledge-digitizer/03_adr/001-book-to-skillpack-pipeline.md)) |
|
|
304
|
-
| [@dzhechkov/skills-12factor](https://www.npmjs.com/package/@dzhechkov/skills-12factor) | 12 | *generated by the digitizer, CC BY 4.0* — The Twelve-Factor App distilled into 12 decision-moment skills (one per factor). The first PUBLIC digitized-book pack; paraphrased (shingling-gated), routing-gated
|
|
379
|
+
| [@dzhechkov/skills-12factor](https://www.npmjs.com/package/@dzhechkov/skills-12factor) | 12 | *generated by the digitizer, CC BY 4.0* — The Twelve-Factor App distilled into 12 decision-moment skills (one per factor). The first PUBLIC digitized-book pack; paraphrased (shingling-gated), routing-gated (every factor carries triggers), attributed (`NOTICE`) (`dz init --select 12factor-config-in-environment,…`) |
|
|
305
380
|
|
|
306
381
|
### Available Presets (13)
|
|
307
382
|
|
|
@@ -356,7 +431,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
|
|
|
356
431
|
|
|
357
432
|
> **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
|
|
358
433
|
|
|
359
|
-
## All Commands (
|
|
434
|
+
## All Commands (43)
|
|
360
435
|
|
|
361
436
|
```
|
|
362
437
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
|
|
@@ -1049,7 +1124,7 @@ Or cherry-pick: `dz compose meta+keysarium` for competitive analysis.
|
|
|
1049
1124
|
|
|
1050
1125
|
Without optional skills, design-thinking uses built-in fallbacks.
|
|
1051
1126
|
|
|
1052
|
-
BTO benchmark: L0 Grade A
|
|
1127
|
+
BTO benchmark: L0 Grade A, L2 Opus weighted 7.58/10.
|
|
1053
1128
|
|
|
1054
1129
|
---
|
|
1055
1130
|
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqLH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAyiID,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAsH5E"}
|
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmdirSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { execSync } from 'node:child_process';
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
11
|
import { createRequire } from 'node:module';
|
|
11
|
-
import {
|
|
12
|
-
import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, } from '@dzhechkov/harness-core';
|
|
12
|
+
import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, decideProvenance, isInsideTree, signManifest, verifyManifest, assertKeyOutsideTree, decidePublishGate, MANIFEST_NAME, SBOM_NAME, } from '@dzhechkov/harness-core';
|
|
13
13
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
14
14
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
15
15
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
@@ -22,13 +22,16 @@ Usage:
|
|
|
22
22
|
dz list [--skills-dir <dir>]
|
|
23
23
|
dz info <skill-id> [--skills-dir <dir>]
|
|
24
24
|
dz migrate [--project <dir>]
|
|
25
|
-
dz doctor [--project <dir>]
|
|
26
25
|
dz create-skill --name <id> [--description <text>] [--skills-dir <dir>] [--tier <1-3>] [--with-references] [--no-evals] [--bto]
|
|
27
26
|
dz scout [--topics <list>] [--since <date>] [--deep] [--output <file>] [--diff] [--report]
|
|
28
27
|
dz workflow <task> [--dry-run]
|
|
29
28
|
dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
|
|
30
29
|
dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
|
|
31
|
-
dz
|
|
30
|
+
dz doctor [--project <dir>] [--pubkey <path>] [--require-signing] (health + signature check of installed packs)
|
|
31
|
+
dz upgrade [--target <name>] [--pubkey <path>] [--require-signing] (a TAMPERED pack aborts the upgrade)
|
|
32
|
+
dz sign --pack <dir> --key <path-outside-repo> (Ed25519 manifest + CycloneDX SBOM for a pack)
|
|
33
|
+
dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
|
|
34
|
+
dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--require-signing] [--provenance|--no-provenance] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live; claim-check gate default warn — surfaces README claim findings, never blocks; error fails an offending package)
|
|
32
35
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
|
|
33
36
|
dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
|
|
34
37
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
@@ -57,7 +60,6 @@ Usage:
|
|
|
57
60
|
dz recommend "<task description>"
|
|
58
61
|
dz compose <preset1+preset2+...> [--target <name>]
|
|
59
62
|
dz diff <skill-dir>
|
|
60
|
-
dz upgrade [--target <name>] [--project <dir>]
|
|
61
63
|
dz auto-canonicalize --source <github-url> --pack <skills-pack>
|
|
62
64
|
dz registry [search <query>] [--category <cat>]
|
|
63
65
|
dz benchmark <skill-dir> [--compare <dir>] [--all]
|
|
@@ -505,14 +507,18 @@ function cmdMigrate(options, cwd, write) {
|
|
|
505
507
|
write(`\nRecommendation: ${report.recommendation}`);
|
|
506
508
|
return 0;
|
|
507
509
|
}
|
|
508
|
-
async function cmdDoctor(options, cwd, write) {
|
|
510
|
+
async function cmdDoctor(options, flags, cwd, write) {
|
|
509
511
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
510
512
|
const report = await runDoctor({ projectRoot });
|
|
511
513
|
write(`dz doctor (${report.node}):`);
|
|
512
514
|
for (const check of report.checks) {
|
|
513
515
|
write(` [${check.ok ? 'OK' : 'XX'}] ${check.name} - ${check.detail}`);
|
|
514
516
|
}
|
|
515
|
-
|
|
517
|
+
// ADR-001 (verify-apply-leg): the consumer-side apply-leg. A TAMPERED pack is fatal; an unsigned
|
|
518
|
+
// pack or a missing trust root is reported. A signature proves the bytes are unmodified — never
|
|
519
|
+
// that the skill is any good.
|
|
520
|
+
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
521
|
+
return report.ok && sigFatal === 0 ? 0 : 1;
|
|
516
522
|
}
|
|
517
523
|
function cmdRoam(options, flags, cwd, write) {
|
|
518
524
|
const apply = flags.has('apply');
|
|
@@ -2653,7 +2659,7 @@ function cmdRecommend(options, cwd, write) {
|
|
|
2653
2659
|
write(` Get it: dz init --target claude-code --select skill-advisor (or --preset meta)`);
|
|
2654
2660
|
return 0;
|
|
2655
2661
|
}
|
|
2656
|
-
function cmdUpgrade(options, cwd, write) {
|
|
2662
|
+
function cmdUpgrade(options, flags, cwd, write) {
|
|
2657
2663
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
2658
2664
|
if (!isTargetName(targetOpt)) {
|
|
2659
2665
|
write(`dz upgrade: --target must be one of: ${TARGET_NAMES.join(', ')}`);
|
|
@@ -2692,6 +2698,12 @@ function cmdUpgrade(options, cwd, write) {
|
|
|
2692
2698
|
if (report.needsUpdate > 0) {
|
|
2693
2699
|
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${targetOpt} --force`);
|
|
2694
2700
|
}
|
|
2701
|
+
// ADR-001 (verify-apply-leg): verify what we just left on disk. A TAMPERED pack aborts.
|
|
2702
|
+
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
2703
|
+
if (sigFatal !== 0) {
|
|
2704
|
+
write('dz upgrade: aborting — an installed pack does not match its signed manifest');
|
|
2705
|
+
return 1;
|
|
2706
|
+
}
|
|
2695
2707
|
return 0;
|
|
2696
2708
|
}
|
|
2697
2709
|
async function cmdAutoCanonicalize(options, cwd, write) {
|
|
@@ -2753,10 +2765,212 @@ async function cmdAutoCanonicalize(options, cwd, write) {
|
|
|
2753
2765
|
return 1;
|
|
2754
2766
|
}
|
|
2755
2767
|
}
|
|
2768
|
+
/** The pinned trust root. A key inside the artifact under verification is data, not a key (ADR-001). */
|
|
2769
|
+
const TRUST_ROOT_REL = 'keys/dz.pub';
|
|
2770
|
+
function packFiles(dir) {
|
|
2771
|
+
const out = [];
|
|
2772
|
+
const walk = (d, rel) => {
|
|
2773
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
2774
|
+
if (e.name === 'node_modules' || e.name === '.git')
|
|
2775
|
+
continue;
|
|
2776
|
+
if (e.name === MANIFEST_NAME || e.name === SBOM_NAME)
|
|
2777
|
+
continue;
|
|
2778
|
+
const abs = join(d, e.name);
|
|
2779
|
+
const r = rel ? rel + '/' + e.name : e.name;
|
|
2780
|
+
if (e.isDirectory())
|
|
2781
|
+
walk(abs, r);
|
|
2782
|
+
else if (e.isFile())
|
|
2783
|
+
out.push(r);
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
walk(dir, '');
|
|
2787
|
+
return out.sort();
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* The consumer-side apply-leg (ADR-001, verify-apply-leg). All security content lives in the two pure
|
|
2791
|
+
* functions in harness-core; this only resolves paths, reads bytes, and reports.
|
|
2792
|
+
*
|
|
2793
|
+
* The packaged key sits inside harness-cli — the VERIFIER — and vouches for other packs. It never
|
|
2794
|
+
* comes from the pack under verification.
|
|
2795
|
+
*/
|
|
2796
|
+
function packagedTrustRootPath() {
|
|
2797
|
+
// dist/cli.js -> ../keys/dz.pub (and src/cli.ts -> ../keys/dz.pub when run from source)
|
|
2798
|
+
const p = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'keys', 'dz.pub');
|
|
2799
|
+
return existsSync(p) ? p : undefined;
|
|
2800
|
+
}
|
|
2801
|
+
function verifyInstalledPacks(cwd, explicitPubkey) {
|
|
2802
|
+
// Cross-model review: `--pubkey ./missing.pub` used to fall back to the repo/packaged key and could
|
|
2803
|
+
// then report `verified` against a key the caller never asked for. An explicit request that cannot be
|
|
2804
|
+
// honoured is an error, not a suggestion.
|
|
2805
|
+
const explicit = explicitPubkey ? resolve(cwd, explicitPubkey) : undefined;
|
|
2806
|
+
if (explicit !== undefined && !existsSync(explicit)) {
|
|
2807
|
+
throw new Error('dz: --pubkey ' + explicit + ' does not exist — refusing to fall back to another key');
|
|
2808
|
+
}
|
|
2809
|
+
const repoKey = resolve(cwd, TRUST_ROOT_REL);
|
|
2810
|
+
const trustRoot = resolveTrustRoot({
|
|
2811
|
+
explicit: explicit && existsSync(explicit) ? explicit : undefined,
|
|
2812
|
+
repo: existsSync(repoKey) ? repoKey : undefined,
|
|
2813
|
+
packaged: packagedTrustRootPath(),
|
|
2814
|
+
});
|
|
2815
|
+
const packs = discoverSkillPackDirs(cwd);
|
|
2816
|
+
// Cross-model review: `--pubkey <pack>/evil.pub` would let the artifact supply its own verifying key
|
|
2817
|
+
// through the caller. The tool must never verify a pack against a key that lives inside it.
|
|
2818
|
+
if (trustRoot?.source === 'explicit') {
|
|
2819
|
+
for (const { dir } of packs) {
|
|
2820
|
+
if (isInsideTree(trustRoot.path, dir)) {
|
|
2821
|
+
throw new Error('dz: --pubkey lives inside the pack being verified (' + dir + ') — refusing');
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
const checks = [];
|
|
2826
|
+
for (const { pack, dir } of packs) {
|
|
2827
|
+
if (trustRoot === null) {
|
|
2828
|
+
checks.push({ pack, verdict: 'no-trust-root', failures: [] });
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
const manifestPath = join(dir, MANIFEST_NAME);
|
|
2832
|
+
if (!existsSync(manifestPath)) {
|
|
2833
|
+
checks.push({ pack, verdict: 'unsigned', failures: [] });
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
let signed;
|
|
2837
|
+
try {
|
|
2838
|
+
signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
2839
|
+
}
|
|
2840
|
+
catch {
|
|
2841
|
+
checks.push({ pack, verdict: 'tampered', failures: [{ path: MANIFEST_NAME, reason: 'not valid JSON' }] });
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
// The key existed when the trust root was resolved; it can vanish before it is read. A crash is
|
|
2845
|
+
// not a verdict — fail closed with a named reason.
|
|
2846
|
+
let keyPem;
|
|
2847
|
+
try {
|
|
2848
|
+
keyPem = readFileSync(trustRoot.path, 'utf8');
|
|
2849
|
+
}
|
|
2850
|
+
catch {
|
|
2851
|
+
checks.push({ pack, verdict: 'no-trust-root', failures: [] });
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
const res = verifyManifest(dir, signed, keyPem);
|
|
2855
|
+
checks.push({
|
|
2856
|
+
pack,
|
|
2857
|
+
verdict: res.ok ? 'verified' : 'tampered',
|
|
2858
|
+
failures: res.failures.map((f) => ({ path: f.path, reason: f.reason })),
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
return { trustRoot, checks };
|
|
2862
|
+
}
|
|
2863
|
+
/** Print the pack verdicts and return 1 iff the policy says any of them is fatal. */
|
|
2864
|
+
function reportPackVerification(cwd, explicitPubkey, requireSigning, write) {
|
|
2865
|
+
let trustRoot;
|
|
2866
|
+
let checks;
|
|
2867
|
+
try {
|
|
2868
|
+
({ trustRoot, checks } = verifyInstalledPacks(cwd, explicitPubkey));
|
|
2869
|
+
}
|
|
2870
|
+
catch (err) {
|
|
2871
|
+
// A refusal is a result, not a crash: the user gets one line, not a stack trace.
|
|
2872
|
+
write(` [XX] ${err.message}`);
|
|
2873
|
+
return 1;
|
|
2874
|
+
}
|
|
2875
|
+
if (checks.length === 0)
|
|
2876
|
+
return 0;
|
|
2877
|
+
const counts = { verified: 0, unsigned: 0, tampered: 0, 'no-trust-root': 0 };
|
|
2878
|
+
let fatal = 0;
|
|
2879
|
+
for (const c of checks) {
|
|
2880
|
+
counts[c.verdict]++;
|
|
2881
|
+
const decision = decideVerifyPolicy(c.verdict, requireSigning);
|
|
2882
|
+
// Only a FATAL verdict earns a line of its own. 22 identical "unverifiable" lines is noise, and
|
|
2883
|
+
// noise is how a real failure gets scrolled past.
|
|
2884
|
+
if (decision.action === 'fail') {
|
|
2885
|
+
fatal++;
|
|
2886
|
+
write(` [XX] ${c.pack} - ${decision.reason}`);
|
|
2887
|
+
for (const f of c.failures)
|
|
2888
|
+
write(` ${f.path}: ${f.reason}`);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const root = trustRoot ? `${trustRoot.source} (${trustRoot.path})` : 'none';
|
|
2892
|
+
write(` signatures: ${counts.verified} verified, ${counts.unsigned} unsigned, ` +
|
|
2893
|
+
`${counts.tampered} TAMPERED, ${counts['no-trust-root']} unverifiable; trust root: ${root}`);
|
|
2894
|
+
// A signature proves the bytes are unmodified. It never proves the skill is any good.
|
|
2895
|
+
return fatal > 0 ? 1 : 0;
|
|
2896
|
+
}
|
|
2897
|
+
function cmdSign(options, flags, cwd, write) {
|
|
2898
|
+
const pack = options.get('pack');
|
|
2899
|
+
const key = options.get('key');
|
|
2900
|
+
if (!pack || !key) {
|
|
2901
|
+
write('dz sign: --pack <dir> and --key <path> are both required');
|
|
2902
|
+
write(' the key path MUST be outside the repository working tree (a leaked signing key is not revertible)');
|
|
2903
|
+
return 1;
|
|
2904
|
+
}
|
|
2905
|
+
const packDir = resolve(cwd, pack);
|
|
2906
|
+
if (!existsSync(packDir)) {
|
|
2907
|
+
write(`dz sign: no such pack: ${packDir}`);
|
|
2908
|
+
return 1;
|
|
2909
|
+
}
|
|
2910
|
+
try {
|
|
2911
|
+
assertKeyOutsideTree(resolve(cwd, key), cwd);
|
|
2912
|
+
}
|
|
2913
|
+
catch (err) {
|
|
2914
|
+
write(`dz sign: ${err.message}`);
|
|
2915
|
+
return 1;
|
|
2916
|
+
}
|
|
2917
|
+
if (!existsSync(resolve(cwd, key))) {
|
|
2918
|
+
write(`dz sign: private key not found: ${resolve(cwd, key)}`);
|
|
2919
|
+
return 1;
|
|
2920
|
+
}
|
|
2921
|
+
const files = packFiles(packDir);
|
|
2922
|
+
if (files.length === 0) {
|
|
2923
|
+
write('dz sign: the pack contains no files — refusing to sign nothing');
|
|
2924
|
+
return 1;
|
|
2925
|
+
}
|
|
2926
|
+
const manifest = buildManifest(packDir, basename(packDir), files);
|
|
2927
|
+
const signed = signManifest(manifest, readFileSync(resolve(cwd, key), 'utf8'));
|
|
2928
|
+
writeFileSync(join(packDir, MANIFEST_NAME), JSON.stringify(signed, null, 2) + '\n');
|
|
2929
|
+
writeFileSync(join(packDir, SBOM_NAME), JSON.stringify(buildSbom(manifest), null, 2) + '\n');
|
|
2930
|
+
write(`dz sign: signed ${files.length} file(s) in ${packDir}`);
|
|
2931
|
+
write(` ${MANIFEST_NAME} + ${SBOM_NAME} written. Ed25519 gives tamper-evidence, never truthfulness.`);
|
|
2932
|
+
return 0;
|
|
2933
|
+
}
|
|
2934
|
+
function cmdVerifyPack(options, flags, cwd, write) {
|
|
2935
|
+
const pack = options.get('pack');
|
|
2936
|
+
if (!pack) {
|
|
2937
|
+
write('dz verify-pack: --pack <dir> is required');
|
|
2938
|
+
return 1;
|
|
2939
|
+
}
|
|
2940
|
+
const packDir = resolve(cwd, pack);
|
|
2941
|
+
// The key is pinned in the repo. Never read it from the pack (ADR-001, recalled lesson).
|
|
2942
|
+
const pubPath = resolve(cwd, options.get('pubkey') ?? TRUST_ROOT_REL);
|
|
2943
|
+
if (!existsSync(pubPath)) {
|
|
2944
|
+
write(`dz verify-pack: no trust root at ${pubPath} — refusing to verify (fail closed)`);
|
|
2945
|
+
return 1;
|
|
2946
|
+
}
|
|
2947
|
+
const manifestPath = join(packDir, MANIFEST_NAME);
|
|
2948
|
+
if (!existsSync(manifestPath)) {
|
|
2949
|
+
write(`dz verify-pack: ${packDir} carries no ${MANIFEST_NAME}`);
|
|
2950
|
+
return 1;
|
|
2951
|
+
}
|
|
2952
|
+
let signed = null;
|
|
2953
|
+
try {
|
|
2954
|
+
signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
2955
|
+
}
|
|
2956
|
+
catch {
|
|
2957
|
+
write(`dz verify-pack: ${MANIFEST_NAME} is not valid JSON`);
|
|
2958
|
+
return 1;
|
|
2959
|
+
}
|
|
2960
|
+
const res = verifyManifest(packDir, signed, readFileSync(pubPath, 'utf8'));
|
|
2961
|
+
if (res.ok) {
|
|
2962
|
+
write(`dz verify-pack: OK — ${packDir} matches its signed manifest`);
|
|
2963
|
+
return 0;
|
|
2964
|
+
}
|
|
2965
|
+
write(`dz verify-pack: FAILED — ${packDir}`);
|
|
2966
|
+
for (const f of res.failures)
|
|
2967
|
+
write(` ${f.path}: ${f.reason}`);
|
|
2968
|
+
return 1;
|
|
2969
|
+
}
|
|
2756
2970
|
function cmdPublish(options, flags, cwd, write) {
|
|
2757
2971
|
// Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
|
|
2758
2972
|
// silently swallowed and flip the command into live-publish mode.
|
|
2759
|
-
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
|
|
2973
|
+
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance']);
|
|
2760
2974
|
const allowedOptions = new Set(['filter', 'claim-check']);
|
|
2761
2975
|
const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>';
|
|
2762
2976
|
for (const flag of flags) {
|
|
@@ -2775,6 +2989,20 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
2775
2989
|
return 1;
|
|
2776
2990
|
}
|
|
2777
2991
|
}
|
|
2992
|
+
// ADR-001 (publish-provenance): decide BEFORE any work — flag validation, then a pre-flight that
|
|
2993
|
+
// refuses `--provenance` where no OIDC token can be minted. `off` is an escape hatch that names itself.
|
|
2994
|
+
if (flags.has('provenance') && flags.has('no-provenance')) {
|
|
2995
|
+
write('dz publish: --provenance and --no-provenance are mutually exclusive');
|
|
2996
|
+
return 1;
|
|
2997
|
+
}
|
|
2998
|
+
const provenance = flags.has('provenance') ? 'on' : flags.has('no-provenance') ? 'off' : 'auto';
|
|
2999
|
+
try {
|
|
3000
|
+
write(`dz publish: ${decideProvenance(provenance, process.env).reason}`);
|
|
3001
|
+
}
|
|
3002
|
+
catch (err) {
|
|
3003
|
+
write(err.message);
|
|
3004
|
+
return 1;
|
|
3005
|
+
}
|
|
2778
3006
|
// Pre-publish claim-check gate strictness: reject (never coerce) an invalid value. Default 'warn'
|
|
2779
3007
|
// per ADR-001 — findings are SURFACED on every publish, but 'warn' never changes publish status,
|
|
2780
3008
|
// so the success path is unchanged. 'off' disables the gate; 'error' fails an offending package.
|
|
@@ -2825,7 +3053,44 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
2825
3053
|
}
|
|
2826
3054
|
write(`╚══════════════════════════════════════════════════════════════════════╝`);
|
|
2827
3055
|
}
|
|
2828
|
-
|
|
3056
|
+
// FR-4 — the signature gate, BEFORE anything is published (ADR-001).
|
|
3057
|
+
// Strictness follows the trust root: with no keys/dz.pub committed there is nothing to verify
|
|
3058
|
+
// against, and blocking would refuse every release forever. That is stated on every run.
|
|
3059
|
+
{
|
|
3060
|
+
const trustRoot = resolve(cwd, TRUST_ROOT_REL);
|
|
3061
|
+
const trustRootPresent = existsSync(trustRoot);
|
|
3062
|
+
const requireSigning = flags.has('require-signing');
|
|
3063
|
+
// `filter` is a string[] of substrings (matching publishPackages' own semantics), not a string.
|
|
3064
|
+
const targets = discoverPackages(cwd).filter((pk) => !filter || filter.length === 0 || filter.some((f) => pk.name.includes(f)));
|
|
3065
|
+
let blocked = 0;
|
|
3066
|
+
for (const pk of targets) {
|
|
3067
|
+
const manifestPath = join(pk.dir, MANIFEST_NAME);
|
|
3068
|
+
const manifestPresent = existsSync(manifestPath);
|
|
3069
|
+
let verifyOk = false;
|
|
3070
|
+
if (trustRootPresent && manifestPresent) {
|
|
3071
|
+
try {
|
|
3072
|
+
const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
3073
|
+
verifyOk = verifyManifest(pk.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
|
|
3074
|
+
}
|
|
3075
|
+
catch {
|
|
3076
|
+
verifyOk = false;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning });
|
|
3080
|
+
if (decision.action === 'block') {
|
|
3081
|
+
write(`dz publish: BLOCKED ${pk.name} — ${decision.reason}`);
|
|
3082
|
+
blocked++;
|
|
3083
|
+
}
|
|
3084
|
+
else if (decision.action === 'publish-unsigned') {
|
|
3085
|
+
write(`dz publish: ${pk.name} — ${decision.reason}`);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (blocked > 0) {
|
|
3089
|
+
write(`dz publish: refusing to publish (${blocked} package(s) failed the signature gate)`);
|
|
3090
|
+
return 1;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
const report = publishPackages(cwd, { provenance, dryRun, filter, bumpOnly, claimGate: claimCheckOpt });
|
|
2829
3094
|
write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
|
|
2830
3095
|
write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
|
|
2831
3096
|
for (const pkg of report.packages) {
|
|
@@ -3799,7 +4064,7 @@ export async function runCli(argv, io = {}) {
|
|
|
3799
4064
|
case 'migrate':
|
|
3800
4065
|
return cmdMigrate(options, cwd, write);
|
|
3801
4066
|
case 'doctor':
|
|
3802
|
-
return await cmdDoctor(options, cwd, write);
|
|
4067
|
+
return await cmdDoctor(options, flags, cwd, write);
|
|
3803
4068
|
case 'install':
|
|
3804
4069
|
return await cmdInstall(options, flags, cwd, write);
|
|
3805
4070
|
case 'bundle':
|
|
@@ -3820,6 +4085,10 @@ export async function runCli(argv, io = {}) {
|
|
|
3820
4085
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
3821
4086
|
case 'claim-check':
|
|
3822
4087
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
4088
|
+
case 'sign':
|
|
4089
|
+
return cmdSign(options, flags, cwd, write);
|
|
4090
|
+
case 'verify-pack':
|
|
4091
|
+
return cmdVerifyPack(options, flags, cwd, write);
|
|
3823
4092
|
case 'setup':
|
|
3824
4093
|
return await cmdSetup(options, flags, cwd, write);
|
|
3825
4094
|
case 'pretrain':
|
|
@@ -3831,7 +4100,7 @@ export async function runCli(argv, io = {}) {
|
|
|
3831
4100
|
case 'recommend':
|
|
3832
4101
|
return cmdRecommend(options, cwd, write);
|
|
3833
4102
|
case 'upgrade':
|
|
3834
|
-
return cmdUpgrade(options, cwd, write);
|
|
4103
|
+
return cmdUpgrade(options, flags, cwd, write);
|
|
3835
4104
|
case 'auto-canonicalize':
|
|
3836
4105
|
return await cmdAutoCanonicalize(options, cwd, write);
|
|
3837
4106
|
case 'publish':
|