@blamejs/exceptd-skills 0.12.18 → 0.12.20
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/CHANGELOG.md +126 -52
- package/README.md +1 -1
- package/bin/exceptd.js +355 -45
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/ai-api.json +2 -1
- package/lib/auto-discovery.js +62 -9
- package/lib/playbook-runner.js +188 -32
- package/lib/prefetch.js +62 -15
- package/lib/refresh-external.js +27 -0
- package/lib/refresh-network.js +91 -3
- package/lib/sign.js +10 -2
- package/lib/verify.js +26 -4
- package/manifest.json +40 -41
- package/orchestrator/scheduler.js +10 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/scripts/predeploy.js +5 -0
- package/scripts/verify-shipped-tarball.js +89 -0
|
@@ -58,6 +58,65 @@ function normalizeSkillBytes(buf) {
|
|
|
58
58
|
return Buffer.from(s.replace(/\r\n/g, "\n"), "utf8");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Audit O P1-C: in-line manifest-signature verifier for the extracted
|
|
62
|
+
// tarball. Kept here (rather than imported) for the same defense-in-depth
|
|
63
|
+
// reasoning as normalizeSkillBytes: a bug in lib/verify.js's verifier
|
|
64
|
+
// should not also disable this gate (we want at least one independent
|
|
65
|
+
// check). The canonical-bytes computation MUST stay in lockstep with
|
|
66
|
+
// lib/sign.js + lib/verify.js + lib/refresh-network.js — enforced by
|
|
67
|
+
// tests/normalize-contract.test.js.
|
|
68
|
+
function canonicalizeForTarball(value) {
|
|
69
|
+
if (Array.isArray(value)) return value.map(canonicalizeForTarball);
|
|
70
|
+
if (value && typeof value === "object") {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const key of Object.keys(value).sort()) {
|
|
73
|
+
out[key] = canonicalizeForTarball(value[key]);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function canonicalManifestBytesForTarball(manifest) {
|
|
80
|
+
const clone = Object.assign({}, manifest);
|
|
81
|
+
delete clone.manifest_signature;
|
|
82
|
+
const cryptoMod = require("crypto"); // eslint-disable-line no-unused-vars
|
|
83
|
+
const json = JSON.stringify(canonicalizeForTarball(clone), null, 2);
|
|
84
|
+
return normalizeSkillBytes(Buffer.from(json, "utf8"));
|
|
85
|
+
}
|
|
86
|
+
function verifyExtractedManifestSignature(manifest, publicKeyPem) {
|
|
87
|
+
const cryptoMod = require("crypto");
|
|
88
|
+
const sig = manifest && manifest.manifest_signature;
|
|
89
|
+
if (!sig || typeof sig !== "object") return { status: "missing" };
|
|
90
|
+
if (typeof sig.signature_base64 !== "string") {
|
|
91
|
+
return { status: "invalid", reason: "manifest_signature.signature_base64 missing or not a string" };
|
|
92
|
+
}
|
|
93
|
+
if (sig.algorithm !== "Ed25519") {
|
|
94
|
+
return { status: "invalid", reason: `manifest_signature.algorithm must be 'Ed25519' (got ${JSON.stringify(sig.algorithm)})` };
|
|
95
|
+
}
|
|
96
|
+
let signatureBytes;
|
|
97
|
+
try { signatureBytes = Buffer.from(sig.signature_base64, "base64"); }
|
|
98
|
+
catch (e) { return { status: "invalid", reason: `malformed base64: ${e.message}` }; }
|
|
99
|
+
const bytes = canonicalManifestBytesForTarball(manifest);
|
|
100
|
+
let ok = false;
|
|
101
|
+
try {
|
|
102
|
+
ok = cryptoMod.verify(null, bytes, {
|
|
103
|
+
key: publicKeyPem,
|
|
104
|
+
dsaEncoding: "ieee-p1363",
|
|
105
|
+
}, signatureBytes);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { status: "invalid", reason: `crypto.verify threw: ${e.message}` };
|
|
108
|
+
}
|
|
109
|
+
return ok ? { status: "valid" } : { status: "invalid", reason: "Ed25519 manifest signature did not verify against extracted public.pem" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Audit P P1-A: exported so tests/normalize-contract.test.js can assert
|
|
113
|
+
// byte-identical normalize() behavior across all four implementations.
|
|
114
|
+
module.exports = {
|
|
115
|
+
normalizeSkillBytes,
|
|
116
|
+
verifyExtractedManifestSignature,
|
|
117
|
+
canonicalManifestBytesForTarball,
|
|
118
|
+
};
|
|
119
|
+
|
|
61
120
|
const ROOT = path.resolve(__dirname, "..");
|
|
62
121
|
|
|
63
122
|
function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
|
|
@@ -66,6 +125,16 @@ function fail(msg, code = 1) {
|
|
|
66
125
|
process.exit(code);
|
|
67
126
|
}
|
|
68
127
|
|
|
128
|
+
// Audit P P1-A: gate the script body behind require.main === module so
|
|
129
|
+
// tests can `require()` this file to load the exported helpers (notably
|
|
130
|
+
// normalizeSkillBytes for the byte-stability contract test) without
|
|
131
|
+
// invoking npm pack as a side effect of import.
|
|
132
|
+
if (require.main !== module) {
|
|
133
|
+
// Loaded as a library (e.g. by tests/normalize-contract.test.js).
|
|
134
|
+
// Skip the script body; consumers use the module.exports surface above.
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
69
138
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "verify-shipped-"));
|
|
70
139
|
try {
|
|
71
140
|
emit(`packing into ${tmpRoot} ...`);
|
|
@@ -178,6 +247,26 @@ try {
|
|
|
178
247
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
179
248
|
const pubPem = fs.readFileSync(pubKeyPath, "utf8");
|
|
180
249
|
const pubKey = crypto.createPublicKey(pubPem);
|
|
250
|
+
|
|
251
|
+
// Audit O P1-C: verify the top-level manifest_signature on the
|
|
252
|
+
// EXTRACTED manifest.json. Per-skill signatures only sign the skill body
|
|
253
|
+
// bytes — they do not sign skill.name / skill.path / skill.atlas_refs or
|
|
254
|
+
// any other manifest envelope metadata. A tarball whose body bytes are
|
|
255
|
+
// signed but whose manifest envelope was rewritten (re-routing a skill
|
|
256
|
+
// path, renaming a skill, changing atlas refs) would pass per-skill
|
|
257
|
+
// verification but fail this gate. v0.12.17+ shipped tarballs always
|
|
258
|
+
// include manifest_signature, so a missing signature here is also a
|
|
259
|
+
// refusal (the audit's stricter posture vs. the post-install warn-and-
|
|
260
|
+
// continue path, which tolerates legacy v0.12.16-and-earlier installs).
|
|
261
|
+
const manifestSigStatus = verifyExtractedManifestSignature(manifest, pubPem);
|
|
262
|
+
if (manifestSigStatus.status !== "valid") {
|
|
263
|
+
fail(
|
|
264
|
+
`tarball manifest_signature ${manifestSigStatus.status} — refusing to publish. ` +
|
|
265
|
+
`reason=${manifestSigStatus.reason || "(none)"}`,
|
|
266
|
+
1
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
emit(`manifest envelope signature: valid (Ed25519, signed by extracted public.pem)`);
|
|
181
270
|
const pubFp = crypto.createHash("sha256")
|
|
182
271
|
.update(pubKey.export({ type: "spki", format: "der" }))
|
|
183
272
|
.digest("base64");
|