@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/keys/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Trust root
|
|
2
|
+
|
|
3
|
+
`dz.pub` — the pinned Ed25519 public key that `dz doctor` / `dz upgrade` use to verify **other**
|
|
4
|
+
`skills-*` packs. It is not here yet: no project key has been generated (see task #36). Until it is,
|
|
5
|
+
every pack reports `no-trust-root`, and nothing fails.
|
|
6
|
+
|
|
7
|
+
This key verifies other packs, never `harness-cli` itself. A compromised verifier is outside the threat
|
|
8
|
+
model: you have already executed its code. See `features/verify-apply-leg/03_adr/001-keyring-in-the-verifier.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.212",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
37
|
"src",
|
|
38
|
-
"README.md"
|
|
38
|
+
"README.md",
|
|
39
|
+
"keys"
|
|
39
40
|
],
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@dzhechkov/harness-presets": "^0.5.0",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"@dzhechkov/skills-reverse-engineering": "^0.1.0",
|
|
55
56
|
"@dzhechkov/skills-presentation-storyteller": "^0.1.0",
|
|
56
57
|
"@dzhechkov/skills-website-cloner": "^0.1.0",
|
|
57
|
-
"@dzhechkov/harness-core": "0.3.
|
|
58
|
+
"@dzhechkov/harness-core": "0.3.109"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^25.6.0",
|
package/src/cli.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmdirSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { execSync } from 'node:child_process';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
createSkill,
|
|
@@ -96,8 +96,20 @@ import {
|
|
|
96
96
|
RECALL_USAGE_LOG_MAX_BYTES,
|
|
97
97
|
parseRecallUsageLog,
|
|
98
98
|
buildRecallUsageReport,
|
|
99
|
+
buildManifest,
|
|
100
|
+
buildSbom,
|
|
101
|
+
resolveTrustRoot,
|
|
102
|
+
decideVerifyPolicy,
|
|
103
|
+
decideProvenance,
|
|
104
|
+
isInsideTree,
|
|
105
|
+
signManifest,
|
|
106
|
+
verifyManifest,
|
|
107
|
+
assertKeyOutsideTree,
|
|
108
|
+
decidePublishGate,
|
|
109
|
+
MANIFEST_NAME,
|
|
110
|
+
SBOM_NAME,
|
|
99
111
|
} from '@dzhechkov/harness-core';
|
|
100
|
-
import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow } from '@dzhechkov/harness-core';
|
|
112
|
+
import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow } from '@dzhechkov/harness-core';
|
|
101
113
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
102
114
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
103
115
|
|
|
@@ -111,13 +123,16 @@ Usage:
|
|
|
111
123
|
dz list [--skills-dir <dir>]
|
|
112
124
|
dz info <skill-id> [--skills-dir <dir>]
|
|
113
125
|
dz migrate [--project <dir>]
|
|
114
|
-
dz doctor [--project <dir>]
|
|
115
126
|
dz create-skill --name <id> [--description <text>] [--skills-dir <dir>] [--tier <1-3>] [--with-references] [--no-evals] [--bto]
|
|
116
127
|
dz scout [--topics <list>] [--since <date>] [--deep] [--output <file>] [--diff] [--report]
|
|
117
128
|
dz workflow <task> [--dry-run]
|
|
118
129
|
dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
|
|
119
130
|
dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
|
|
120
|
-
dz
|
|
131
|
+
dz doctor [--project <dir>] [--pubkey <path>] [--require-signing] (health + signature check of installed packs)
|
|
132
|
+
dz upgrade [--target <name>] [--pubkey <path>] [--require-signing] (a TAMPERED pack aborts the upgrade)
|
|
133
|
+
dz sign --pack <dir> --key <path-outside-repo> (Ed25519 manifest + CycloneDX SBOM for a pack)
|
|
134
|
+
dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
|
|
135
|
+
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)
|
|
121
136
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
|
|
122
137
|
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)
|
|
123
138
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
@@ -146,7 +161,6 @@ Usage:
|
|
|
146
161
|
dz recommend "<task description>"
|
|
147
162
|
dz compose <preset1+preset2+...> [--target <name>]
|
|
148
163
|
dz diff <skill-dir>
|
|
149
|
-
dz upgrade [--target <name>] [--project <dir>]
|
|
150
164
|
dz auto-canonicalize --source <github-url> --pack <skills-pack>
|
|
151
165
|
dz registry [search <query>] [--category <cat>]
|
|
152
166
|
dz benchmark <skill-dir> [--compare <dir>] [--all]
|
|
@@ -653,14 +667,18 @@ function cmdMigrate(options: Map<string, string>, cwd: string, write: Write): nu
|
|
|
653
667
|
return 0;
|
|
654
668
|
}
|
|
655
669
|
|
|
656
|
-
async function cmdDoctor(options: Map<string, string>, cwd: string, write: Write): Promise<number> {
|
|
670
|
+
async function cmdDoctor(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
657
671
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
658
672
|
const report = await runDoctor({ projectRoot });
|
|
659
673
|
write(`dz doctor (${report.node}):`);
|
|
660
674
|
for (const check of report.checks) {
|
|
661
675
|
write(` [${check.ok ? 'OK' : 'XX'}] ${check.name} - ${check.detail}`);
|
|
662
676
|
}
|
|
663
|
-
|
|
677
|
+
// ADR-001 (verify-apply-leg): the consumer-side apply-leg. A TAMPERED pack is fatal; an unsigned
|
|
678
|
+
// pack or a missing trust root is reported. A signature proves the bytes are unmodified — never
|
|
679
|
+
// that the skill is any good.
|
|
680
|
+
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
681
|
+
return report.ok && sigFatal === 0 ? 0 : 1;
|
|
664
682
|
}
|
|
665
683
|
|
|
666
684
|
function cmdRoam(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
@@ -2873,7 +2891,7 @@ function cmdRecommend(options: Map<string, string>, cwd: string, write: Write):
|
|
|
2873
2891
|
return 0;
|
|
2874
2892
|
}
|
|
2875
2893
|
|
|
2876
|
-
function cmdUpgrade(options: Map<string, string>, cwd: string, write: Write): number {
|
|
2894
|
+
function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
2877
2895
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
2878
2896
|
if (!isTargetName(targetOpt)) {
|
|
2879
2897
|
write(`dz upgrade: --target must be one of: ${TARGET_NAMES.join(', ')}`);
|
|
@@ -2917,6 +2935,12 @@ function cmdUpgrade(options: Map<string, string>, cwd: string, write: Write): nu
|
|
|
2917
2935
|
if (report.needsUpdate > 0) {
|
|
2918
2936
|
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${targetOpt} --force`);
|
|
2919
2937
|
}
|
|
2938
|
+
// ADR-001 (verify-apply-leg): verify what we just left on disk. A TAMPERED pack aborts.
|
|
2939
|
+
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
2940
|
+
if (sigFatal !== 0) {
|
|
2941
|
+
write('dz upgrade: aborting — an installed pack does not match its signed manifest');
|
|
2942
|
+
return 1;
|
|
2943
|
+
}
|
|
2920
2944
|
return 0;
|
|
2921
2945
|
}
|
|
2922
2946
|
|
|
@@ -2988,10 +3012,213 @@ async function cmdAutoCanonicalize(options: Map<string, string>, cwd: string, wr
|
|
|
2988
3012
|
}
|
|
2989
3013
|
}
|
|
2990
3014
|
|
|
3015
|
+
|
|
3016
|
+
/** The pinned trust root. A key inside the artifact under verification is data, not a key (ADR-001). */
|
|
3017
|
+
const TRUST_ROOT_REL = 'keys/dz.pub';
|
|
3018
|
+
|
|
3019
|
+
function packFiles(dir: string): string[] {
|
|
3020
|
+
const out: string[] = [];
|
|
3021
|
+
const walk = (d: string, rel: string): void => {
|
|
3022
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
3023
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
3024
|
+
if (e.name === MANIFEST_NAME || e.name === SBOM_NAME) continue;
|
|
3025
|
+
const abs = join(d, e.name);
|
|
3026
|
+
const r = rel ? rel + '/' + e.name : e.name;
|
|
3027
|
+
if (e.isDirectory()) walk(abs, r);
|
|
3028
|
+
else if (e.isFile()) out.push(r);
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
walk(dir, '');
|
|
3032
|
+
return out.sort();
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
|
|
3036
|
+
/**
|
|
3037
|
+
* The consumer-side apply-leg (ADR-001, verify-apply-leg). All security content lives in the two pure
|
|
3038
|
+
* functions in harness-core; this only resolves paths, reads bytes, and reports.
|
|
3039
|
+
*
|
|
3040
|
+
* The packaged key sits inside harness-cli — the VERIFIER — and vouches for other packs. It never
|
|
3041
|
+
* comes from the pack under verification.
|
|
3042
|
+
*/
|
|
3043
|
+
function packagedTrustRootPath(): string | undefined {
|
|
3044
|
+
// dist/cli.js -> ../keys/dz.pub (and src/cli.ts -> ../keys/dz.pub when run from source)
|
|
3045
|
+
const p = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'keys', 'dz.pub');
|
|
3046
|
+
return existsSync(p) ? p : undefined;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
interface PackCheck {
|
|
3050
|
+
readonly pack: string;
|
|
3051
|
+
readonly verdict: PackVerdict;
|
|
3052
|
+
readonly failures: readonly { path: string; reason: string }[];
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
function verifyInstalledPacks(cwd: string, explicitPubkey?: string | undefined): {
|
|
3056
|
+
readonly trustRoot: ReturnType<typeof resolveTrustRoot>;
|
|
3057
|
+
readonly checks: PackCheck[];
|
|
3058
|
+
} {
|
|
3059
|
+
// Cross-model review: `--pubkey ./missing.pub` used to fall back to the repo/packaged key and could
|
|
3060
|
+
// then report `verified` against a key the caller never asked for. An explicit request that cannot be
|
|
3061
|
+
// honoured is an error, not a suggestion.
|
|
3062
|
+
const explicit = explicitPubkey ? resolve(cwd, explicitPubkey) : undefined;
|
|
3063
|
+
if (explicit !== undefined && !existsSync(explicit)) {
|
|
3064
|
+
throw new Error('dz: --pubkey ' + explicit + ' does not exist — refusing to fall back to another key');
|
|
3065
|
+
}
|
|
3066
|
+
const repoKey = resolve(cwd, TRUST_ROOT_REL);
|
|
3067
|
+
const trustRoot = resolveTrustRoot({
|
|
3068
|
+
explicit: explicit && existsSync(explicit) ? explicit : undefined,
|
|
3069
|
+
repo: existsSync(repoKey) ? repoKey : undefined,
|
|
3070
|
+
packaged: packagedTrustRootPath(),
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
const packs = discoverSkillPackDirs(cwd);
|
|
3074
|
+
|
|
3075
|
+
// Cross-model review: `--pubkey <pack>/evil.pub` would let the artifact supply its own verifying key
|
|
3076
|
+
// through the caller. The tool must never verify a pack against a key that lives inside it.
|
|
3077
|
+
if (trustRoot?.source === 'explicit') {
|
|
3078
|
+
for (const { dir } of packs) {
|
|
3079
|
+
if (isInsideTree(trustRoot.path, dir)) {
|
|
3080
|
+
throw new Error('dz: --pubkey lives inside the pack being verified (' + dir + ') — refusing');
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
const checks: PackCheck[] = [];
|
|
3086
|
+
for (const { pack, dir } of packs) {
|
|
3087
|
+
if (trustRoot === null) {
|
|
3088
|
+
checks.push({ pack, verdict: 'no-trust-root', failures: [] });
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
const manifestPath = join(dir, MANIFEST_NAME);
|
|
3092
|
+
if (!existsSync(manifestPath)) {
|
|
3093
|
+
checks.push({ pack, verdict: 'unsigned', failures: [] });
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
let signed: unknown;
|
|
3097
|
+
try {
|
|
3098
|
+
signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
3099
|
+
} catch {
|
|
3100
|
+
checks.push({ pack, verdict: 'tampered', failures: [{ path: MANIFEST_NAME, reason: 'not valid JSON' }] });
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
// The key existed when the trust root was resolved; it can vanish before it is read. A crash is
|
|
3104
|
+
// not a verdict — fail closed with a named reason.
|
|
3105
|
+
let keyPem: string;
|
|
3106
|
+
try {
|
|
3107
|
+
keyPem = readFileSync(trustRoot.path, 'utf8');
|
|
3108
|
+
} catch {
|
|
3109
|
+
checks.push({ pack, verdict: 'no-trust-root', failures: [] });
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
const res = verifyManifest(dir, signed as never, keyPem);
|
|
3113
|
+
checks.push({
|
|
3114
|
+
pack,
|
|
3115
|
+
verdict: res.ok ? 'verified' : 'tampered',
|
|
3116
|
+
failures: res.failures.map((f) => ({ path: f.path, reason: f.reason })),
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
return { trustRoot, checks };
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
/** Print the pack verdicts and return 1 iff the policy says any of them is fatal. */
|
|
3123
|
+
function reportPackVerification(
|
|
3124
|
+
cwd: string,
|
|
3125
|
+
explicitPubkey: string | undefined,
|
|
3126
|
+
requireSigning: boolean,
|
|
3127
|
+
write: Write,
|
|
3128
|
+
): number {
|
|
3129
|
+
let trustRoot: ReturnType<typeof resolveTrustRoot>;
|
|
3130
|
+
let checks: PackCheck[];
|
|
3131
|
+
try {
|
|
3132
|
+
({ trustRoot, checks } = verifyInstalledPacks(cwd, explicitPubkey));
|
|
3133
|
+
} catch (err) {
|
|
3134
|
+
// A refusal is a result, not a crash: the user gets one line, not a stack trace.
|
|
3135
|
+
write(` [XX] ${(err as Error).message}`);
|
|
3136
|
+
return 1;
|
|
3137
|
+
}
|
|
3138
|
+
if (checks.length === 0) return 0;
|
|
3139
|
+
|
|
3140
|
+
const counts = { verified: 0, unsigned: 0, tampered: 0, 'no-trust-root': 0 } as Record<PackVerdict, number>;
|
|
3141
|
+
let fatal = 0;
|
|
3142
|
+
for (const c of checks) {
|
|
3143
|
+
counts[c.verdict]++;
|
|
3144
|
+
const decision = decideVerifyPolicy(c.verdict, requireSigning);
|
|
3145
|
+
// Only a FATAL verdict earns a line of its own. 22 identical "unverifiable" lines is noise, and
|
|
3146
|
+
// noise is how a real failure gets scrolled past.
|
|
3147
|
+
if (decision.action === 'fail') {
|
|
3148
|
+
fatal++;
|
|
3149
|
+
write(` [XX] ${c.pack} - ${decision.reason}`);
|
|
3150
|
+
for (const f of c.failures) write(` ${f.path}: ${f.reason}`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
const root = trustRoot ? `${trustRoot.source} (${trustRoot.path})` : 'none';
|
|
3154
|
+
write(
|
|
3155
|
+
` signatures: ${counts.verified} verified, ${counts.unsigned} unsigned, ` +
|
|
3156
|
+
`${counts.tampered} TAMPERED, ${counts['no-trust-root']} unverifiable; trust root: ${root}`,
|
|
3157
|
+
);
|
|
3158
|
+
// A signature proves the bytes are unmodified. It never proves the skill is any good.
|
|
3159
|
+
return fatal > 0 ? 1 : 0;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
function cmdSign(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
3163
|
+
const pack = options.get('pack');
|
|
3164
|
+
const key = options.get('key');
|
|
3165
|
+
if (!pack || !key) {
|
|
3166
|
+
write('dz sign: --pack <dir> and --key <path> are both required');
|
|
3167
|
+
write(' the key path MUST be outside the repository working tree (a leaked signing key is not revertible)');
|
|
3168
|
+
return 1;
|
|
3169
|
+
}
|
|
3170
|
+
const packDir = resolve(cwd, pack);
|
|
3171
|
+
if (!existsSync(packDir)) { write(`dz sign: no such pack: ${packDir}`); return 1; }
|
|
3172
|
+
|
|
3173
|
+
try {
|
|
3174
|
+
assertKeyOutsideTree(resolve(cwd, key), cwd);
|
|
3175
|
+
} catch (err) {
|
|
3176
|
+
write(`dz sign: ${(err as Error).message}`);
|
|
3177
|
+
return 1;
|
|
3178
|
+
}
|
|
3179
|
+
if (!existsSync(resolve(cwd, key))) { write(`dz sign: private key not found: ${resolve(cwd, key)}`); return 1; }
|
|
3180
|
+
|
|
3181
|
+
const files = packFiles(packDir);
|
|
3182
|
+
if (files.length === 0) { write('dz sign: the pack contains no files — refusing to sign nothing'); return 1; }
|
|
3183
|
+
|
|
3184
|
+
const manifest = buildManifest(packDir, basename(packDir), files);
|
|
3185
|
+
const signed = signManifest(manifest, readFileSync(resolve(cwd, key), 'utf8'));
|
|
3186
|
+
writeFileSync(join(packDir, MANIFEST_NAME), JSON.stringify(signed, null, 2) + '\n');
|
|
3187
|
+
writeFileSync(join(packDir, SBOM_NAME), JSON.stringify(buildSbom(manifest), null, 2) + '\n');
|
|
3188
|
+
write(`dz sign: signed ${files.length} file(s) in ${packDir}`);
|
|
3189
|
+
write(` ${MANIFEST_NAME} + ${SBOM_NAME} written. Ed25519 gives tamper-evidence, never truthfulness.`);
|
|
3190
|
+
return 0;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function cmdVerifyPack(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
3194
|
+
const pack = options.get('pack');
|
|
3195
|
+
if (!pack) { write('dz verify-pack: --pack <dir> is required'); return 1; }
|
|
3196
|
+
const packDir = resolve(cwd, pack);
|
|
3197
|
+
|
|
3198
|
+
// The key is pinned in the repo. Never read it from the pack (ADR-001, recalled lesson).
|
|
3199
|
+
const pubPath = resolve(cwd, options.get('pubkey') ?? TRUST_ROOT_REL);
|
|
3200
|
+
if (!existsSync(pubPath)) {
|
|
3201
|
+
write(`dz verify-pack: no trust root at ${pubPath} — refusing to verify (fail closed)`);
|
|
3202
|
+
return 1;
|
|
3203
|
+
}
|
|
3204
|
+
const manifestPath = join(packDir, MANIFEST_NAME);
|
|
3205
|
+
if (!existsSync(manifestPath)) { write(`dz verify-pack: ${packDir} carries no ${MANIFEST_NAME}`); return 1; }
|
|
3206
|
+
|
|
3207
|
+
let signed: unknown = null;
|
|
3208
|
+
try { signed = JSON.parse(readFileSync(manifestPath, 'utf8')); }
|
|
3209
|
+
catch { write(`dz verify-pack: ${MANIFEST_NAME} is not valid JSON`); return 1; }
|
|
3210
|
+
|
|
3211
|
+
const res = verifyManifest(packDir, signed as never, readFileSync(pubPath, 'utf8'));
|
|
3212
|
+
if (res.ok) { write(`dz verify-pack: OK — ${packDir} matches its signed manifest`); return 0; }
|
|
3213
|
+
write(`dz verify-pack: FAILED — ${packDir}`);
|
|
3214
|
+
for (const f of res.failures) write(` ${f.path}: ${f.reason}`);
|
|
3215
|
+
return 1;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
2991
3218
|
function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
2992
3219
|
// Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
|
|
2993
3220
|
// silently swallowed and flip the command into live-publish mode.
|
|
2994
|
-
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help']);
|
|
3221
|
+
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance']);
|
|
2995
3222
|
const allowedOptions = new Set(['filter', 'claim-check']);
|
|
2996
3223
|
const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>';
|
|
2997
3224
|
for (const flag of flags) {
|
|
@@ -3010,6 +3237,20 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
3010
3237
|
}
|
|
3011
3238
|
}
|
|
3012
3239
|
|
|
3240
|
+
// ADR-001 (publish-provenance): decide BEFORE any work — flag validation, then a pre-flight that
|
|
3241
|
+
// refuses `--provenance` where no OIDC token can be minted. `off` is an escape hatch that names itself.
|
|
3242
|
+
if (flags.has('provenance') && flags.has('no-provenance')) {
|
|
3243
|
+
write('dz publish: --provenance and --no-provenance are mutually exclusive');
|
|
3244
|
+
return 1;
|
|
3245
|
+
}
|
|
3246
|
+
const provenance: ProvenanceMode = flags.has('provenance') ? 'on' : flags.has('no-provenance') ? 'off' : 'auto';
|
|
3247
|
+
try {
|
|
3248
|
+
write(`dz publish: ${decideProvenance(provenance, process.env).reason}`);
|
|
3249
|
+
} catch (err) {
|
|
3250
|
+
write((err as Error).message);
|
|
3251
|
+
return 1;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3013
3254
|
// Pre-publish claim-check gate strictness: reject (never coerce) an invalid value. Default 'warn'
|
|
3014
3255
|
// per ADR-001 — findings are SURFACED on every publish, but 'warn' never changes publish status,
|
|
3015
3256
|
// so the success path is unchanged. 'off' disables the gate; 'error' fails an offending package.
|
|
@@ -3065,7 +3306,46 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
3065
3306
|
write(`╚══════════════════════════════════════════════════════════════════════╝`);
|
|
3066
3307
|
}
|
|
3067
3308
|
|
|
3068
|
-
|
|
3309
|
+
// FR-4 — the signature gate, BEFORE anything is published (ADR-001).
|
|
3310
|
+
// Strictness follows the trust root: with no keys/dz.pub committed there is nothing to verify
|
|
3311
|
+
// against, and blocking would refuse every release forever. That is stated on every run.
|
|
3312
|
+
{
|
|
3313
|
+
const trustRoot = resolve(cwd, TRUST_ROOT_REL);
|
|
3314
|
+
const trustRootPresent = existsSync(trustRoot);
|
|
3315
|
+
const requireSigning = flags.has('require-signing');
|
|
3316
|
+
// `filter` is a string[] of substrings (matching publishPackages' own semantics), not a string.
|
|
3317
|
+
const targets = discoverPackages(cwd).filter(
|
|
3318
|
+
(pk) => !filter || filter.length === 0 || filter.some((f) => pk.name.includes(f)),
|
|
3319
|
+
);
|
|
3320
|
+
let blocked = 0;
|
|
3321
|
+
|
|
3322
|
+
for (const pk of targets) {
|
|
3323
|
+
const manifestPath = join(pk.dir, MANIFEST_NAME);
|
|
3324
|
+
const manifestPresent = existsSync(manifestPath);
|
|
3325
|
+
let verifyOk = false;
|
|
3326
|
+
if (trustRootPresent && manifestPresent) {
|
|
3327
|
+
try {
|
|
3328
|
+
const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
3329
|
+
verifyOk = verifyManifest(pk.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
|
|
3330
|
+
} catch {
|
|
3331
|
+
verifyOk = false;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning });
|
|
3335
|
+
if (decision.action === 'block') {
|
|
3336
|
+
write(`dz publish: BLOCKED ${pk.name} — ${decision.reason}`);
|
|
3337
|
+
blocked++;
|
|
3338
|
+
} else if (decision.action === 'publish-unsigned') {
|
|
3339
|
+
write(`dz publish: ${pk.name} — ${decision.reason}`);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
if (blocked > 0) {
|
|
3343
|
+
write(`dz publish: refusing to publish (${blocked} package(s) failed the signature gate)`);
|
|
3344
|
+
return 1;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
const report = publishPackages(cwd, { provenance, dryRun, filter, bumpOnly, claimGate: claimCheckOpt });
|
|
3069
3349
|
|
|
3070
3350
|
write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
|
|
3071
3351
|
write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
|
|
@@ -4094,7 +4374,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
4094
4374
|
case 'migrate':
|
|
4095
4375
|
return cmdMigrate(options, cwd, write);
|
|
4096
4376
|
case 'doctor':
|
|
4097
|
-
return await cmdDoctor(options, cwd, write);
|
|
4377
|
+
return await cmdDoctor(options, flags, cwd, write);
|
|
4098
4378
|
case 'install':
|
|
4099
4379
|
return await cmdInstall(options, flags, cwd, write);
|
|
4100
4380
|
case 'bundle':
|
|
@@ -4115,6 +4395,10 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
4115
4395
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
4116
4396
|
case 'claim-check':
|
|
4117
4397
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
4398
|
+
case 'sign':
|
|
4399
|
+
return cmdSign(options, flags, cwd, write);
|
|
4400
|
+
case 'verify-pack':
|
|
4401
|
+
return cmdVerifyPack(options, flags, cwd, write);
|
|
4118
4402
|
case 'setup':
|
|
4119
4403
|
return await cmdSetup(options, flags, cwd, write);
|
|
4120
4404
|
case 'pretrain':
|
|
@@ -4126,7 +4410,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
4126
4410
|
case 'recommend':
|
|
4127
4411
|
return cmdRecommend(options, cwd, write);
|
|
4128
4412
|
case 'upgrade':
|
|
4129
|
-
return cmdUpgrade(options, cwd, write);
|
|
4413
|
+
return cmdUpgrade(options, flags, cwd, write);
|
|
4130
4414
|
case 'auto-canonicalize':
|
|
4131
4415
|
return await cmdAutoCanonicalize(options, cwd, write);
|
|
4132
4416
|
case 'publish':
|