jorgex-stack 1.4.0 → 1.5.0
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/cli.js +903 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import * as p6 from "@clack/prompts";
|
|
5
|
+
import fs24 from "fs";
|
|
5
6
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6
7
|
|
|
7
8
|
// src/install.ts
|
|
@@ -5192,7 +5193,7 @@ function readOptional(file, fallback) {
|
|
|
5192
5193
|
}
|
|
5193
5194
|
function hashPiTarball(file) {
|
|
5194
5195
|
const descriptor = fs23.openSync(file, "r");
|
|
5195
|
-
const
|
|
5196
|
+
const sha2562 = createHash("sha256");
|
|
5196
5197
|
const sha512 = createHash("sha512");
|
|
5197
5198
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
5198
5199
|
let bytes = 0;
|
|
@@ -5202,13 +5203,13 @@ function hashPiTarball(file) {
|
|
|
5202
5203
|
if (read === 0) break;
|
|
5203
5204
|
bytes += read;
|
|
5204
5205
|
const chunk = buffer.subarray(0, read);
|
|
5205
|
-
|
|
5206
|
+
sha2562.update(chunk);
|
|
5206
5207
|
sha512.update(chunk);
|
|
5207
5208
|
}
|
|
5208
5209
|
} finally {
|
|
5209
5210
|
fs23.closeSync(descriptor);
|
|
5210
5211
|
}
|
|
5211
|
-
return { path: file, bytes, sha256:
|
|
5212
|
+
return { path: file, bytes, sha256: sha2562.digest("hex"), sha512: sha512.digest("hex") };
|
|
5212
5213
|
}
|
|
5213
5214
|
async function acquirePiTarball(destination) {
|
|
5214
5215
|
const existing = fs23.statSync(destination, { throwIfNoEntry: false });
|
|
@@ -5443,9 +5444,848 @@ async function runManagedPiSystem(input) {
|
|
|
5443
5444
|
});
|
|
5444
5445
|
}
|
|
5445
5446
|
|
|
5447
|
+
// src/lib/quality-runner.ts
|
|
5448
|
+
import path33 from "path";
|
|
5449
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
5450
|
+
|
|
5451
|
+
// src/lib/quality-policy.ts
|
|
5452
|
+
var QUALITY_PROFILES = ["routine", "elevated", "high", "release"];
|
|
5453
|
+
function hasText(value) {
|
|
5454
|
+
return typeof value === "string" && value.trim() !== "";
|
|
5455
|
+
}
|
|
5456
|
+
function isQualityProfile(value) {
|
|
5457
|
+
return typeof value === "string" && QUALITY_PROFILES.includes(value);
|
|
5458
|
+
}
|
|
5459
|
+
function mergeStatus(current, next) {
|
|
5460
|
+
if (current === "fail" || next === "fail") return "fail";
|
|
5461
|
+
if (current === "incomplete" || next === "incomplete") return "incomplete";
|
|
5462
|
+
return "pass";
|
|
5463
|
+
}
|
|
5464
|
+
function resultStatus(result) {
|
|
5465
|
+
switch (result.status) {
|
|
5466
|
+
case "fail":
|
|
5467
|
+
return "fail";
|
|
5468
|
+
case "incomplete":
|
|
5469
|
+
return "incomplete";
|
|
5470
|
+
case "not-applicable":
|
|
5471
|
+
return "pass";
|
|
5472
|
+
case "pass":
|
|
5473
|
+
return hasText(result.evidence) ? "pass" : "incomplete";
|
|
5474
|
+
default:
|
|
5475
|
+
return "incomplete";
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
function evaluateQualityPolicy(input) {
|
|
5479
|
+
const requestedProfile = input.profile === void 0 ? "routine" : input.profile;
|
|
5480
|
+
if (!isQualityProfile(requestedProfile)) {
|
|
5481
|
+
throw new Error(`Unknown quality profile: ${String(requestedProfile)}`);
|
|
5482
|
+
}
|
|
5483
|
+
const controls = /* @__PURE__ */ new Map();
|
|
5484
|
+
let status = "pass";
|
|
5485
|
+
let hasRequired = false;
|
|
5486
|
+
for (const control of input.controls) {
|
|
5487
|
+
if (!hasText(control.id) || control.requirement !== "required" && control.requirement !== "optional" || controls.has(control.id)) {
|
|
5488
|
+
status = mergeStatus(status, "incomplete");
|
|
5489
|
+
continue;
|
|
5490
|
+
}
|
|
5491
|
+
controls.set(control.id, control);
|
|
5492
|
+
if (control.requirement === "required") hasRequired = true;
|
|
5493
|
+
}
|
|
5494
|
+
const results = /* @__PURE__ */ new Map();
|
|
5495
|
+
for (const result of input.results) {
|
|
5496
|
+
const control = controls.get(result.controlId);
|
|
5497
|
+
if (!control || results.has(result.controlId)) {
|
|
5498
|
+
status = mergeStatus(status, "incomplete");
|
|
5499
|
+
}
|
|
5500
|
+
if (control && !results.has(result.controlId)) {
|
|
5501
|
+
results.set(result.controlId, result);
|
|
5502
|
+
}
|
|
5503
|
+
if (control && result.status === "not-applicable") {
|
|
5504
|
+
const validException = control.requirement === "optional" && control.notApplicable === true && hasText(result.reason);
|
|
5505
|
+
status = mergeStatus(status, validException ? "pass" : "incomplete");
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
if (control) status = mergeStatus(status, resultStatus(result));
|
|
5509
|
+
}
|
|
5510
|
+
if (!hasRequired) status = mergeStatus(status, "incomplete");
|
|
5511
|
+
for (const control of controls.values()) {
|
|
5512
|
+
if (control.requirement !== "required") continue;
|
|
5513
|
+
const result = results.get(control.id);
|
|
5514
|
+
if (!result) {
|
|
5515
|
+
status = mergeStatus(status, "incomplete");
|
|
5516
|
+
continue;
|
|
5517
|
+
}
|
|
5518
|
+
if (result.status === "not-applicable") {
|
|
5519
|
+
status = mergeStatus(status, "incomplete");
|
|
5520
|
+
} else {
|
|
5521
|
+
status = mergeStatus(status, resultStatus(result));
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
return { profile: requestedProfile, status };
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
// src/lib/quality-receipt.ts
|
|
5528
|
+
import { createHash as createHash2 } from "crypto";
|
|
5529
|
+
var QUALITY_RECEIPT_NAMESPACE = "jorgex.quality.receipt";
|
|
5530
|
+
var QUALITY_RECEIPT_VERSION = 1;
|
|
5531
|
+
var COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
5532
|
+
var SHA256_PATTERN = /^[0-9a-f]{64}$/i;
|
|
5533
|
+
var MAX_EXCERPT_LENGTH = 512;
|
|
5534
|
+
var REDACTED = "[REDACTED]";
|
|
5535
|
+
var QUALITY_RESULT_STATUSES = [
|
|
5536
|
+
"pass",
|
|
5537
|
+
"fail",
|
|
5538
|
+
"incomplete",
|
|
5539
|
+
"not-applicable"
|
|
5540
|
+
];
|
|
5541
|
+
var SENSITIVE_FLAG_PATTERN = /^(?:-{1,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))$/i;
|
|
5542
|
+
var SENSITIVE_ASSIGNMENT_PATTERN = /^(?:-{0,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))\s*[=:]\s*.+$/i;
|
|
5543
|
+
var SENSITIVE_OUTPUT_PATTERN = /((?:authorization\s*:\s*bearer\s+|bearer\s+))[^\s,;]+/gi;
|
|
5544
|
+
var SENSITIVE_KEY_VALUE_PATTERN = /((?:^|(?<=[^\w.-]))(?:-{0,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\r\n,;]+)/gi;
|
|
5545
|
+
var SENSITIVE_SEPARATE_FLAG_PATTERN = /((?:^|(?<=[^\w.-]))-{1,2}(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))[ \t]+)(?:"[^"]*"|'[^']*'|[^\r\n,;]+)/gi;
|
|
5546
|
+
var SENSITIVE_STRUCTURED_VALUE_PATTERN = /((?:"|')?(?:token|password|api[-_]?key|access[-_]?token|_?auth(?:orization)?(?:[-_.]?token)?|aws[-_]?secret[-_]?access[-_]?key|private[-_]?key)(?:"|')?\s*:\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/gi;
|
|
5547
|
+
function isRecord2(value) {
|
|
5548
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5549
|
+
}
|
|
5550
|
+
function hasOwn(value, key) {
|
|
5551
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
5552
|
+
}
|
|
5553
|
+
function isDenseStringArray(value) {
|
|
5554
|
+
if (!Array.isArray(value)) return false;
|
|
5555
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
5556
|
+
if (!hasOwn(value, String(index)) || typeof value[index] !== "string") return false;
|
|
5557
|
+
}
|
|
5558
|
+
return true;
|
|
5559
|
+
}
|
|
5560
|
+
function assertExactKeys(value, allowed, label) {
|
|
5561
|
+
const allowedKeys = new Set(allowed);
|
|
5562
|
+
const unexpected = Object.keys(value).find((key) => !allowedKeys.has(key));
|
|
5563
|
+
if (unexpected !== void 0) {
|
|
5564
|
+
throw new Error(`Unexpected ${label} field: ${unexpected}`);
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
function requireRecord(value, label) {
|
|
5568
|
+
if (!isRecord2(value)) throw new Error(`Invalid ${label}`);
|
|
5569
|
+
return value;
|
|
5570
|
+
}
|
|
5571
|
+
function requireText(value, label) {
|
|
5572
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
5573
|
+
throw new Error(`Invalid ${label}`);
|
|
5574
|
+
}
|
|
5575
|
+
return value;
|
|
5576
|
+
}
|
|
5577
|
+
function isQualityProfile2(value) {
|
|
5578
|
+
return QUALITY_PROFILES.includes(value);
|
|
5579
|
+
}
|
|
5580
|
+
function isQualityReceiptResultStatus(value) {
|
|
5581
|
+
return QUALITY_RESULT_STATUSES.includes(value);
|
|
5582
|
+
}
|
|
5583
|
+
function requireSha(value, label, pattern) {
|
|
5584
|
+
const text2 = requireText(value, label);
|
|
5585
|
+
if (!pattern.test(text2)) throw new Error(`Invalid ${label}`);
|
|
5586
|
+
return text2;
|
|
5587
|
+
}
|
|
5588
|
+
function redactText(value) {
|
|
5589
|
+
return value.replace(SENSITIVE_STRUCTURED_VALUE_PATTERN, (_match, prefix, quotedValue) => {
|
|
5590
|
+
const quote = quotedValue[0];
|
|
5591
|
+
return `${prefix}${quote}${REDACTED}${quote}`;
|
|
5592
|
+
}).replace(SENSITIVE_OUTPUT_PATTERN, (_match, prefix) => {
|
|
5593
|
+
const normalizedPrefix = prefix.toLowerCase().startsWith("authorization") ? prefix.slice(0, prefix.toLowerCase().indexOf("bearer") + "bearer".length) : "Bearer ";
|
|
5594
|
+
return `${normalizedPrefix}${REDACTED}`;
|
|
5595
|
+
}).replace(SENSITIVE_KEY_VALUE_PATTERN, `$1${REDACTED}`).replace(SENSITIVE_SEPARATE_FLAG_PATTERN, `$1${REDACTED}`);
|
|
5596
|
+
}
|
|
5597
|
+
function redactAssignment(value) {
|
|
5598
|
+
const separator = value.indexOf("=") >= 0 ? "=" : ":";
|
|
5599
|
+
const name = value.slice(0, value.indexOf(separator));
|
|
5600
|
+
return `${name}${separator}${REDACTED}`;
|
|
5601
|
+
}
|
|
5602
|
+
function redactArgv(argv) {
|
|
5603
|
+
if (!isDenseStringArray(argv)) throw new Error("Invalid argv: sparse or non-string array");
|
|
5604
|
+
let redactNext = false;
|
|
5605
|
+
return argv.map((argument) => {
|
|
5606
|
+
if (redactNext) {
|
|
5607
|
+
redactNext = false;
|
|
5608
|
+
return REDACTED;
|
|
5609
|
+
}
|
|
5610
|
+
if (SENSITIVE_FLAG_PATTERN.test(argument)) {
|
|
5611
|
+
redactNext = true;
|
|
5612
|
+
return argument;
|
|
5613
|
+
}
|
|
5614
|
+
if (SENSITIVE_ASSIGNMENT_PATTERN.test(argument)) {
|
|
5615
|
+
return redactAssignment(argument);
|
|
5616
|
+
}
|
|
5617
|
+
return redactText(argument);
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
function excerptFor(output) {
|
|
5621
|
+
const stdout = redactText(output.stdout);
|
|
5622
|
+
const stderr = redactText(output.stderr);
|
|
5623
|
+
const combined = [stdout, stderr].filter((part) => part !== "").join("\n");
|
|
5624
|
+
return Array.from(combined).slice(0, MAX_EXCERPT_LENGTH).join("");
|
|
5625
|
+
}
|
|
5626
|
+
function outputDigestFor(output) {
|
|
5627
|
+
const sanitized = {
|
|
5628
|
+
stdout: redactText(output.stdout),
|
|
5629
|
+
stderr: redactText(output.stderr)
|
|
5630
|
+
};
|
|
5631
|
+
return sha256(canonicalJson(sanitized));
|
|
5632
|
+
}
|
|
5633
|
+
function normalizeCommand(command) {
|
|
5634
|
+
return {
|
|
5635
|
+
commandId: command.commandId,
|
|
5636
|
+
executable: command.executable,
|
|
5637
|
+
argv: redactArgv(command.argv),
|
|
5638
|
+
exitCode: command.exitCode,
|
|
5639
|
+
durationMs: command.durationMs,
|
|
5640
|
+
excerpt: excerptFor(command.output),
|
|
5641
|
+
outputDigest: outputDigestFor(command.output)
|
|
5642
|
+
};
|
|
5643
|
+
}
|
|
5644
|
+
function normalizeResult(result) {
|
|
5645
|
+
if (result.status === "pass") {
|
|
5646
|
+
return {
|
|
5647
|
+
controlId: result.controlId,
|
|
5648
|
+
status: "pass",
|
|
5649
|
+
evidence: result.evidence,
|
|
5650
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
return {
|
|
5654
|
+
controlId: result.controlId,
|
|
5655
|
+
status: result.status,
|
|
5656
|
+
...result.evidence === void 0 ? {} : { evidence: result.evidence },
|
|
5657
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
5658
|
+
};
|
|
5659
|
+
}
|
|
5660
|
+
function normalizeProvenance(provenance) {
|
|
5661
|
+
return {
|
|
5662
|
+
issuer: provenance.issuer,
|
|
5663
|
+
executionId: provenance.executionId,
|
|
5664
|
+
evidenceLocator: provenance.evidenceLocator,
|
|
5665
|
+
evidenceDigest: provenance.evidenceDigest
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
function canonicalValue(value) {
|
|
5669
|
+
if (value === null) return "null";
|
|
5670
|
+
switch (typeof value) {
|
|
5671
|
+
case "string": {
|
|
5672
|
+
const result = JSON.stringify(value);
|
|
5673
|
+
if (result === void 0) throw new Error("Unable to canonicalize string");
|
|
5674
|
+
return result;
|
|
5675
|
+
}
|
|
5676
|
+
case "boolean":
|
|
5677
|
+
return value ? "true" : "false";
|
|
5678
|
+
case "number": {
|
|
5679
|
+
if (!Number.isFinite(value)) throw new Error("Cannot canonicalize non-finite number");
|
|
5680
|
+
const result = JSON.stringify(value);
|
|
5681
|
+
if (result === void 0) throw new Error("Unable to canonicalize number");
|
|
5682
|
+
return result;
|
|
5683
|
+
}
|
|
5684
|
+
case "object": {
|
|
5685
|
+
if (Array.isArray(value)) {
|
|
5686
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
5687
|
+
if (!hasOwn(value, String(index))) {
|
|
5688
|
+
throw new Error("Cannot canonicalize sparse array");
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
return `[${value.map((item) => canonicalValue(item)).join(",")}]`;
|
|
5692
|
+
}
|
|
5693
|
+
const prototype = Object.getPrototypeOf(value);
|
|
5694
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
5695
|
+
throw new Error("Cannot canonicalize non-plain object");
|
|
5696
|
+
}
|
|
5697
|
+
const object = value;
|
|
5698
|
+
return `{${Object.keys(object).sort().map((key) => {
|
|
5699
|
+
return `${JSON.stringify(key)}:${canonicalValue(object[key])}`;
|
|
5700
|
+
}).join(",")}}`;
|
|
5701
|
+
}
|
|
5702
|
+
default:
|
|
5703
|
+
throw new Error(`Cannot canonicalize ${typeof value}`);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
function canonicalJson(value) {
|
|
5707
|
+
return canonicalValue(value);
|
|
5708
|
+
}
|
|
5709
|
+
function sha256(value) {
|
|
5710
|
+
return createHash2("sha256").update(value, "utf8").digest("hex");
|
|
5711
|
+
}
|
|
5712
|
+
function validateIdentity(value, expected) {
|
|
5713
|
+
const identity = requireRecord(value, "identity");
|
|
5714
|
+
assertExactKeys(identity, ["profile", "baseSha", "headSha", "policyDigest"], "identity");
|
|
5715
|
+
const profile = requireText(identity.profile, "identity.profile");
|
|
5716
|
+
if (!isQualityProfile2(profile)) throw new Error(`Invalid identity.profile: ${profile}`);
|
|
5717
|
+
const baseSha = requireSha(identity.baseSha, "identity.baseSha", COMMIT_SHA_PATTERN);
|
|
5718
|
+
const headSha = requireSha(identity.headSha, "identity.headSha", COMMIT_SHA_PATTERN);
|
|
5719
|
+
const policyDigest = requireSha(identity.policyDigest, "identity.policyDigest", SHA256_PATTERN);
|
|
5720
|
+
const actual = { profile, baseSha, headSha, policyDigest };
|
|
5721
|
+
if (expected !== void 0) {
|
|
5722
|
+
for (const field of ["profile", "baseSha", "headSha", "policyDigest"]) {
|
|
5723
|
+
if (actual[field] !== expected[field]) {
|
|
5724
|
+
throw new Error(`Quality receipt identity mismatch: ${field}`);
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
return actual;
|
|
5729
|
+
}
|
|
5730
|
+
function validateProvenance(value) {
|
|
5731
|
+
const provenance = requireRecord(value, "provenance");
|
|
5732
|
+
assertExactKeys(provenance, ["issuer", "executionId", "evidenceLocator", "evidenceDigest"], "provenance");
|
|
5733
|
+
const issuer = requireText(provenance.issuer, "provenance.issuer");
|
|
5734
|
+
const executionId = requireText(provenance.executionId, "provenance.executionId");
|
|
5735
|
+
const evidenceLocator = requireText(provenance.evidenceLocator, "provenance.evidenceLocator");
|
|
5736
|
+
if (!/^https?:\/\/\S+$/.test(evidenceLocator)) {
|
|
5737
|
+
throw new Error("Invalid provenance.evidenceLocator");
|
|
5738
|
+
}
|
|
5739
|
+
try {
|
|
5740
|
+
const parsed = new URL(evidenceLocator);
|
|
5741
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
5742
|
+
throw new Error("unsupported locator protocol");
|
|
5743
|
+
}
|
|
5744
|
+
} catch {
|
|
5745
|
+
throw new Error("Invalid provenance.evidenceLocator");
|
|
5746
|
+
}
|
|
5747
|
+
const evidenceDigest = requireSha(provenance.evidenceDigest, "provenance.evidenceDigest", SHA256_PATTERN);
|
|
5748
|
+
return { issuer, executionId, evidenceLocator, evidenceDigest };
|
|
5749
|
+
}
|
|
5750
|
+
function validateCommand(value, index) {
|
|
5751
|
+
const command = requireRecord(value, `commands[${index}]`);
|
|
5752
|
+
assertExactKeys(
|
|
5753
|
+
command,
|
|
5754
|
+
["commandId", "executable", "argv", "exitCode", "durationMs", "excerpt", "outputDigest"],
|
|
5755
|
+
`commands[${index}]`
|
|
5756
|
+
);
|
|
5757
|
+
const commandId = requireText(command.commandId, `commands[${index}].commandId`);
|
|
5758
|
+
const executable = requireText(command.executable, `commands[${index}].executable`);
|
|
5759
|
+
if (!isDenseStringArray(command.argv)) {
|
|
5760
|
+
throw new Error(`Invalid commands[${index}].argv`);
|
|
5761
|
+
}
|
|
5762
|
+
if (typeof command.exitCode !== "number" || !Number.isInteger(command.exitCode)) {
|
|
5763
|
+
throw new Error(`Invalid commands[${index}].exitCode`);
|
|
5764
|
+
}
|
|
5765
|
+
if (typeof command.durationMs !== "number" || !Number.isFinite(command.durationMs) || command.durationMs < 0) {
|
|
5766
|
+
throw new Error(`Invalid commands[${index}].durationMs`);
|
|
5767
|
+
}
|
|
5768
|
+
if (!hasOwn(command, "excerpt") || typeof command.excerpt !== "string") {
|
|
5769
|
+
throw new Error(`Invalid commands[${index}].excerpt`);
|
|
5770
|
+
}
|
|
5771
|
+
const excerpt = command.excerpt;
|
|
5772
|
+
if (Array.from(excerpt).length > MAX_EXCERPT_LENGTH) throw new Error(`Invalid commands[${index}].excerpt`);
|
|
5773
|
+
if (redactText(excerpt) !== excerpt) {
|
|
5774
|
+
throw new Error(`Invalid commands[${index}].excerpt: contains an unredacted secret`);
|
|
5775
|
+
}
|
|
5776
|
+
const argv = [...command.argv];
|
|
5777
|
+
const sanitizedArgv = redactArgv(argv);
|
|
5778
|
+
if (sanitizedArgv.some((argument, argumentIndex) => argument !== argv[argumentIndex])) {
|
|
5779
|
+
throw new Error(`Invalid commands[${index}].argv: contains an unredacted secret`);
|
|
5780
|
+
}
|
|
5781
|
+
const outputDigest = requireSha(command.outputDigest, `commands[${index}].outputDigest`, SHA256_PATTERN);
|
|
5782
|
+
return {
|
|
5783
|
+
commandId,
|
|
5784
|
+
executable,
|
|
5785
|
+
argv,
|
|
5786
|
+
exitCode: command.exitCode,
|
|
5787
|
+
durationMs: command.durationMs,
|
|
5788
|
+
excerpt,
|
|
5789
|
+
outputDigest
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
function validateResult(value, index) {
|
|
5793
|
+
const result = requireRecord(value, `results[${index}]`);
|
|
5794
|
+
assertExactKeys(result, ["controlId", "status", "evidence", "reason"], `results[${index}]`);
|
|
5795
|
+
const controlId = requireText(result.controlId, `results[${index}].controlId`);
|
|
5796
|
+
const status = result.status;
|
|
5797
|
+
if (typeof status !== "string" || !isQualityReceiptResultStatus(status)) {
|
|
5798
|
+
throw new Error(`Invalid results[${index}].status`);
|
|
5799
|
+
}
|
|
5800
|
+
let evidence;
|
|
5801
|
+
if (hasOwn(result, "evidence")) {
|
|
5802
|
+
if (typeof result.evidence !== "string") {
|
|
5803
|
+
throw new Error(`Invalid results[${index}].evidence`);
|
|
5804
|
+
}
|
|
5805
|
+
evidence = result.evidence;
|
|
5806
|
+
}
|
|
5807
|
+
let reason;
|
|
5808
|
+
if (hasOwn(result, "reason")) {
|
|
5809
|
+
if (typeof result.reason !== "string") {
|
|
5810
|
+
throw new Error(`Invalid results[${index}].reason`);
|
|
5811
|
+
}
|
|
5812
|
+
reason = result.reason;
|
|
5813
|
+
}
|
|
5814
|
+
if (status === "pass") {
|
|
5815
|
+
if (evidence === void 0 || evidence.trim() === "") {
|
|
5816
|
+
throw new Error(`Invalid results[${index}].evidence: pass requires evidence`);
|
|
5817
|
+
}
|
|
5818
|
+
return {
|
|
5819
|
+
controlId,
|
|
5820
|
+
status,
|
|
5821
|
+
evidence,
|
|
5822
|
+
...reason === void 0 ? {} : { reason }
|
|
5823
|
+
};
|
|
5824
|
+
}
|
|
5825
|
+
return {
|
|
5826
|
+
controlId,
|
|
5827
|
+
status,
|
|
5828
|
+
...evidence === void 0 ? {} : { evidence },
|
|
5829
|
+
...reason === void 0 ? {} : { reason }
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
function validateQualityReceipt(value, expectedIdentity) {
|
|
5833
|
+
const receipt = requireRecord(value, "quality receipt");
|
|
5834
|
+
if (receipt.namespace !== QUALITY_RECEIPT_NAMESPACE) {
|
|
5835
|
+
throw new Error(`Invalid quality receipt namespace: ${String(receipt.namespace)}`);
|
|
5836
|
+
}
|
|
5837
|
+
if (receipt.version !== QUALITY_RECEIPT_VERSION) {
|
|
5838
|
+
throw new Error(`Unsupported quality receipt version: ${String(receipt.version)}`);
|
|
5839
|
+
}
|
|
5840
|
+
assertExactKeys(receipt, ["namespace", "version", "authority", "identity", "commands", "results", "provenance"], "receipt");
|
|
5841
|
+
if (receipt.authority !== "local" && receipt.authority !== "enforced") {
|
|
5842
|
+
throw new Error(`Invalid quality receipt authority: ${String(receipt.authority)}`);
|
|
5843
|
+
}
|
|
5844
|
+
const identity = validateIdentity(receipt.identity, expectedIdentity);
|
|
5845
|
+
if (!Array.isArray(receipt.commands)) throw new Error("Invalid quality receipt commands");
|
|
5846
|
+
if (!Array.isArray(receipt.results)) throw new Error("Invalid quality receipt results");
|
|
5847
|
+
for (let index = 0; index < receipt.commands.length; index += 1) {
|
|
5848
|
+
validateCommand(receipt.commands[index], index);
|
|
5849
|
+
}
|
|
5850
|
+
for (let index = 0; index < receipt.results.length; index += 1) {
|
|
5851
|
+
validateResult(receipt.results[index], index);
|
|
5852
|
+
}
|
|
5853
|
+
const hasProvenance = hasOwn(receipt, "provenance");
|
|
5854
|
+
if (receipt.authority === "enforced" && (!hasProvenance || receipt.provenance === void 0)) {
|
|
5855
|
+
throw new Error("Enforced quality receipts require provenance");
|
|
5856
|
+
}
|
|
5857
|
+
if (hasProvenance) {
|
|
5858
|
+
if (receipt.provenance === void 0) throw new Error("Invalid quality receipt provenance");
|
|
5859
|
+
validateProvenance(receipt.provenance);
|
|
5860
|
+
}
|
|
5861
|
+
void identity;
|
|
5862
|
+
}
|
|
5863
|
+
function createQualityReceipt(input) {
|
|
5864
|
+
const authority = input.authority;
|
|
5865
|
+
if (authority !== "local" && authority !== "enforced") {
|
|
5866
|
+
throw new Error(`Invalid quality receipt authority: ${String(authority)}`);
|
|
5867
|
+
}
|
|
5868
|
+
if (input.authority === "enforced" && input.provenance === void 0) {
|
|
5869
|
+
throw new Error("Enforced quality receipts require provenance");
|
|
5870
|
+
}
|
|
5871
|
+
const base = {
|
|
5872
|
+
namespace: QUALITY_RECEIPT_NAMESPACE,
|
|
5873
|
+
version: QUALITY_RECEIPT_VERSION,
|
|
5874
|
+
identity: {
|
|
5875
|
+
profile: input.identity.profile,
|
|
5876
|
+
baseSha: input.identity.baseSha,
|
|
5877
|
+
headSha: input.identity.headSha,
|
|
5878
|
+
policyDigest: input.identity.policyDigest
|
|
5879
|
+
},
|
|
5880
|
+
commands: input.commands.map(normalizeCommand),
|
|
5881
|
+
results: input.results.map(normalizeResult)
|
|
5882
|
+
};
|
|
5883
|
+
let receipt;
|
|
5884
|
+
if (input.authority === "enforced") {
|
|
5885
|
+
const provenance = input.provenance;
|
|
5886
|
+
if (provenance === void 0) throw new Error("Enforced quality receipts require provenance");
|
|
5887
|
+
receipt = {
|
|
5888
|
+
...base,
|
|
5889
|
+
authority: "enforced",
|
|
5890
|
+
provenance: normalizeProvenance(provenance)
|
|
5891
|
+
};
|
|
5892
|
+
} else {
|
|
5893
|
+
receipt = {
|
|
5894
|
+
...base,
|
|
5895
|
+
authority: "local",
|
|
5896
|
+
...input.provenance === void 0 ? {} : { provenance: normalizeProvenance(input.provenance) }
|
|
5897
|
+
};
|
|
5898
|
+
}
|
|
5899
|
+
validateQualityReceipt(receipt);
|
|
5900
|
+
return receipt;
|
|
5901
|
+
}
|
|
5902
|
+
function serializeQualityReceipt(receipt) {
|
|
5903
|
+
validateQualityReceipt(receipt);
|
|
5904
|
+
return canonicalJson(receipt);
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
// src/lib/quality-runner.ts
|
|
5908
|
+
function normalizeLimit(value, name) {
|
|
5909
|
+
if (value === void 0) return void 0;
|
|
5910
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
5911
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
5912
|
+
}
|
|
5913
|
+
return value;
|
|
5914
|
+
}
|
|
5915
|
+
var MAX_NODE_TIMEOUT_MS = 2147483647;
|
|
5916
|
+
var TERMINATION_GRACE_MS = 250;
|
|
5917
|
+
var TASKKILL_TIMEOUT_MS = 2e3;
|
|
5918
|
+
function normalizeTimeout(value) {
|
|
5919
|
+
const timeoutMs = normalizeLimit(value, "timeoutMs");
|
|
5920
|
+
if (timeoutMs !== void 0 && timeoutMs > MAX_NODE_TIMEOUT_MS) {
|
|
5921
|
+
throw new Error(`timeoutMs must not exceed ${MAX_NODE_TIMEOUT_MS}`);
|
|
5922
|
+
}
|
|
5923
|
+
return timeoutMs;
|
|
5924
|
+
}
|
|
5925
|
+
var COMMIT_SHA_PATTERN2 = /^[0-9a-f]{40}$/i;
|
|
5926
|
+
function isRecord3(value) {
|
|
5927
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5928
|
+
}
|
|
5929
|
+
function hasText2(value) {
|
|
5930
|
+
return typeof value === "string" && value.trim() !== "";
|
|
5931
|
+
}
|
|
5932
|
+
function isDenseStringArray2(value) {
|
|
5933
|
+
if (!Array.isArray(value)) return false;
|
|
5934
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
5935
|
+
if (!Object.prototype.hasOwnProperty.call(value, String(index)) || typeof value[index] !== "string") return false;
|
|
5936
|
+
}
|
|
5937
|
+
return true;
|
|
5938
|
+
}
|
|
5939
|
+
function isNonNegativeSafeInteger(value) {
|
|
5940
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
5941
|
+
}
|
|
5942
|
+
function isQualityProfile3(value) {
|
|
5943
|
+
return typeof value === "string" && QUALITY_PROFILES.includes(value);
|
|
5944
|
+
}
|
|
5945
|
+
function assertValidEnvironment(value, label) {
|
|
5946
|
+
if (!isRecord3(value)) throw new Error(`Invalid ${label}`);
|
|
5947
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
5948
|
+
if (typeof entry !== "string") throw new Error(`Invalid ${label}.${key}`);
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
function assertQualityPlanInput(value) {
|
|
5952
|
+
if (!isRecord3(value)) throw new Error("Invalid quality plan");
|
|
5953
|
+
if (!isRecord3(value.identity)) throw new Error("Invalid quality plan identity");
|
|
5954
|
+
if (typeof value.identity.baseSha !== "string" || !COMMIT_SHA_PATTERN2.test(value.identity.baseSha)) {
|
|
5955
|
+
throw new Error("Invalid quality plan identity.baseSha");
|
|
5956
|
+
}
|
|
5957
|
+
if (typeof value.identity.headSha !== "string" || !COMMIT_SHA_PATTERN2.test(value.identity.headSha)) {
|
|
5958
|
+
throw new Error("Invalid quality plan identity.headSha");
|
|
5959
|
+
}
|
|
5960
|
+
if (!isQualityProfile3(value.profile)) throw new Error("Invalid quality plan profile");
|
|
5961
|
+
if (!Array.isArray(value.controls)) throw new Error("Invalid quality plan controls");
|
|
5962
|
+
const controlIds = /* @__PURE__ */ new Set();
|
|
5963
|
+
for (let index = 0; index < value.controls.length; index += 1) {
|
|
5964
|
+
const control = value.controls[index];
|
|
5965
|
+
if (!isRecord3(control) || !hasText2(control.id) || control.requirement !== "required" && control.requirement !== "optional") {
|
|
5966
|
+
throw new Error(`Invalid quality plan controls[${index}]`);
|
|
5967
|
+
}
|
|
5968
|
+
if (controlIds.has(control.id)) {
|
|
5969
|
+
throw new Error(`Duplicate quality plan control id: ${control.id}`);
|
|
5970
|
+
}
|
|
5971
|
+
controlIds.add(control.id);
|
|
5972
|
+
if (Object.prototype.hasOwnProperty.call(control, "notApplicable") && typeof control.notApplicable !== "boolean") {
|
|
5973
|
+
throw new Error(`Invalid quality plan controls[${index}].notApplicable`);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
if (!Array.isArray(value.commands)) throw new Error("Invalid quality plan commands");
|
|
5977
|
+
const commandIds = /* @__PURE__ */ new Set();
|
|
5978
|
+
const commandControlIds = /* @__PURE__ */ new Set();
|
|
5979
|
+
for (let index = 0; index < value.commands.length; index += 1) {
|
|
5980
|
+
const command = value.commands[index];
|
|
5981
|
+
if (!isRecord3(command)) throw new Error(`Invalid quality plan commands[${index}]`);
|
|
5982
|
+
const timeoutMs = command.timeoutMs;
|
|
5983
|
+
if (!hasText2(command.controlId) || !hasText2(command.commandId) || !hasText2(command.executable) || !isDenseStringArray2(command.argv) || !isNonNegativeSafeInteger(timeoutMs)) {
|
|
5984
|
+
throw new Error(`Invalid quality plan commands[${index}]`);
|
|
5985
|
+
}
|
|
5986
|
+
if (timeoutMs > MAX_NODE_TIMEOUT_MS) {
|
|
5987
|
+
throw new Error(`Invalid quality plan commands[${index}].timeoutMs: maximum is ${MAX_NODE_TIMEOUT_MS}`);
|
|
5988
|
+
}
|
|
5989
|
+
if (!controlIds.has(command.controlId)) {
|
|
5990
|
+
throw new Error(`Unknown quality plan command control id: ${command.controlId}`);
|
|
5991
|
+
}
|
|
5992
|
+
if (commandIds.has(command.commandId)) {
|
|
5993
|
+
throw new Error(`Duplicate quality plan command id: ${command.commandId}`);
|
|
5994
|
+
}
|
|
5995
|
+
if (commandControlIds.has(command.controlId)) {
|
|
5996
|
+
throw new Error(`Multiple quality plan commands for control id: ${command.controlId}`);
|
|
5997
|
+
}
|
|
5998
|
+
commandIds.add(command.commandId);
|
|
5999
|
+
commandControlIds.add(command.controlId);
|
|
6000
|
+
if (command.maxOutputBytes !== void 0 && !isNonNegativeSafeInteger(command.maxOutputBytes)) {
|
|
6001
|
+
throw new Error(`Invalid quality plan commands[${index}].maxOutputBytes`);
|
|
6002
|
+
}
|
|
6003
|
+
if (command.env !== void 0) assertValidEnvironment(command.env, `quality plan commands[${index}].env`);
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
function appendOutput(captured, stream, chunk, maxOutputBytes) {
|
|
6007
|
+
if (maxOutputBytes === void 0) {
|
|
6008
|
+
captured[stream].push(chunk);
|
|
6009
|
+
return false;
|
|
6010
|
+
}
|
|
6011
|
+
const remaining = maxOutputBytes - captured.bytes;
|
|
6012
|
+
if (remaining <= 0) return chunk.length > 0;
|
|
6013
|
+
if (chunk.length > remaining) {
|
|
6014
|
+
captured[stream].push(chunk.subarray(0, remaining));
|
|
6015
|
+
captured.bytes += remaining;
|
|
6016
|
+
return true;
|
|
6017
|
+
}
|
|
6018
|
+
captured[stream].push(chunk);
|
|
6019
|
+
captured.bytes += chunk.length;
|
|
6020
|
+
return captured.bytes >= maxOutputBytes;
|
|
6021
|
+
}
|
|
6022
|
+
function decodeOutput(chunks, maxOutputBytes) {
|
|
6023
|
+
const output = Buffer.concat(chunks);
|
|
6024
|
+
if (maxOutputBytes === void 0 || output.length === 0) return output.toString("utf8");
|
|
6025
|
+
let end = output.length;
|
|
6026
|
+
let decoded = output.subarray(0, end).toString("utf8");
|
|
6027
|
+
while (Buffer.byteLength(decoded, "utf8") > maxOutputBytes && end > 0) {
|
|
6028
|
+
end -= 1;
|
|
6029
|
+
decoded = output.subarray(0, end).toString("utf8");
|
|
6030
|
+
}
|
|
6031
|
+
return decoded;
|
|
6032
|
+
}
|
|
6033
|
+
function killProcessTree(child) {
|
|
6034
|
+
const pid = child.pid;
|
|
6035
|
+
if (pid === void 0) return;
|
|
6036
|
+
if (process.platform === "win32") {
|
|
6037
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
|
|
6038
|
+
const taskkill = path33.join(systemRoot, "System32", "taskkill.exe");
|
|
6039
|
+
const result = spawnSync2(taskkill, ["/pid", String(pid), "/t", "/f"], {
|
|
6040
|
+
shell: false,
|
|
6041
|
+
stdio: "ignore",
|
|
6042
|
+
timeout: TASKKILL_TIMEOUT_MS,
|
|
6043
|
+
windowsHide: true
|
|
6044
|
+
});
|
|
6045
|
+
if (result.error !== void 0 || result.status !== 0) {
|
|
6046
|
+
try {
|
|
6047
|
+
child.kill("SIGKILL");
|
|
6048
|
+
} catch {
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
try {
|
|
6054
|
+
process.kill(-pid, "SIGKILL");
|
|
6055
|
+
} catch {
|
|
6056
|
+
try {
|
|
6057
|
+
child.kill("SIGKILL");
|
|
6058
|
+
} catch {
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
function resultFromCaptured(input, status, exitCode, startedAt, captured, maxOutputBytes, reason) {
|
|
6063
|
+
return {
|
|
6064
|
+
commandId: input.commandId,
|
|
6065
|
+
status,
|
|
6066
|
+
exitCode,
|
|
6067
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
6068
|
+
output: {
|
|
6069
|
+
stdout: decodeOutput(captured.stdout, maxOutputBytes),
|
|
6070
|
+
stderr: decodeOutput(captured.stderr, maxOutputBytes)
|
|
6071
|
+
},
|
|
6072
|
+
...reason === void 0 ? {} : { reason }
|
|
6073
|
+
};
|
|
6074
|
+
}
|
|
6075
|
+
async function runQualityCommand(input, deps = { terminate: killProcessTree }) {
|
|
6076
|
+
const timeoutMs = normalizeTimeout(input.timeoutMs);
|
|
6077
|
+
const maxOutputBytes = normalizeLimit(input.maxOutputBytes, "maxOutputBytes");
|
|
6078
|
+
if (timeoutMs === void 0) throw new Error("timeoutMs is required");
|
|
6079
|
+
const planned = planDetectedBinCommand(input.executable, [...input.argv]);
|
|
6080
|
+
const startedAt = Date.now();
|
|
6081
|
+
const captured = { stdout: [], stderr: [], bytes: 0 };
|
|
6082
|
+
if (planned === null) {
|
|
6083
|
+
return resultFromCaptured(input, "error", null, startedAt, captured, maxOutputBytes, "unsafe-command");
|
|
6084
|
+
}
|
|
6085
|
+
return new Promise((resolve) => {
|
|
6086
|
+
let settled = false;
|
|
6087
|
+
let terminationReason;
|
|
6088
|
+
let spawnError;
|
|
6089
|
+
let timeout;
|
|
6090
|
+
let terminationDeadline;
|
|
6091
|
+
let child;
|
|
6092
|
+
let cleanedUp = false;
|
|
6093
|
+
let onStdoutData = (_chunk) => {
|
|
6094
|
+
};
|
|
6095
|
+
let onStderrData = (_chunk) => {
|
|
6096
|
+
};
|
|
6097
|
+
let onError = (_error) => {
|
|
6098
|
+
};
|
|
6099
|
+
const onErrorSink = () => {
|
|
6100
|
+
};
|
|
6101
|
+
let onClose = (_exitCode) => {
|
|
6102
|
+
};
|
|
6103
|
+
const cleanupChild = () => {
|
|
6104
|
+
if (cleanedUp || child === void 0) return;
|
|
6105
|
+
cleanedUp = true;
|
|
6106
|
+
child.removeListener("close", onClose);
|
|
6107
|
+
child.removeListener("error", onError);
|
|
6108
|
+
child.on("error", onErrorSink);
|
|
6109
|
+
child.stdout?.removeListener("data", onStdoutData);
|
|
6110
|
+
child.stderr?.removeListener("data", onStderrData);
|
|
6111
|
+
child.stdout?.destroy();
|
|
6112
|
+
child.stderr?.destroy();
|
|
6113
|
+
child.unref();
|
|
6114
|
+
};
|
|
6115
|
+
const settle = (result) => {
|
|
6116
|
+
if (settled) return;
|
|
6117
|
+
settled = true;
|
|
6118
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
6119
|
+
if (terminationDeadline !== void 0) clearTimeout(terminationDeadline);
|
|
6120
|
+
cleanupChild();
|
|
6121
|
+
resolve(result);
|
|
6122
|
+
};
|
|
6123
|
+
try {
|
|
6124
|
+
child = spawn(planned.command, planned.args, {
|
|
6125
|
+
detached: true,
|
|
6126
|
+
env: input.env === void 0 ? {} : { ...input.env },
|
|
6127
|
+
shell: false,
|
|
6128
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6129
|
+
windowsHide: true
|
|
6130
|
+
});
|
|
6131
|
+
} catch (error) {
|
|
6132
|
+
settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
|
|
6133
|
+
return;
|
|
6134
|
+
}
|
|
6135
|
+
const stopFor = (reason) => {
|
|
6136
|
+
if (settled || terminationReason !== void 0) return;
|
|
6137
|
+
terminationReason = reason;
|
|
6138
|
+
try {
|
|
6139
|
+
deps.terminate(child);
|
|
6140
|
+
} catch {
|
|
6141
|
+
}
|
|
6142
|
+
if (settled) return;
|
|
6143
|
+
terminationDeadline = setTimeout(() => {
|
|
6144
|
+
try {
|
|
6145
|
+
deps.terminate(child);
|
|
6146
|
+
} catch {
|
|
6147
|
+
}
|
|
6148
|
+
cleanupChild();
|
|
6149
|
+
settle(resultFromCaptured(input, "error", null, startedAt, captured, maxOutputBytes, "termination-timeout"));
|
|
6150
|
+
}, TERMINATION_GRACE_MS);
|
|
6151
|
+
};
|
|
6152
|
+
const onOutput = (stream, chunk) => {
|
|
6153
|
+
if (settled || appendOutput(captured, stream, chunk, maxOutputBytes)) {
|
|
6154
|
+
if (!settled && maxOutputBytes !== void 0) stopFor("output-limit");
|
|
6155
|
+
}
|
|
6156
|
+
};
|
|
6157
|
+
onStdoutData = (chunk) => onOutput("stdout", chunk);
|
|
6158
|
+
onStderrData = (chunk) => onOutput("stderr", chunk);
|
|
6159
|
+
onError = (error) => {
|
|
6160
|
+
spawnError = error;
|
|
6161
|
+
if (terminationReason === void 0 && (error.code === "ENOENT" || error.code === "EACCES")) {
|
|
6162
|
+
settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
|
|
6163
|
+
}
|
|
6164
|
+
};
|
|
6165
|
+
onClose = (exitCode) => {
|
|
6166
|
+
if (terminationReason === "timeout") {
|
|
6167
|
+
settle(resultFromCaptured(input, "timeout", exitCode, startedAt, captured, maxOutputBytes));
|
|
6168
|
+
return;
|
|
6169
|
+
}
|
|
6170
|
+
if (terminationReason === "output-limit") {
|
|
6171
|
+
settle(resultFromCaptured(input, "error", exitCode, startedAt, captured, maxOutputBytes, "output-limit"));
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
6174
|
+
if (spawnError !== void 0) {
|
|
6175
|
+
settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
|
|
6176
|
+
return;
|
|
6177
|
+
}
|
|
6178
|
+
settle(resultFromCaptured(
|
|
6179
|
+
input,
|
|
6180
|
+
exitCode === 0 ? "pass" : "fail",
|
|
6181
|
+
exitCode,
|
|
6182
|
+
startedAt,
|
|
6183
|
+
captured,
|
|
6184
|
+
maxOutputBytes
|
|
6185
|
+
));
|
|
6186
|
+
};
|
|
6187
|
+
child.stdout?.on("data", onStdoutData);
|
|
6188
|
+
child.stderr?.on("data", onStderrData);
|
|
6189
|
+
child.once("error", onError);
|
|
6190
|
+
child.once("close", onClose);
|
|
6191
|
+
timeout = setTimeout(() => stopFor("timeout"), timeoutMs);
|
|
6192
|
+
});
|
|
6193
|
+
}
|
|
6194
|
+
function resultEvidence(result) {
|
|
6195
|
+
const exitCode = result.exitCode === null ? "none" : String(result.exitCode);
|
|
6196
|
+
return `exit=${exitCode}; status=${result.status}; durationMs=${result.durationMs}`;
|
|
6197
|
+
}
|
|
6198
|
+
function resultReason(result) {
|
|
6199
|
+
switch (result.status) {
|
|
6200
|
+
case "pass":
|
|
6201
|
+
return void 0;
|
|
6202
|
+
case "fail":
|
|
6203
|
+
return "nonzero-exit";
|
|
6204
|
+
case "timeout":
|
|
6205
|
+
return "timeout";
|
|
6206
|
+
case "unavailable":
|
|
6207
|
+
return result.reason ?? "unavailable";
|
|
6208
|
+
case "error":
|
|
6209
|
+
return result.reason ?? "error";
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
function receiptResultFor(controlId, result) {
|
|
6213
|
+
const evidence = resultEvidence(result);
|
|
6214
|
+
if (result.status === "pass") return { controlId, status: "pass", evidence };
|
|
6215
|
+
return {
|
|
6216
|
+
controlId,
|
|
6217
|
+
status: result.status === "fail" ? "fail" : "incomplete",
|
|
6218
|
+
evidence,
|
|
6219
|
+
reason: resultReason(result)
|
|
6220
|
+
};
|
|
6221
|
+
}
|
|
6222
|
+
function missingRequiredResults(controls, commands) {
|
|
6223
|
+
const commandControlIds = new Set(commands.map((command) => command.controlId));
|
|
6224
|
+
const missing = /* @__PURE__ */ new Set();
|
|
6225
|
+
for (const control of controls) {
|
|
6226
|
+
if (control.requirement !== "required" || !hasText2(control.id) || commandControlIds.has(control.id)) continue;
|
|
6227
|
+
missing.add(control.id);
|
|
6228
|
+
}
|
|
6229
|
+
return [...missing].map((controlId) => ({
|
|
6230
|
+
controlId,
|
|
6231
|
+
status: "incomplete",
|
|
6232
|
+
evidence: "required control has no declared command",
|
|
6233
|
+
reason: "required-control-missing"
|
|
6234
|
+
}));
|
|
6235
|
+
}
|
|
6236
|
+
function receiptCommandFor(command, result) {
|
|
6237
|
+
return {
|
|
6238
|
+
commandId: command.commandId,
|
|
6239
|
+
executable: command.executable,
|
|
6240
|
+
argv: command.argv,
|
|
6241
|
+
exitCode: result.exitCode ?? -1,
|
|
6242
|
+
durationMs: result.durationMs,
|
|
6243
|
+
output: result.output
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
async function runQualityPlan(input) {
|
|
6247
|
+
assertQualityPlanInput(input);
|
|
6248
|
+
const policy = { controls: input.controls, profile: input.profile };
|
|
6249
|
+
const policyDigest = sha256(canonicalJson(policy));
|
|
6250
|
+
const identity = {
|
|
6251
|
+
profile: input.profile,
|
|
6252
|
+
baseSha: input.identity.baseSha,
|
|
6253
|
+
headSha: input.identity.headSha,
|
|
6254
|
+
policyDigest
|
|
6255
|
+
};
|
|
6256
|
+
const commands = [];
|
|
6257
|
+
const results = [];
|
|
6258
|
+
for (const command of input.commands) {
|
|
6259
|
+
const result = await runQualityCommand(command);
|
|
6260
|
+
commands.push(receiptCommandFor(command, result));
|
|
6261
|
+
results.push(receiptResultFor(command.controlId, result));
|
|
6262
|
+
}
|
|
6263
|
+
results.push(...missingRequiredResults(input.controls, input.commands));
|
|
6264
|
+
const evaluation = evaluateQualityPolicy({
|
|
6265
|
+
profile: input.profile,
|
|
6266
|
+
controls: input.controls,
|
|
6267
|
+
results
|
|
6268
|
+
});
|
|
6269
|
+
const receipt = createQualityReceipt({
|
|
6270
|
+
authority: "local",
|
|
6271
|
+
identity,
|
|
6272
|
+
commands,
|
|
6273
|
+
results
|
|
6274
|
+
});
|
|
6275
|
+
validateQualityReceipt(receipt, identity);
|
|
6276
|
+
return { evaluation, receipt };
|
|
6277
|
+
}
|
|
6278
|
+
|
|
5446
6279
|
// src/cli.ts
|
|
5447
6280
|
var VERSION = readPackageVersion();
|
|
5448
|
-
var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
|
|
6281
|
+
var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall", "quality"];
|
|
6282
|
+
var QUALITY_REJECTED_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
6283
|
+
"--agents",
|
|
6284
|
+
"-a",
|
|
6285
|
+
"--target-dir",
|
|
6286
|
+
"--mode",
|
|
6287
|
+
"--subagent-concurrency"
|
|
6288
|
+
]);
|
|
5449
6289
|
async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
|
|
5450
6290
|
if (!runtimes.includes("opencode") || loadModelMap().opencode) return true;
|
|
5451
6291
|
const canPrompt = command === "install" && !flags.yes && !flags.dryRun && process.stdout.isTTY;
|
|
@@ -5458,7 +6298,7 @@ async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
|
|
|
5458
6298
|
);
|
|
5459
6299
|
return false;
|
|
5460
6300
|
}
|
|
5461
|
-
function parseFlags(args) {
|
|
6301
|
+
function parseFlags(args, allowReceipt = false) {
|
|
5462
6302
|
const flags = {
|
|
5463
6303
|
agents: [],
|
|
5464
6304
|
dryRun: false,
|
|
@@ -5474,6 +6314,7 @@ function parseFlags(args) {
|
|
|
5474
6314
|
removePlaywright: false,
|
|
5475
6315
|
devtools: false,
|
|
5476
6316
|
noDevtools: false,
|
|
6317
|
+
receipt: void 0,
|
|
5477
6318
|
positional: [],
|
|
5478
6319
|
unknownFlags: []
|
|
5479
6320
|
};
|
|
@@ -5484,6 +6325,26 @@ function parseFlags(args) {
|
|
|
5484
6325
|
};
|
|
5485
6326
|
for (let i = 0; i < args.length; i++) {
|
|
5486
6327
|
const arg = args[i];
|
|
6328
|
+
if (allowReceipt) {
|
|
6329
|
+
if (arg === "--help" || arg === "-h") flags.help = true;
|
|
6330
|
+
else if (arg === "--version" || arg === "-v") flags.version = true;
|
|
6331
|
+
else if (arg === "--receipt") {
|
|
6332
|
+
const [value, nextIndex] = readValue(i);
|
|
6333
|
+
if (value === void 0 || value === "") flags.unknownFlags.push(arg);
|
|
6334
|
+
else flags.receipt = value;
|
|
6335
|
+
i = nextIndex;
|
|
6336
|
+
} else if (arg.startsWith("--receipt=")) {
|
|
6337
|
+
const value = arg.slice(10);
|
|
6338
|
+
if (value === "") flags.unknownFlags.push(arg);
|
|
6339
|
+
else flags.receipt = value;
|
|
6340
|
+
} else if (QUALITY_REJECTED_VALUE_FLAGS.has(arg)) {
|
|
6341
|
+
flags.unknownFlags.push(arg);
|
|
6342
|
+
const [, nextIndex] = readValue(i);
|
|
6343
|
+
i = nextIndex;
|
|
6344
|
+
} else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
|
|
6345
|
+
else flags.positional.push(arg);
|
|
6346
|
+
continue;
|
|
6347
|
+
}
|
|
5487
6348
|
if (arg === "--agents" || arg === "-a") {
|
|
5488
6349
|
const [value, nextIndex] = readValue(i);
|
|
5489
6350
|
flags.agents = (value ?? "").split(",").filter(Boolean);
|
|
@@ -5635,7 +6496,7 @@ function parseCliArgs(argv) {
|
|
|
5635
6496
|
};
|
|
5636
6497
|
}
|
|
5637
6498
|
const command = isCommand ? first ?? "install" : "install";
|
|
5638
|
-
const flags = parseFlags(isCommand ? rest : argv);
|
|
6499
|
+
const flags = parseFlags(isCommand ? rest : argv, command === "quality");
|
|
5639
6500
|
if (first === "--help" || first === "-h" || flags.help) return { action: "help", command, flags };
|
|
5640
6501
|
if (first === "--version" || first === "-v" || flags.version) return { action: "version", command, flags };
|
|
5641
6502
|
if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
|
|
@@ -5727,8 +6588,9 @@ Comandos:
|
|
|
5727
6588
|
doctor Estado: Engram, drift de config, hooks de Codex, key de context7
|
|
5728
6589
|
restore --list para ver backups \xB7 'restore <id>' para restaurar
|
|
5729
6590
|
uninstall Retira SOLO lo gestionado por el stack (con backup).
|
|
5730
|
-
|
|
5731
|
-
|
|
6591
|
+
Engram se CONSERVA por defecto (memorias, binario y registro);
|
|
6592
|
+
desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
|
|
6593
|
+
quality Ejecuta un plan JSON expl\xEDcito y emite un receipt local
|
|
5732
6594
|
|
|
5733
6595
|
Opciones:
|
|
5734
6596
|
--agents, -a opencode,claude-code,codex,pi Runtimes destino (default: detectados)
|
|
@@ -5744,6 +6606,7 @@ Opciones:
|
|
|
5744
6606
|
memorias y binario quedan intactos igualmente
|
|
5745
6607
|
--remove-playwright (uninstall) retira solo el paquete global de Playwright;
|
|
5746
6608
|
nunca perfiles, cach\xE9 ni navegadores
|
|
6609
|
+
--receipt <path> (quality) escribe el receipt en ese path de forma at\xF3mica
|
|
5747
6610
|
|
|
5748
6611
|
Ver PRD.md para el dise\xF1o completo.`);
|
|
5749
6612
|
}
|
|
@@ -5770,12 +6633,43 @@ Flags disponibles: jorgex-stack --help`
|
|
|
5770
6633
|
return;
|
|
5771
6634
|
}
|
|
5772
6635
|
const { command, flags } = parsed;
|
|
5773
|
-
if (flags.targetDir !== void 0 && flags.agents.length !== 1) {
|
|
6636
|
+
if (command !== "quality" && flags.targetDir !== void 0 && flags.agents.length !== 1) {
|
|
5774
6637
|
console.error("--target-dir requiere exactamente un runtime en --agents.");
|
|
5775
6638
|
process.exitCode = 1;
|
|
5776
6639
|
return;
|
|
5777
6640
|
}
|
|
5778
6641
|
switch (command) {
|
|
6642
|
+
case "quality": {
|
|
6643
|
+
if (flags.targetDir !== void 0 || flags.agents.length > 0 || flags.dryRun || flags.yes || flags.mode !== void 0 || flags.subagentConcurrency !== void 0 || flags.list || flags.check || flags.removeEngram || flags.playwright || flags.removePlaywright || flags.devtools || flags.noDevtools) {
|
|
6644
|
+
console.error("quality solo admite <plan.json> y, opcionalmente, --receipt <path>.");
|
|
6645
|
+
process.exitCode = 1;
|
|
6646
|
+
return;
|
|
6647
|
+
}
|
|
6648
|
+
if (flags.positional.length !== 1) {
|
|
6649
|
+
console.error("Uso: jorgex-stack quality <plan.json> [--receipt <path>]");
|
|
6650
|
+
process.exitCode = 1;
|
|
6651
|
+
return;
|
|
6652
|
+
}
|
|
6653
|
+
if (flags.receipt !== void 0 && flags.receipt.trim() === "") {
|
|
6654
|
+
console.error("--receipt requiere un path no vac\xEDo.");
|
|
6655
|
+
process.exitCode = 1;
|
|
6656
|
+
return;
|
|
6657
|
+
}
|
|
6658
|
+
try {
|
|
6659
|
+
const plan = JSON.parse(fs24.readFileSync(flags.positional[0], "utf8"));
|
|
6660
|
+
const result = await runQualityPlan(plan);
|
|
6661
|
+
const serialized = serializeQualityReceipt(result.receipt);
|
|
6662
|
+
if (flags.receipt === void 0) process.stdout.write(`${serialized}
|
|
6663
|
+
`);
|
|
6664
|
+
else writeText(flags.receipt, `${serialized}
|
|
6665
|
+
`);
|
|
6666
|
+
process.exitCode = result.evaluation.status === "pass" ? 0 : 1;
|
|
6667
|
+
} catch (error) {
|
|
6668
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6669
|
+
process.exitCode = 1;
|
|
6670
|
+
}
|
|
6671
|
+
return;
|
|
6672
|
+
}
|
|
5779
6673
|
case "install":
|
|
5780
6674
|
case "sync": {
|
|
5781
6675
|
const runtimes = await resolveRuntimes(flags, command === "install");
|
package/package.json
CHANGED