@markdown-ai/cli 1.0.0-rc.2 → 1.1.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/HOW-TO-USE.md +279 -173
- package/README.md +193 -5
- package/dist/cli.js +3488 -272
- package/dist/conformance/compile/46-basic-targets/expected/AGENTS.md +20 -0
- package/dist/conformance/compile/46-basic-targets/expected/MCP-SERVER.md +20 -0
- package/dist/conformance/compile/46-basic-targets/expected/SKILL.md +19 -0
- package/dist/conformance/compile/46-basic-targets/expected/mcp-server.json +5 -0
- package/dist/conformance/compile/46-basic-targets/input.mda +18 -0
- package/dist/conformance/manifest.yaml +55 -49
- package/dist/conformance/valid/27-trust-policy-github-actions.json +14 -11
- package/dist/schemas/mda-trust-policy.schema.json +144 -111
- package/package.json +47 -40
package/dist/cli.js
CHANGED
|
@@ -2976,7 +2976,7 @@ var require_compile = __commonJS({
|
|
|
2976
2976
|
const schOrFunc = root.refs[ref];
|
|
2977
2977
|
if (schOrFunc)
|
|
2978
2978
|
return schOrFunc;
|
|
2979
|
-
let _sch =
|
|
2979
|
+
let _sch = resolve4.call(this, root, ref);
|
|
2980
2980
|
if (_sch === void 0) {
|
|
2981
2981
|
const schema2 = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2982
2982
|
const { schemaId } = this.opts;
|
|
@@ -3003,7 +3003,7 @@ var require_compile = __commonJS({
|
|
|
3003
3003
|
function sameSchemaEnv(s1, s2) {
|
|
3004
3004
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3005
3005
|
}
|
|
3006
|
-
function
|
|
3006
|
+
function resolve4(root, ref) {
|
|
3007
3007
|
let sch;
|
|
3008
3008
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3009
3009
|
ref = sch;
|
|
@@ -3578,55 +3578,55 @@ var require_fast_uri = __commonJS({
|
|
|
3578
3578
|
}
|
|
3579
3579
|
return uri;
|
|
3580
3580
|
}
|
|
3581
|
-
function
|
|
3581
|
+
function resolve4(baseURI, relativeURI, options) {
|
|
3582
3582
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3583
3583
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3584
3584
|
schemelessOptions.skipEscape = true;
|
|
3585
3585
|
return serialize(resolved, schemelessOptions);
|
|
3586
3586
|
}
|
|
3587
|
-
function resolveComponent(base,
|
|
3587
|
+
function resolveComponent(base, relative3, options, skipNormalization) {
|
|
3588
3588
|
const target = {};
|
|
3589
3589
|
if (!skipNormalization) {
|
|
3590
3590
|
base = parse(serialize(base, options), options);
|
|
3591
|
-
|
|
3591
|
+
relative3 = parse(serialize(relative3, options), options);
|
|
3592
3592
|
}
|
|
3593
3593
|
options = options || {};
|
|
3594
|
-
if (!options.tolerant &&
|
|
3595
|
-
target.scheme =
|
|
3596
|
-
target.userinfo =
|
|
3597
|
-
target.host =
|
|
3598
|
-
target.port =
|
|
3599
|
-
target.path = removeDotSegments(
|
|
3600
|
-
target.query =
|
|
3594
|
+
if (!options.tolerant && relative3.scheme) {
|
|
3595
|
+
target.scheme = relative3.scheme;
|
|
3596
|
+
target.userinfo = relative3.userinfo;
|
|
3597
|
+
target.host = relative3.host;
|
|
3598
|
+
target.port = relative3.port;
|
|
3599
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3600
|
+
target.query = relative3.query;
|
|
3601
3601
|
} else {
|
|
3602
|
-
if (
|
|
3603
|
-
target.userinfo =
|
|
3604
|
-
target.host =
|
|
3605
|
-
target.port =
|
|
3606
|
-
target.path = removeDotSegments(
|
|
3607
|
-
target.query =
|
|
3602
|
+
if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
|
|
3603
|
+
target.userinfo = relative3.userinfo;
|
|
3604
|
+
target.host = relative3.host;
|
|
3605
|
+
target.port = relative3.port;
|
|
3606
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3607
|
+
target.query = relative3.query;
|
|
3608
3608
|
} else {
|
|
3609
|
-
if (!
|
|
3609
|
+
if (!relative3.path) {
|
|
3610
3610
|
target.path = base.path;
|
|
3611
|
-
if (
|
|
3612
|
-
target.query =
|
|
3611
|
+
if (relative3.query !== void 0) {
|
|
3612
|
+
target.query = relative3.query;
|
|
3613
3613
|
} else {
|
|
3614
3614
|
target.query = base.query;
|
|
3615
3615
|
}
|
|
3616
3616
|
} else {
|
|
3617
|
-
if (
|
|
3618
|
-
target.path = removeDotSegments(
|
|
3617
|
+
if (relative3.path[0] === "/") {
|
|
3618
|
+
target.path = removeDotSegments(relative3.path);
|
|
3619
3619
|
} else {
|
|
3620
3620
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3621
|
-
target.path = "/" +
|
|
3621
|
+
target.path = "/" + relative3.path;
|
|
3622
3622
|
} else if (!base.path) {
|
|
3623
|
-
target.path =
|
|
3623
|
+
target.path = relative3.path;
|
|
3624
3624
|
} else {
|
|
3625
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3625
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
|
|
3626
3626
|
}
|
|
3627
3627
|
target.path = removeDotSegments(target.path);
|
|
3628
3628
|
}
|
|
3629
|
-
target.query =
|
|
3629
|
+
target.query = relative3.query;
|
|
3630
3630
|
}
|
|
3631
3631
|
target.userinfo = base.userinfo;
|
|
3632
3632
|
target.host = base.host;
|
|
@@ -3634,7 +3634,7 @@ var require_fast_uri = __commonJS({
|
|
|
3634
3634
|
}
|
|
3635
3635
|
target.scheme = base.scheme;
|
|
3636
3636
|
}
|
|
3637
|
-
target.fragment =
|
|
3637
|
+
target.fragment = relative3.fragment;
|
|
3638
3638
|
return target;
|
|
3639
3639
|
}
|
|
3640
3640
|
function equal(uriA, uriB, options) {
|
|
@@ -3805,7 +3805,7 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3805
|
var fastUri = {
|
|
3806
3806
|
SCHEMES,
|
|
3807
3807
|
normalize,
|
|
3808
|
-
resolve:
|
|
3808
|
+
resolve: resolve4,
|
|
3809
3809
|
resolveComponent,
|
|
3810
3810
|
equal,
|
|
3811
3811
|
serialize,
|
|
@@ -7618,9 +7618,6 @@ var require_dist = __commonJS({
|
|
|
7618
7618
|
}
|
|
7619
7619
|
});
|
|
7620
7620
|
|
|
7621
|
-
// src/cli.ts
|
|
7622
|
-
import { existsSync as existsSync2, rmSync as rmSync2 } from "node:fs";
|
|
7623
|
-
|
|
7624
7621
|
// src/types.ts
|
|
7625
7622
|
var EXIT = {
|
|
7626
7623
|
ok: 0,
|
|
@@ -7645,22 +7642,264 @@ var MDA_EXTENDED = [
|
|
|
7645
7642
|
var TARGET_ORDER = ["SKILL.md", "AGENTS.md", "MCP-SERVER.md"];
|
|
7646
7643
|
var MCP_BOUNDARY = "\n--MDA-FILE-BOUNDARY--\n";
|
|
7647
7644
|
function commandResult(ok, command, exitCode, diagnostics, extra = {}) {
|
|
7648
|
-
|
|
7645
|
+
const stableDiagnostics = diagnostics.map(stabilizeDiagnosticCode);
|
|
7646
|
+
return {
|
|
7647
|
+
ok,
|
|
7648
|
+
command,
|
|
7649
|
+
exitCode,
|
|
7650
|
+
summary: ok ? `${command} completed` : `${command} failed`,
|
|
7651
|
+
artifacts: [],
|
|
7652
|
+
diagnostics: stableDiagnostics,
|
|
7653
|
+
nextActions: [],
|
|
7654
|
+
...extra
|
|
7655
|
+
};
|
|
7649
7656
|
}
|
|
7650
7657
|
function diag(code, message, extra = {}) {
|
|
7651
7658
|
return { code, message, severity: "error", ...extra };
|
|
7652
7659
|
}
|
|
7660
|
+
function stabilizeDiagnosticCode(diagnostic) {
|
|
7661
|
+
return { ...diagnostic, code: stableDiagnosticCode(diagnostic.code) };
|
|
7662
|
+
}
|
|
7663
|
+
function stableDiagnosticCode(code) {
|
|
7664
|
+
if (/^(input|schema|integrity|signature|trust_policy|sigstore|rekor|did_web|llmix|release|compat|filesystem|conformance)\./.test(code)) {
|
|
7665
|
+
return code;
|
|
7666
|
+
}
|
|
7667
|
+
const mapped = DIAGNOSTIC_CODE_MAP[code];
|
|
7668
|
+
if (mapped) return mapped;
|
|
7669
|
+
return `input.${code.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase() || "unknown"}`;
|
|
7670
|
+
}
|
|
7671
|
+
var DIAGNOSTIC_CODE_MAP = {
|
|
7672
|
+
"usage-error": "input.usage",
|
|
7673
|
+
"invalid-encoding": "input.invalid_encoding",
|
|
7674
|
+
"unterminated-frontmatter": "input.unterminated_frontmatter",
|
|
7675
|
+
"frontmatter-yaml-parse-error": "input.frontmatter_yaml_parse",
|
|
7676
|
+
"missing-required-frontmatter": "input.missing_required_frontmatter",
|
|
7677
|
+
"missing-required-body": "input.missing_required_body",
|
|
7678
|
+
"invalid-json": "input.invalid_json",
|
|
7679
|
+
"io-error": "filesystem.io",
|
|
7680
|
+
"rollback-error": "filesystem.rollback",
|
|
7681
|
+
"schema-validation-error": "schema.validation",
|
|
7682
|
+
"relationship-footnote-json-parse-error": "schema.relationship_footnote_json_parse",
|
|
7683
|
+
"missing-required-sidecar": "integrity.missing_required_sidecar",
|
|
7684
|
+
"missing-required-integrity": "integrity.missing_required",
|
|
7685
|
+
"unsupported-integrity-algorithm": "integrity.unsupported_algorithm",
|
|
7686
|
+
"integrity-mismatch": "integrity.mismatch",
|
|
7687
|
+
"missing-required-signature": "signature.missing_required",
|
|
7688
|
+
"signature-digest-mismatch": "signature.digest_mismatch",
|
|
7689
|
+
"signature-verification-unavailable": "signature.verification_unavailable",
|
|
7690
|
+
"signing-unavailable": "signature.signing_unavailable",
|
|
7691
|
+
"trust-policy-violation": "trust_policy.violation",
|
|
7692
|
+
"no-trusted-signature": "trust_policy.no_trusted_signature",
|
|
7693
|
+
"insufficient-trusted-signatures": "trust_policy.insufficient_trusted_signatures",
|
|
7694
|
+
"fixture-missing": "conformance.fixture_missing",
|
|
7695
|
+
"conformance-manifest-invalid": "conformance.manifest_invalid",
|
|
7696
|
+
"extraction-mismatch": "conformance.extraction_mismatch",
|
|
7697
|
+
"expected-error-mismatch": "conformance.expected_error_mismatch",
|
|
7698
|
+
"compile-fixture-unavailable": "conformance.compile_fixture_unavailable",
|
|
7699
|
+
"internal-error": "input.internal_error"
|
|
7700
|
+
};
|
|
7653
7701
|
function usage(command, message, extra = {}) {
|
|
7654
|
-
return commandResult(false, command, EXIT.usage, [diag("usage-error", message)],
|
|
7702
|
+
return commandResult(false, command, EXIT.usage, [diag("usage-error", message)], {
|
|
7703
|
+
summary: `${command} usage error`,
|
|
7704
|
+
nextActions: [
|
|
7705
|
+
{
|
|
7706
|
+
id: "show-help",
|
|
7707
|
+
required: true,
|
|
7708
|
+
reason: "Review command usage",
|
|
7709
|
+
command: "mda --help"
|
|
7710
|
+
}
|
|
7711
|
+
],
|
|
7712
|
+
...extra
|
|
7713
|
+
});
|
|
7655
7714
|
}
|
|
7656
7715
|
function ioError(command, message, extra = {}) {
|
|
7657
|
-
return commandResult(false, command, EXIT.io, [diag("io-error", message)],
|
|
7716
|
+
return commandResult(false, command, EXIT.io, [diag("io-error", message)], {
|
|
7717
|
+
summary: `${command} IO error`,
|
|
7718
|
+
nextActions: [
|
|
7719
|
+
{
|
|
7720
|
+
id: "fix-filesystem",
|
|
7721
|
+
required: true,
|
|
7722
|
+
reason: "Fix the filesystem path or permissions and retry",
|
|
7723
|
+
command: `mda ${command}`
|
|
7724
|
+
}
|
|
7725
|
+
],
|
|
7726
|
+
...extra
|
|
7727
|
+
});
|
|
7658
7728
|
}
|
|
7659
7729
|
|
|
7730
|
+
// src/cli/help.ts
|
|
7731
|
+
var HELP = `Markdown AI CLI (@markdown-ai/cli)
|
|
7732
|
+
|
|
7733
|
+
Usage:
|
|
7734
|
+
mda
|
|
7735
|
+
mda --help
|
|
7736
|
+
mda init <name> [--out <file>] [--json]
|
|
7737
|
+
mda init --template llmix-preset --module <name> --preset <name> --provider <provider> --model <model> [--out <file>] [--json]
|
|
7738
|
+
mda validate <file> [--target source|SKILL.md|AGENTS.md|MCP-SERVER.md|auto] [--json]
|
|
7739
|
+
mda compile <file.mda> --target <target...> [--out-dir <dir>] [--integrity] [--manifest <path>] [--strict-compat] [--json]
|
|
7740
|
+
mda canonicalize <file> [--target source|SKILL.md|AGENTS.md|MCP-SERVER.md|auto] [--sidecar <path>] [--json]
|
|
7741
|
+
mda integrity compute <file> [--target source|SKILL.md|AGENTS.md|MCP-SERVER.md|auto] [--sidecar <path>] [--algorithm sha256|sha384|sha512] [--write] [--json]
|
|
7742
|
+
mda integrity verify <file> [--target source|SKILL.md|AGENTS.md|MCP-SERVER.md|auto] [--sidecar <path>] [--json]
|
|
7743
|
+
mda sign <file> --profile did-web --did <did> --key-id <key-id> --key-file <path> (--out <file>|--in-place) [--json]
|
|
7744
|
+
mda sign <file> --profile github-actions --repo <owner/repo> --workflow <workflow> --ref <ref> --rekor --offline-sigstore-fixture <path> (--out <file>|--in-place) [--json]
|
|
7745
|
+
mda sign <file> --method did-web --key <path> --identity <domain> (--out <file>|--in-place) [--json]
|
|
7746
|
+
mda verify <file> --policy <path> [--did-document <path>] [--offline-sigstore-fixture <path>] [--target source|SKILL.md|AGENTS.md|MCP-SERVER.md|auto] [--sidecar <path>] [--json]
|
|
7747
|
+
mda release trust policy --target llmix-registry --profile github-actions --repo <owner/repo> --workflow <workflow> --ref <ref> [--out <file>] [--json]
|
|
7748
|
+
mda release trust policy --target llmix-registry --profile did-web --domain <domain> [--min-signatures <n>] [--out <file>] [--json]
|
|
7749
|
+
mda release prepare --target llmix-registry --source <dir> --registry-dir <dir> --policy <path> --out <file> [--did-document <path>] [--offline-sigstore-fixture <path>] [--json]
|
|
7750
|
+
mda release finalize --target llmix-registry --registry-dir <dir> --registry-root <file> --release-plan <file> --policy <path> (--expected-root-digest <digest>|--derive-root-digest) [--minimum-revision <rev>] [--minimum-published-at <iso>] [--high-watermark <value>] --out <file> [--did-document <path>] [--offline-sigstore-fixture <path>] [--json]
|
|
7751
|
+
mda release finalize --target llmix-registry --registry-dir <dir> --manifest <path> --snippet-format json|env|kubernetes|github-actions|terraform|typescript|python|rust --snippet-out <path> [--json]
|
|
7752
|
+
mda doctor release --target llmix-registry --source <dir> --registry-dir <dir> --release-plan <path> --manifest <path> [--did-document <path>] [--offline-sigstore-fixture <path>] [--json]
|
|
7753
|
+
mda conformance [--suite <path>] [--level V|C] [--json]
|
|
7754
|
+
|
|
7755
|
+
Global flags:
|
|
7756
|
+
--json Print stable JSON only on stdout.
|
|
7757
|
+
--quiet Suppress non-essential human output.
|
|
7758
|
+
--verbose Include extra diagnostic context where available.
|
|
7759
|
+
--no-color Disable ANSI color.
|
|
7760
|
+
--no-next Omit human Next: guidance. JSON nextActions are unchanged.
|
|
7761
|
+
-h, --help Print this full help.
|
|
7762
|
+
|
|
7763
|
+
Commands and options:
|
|
7764
|
+
init <name>
|
|
7765
|
+
--out <file> Write the scaffold atomically. Refuses overwrite.
|
|
7766
|
+
--json Return scaffold in JSON instead of raw .mda text.
|
|
7767
|
+
--template llmix-preset Generate an LLMix preset source artifact.
|
|
7768
|
+
--module <name> LLMix module name, e.g. search_summary or _default.
|
|
7769
|
+
--preset <name> LLMix preset name, e.g. openai_fast or _base.
|
|
7770
|
+
--provider <provider> openai, anthropic, google, deepseek, openrouter, deepinfra, novita, together, or sno-gpu.
|
|
7771
|
+
--model <model> Provider model identifier.
|
|
7772
|
+
|
|
7773
|
+
validate <file>
|
|
7774
|
+
--target <target> source, SKILL.md, AGENTS.md, MCP-SERVER.md, or auto. Default: auto.
|
|
7775
|
+
|
|
7776
|
+
compile <file.mda>
|
|
7777
|
+
--target <target...> Required. One or more of SKILL.md, AGENTS.md, MCP-SERVER.md.
|
|
7778
|
+
--out-dir <dir> Output directory. Default: current working directory.
|
|
7779
|
+
--integrity Add sha256 integrity to emitted artifacts.
|
|
7780
|
+
--manifest <path> Write compile evidence with source digest, output digests, capabilities, and warnings.
|
|
7781
|
+
--strict-compat Treat compatibility warnings as compile failures before writing outputs.
|
|
7782
|
+
|
|
7783
|
+
canonicalize <file>
|
|
7784
|
+
--target <target> Default: auto.
|
|
7785
|
+
--sidecar <path> Required only for MCP-SERVER.md multi-file canonical bytes.
|
|
7786
|
+
|
|
7787
|
+
integrity compute <file>
|
|
7788
|
+
--target <target> Default: auto.
|
|
7789
|
+
--sidecar <path> Required only for MCP-SERVER.md.
|
|
7790
|
+
--algorithm <name> sha256, sha384, or sha512. Default: sha256.
|
|
7791
|
+
--write Write the computed digest into frontmatter.integrity. Refuses mismatched existing integrity.
|
|
7792
|
+
|
|
7793
|
+
integrity verify <file>
|
|
7794
|
+
--target <target> Default: auto.
|
|
7795
|
+
--sidecar <path> Required only for MCP-SERVER.md.
|
|
7796
|
+
|
|
7797
|
+
verify <file>
|
|
7798
|
+
--policy <path> Required trust policy JSON.
|
|
7799
|
+
--did-document <path> Local did:web document fixture for deterministic verification.
|
|
7800
|
+
--offline-sigstore-fixture <path>
|
|
7801
|
+
Local Sigstore/Rekor fixture for deterministic GitHub Actions verification.
|
|
7802
|
+
--target <target> Default: auto.
|
|
7803
|
+
--sidecar <path> Required only for MCP-SERVER.md.
|
|
7804
|
+
--offline Unsupported; use explicit profile evidence fixtures instead.
|
|
7805
|
+
|
|
7806
|
+
sign <file>
|
|
7807
|
+
--profile did-web Sign with a did:web key and DSSE PAE integrity payload.
|
|
7808
|
+
--did <did> Required for --profile did-web, e.g. did:web:example.com.
|
|
7809
|
+
--key-id <key-id> Required verification method id in the DID document.
|
|
7810
|
+
--key-file <path> Required private key PEM.
|
|
7811
|
+
--method did-web Compatibility alias for --profile did-web.
|
|
7812
|
+
--key <path> Compatibility alias for --key-file.
|
|
7813
|
+
--identity <domain> Compatibility alias for --did did:web:<domain>.
|
|
7814
|
+
--profile github-actions Sign with explicit GitHub Actions Sigstore/Rekor fixture evidence.
|
|
7815
|
+
--repo <owner/repo> Required GitHub repository for --profile github-actions.
|
|
7816
|
+
--workflow <workflow> Required workflow file or workflow identity.
|
|
7817
|
+
--ref <ref> Required exact Git ref.
|
|
7818
|
+
--rekor Required explicit Rekor evidence acknowledgement.
|
|
7819
|
+
--offline-sigstore-fixture <path>
|
|
7820
|
+
Required local Sigstore/Rekor fixture for deterministic signing.
|
|
7821
|
+
--out <file> Write signed output.
|
|
7822
|
+
--in-place Replace the input file.
|
|
7823
|
+
|
|
7824
|
+
release trust policy
|
|
7825
|
+
--target llmix-registry Required. Generate trust policy for the LLMix registry release target.
|
|
7826
|
+
--profile did-web Generate a schema-valid did:web trust policy.
|
|
7827
|
+
--domain <domain> Trusted did:web domain.
|
|
7828
|
+
--min-signatures <n> Minimum trusted signatures. Default: 1.
|
|
7829
|
+
--profile github-actions Generate a schema-valid Sigstore/Rekor policy.
|
|
7830
|
+
--repo <owner/repo> Trusted GitHub repository.
|
|
7831
|
+
--workflow <workflow> Trusted workflow file or workflow identity.
|
|
7832
|
+
--ref <ref> Trusted exact Git ref, e.g. refs/heads/main.
|
|
7833
|
+
--out <file> Write policy JSON atomically. Refuses overwrite.
|
|
7834
|
+
|
|
7835
|
+
release prepare
|
|
7836
|
+
--target llmix-registry Required. Prepare a verified LLMix registry release plan.
|
|
7837
|
+
--source <dir> Directory containing signed LLMix .mda preset sources.
|
|
7838
|
+
--registry-dir <dir> Target LLMix registry directory. This command reads only and never publishes registry files.
|
|
7839
|
+
--policy <path> Trust policy used to verify every source signature.
|
|
7840
|
+
--did-document <path> Local did:web document fixture when using did:web signatures.
|
|
7841
|
+
--offline-sigstore-fixture <path>
|
|
7842
|
+
Local Sigstore/Rekor fixture when using GitHub Actions signatures.
|
|
7843
|
+
--out <file> Write the deterministic release plan atomically. Refuses overwrite.
|
|
7844
|
+
|
|
7845
|
+
release finalize
|
|
7846
|
+
--target llmix-registry Required. Finalize LLMix registry release trust artifacts.
|
|
7847
|
+
--registry-dir <dir> Published LLMix registry directory. Manifest output is rejected inside this directory.
|
|
7848
|
+
--registry-root <file> Signed registry-root evidence JSON.
|
|
7849
|
+
--release-plan <file> Verified release plan produced before registry publication.
|
|
7850
|
+
--policy <path> Trust policy used to verify registry-root signatures.
|
|
7851
|
+
--expected-root-digest <d> Pin the exact registry-root digest.
|
|
7852
|
+
--derive-root-digest Derive expectedRootDigest from verified registry-root evidence.
|
|
7853
|
+
--minimum-revision <rev> Reject registry roots older than this revision.
|
|
7854
|
+
--minimum-published-at <iso> Reject registry roots published before this timestamp.
|
|
7855
|
+
--high-watermark <value> Reject roots below this monotonic high-watermark.
|
|
7856
|
+
--out <file> Write the external deployment trust manifest. Refuses overwrite.
|
|
7857
|
+
--manifest <path> Existing external trust manifest for snippet generation.
|
|
7858
|
+
--snippet-format <format> json, env, kubernetes, github-actions, terraform, typescript, python, or rust.
|
|
7859
|
+
--snippet-out <file> Write the deployment snippet atomically. Refuses overwrite.
|
|
7860
|
+
|
|
7861
|
+
doctor release
|
|
7862
|
+
--target llmix-registry Required. Check LLMix registry release readiness.
|
|
7863
|
+
--source <dir> Directory containing LLMix .mda preset sources.
|
|
7864
|
+
--registry-dir <dir> Published LLMix registry directory.
|
|
7865
|
+
--release-plan <path> Verified release plan used for the signed registry root.
|
|
7866
|
+
--manifest <path> External deployment trust manifest.
|
|
7867
|
+
--did-document <path> Local DID document for did:web registry-root signature verification.
|
|
7868
|
+
--offline-sigstore-fixture <path>
|
|
7869
|
+
Deterministic Sigstore/Rekor fixture for registry-root verification.
|
|
7870
|
+
|
|
7871
|
+
Examples:
|
|
7872
|
+
mda init hello-skill --out hello.mda
|
|
7873
|
+
mda init --template llmix-preset --module search_summary --preset openai_fast --provider openai --model gpt-5-mini --out search_summary/openai_fast.mda
|
|
7874
|
+
mda validate hello.mda --json
|
|
7875
|
+
mda compile hello.mda --target SKILL.md AGENTS.md MCP-SERVER.md --out-dir out --integrity --manifest out/compile-manifest.json
|
|
7876
|
+
mda canonicalize out/SKILL.md --target SKILL.md --json
|
|
7877
|
+
mda integrity compute out/SKILL.md --target SKILL.md --algorithm sha256 --json
|
|
7878
|
+
mda integrity verify out/SKILL.md --target SKILL.md
|
|
7879
|
+
mda verify signed.md --policy policy.json --json
|
|
7880
|
+
mda release trust policy --target llmix-registry --profile github-actions --repo owner/repo --workflow release.yml --ref refs/heads/main --out release/source-policy.json --json
|
|
7881
|
+
mda release prepare --target llmix-registry --source authoring --registry-dir registry --policy release/source-policy.json --out release/plan.json --json
|
|
7882
|
+
mda release finalize --target llmix-registry --registry-dir registry --registry-root registry/snapshots/current/registry-root.json --release-plan release/plan.json --policy release/root-policy.json --derive-root-digest --out release/llmix-trust.json --json
|
|
7883
|
+
mda release finalize --target llmix-registry --registry-dir registry --manifest release/llmix-trust.json --snippet-format json --snippet-out release/llmix-trust-snippet.json --json
|
|
7884
|
+
mda doctor release --target llmix-registry --source authoring --registry-dir registry --release-plan release/plan.json --manifest release/llmix-trust.json --did-document did.json --json
|
|
7885
|
+
mda conformance --suite conformance --level V --json
|
|
7886
|
+
|
|
7887
|
+
Exit codes:
|
|
7888
|
+
0 Success.
|
|
7889
|
+
1 Valid command, but artifact validation or verification failed.
|
|
7890
|
+
2 CLI usage error: missing argument, unknown flag, ambiguous target.
|
|
7891
|
+
3 IO or configuration error: missing file, overwrite refusal, unreadable policy.
|
|
7892
|
+
4 Internal bug or invariant failure.
|
|
7893
|
+
`;
|
|
7894
|
+
|
|
7895
|
+
// src/cli/core-commands.ts
|
|
7896
|
+
import { existsSync as existsSync2, rmSync as rmSync2 } from "node:fs";
|
|
7897
|
+
import { resolve as resolve2 } from "node:path";
|
|
7898
|
+
|
|
7660
7899
|
// src/mda.ts
|
|
7661
7900
|
var import__ = __toESM(require__(), 1);
|
|
7662
7901
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
7663
|
-
import { createHash } from "node:crypto";
|
|
7902
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7664
7903
|
import {
|
|
7665
7904
|
closeSync,
|
|
7666
7905
|
existsSync,
|
|
@@ -7669,10 +7908,11 @@ import {
|
|
|
7669
7908
|
openSync,
|
|
7670
7909
|
readFileSync,
|
|
7671
7910
|
readdirSync,
|
|
7911
|
+
renameSync,
|
|
7672
7912
|
rmSync,
|
|
7673
7913
|
writeFileSync
|
|
7674
7914
|
} from "node:fs";
|
|
7675
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
7915
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
7676
7916
|
import { fileURLToPath } from "node:url";
|
|
7677
7917
|
|
|
7678
7918
|
// ../../node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/dist/js-yaml.mjs
|
|
@@ -10311,6 +10551,29 @@ description: "Describe what this Markdown AI document does."
|
|
|
10311
10551
|
Describe the instructions, workflow, or capability here.
|
|
10312
10552
|
`;
|
|
10313
10553
|
}
|
|
10554
|
+
function makeLlmixPresetScaffold(moduleName, presetName, provider, model) {
|
|
10555
|
+
const artifactName = `llmix-${moduleName.replace(/^_/, "").replace(/_/g, "-")}-${presetName.replace(/^_/, "").replace(/_/g, "-")}`;
|
|
10556
|
+
return renderMarkdown(
|
|
10557
|
+
{
|
|
10558
|
+
name: artifactName,
|
|
10559
|
+
description: `LLMix preset ${moduleName}/${presetName}`,
|
|
10560
|
+
metadata: {
|
|
10561
|
+
"snoai-llmix": {
|
|
10562
|
+
module: moduleName,
|
|
10563
|
+
preset: presetName,
|
|
10564
|
+
common: {
|
|
10565
|
+
provider,
|
|
10566
|
+
model
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
}
|
|
10570
|
+
},
|
|
10571
|
+
`# ${moduleName}/${presetName}
|
|
10572
|
+
|
|
10573
|
+
Describe when this LLMix preset should be used.
|
|
10574
|
+
`
|
|
10575
|
+
);
|
|
10576
|
+
}
|
|
10314
10577
|
function resolveTarget(file, target) {
|
|
10315
10578
|
if (target !== "auto") return { ok: true, target };
|
|
10316
10579
|
const base = file.split(/[\\/]/).pop() ?? file;
|
|
@@ -10390,7 +10653,10 @@ function validateArtifact(file, target) {
|
|
|
10390
10653
|
return { ok: false, diagnostics: [diag("missing-required-frontmatter", `${target} requires YAML frontmatter`, { path: file })] };
|
|
10391
10654
|
}
|
|
10392
10655
|
if (ext.body.trim().length === 0) {
|
|
10393
|
-
return {
|
|
10656
|
+
return {
|
|
10657
|
+
ok: false,
|
|
10658
|
+
diagnostics: [diag("missing-required-body", "AGENTS.md without frontmatter requires a non-empty body", { path: file })]
|
|
10659
|
+
};
|
|
10394
10660
|
}
|
|
10395
10661
|
}
|
|
10396
10662
|
const fm = ext.kind === "ok" ? ext.frontmatter ?? {} : {};
|
|
@@ -10404,6 +10670,7 @@ function validateArtifact(file, target) {
|
|
|
10404
10670
|
}
|
|
10405
10671
|
if (isRecord(fm)) {
|
|
10406
10672
|
diagnostics.push(...checkSignatureDigestEquality(fm));
|
|
10673
|
+
if (target === "source") diagnostics.push(...validateLlmixNamespace(fm));
|
|
10407
10674
|
}
|
|
10408
10675
|
return { ok: diagnostics.length === 0, diagnostics };
|
|
10409
10676
|
}
|
|
@@ -10419,10 +10686,12 @@ function validateJsonAgainst(value, key) {
|
|
|
10419
10686
|
const ok = validator(value);
|
|
10420
10687
|
return {
|
|
10421
10688
|
ok: Boolean(ok),
|
|
10422
|
-
diagnostics: ok ? [] : (validator.errors ?? []).slice(0, 5).map(
|
|
10423
|
-
schema
|
|
10424
|
-
|
|
10425
|
-
|
|
10689
|
+
diagnostics: ok ? [] : (validator.errors ?? []).slice(0, 5).map(
|
|
10690
|
+
(error) => diag("schema-validation-error", `${error.instancePath || "(root)"} ${error.message}`, {
|
|
10691
|
+
schema: error.schemaPath,
|
|
10692
|
+
instancePath: error.instancePath
|
|
10693
|
+
})
|
|
10694
|
+
)
|
|
10426
10695
|
};
|
|
10427
10696
|
}
|
|
10428
10697
|
function getSchemas() {
|
|
@@ -10498,15 +10767,124 @@ function checkSignatureDigestEquality(fm) {
|
|
|
10498
10767
|
});
|
|
10499
10768
|
return diagnostics;
|
|
10500
10769
|
}
|
|
10770
|
+
var LLMIX_PROVIDERS = /* @__PURE__ */ new Set(["openai", "anthropic", "google", "deepseek", "openrouter", "deepinfra", "novita", "together", "sno-gpu"]);
|
|
10771
|
+
var LLMIX_COMMON_KEYS = /* @__PURE__ */ new Set([
|
|
10772
|
+
"provider",
|
|
10773
|
+
"model",
|
|
10774
|
+
"maxOutputTokens",
|
|
10775
|
+
"temperature",
|
|
10776
|
+
"topP",
|
|
10777
|
+
"topK",
|
|
10778
|
+
"presencePenalty",
|
|
10779
|
+
"frequencyPenalty",
|
|
10780
|
+
"stopSequences",
|
|
10781
|
+
"seed",
|
|
10782
|
+
"maxRetries",
|
|
10783
|
+
"enableThinking",
|
|
10784
|
+
"keepThinkingOutput"
|
|
10785
|
+
]);
|
|
10786
|
+
var LLMIX_MODULE_NAME = /^(?:_default|[a-z][a-z0-9_]{0,63})$/;
|
|
10787
|
+
var LLMIX_PRESET_NAME = /^(?:_base[a-z0-9_]*|[a-z][a-z0-9_]{0,63})$/;
|
|
10788
|
+
var LLMIX_NAMESPACE_KEYS = /* @__PURE__ */ new Set(["module", "preset", "common", "providerOptions", "caching"]);
|
|
10789
|
+
var LLMIX_CACHING_KEYS = /* @__PURE__ */ new Set(["strategy", "key", "ttl", "maxItems"]);
|
|
10790
|
+
var LLMIX_CACHING_STRATEGIES = /* @__PURE__ */ new Set(["native", "gateway", "disabled", "redis", "redis-or-memory", "memory"]);
|
|
10791
|
+
function validateLlmixNamespace(fm) {
|
|
10792
|
+
const metadata = isRecord(fm.metadata) ? fm.metadata : null;
|
|
10793
|
+
const namespace = metadata && isRecord(metadata["snoai-llmix"]) ? metadata["snoai-llmix"] : null;
|
|
10794
|
+
if (!namespace) return [];
|
|
10795
|
+
const diagnostics = [];
|
|
10796
|
+
for (const key of Object.keys(namespace)) {
|
|
10797
|
+
if (!LLMIX_NAMESPACE_KEYS.has(key))
|
|
10798
|
+
diagnostics.push(diag("llmix.unknown_namespace_key", `metadata.snoai-llmix.${key} is not a supported key`));
|
|
10799
|
+
}
|
|
10800
|
+
const common2 = isRecord(namespace.common) ? namespace.common : null;
|
|
10801
|
+
if (namespace.module !== void 0 && (typeof namespace.module !== "string" || !LLMIX_MODULE_NAME.test(namespace.module))) {
|
|
10802
|
+
diagnostics.push(diag("llmix.invalid_identifier", "metadata.snoai-llmix.module must be _default or a lowercase snake_case identifier"));
|
|
10803
|
+
}
|
|
10804
|
+
if (namespace.preset !== void 0 && (typeof namespace.preset !== "string" || !LLMIX_PRESET_NAME.test(namespace.preset))) {
|
|
10805
|
+
diagnostics.push(diag("llmix.invalid_identifier", "metadata.snoai-llmix.preset must be _base* or a lowercase snake_case identifier"));
|
|
10806
|
+
}
|
|
10807
|
+
if (!common2) {
|
|
10808
|
+
diagnostics.push(diag("llmix.missing_common", "metadata.snoai-llmix.common is required"));
|
|
10809
|
+
} else {
|
|
10810
|
+
for (const key of Object.keys(common2)) {
|
|
10811
|
+
if (!LLMIX_COMMON_KEYS.has(key))
|
|
10812
|
+
diagnostics.push(diag("llmix.unknown_common_key", `metadata.snoai-llmix.common.${key} is not supported`));
|
|
10813
|
+
}
|
|
10814
|
+
if (typeof common2.provider !== "string" || !LLMIX_PROVIDERS.has(common2.provider)) {
|
|
10815
|
+
diagnostics.push(diag("llmix.invalid_provider", "metadata.snoai-llmix.common.provider must be a supported provider"));
|
|
10816
|
+
}
|
|
10817
|
+
if (typeof common2.model !== "string" || common2.model.trim().length === 0) {
|
|
10818
|
+
diagnostics.push(diag("llmix.invalid_model", "metadata.snoai-llmix.common.model must be a non-empty string"));
|
|
10819
|
+
}
|
|
10820
|
+
validateOptionalNumber(common2, "temperature", 0, 2, diagnostics);
|
|
10821
|
+
validateOptionalNumber(common2, "topP", 0, 1, diagnostics);
|
|
10822
|
+
validateOptionalPositiveInteger(common2, "maxOutputTokens", diagnostics);
|
|
10823
|
+
validateOptionalPositiveInteger(common2, "topK", diagnostics);
|
|
10824
|
+
validateOptionalNonNegativeInteger(common2, "maxRetries", diagnostics);
|
|
10825
|
+
}
|
|
10826
|
+
if (namespace.providerOptions !== void 0) {
|
|
10827
|
+
if (!isRecord(namespace.providerOptions))
|
|
10828
|
+
diagnostics.push(diag("llmix.invalid_provider_options", "metadata.snoai-llmix.providerOptions must be an object"));
|
|
10829
|
+
else {
|
|
10830
|
+
for (const [provider, options] of Object.entries(namespace.providerOptions)) {
|
|
10831
|
+
if (!LLMIX_PROVIDERS.has(provider) || !isRecord(options)) {
|
|
10832
|
+
diagnostics.push(
|
|
10833
|
+
diag("llmix.invalid_provider_options", `metadata.snoai-llmix.providerOptions.${provider} must be a supported provider object`)
|
|
10834
|
+
);
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10839
|
+
if (namespace.caching !== void 0) {
|
|
10840
|
+
if (!isRecord(namespace.caching)) diagnostics.push(diag("llmix.invalid_caching", "metadata.snoai-llmix.caching must be an object"));
|
|
10841
|
+
else {
|
|
10842
|
+
for (const key of Object.keys(namespace.caching)) {
|
|
10843
|
+
if (!LLMIX_CACHING_KEYS.has(key))
|
|
10844
|
+
diagnostics.push(diag("llmix.unknown_caching_key", `metadata.snoai-llmix.caching.${key} is not supported`));
|
|
10845
|
+
}
|
|
10846
|
+
if (typeof namespace.caching.strategy !== "string" || !LLMIX_CACHING_STRATEGIES.has(namespace.caching.strategy)) {
|
|
10847
|
+
diagnostics.push(diag("llmix.invalid_caching", "metadata.snoai-llmix.caching.strategy must be a supported strategy"));
|
|
10848
|
+
}
|
|
10849
|
+
validateOptionalPositiveInteger(namespace.caching, "ttl", diagnostics);
|
|
10850
|
+
validateOptionalPositiveInteger(namespace.caching, "maxItems", diagnostics);
|
|
10851
|
+
}
|
|
10852
|
+
}
|
|
10853
|
+
return diagnostics;
|
|
10854
|
+
}
|
|
10855
|
+
function validateOptionalNumber(record, key, min, max, diagnostics) {
|
|
10856
|
+
if (record[key] === void 0) return;
|
|
10857
|
+
if (typeof record[key] !== "number" || !Number.isFinite(record[key]) || Number(record[key]) < min || Number(record[key]) > max) {
|
|
10858
|
+
diagnostics.push(diag("llmix.invalid_common", `metadata.snoai-llmix.common.${key} must be a number from ${min} to ${max}`));
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
function validateOptionalPositiveInteger(record, key, diagnostics) {
|
|
10862
|
+
if (record[key] === void 0) return;
|
|
10863
|
+
if (!Number.isInteger(record[key]) || Number(record[key]) <= 0) {
|
|
10864
|
+
diagnostics.push(diag("llmix.invalid_common", `${key} must be a positive integer`));
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
function validateOptionalNonNegativeInteger(record, key, diagnostics) {
|
|
10868
|
+
if (record[key] === void 0) return;
|
|
10869
|
+
if (!Number.isInteger(record[key]) || Number(record[key]) < 0) {
|
|
10870
|
+
diagnostics.push(diag("llmix.invalid_common", `${key} must be a non-negative integer`));
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10501
10873
|
function canonicalizeFromFile(file, target, sidecar) {
|
|
10502
10874
|
if (target === "MCP-SERVER.md" && !sidecar) {
|
|
10503
|
-
return {
|
|
10875
|
+
return {
|
|
10876
|
+
ok: false,
|
|
10877
|
+
exitCode: EXIT.usage,
|
|
10878
|
+
diagnostics: [diag("missing-required-sidecar", "MCP-SERVER.md canonicalization requires --sidecar <path>")],
|
|
10879
|
+
files: [file]
|
|
10880
|
+
};
|
|
10504
10881
|
}
|
|
10505
10882
|
const markdown = canonicalizeMarkdownFile(file);
|
|
10506
10883
|
if (!markdown.ok) return { ok: false, exitCode: EXIT.failure, diagnostics: markdown.diagnostics, files: [file] };
|
|
10507
10884
|
if (target !== "MCP-SERVER.md") return { ok: true, bytes: markdown.bytes, files: [file] };
|
|
10508
10885
|
const sidecarRead = readJson(sidecar);
|
|
10509
|
-
if (!sidecarRead.ok)
|
|
10886
|
+
if (!sidecarRead.ok)
|
|
10887
|
+
return { ok: false, exitCode: EXIT.io, diagnostics: [diag("io-error", sidecarRead.message)], files: [file, sidecar] };
|
|
10510
10888
|
return {
|
|
10511
10889
|
ok: true,
|
|
10512
10890
|
bytes: Buffer.concat([markdown.bytes, Buffer.from(MCP_BOUNDARY), Buffer.from(jcs(sidecarRead.value))]),
|
|
@@ -10553,11 +10931,10 @@ function compileTargets(frontmatter, body, targets, outDir, includeIntegrity) {
|
|
|
10553
10931
|
if (includeIntegrity) {
|
|
10554
10932
|
fm.integrity = {
|
|
10555
10933
|
algorithm: "sha256",
|
|
10556
|
-
digest: computeDigest(
|
|
10557
|
-
canonicalizeRenderedMarkdown(fm, body),
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
]), "sha256")
|
|
10934
|
+
digest: computeDigest(
|
|
10935
|
+
Buffer.concat([canonicalizeRenderedMarkdown(fm, body), Buffer.from(MCP_BOUNDARY), Buffer.from(jcs(sidecar))]),
|
|
10936
|
+
"sha256"
|
|
10937
|
+
)
|
|
10561
10938
|
};
|
|
10562
10939
|
bytes = renderMarkdown(fm, body);
|
|
10563
10940
|
}
|
|
@@ -10602,6 +10979,9 @@ function renderMarkdown(frontmatter, body) {
|
|
|
10602
10979
|
${rendered}---
|
|
10603
10980
|
${body}`;
|
|
10604
10981
|
}
|
|
10982
|
+
function renderArtifact(frontmatter, body) {
|
|
10983
|
+
return renderMarkdown(frontmatter, body);
|
|
10984
|
+
}
|
|
10605
10985
|
function canonicalizeRenderedMarkdown(frontmatter, body) {
|
|
10606
10986
|
const fm = { ...frontmatter };
|
|
10607
10987
|
delete fm.integrity;
|
|
@@ -10640,18 +11020,33 @@ function runConformanceSuite(suite, level) {
|
|
|
10640
11020
|
}
|
|
10641
11021
|
const manifest = jsYaml.load(manifestRead.text);
|
|
10642
11022
|
if (!isRecord(manifest) || !Array.isArray(manifest.fixtures)) {
|
|
10643
|
-
return {
|
|
11023
|
+
return {
|
|
11024
|
+
ok: false,
|
|
11025
|
+
diagnostics: [diag("conformance-manifest-invalid", "manifest.yaml must contain fixtures[]")],
|
|
11026
|
+
passCount: 0,
|
|
11027
|
+
failCount: 1,
|
|
11028
|
+
fixtures: []
|
|
11029
|
+
};
|
|
10644
11030
|
}
|
|
10645
11031
|
const fixtures = [];
|
|
11032
|
+
let compileFixtureCount = 0;
|
|
10646
11033
|
for (const entry of manifest.fixtures) {
|
|
10647
11034
|
if (!isRecord(entry) || typeof entry.id !== "string") continue;
|
|
10648
11035
|
if (entry.verdict === "equal") {
|
|
11036
|
+
compileFixtureCount += 1;
|
|
10649
11037
|
if (level === "V") continue;
|
|
10650
|
-
fixtures.push(
|
|
11038
|
+
fixtures.push(runCompileConformanceFixture(suite, entry));
|
|
10651
11039
|
continue;
|
|
10652
11040
|
}
|
|
10653
11041
|
fixtures.push(runConformanceFixture(suite, entry));
|
|
10654
11042
|
}
|
|
11043
|
+
if (level === "C" && compileFixtureCount === 0) {
|
|
11044
|
+
fixtures.push({
|
|
11045
|
+
id: "level-c-compile-coverage",
|
|
11046
|
+
ok: false,
|
|
11047
|
+
diagnostics: [diag("conformance.compile_fixtures_missing", "Level C requires at least one compile/equality fixture")]
|
|
11048
|
+
});
|
|
11049
|
+
}
|
|
10655
11050
|
const failures = fixtures.filter((f) => !f.ok);
|
|
10656
11051
|
return {
|
|
10657
11052
|
ok: failures.length === 0,
|
|
@@ -10661,6 +11056,71 @@ function runConformanceSuite(suite, level) {
|
|
|
10661
11056
|
fixtures
|
|
10662
11057
|
};
|
|
10663
11058
|
}
|
|
11059
|
+
function runCompileConformanceFixture(suite, entry) {
|
|
11060
|
+
const id = String(entry.id);
|
|
11061
|
+
const input = typeof entry.input === "string" ? resolve(suite, entry.input) : null;
|
|
11062
|
+
const expectedDir = typeof entry.expected_dir === "string" ? resolve(suite, entry.expected_dir) : null;
|
|
11063
|
+
const targetValues = Array.isArray(entry.targets) ? entry.targets.map((value) => normalizeCompileTarget(String(value))) : [];
|
|
11064
|
+
const targets = targetValues.filter((target) => target !== null);
|
|
11065
|
+
const diagnostics = [];
|
|
11066
|
+
if (!input) diagnostics.push(diag("conformance.manifest_invalid", "Compile fixture requires input"));
|
|
11067
|
+
if (!expectedDir) diagnostics.push(diag("conformance.manifest_invalid", "Compile fixture requires expected_dir"));
|
|
11068
|
+
if (targets.length === 0 || targets.length !== targetValues.length) {
|
|
11069
|
+
diagnostics.push(diag("conformance.manifest_invalid", "Compile fixture requires valid targets"));
|
|
11070
|
+
}
|
|
11071
|
+
if (diagnostics.length > 0) return { id, ok: false, diagnostics };
|
|
11072
|
+
const read = readArtifact(input);
|
|
11073
|
+
if (!read.ok) return { id, ok: false, diagnostics: [read.diagnostic] };
|
|
11074
|
+
if (read.extract.kind !== "ok" || !isRecord(read.extract.frontmatter)) {
|
|
11075
|
+
return { id, ok: false, diagnostics: [diag("conformance.compile_input_invalid", "Compile fixture input must be source frontmatter")] };
|
|
11076
|
+
}
|
|
11077
|
+
const validation = validateArtifact(input, "source");
|
|
11078
|
+
if (!validation.ok) {
|
|
11079
|
+
return {
|
|
11080
|
+
id,
|
|
11081
|
+
ok: false,
|
|
11082
|
+
diagnostics: validation.diagnostics.map((diagnostic) => ({ ...diagnostic, code: "conformance.compile_input_invalid" }))
|
|
11083
|
+
};
|
|
11084
|
+
}
|
|
11085
|
+
const outRoot = "__mda_conformance_out__";
|
|
11086
|
+
const staged = compileTargets(read.extract.frontmatter, read.extract.body, targets, outRoot, false);
|
|
11087
|
+
if (!staged.ok) {
|
|
11088
|
+
return {
|
|
11089
|
+
id,
|
|
11090
|
+
ok: false,
|
|
11091
|
+
diagnostics: staged.diagnostics.map((diagnostic) => ({ ...diagnostic, code: "conformance.compile_failed" }))
|
|
11092
|
+
};
|
|
11093
|
+
}
|
|
11094
|
+
const actual = /* @__PURE__ */ new Map();
|
|
11095
|
+
for (const output of staged.outputs) actual.set(relative(outRoot, output.path).replace(/\\/g, "/"), output.bytes);
|
|
11096
|
+
const expectedFiles = listExpectedTree(expectedDir);
|
|
11097
|
+
if (!expectedFiles.ok) return { id, ok: false, diagnostics: [expectedFiles.diagnostic] };
|
|
11098
|
+
const expected = /* @__PURE__ */ new Map();
|
|
11099
|
+
for (const file of expectedFiles.files) {
|
|
11100
|
+
const rel = relative(expectedDir, file).replace(/\\/g, "/");
|
|
11101
|
+
const text = readText(file);
|
|
11102
|
+
if (!text.ok) return { id, ok: false, diagnostics: [diag("conformance.expected_missing", text.message)] };
|
|
11103
|
+
expected.set(rel, text.text);
|
|
11104
|
+
}
|
|
11105
|
+
for (const rel of expected.keys()) {
|
|
11106
|
+
if (!actual.has(rel)) diagnostics.push(diag("conformance.compile_output_missing", `Expected output was not emitted: ${rel}`));
|
|
11107
|
+
}
|
|
11108
|
+
for (const rel of actual.keys()) {
|
|
11109
|
+
if (!expected.has(rel)) diagnostics.push(diag("conformance.compile_output_extra", `Unexpected output was emitted: ${rel}`));
|
|
11110
|
+
}
|
|
11111
|
+
for (const [rel, expectedText] of expected.entries()) {
|
|
11112
|
+
const actualText = actual.get(rel);
|
|
11113
|
+
if (actualText === void 0) continue;
|
|
11114
|
+
const expectedNormalized = normalizeConformanceOutput(rel, expectedText);
|
|
11115
|
+
const actualNormalized = normalizeConformanceOutput(rel, actualText);
|
|
11116
|
+
if (!expectedNormalized.ok) diagnostics.push(expectedNormalized.diagnostic);
|
|
11117
|
+
else if (!actualNormalized.ok) diagnostics.push(actualNormalized.diagnostic);
|
|
11118
|
+
else if (expectedNormalized.bytes !== actualNormalized.bytes) {
|
|
11119
|
+
diagnostics.push(diag("conformance.compile_output_mismatch", `Compiled output differs from expected fixture: ${rel}`));
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
return { id, ok: diagnostics.length === 0, diagnostics };
|
|
11123
|
+
}
|
|
10664
11124
|
function runConformanceFixture(suite, entry) {
|
|
10665
11125
|
const id = String(entry.id);
|
|
10666
11126
|
const fixturePath = resolve(suite, String(entry.path));
|
|
@@ -10682,7 +11142,8 @@ function runConformanceFixture(suite, entry) {
|
|
|
10682
11142
|
if (!read.ok) diagnostics.push(read.diagnostic);
|
|
10683
11143
|
else {
|
|
10684
11144
|
const got = read.extract.kind === "error" ? read.extract.code : read.extract.kind === "no-frontmatter" ? "no-frontmatter" : "ok";
|
|
10685
|
-
if (extractionExpected && got !== extractionExpected)
|
|
11145
|
+
if (extractionExpected && got !== extractionExpected)
|
|
11146
|
+
diagnostics.push(diag("extraction-mismatch", `Expected ${extractionExpected}, got ${got}`));
|
|
10686
11147
|
if (extractionExpected && got === extractionExpected && schemaPaths.length === 0) {
|
|
10687
11148
|
return { id, ok: true, diagnostics: [] };
|
|
10688
11149
|
}
|
|
@@ -10722,13 +11183,19 @@ function runTrustedRuntimeConformance(suite, entry) {
|
|
|
10722
11183
|
if (!policy.ok) return [diag("trust-policy-violation", policy.message)];
|
|
10723
11184
|
const policyValidation = validateJsonAgainst(policy.value, "trustPolicy");
|
|
10724
11185
|
if (!policyValidation.ok) return policyValidation.diagnostics.map((d) => ({ ...d, code: "trust-policy-violation" }));
|
|
10725
|
-
const
|
|
10726
|
-
if (!
|
|
10727
|
-
|
|
11186
|
+
const artifact2 = readArtifact(resolve(suite, String(entry.path)));
|
|
11187
|
+
if (!artifact2.ok || artifact2.extract.kind !== "ok" || !isRecord(artifact2.extract.frontmatter))
|
|
11188
|
+
return [diag("missing-required-integrity", "trusted-runtime requires frontmatter")];
|
|
11189
|
+
return checkTrustedRuntimePolicy(
|
|
11190
|
+
artifact2.extract.frontmatter,
|
|
11191
|
+
policy.value,
|
|
11192
|
+
Array.isArray(entry["verified-identities"]) ? entry["verified-identities"] : []
|
|
11193
|
+
);
|
|
10728
11194
|
}
|
|
10729
11195
|
function checkTrustedRuntimePolicy(fm, policy, verifiedIdentities) {
|
|
10730
11196
|
if (!isRecord(fm.integrity)) return [diag("missing-required-integrity", "trusted-runtime requires integrity")];
|
|
10731
|
-
if (!Array.isArray(fm.signatures) || fm.signatures.length === 0)
|
|
11197
|
+
if (!Array.isArray(fm.signatures) || fm.signatures.length === 0)
|
|
11198
|
+
return [diag("missing-required-signature", "trusted-runtime requires signatures[]")];
|
|
10732
11199
|
const digestErrors = checkSignatureDigestEquality(fm);
|
|
10733
11200
|
if (digestErrors.length) return digestErrors;
|
|
10734
11201
|
const trusted = /* @__PURE__ */ new Set();
|
|
@@ -10740,7 +11207,9 @@ function checkTrustedRuntimePolicy(fm, policy, verifiedIdentities) {
|
|
|
10740
11207
|
}
|
|
10741
11208
|
if (sig.signer.startsWith("sigstore-oidc:")) {
|
|
10742
11209
|
const issuer = sig.signer.slice("sigstore-oidc:".length);
|
|
10743
|
-
const identity = verifiedIdentities.find(
|
|
11210
|
+
const identity = verifiedIdentities.find(
|
|
11211
|
+
(item) => isRecord(item) && item.type === "sigstore-oidc" && item["signature-index"] === index
|
|
11212
|
+
);
|
|
10744
11213
|
if (isRecord(identity) && identity.issuer === issuer && policyAllowsSigstore(policy, identity)) {
|
|
10745
11214
|
trusted.add(`sigstore-oidc:${identity.issuer}
|
|
10746
11215
|
${identity.subject}`);
|
|
@@ -10749,23 +11218,28 @@ ${identity.subject}`);
|
|
|
10749
11218
|
}
|
|
10750
11219
|
if (trusted.size === 0) return [diag("no-trusted-signature", "no signature matched the trust policy")];
|
|
10751
11220
|
const minSignatures = isRecord(policy) && Number.isInteger(policy.minSignatures) ? Number(policy.minSignatures) : 1;
|
|
10752
|
-
if (trusted.size < minSignatures)
|
|
11221
|
+
if (trusted.size < minSignatures)
|
|
11222
|
+
return [diag("insufficient-trusted-signatures", `${trusted.size} trusted signer identities < ${minSignatures}`)];
|
|
10753
11223
|
return [];
|
|
10754
11224
|
}
|
|
10755
11225
|
function policyAllowsDidWeb(policy, domain) {
|
|
10756
11226
|
return isRecord(policy) && Array.isArray(policy.trustedSigners) && policy.trustedSigners.some((s) => isRecord(s) && s.type === "did-web" && s.domain === domain);
|
|
10757
11227
|
}
|
|
10758
11228
|
function policyAllowsSigstore(policy, identity) {
|
|
10759
|
-
return isRecord(policy) && Array.isArray(policy.trustedSigners) && policy.trustedSigners.some(
|
|
11229
|
+
return isRecord(policy) && Array.isArray(policy.trustedSigners) && policy.trustedSigners.some(
|
|
11230
|
+
(s) => isRecord(s) && s.type === "sigstore-oidc" && s.issuer === identity.issuer && s.subject === identity.subject
|
|
11231
|
+
);
|
|
10760
11232
|
}
|
|
10761
11233
|
function atomicWrite(path, bytes) {
|
|
10762
11234
|
const destination = resolve(path);
|
|
10763
11235
|
const dir = dirname(destination);
|
|
10764
11236
|
mkdirSync(dir, { recursive: true });
|
|
10765
|
-
const temp = join(dir, ".
|
|
11237
|
+
const temp = join(dir, `.mda-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`);
|
|
10766
11238
|
let fd = null;
|
|
11239
|
+
let createdTemp = false;
|
|
10767
11240
|
try {
|
|
10768
11241
|
fd = openSync(temp, "wx", 384);
|
|
11242
|
+
createdTemp = true;
|
|
10769
11243
|
writeFileSync(fd, bytes);
|
|
10770
11244
|
closeSync(fd);
|
|
10771
11245
|
fd = null;
|
|
@@ -10777,7 +11251,32 @@ function atomicWrite(path, bytes) {
|
|
|
10777
11251
|
} catch {
|
|
10778
11252
|
}
|
|
10779
11253
|
}
|
|
10780
|
-
rmSync(temp, { force: true });
|
|
11254
|
+
if (createdTemp) rmSync(temp, { force: true });
|
|
11255
|
+
}
|
|
11256
|
+
}
|
|
11257
|
+
function atomicReplace(path, bytes) {
|
|
11258
|
+
const destination = resolve(path);
|
|
11259
|
+
const dir = dirname(destination);
|
|
11260
|
+
mkdirSync(dir, { recursive: true });
|
|
11261
|
+
const temp = join(dir, `.mda-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`);
|
|
11262
|
+
let fd = null;
|
|
11263
|
+
let createdTemp = false;
|
|
11264
|
+
try {
|
|
11265
|
+
fd = openSync(temp, "wx", 384);
|
|
11266
|
+
createdTemp = true;
|
|
11267
|
+
writeFileSync(fd, bytes);
|
|
11268
|
+
closeSync(fd);
|
|
11269
|
+
fd = null;
|
|
11270
|
+
renameSync(temp, destination);
|
|
11271
|
+
createdTemp = false;
|
|
11272
|
+
} finally {
|
|
11273
|
+
if (fd !== null) {
|
|
11274
|
+
try {
|
|
11275
|
+
closeSync(fd);
|
|
11276
|
+
} catch {
|
|
11277
|
+
}
|
|
11278
|
+
}
|
|
11279
|
+
if (createdTemp) rmSync(temp, { force: true });
|
|
10781
11280
|
}
|
|
10782
11281
|
}
|
|
10783
11282
|
function readText(path) {
|
|
@@ -10787,6 +11286,47 @@ function readText(path) {
|
|
|
10787
11286
|
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
10788
11287
|
}
|
|
10789
11288
|
}
|
|
11289
|
+
function listExpectedTree(root) {
|
|
11290
|
+
const files = [];
|
|
11291
|
+
const walk2 = (dir) => {
|
|
11292
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
11293
|
+
const path = join(dir, entry.name);
|
|
11294
|
+
if (entry.isDirectory()) walk2(path);
|
|
11295
|
+
else if (entry.isFile()) files.push(path);
|
|
11296
|
+
}
|
|
11297
|
+
};
|
|
11298
|
+
try {
|
|
11299
|
+
walk2(root);
|
|
11300
|
+
return { ok: true, files: files.sort() };
|
|
11301
|
+
} catch (error) {
|
|
11302
|
+
return {
|
|
11303
|
+
ok: false,
|
|
11304
|
+
diagnostic: diag("conformance.expected_missing", error instanceof Error ? error.message : String(error), { path: root })
|
|
11305
|
+
};
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
function normalizeConformanceOutput(path, text) {
|
|
11309
|
+
if (path.endsWith(".json")) {
|
|
11310
|
+
try {
|
|
11311
|
+
return { ok: true, bytes: jcs(JSON.parse(text)) };
|
|
11312
|
+
} catch (error) {
|
|
11313
|
+
return {
|
|
11314
|
+
ok: false,
|
|
11315
|
+
diagnostic: diag("conformance.expected_invalid_json", error instanceof Error ? error.message : String(error), { path })
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
}
|
|
11319
|
+
if (path.endsWith(".md") || path.endsWith(".mda")) {
|
|
11320
|
+
const extract = extractFrontmatterStrict(Buffer.from(text));
|
|
11321
|
+
if (extract.kind === "error") return { ok: false, diagnostic: diag(extract.code, extract.message, { path }) };
|
|
11322
|
+
if (extract.kind === "no-frontmatter") return { ok: true, bytes: normalizeCanonicalBody(extract.body) };
|
|
11323
|
+
return { ok: true, bytes: `---
|
|
11324
|
+
${jcs(extract.frontmatter)}
|
|
11325
|
+
---
|
|
11326
|
+
${normalizeCanonicalBody(extract.body.replace(/^\n+/, ""))}` };
|
|
11327
|
+
}
|
|
11328
|
+
return { ok: true, bytes: text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") };
|
|
11329
|
+
}
|
|
10790
11330
|
function readJson(path) {
|
|
10791
11331
|
try {
|
|
10792
11332
|
return { ok: true, value: JSON.parse(readFileSync(path, "utf8")) };
|
|
@@ -10826,117 +11366,31 @@ function isRecord(value) {
|
|
|
10826
11366
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10827
11367
|
}
|
|
10828
11368
|
|
|
10829
|
-
// src/cli.ts
|
|
10830
|
-
var
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
Commands and options:
|
|
10853
|
-
init <name>
|
|
10854
|
-
--out <file> Write the scaffold atomically. Refuses overwrite.
|
|
10855
|
-
--json Return scaffold in JSON instead of raw .mda text.
|
|
10856
|
-
|
|
10857
|
-
validate <file>
|
|
10858
|
-
--target <target> source, SKILL.md, AGENTS.md, MCP-SERVER.md, or auto. Default: auto.
|
|
10859
|
-
|
|
10860
|
-
compile <file.mda>
|
|
10861
|
-
--target <target...> Required. One or more of SKILL.md, AGENTS.md, MCP-SERVER.md.
|
|
10862
|
-
--out-dir <dir> Output directory. Default: current working directory.
|
|
10863
|
-
--integrity Add sha256 integrity to emitted artifacts.
|
|
10864
|
-
|
|
10865
|
-
canonicalize <file>
|
|
10866
|
-
--target <target> Default: auto.
|
|
10867
|
-
--sidecar <path> Required only for MCP-SERVER.md multi-file canonical bytes.
|
|
10868
|
-
|
|
10869
|
-
integrity compute <file>
|
|
10870
|
-
--target <target> Default: auto.
|
|
10871
|
-
--sidecar <path> Required only for MCP-SERVER.md.
|
|
10872
|
-
--algorithm <name> sha256, sha384, or sha512. Default: sha256.
|
|
10873
|
-
|
|
10874
|
-
integrity verify <file>
|
|
10875
|
-
--target <target> Default: auto.
|
|
10876
|
-
--sidecar <path> Required only for MCP-SERVER.md.
|
|
10877
|
-
|
|
10878
|
-
verify <file>
|
|
10879
|
-
--policy <path> Required trust policy JSON.
|
|
10880
|
-
--target <target> Default: auto.
|
|
10881
|
-
--sidecar <path> Required only for MCP-SERVER.md.
|
|
10882
|
-
--offline Unsupported in this MVP; exits with usage error.
|
|
10883
|
-
|
|
10884
|
-
sign <file>
|
|
10885
|
-
--method did-web Required. Signing is not yet stable in this MVP.
|
|
10886
|
-
--key <path> Required.
|
|
10887
|
-
--identity <domain> Required.
|
|
10888
|
-
--out <file> Write signed output.
|
|
10889
|
-
--in-place Replace the input file.
|
|
10890
|
-
|
|
10891
|
-
Examples:
|
|
10892
|
-
mda init hello-skill --out hello.mda
|
|
10893
|
-
mda validate hello.mda --json
|
|
10894
|
-
mda compile hello.mda --target SKILL.md AGENTS.md MCP-SERVER.md --out-dir out --integrity
|
|
10895
|
-
mda canonicalize out/SKILL.md --target SKILL.md --json
|
|
10896
|
-
mda integrity compute out/SKILL.md --target SKILL.md --algorithm sha256 --json
|
|
10897
|
-
mda integrity verify out/SKILL.md --target SKILL.md
|
|
10898
|
-
mda verify signed.md --policy policy.json --json
|
|
10899
|
-
mda conformance --suite conformance --level V --json
|
|
11369
|
+
// src/cli/constants.ts
|
|
11370
|
+
var DIGEST_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
|
|
11371
|
+
var CLI_VERSION = "1.1.0";
|
|
11372
|
+
var LLMIX_PROVIDERS2 = /* @__PURE__ */ new Set([
|
|
11373
|
+
"openai",
|
|
11374
|
+
"anthropic",
|
|
11375
|
+
"google",
|
|
11376
|
+
"deepseek",
|
|
11377
|
+
"openrouter",
|
|
11378
|
+
"deepinfra",
|
|
11379
|
+
"novita",
|
|
11380
|
+
"together",
|
|
11381
|
+
"sno-gpu"
|
|
11382
|
+
]);
|
|
11383
|
+
var LLMIX_MODULE_NAME2 = /^(?:_default|[a-z][a-z0-9_]{0,63})$/;
|
|
11384
|
+
var LLMIX_PRESET_NAME2 = /^(?:_base[a-z0-9_]*|[a-z][a-z0-9_]{0,63})$/;
|
|
11385
|
+
var INTEGRITY_PAYLOAD_TYPE = "application/vnd.mda.integrity+json";
|
|
11386
|
+
var GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com";
|
|
11387
|
+
var GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
11388
|
+
var GITHUB_REF = /^refs\/(?:heads|tags)\/\S+$/;
|
|
11389
|
+
var SIGSTORE_REKOR_URL = "https://rekor.sigstore.dev";
|
|
11390
|
+
var DIGEST_PATTERN = /^sha(?:256|384|512):[a-f0-9]+$/;
|
|
11391
|
+
var LLMIX_SNIPPET_FORMATS = /* @__PURE__ */ new Set(["json", "env", "kubernetes", "github-actions", "terraform", "typescript", "python", "rust"]);
|
|
10900
11392
|
|
|
10901
|
-
|
|
10902
|
-
0 Success.
|
|
10903
|
-
1 Valid command, but artifact validation or verification failed.
|
|
10904
|
-
2 CLI usage error: missing argument, unknown flag, ambiguous target.
|
|
10905
|
-
3 IO or configuration error: missing file, overwrite refusal, unreadable policy.
|
|
10906
|
-
4 Internal bug or invariant failure.
|
|
10907
|
-
`;
|
|
10908
|
-
var DIGEST_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
|
|
10909
|
-
async function main() {
|
|
10910
|
-
const { globals, args } = splitGlobals(process.argv.slice(2));
|
|
10911
|
-
try {
|
|
10912
|
-
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
10913
|
-
process.stdout.write(HELP);
|
|
10914
|
-
process.exit(EXIT.ok);
|
|
10915
|
-
}
|
|
10916
|
-
const command = args[0];
|
|
10917
|
-
const rest = args.slice(1);
|
|
10918
|
-
const result = await runCommand(command, rest, globals);
|
|
10919
|
-
writeResult(result, globals);
|
|
10920
|
-
process.exit(result.exitCode);
|
|
10921
|
-
} catch (error) {
|
|
10922
|
-
const result = commandResult(false, "internal", EXIT.internal, [
|
|
10923
|
-
diag("internal-error", error instanceof Error ? error.message : String(error))
|
|
10924
|
-
]);
|
|
10925
|
-
writeResult(result, globals);
|
|
10926
|
-
process.exit(result.exitCode);
|
|
10927
|
-
}
|
|
10928
|
-
}
|
|
10929
|
-
async function runCommand(command, args, globals) {
|
|
10930
|
-
if (command === "init") return runInit(args, globals);
|
|
10931
|
-
if (command === "validate") return runValidate(args);
|
|
10932
|
-
if (command === "compile") return runCompile(args);
|
|
10933
|
-
if (command === "canonicalize") return runCanonicalize(args, globals);
|
|
10934
|
-
if (command === "integrity") return runIntegrity(args);
|
|
10935
|
-
if (command === "verify") return runVerify(args);
|
|
10936
|
-
if (command === "sign") return runSign(args);
|
|
10937
|
-
if (command === "conformance") return runConformance(args);
|
|
10938
|
-
return usage("root", `Unknown command: ${command}`);
|
|
10939
|
-
}
|
|
11393
|
+
// src/cli/shared.ts
|
|
10940
11394
|
function writeResult(result, globals) {
|
|
10941
11395
|
if (globals.json) {
|
|
10942
11396
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -10950,25 +11404,83 @@ function writeResult(result, globals) {
|
|
|
10950
11404
|
`);
|
|
10951
11405
|
else process.stdout.write(`ok: ${result.command}
|
|
10952
11406
|
`);
|
|
11407
|
+
writeNextActions(result, globals);
|
|
10953
11408
|
return;
|
|
10954
11409
|
}
|
|
10955
11410
|
for (const d of result.diagnostics) {
|
|
10956
11411
|
process.stderr.write(`${d.code}: ${d.message}
|
|
10957
11412
|
`);
|
|
10958
11413
|
}
|
|
11414
|
+
writeNextActions(result, globals, process.stderr);
|
|
10959
11415
|
}
|
|
10960
11416
|
function splitGlobals(argv) {
|
|
10961
|
-
const globals = { json: false, quiet: false, verbose: false, color: true };
|
|
11417
|
+
const globals = { json: false, quiet: false, verbose: false, color: true, noNext: false };
|
|
10962
11418
|
const args = [];
|
|
10963
11419
|
for (const arg of argv) {
|
|
10964
11420
|
if (arg === "--json") globals.json = true;
|
|
10965
11421
|
else if (arg === "--quiet") globals.quiet = true;
|
|
10966
11422
|
else if (arg === "--verbose") globals.verbose = true;
|
|
10967
11423
|
else if (arg === "--no-color") globals.color = false;
|
|
11424
|
+
else if (arg === "--no-next") globals.noNext = true;
|
|
10968
11425
|
else args.push(arg);
|
|
10969
11426
|
}
|
|
10970
11427
|
return { globals, args };
|
|
10971
11428
|
}
|
|
11429
|
+
function writeNextActions(result, globals, stream = process.stdout) {
|
|
11430
|
+
if (globals.noNext || result.nextActions.length === 0) return;
|
|
11431
|
+
stream.write("Next:\n");
|
|
11432
|
+
for (const action of result.nextActions) {
|
|
11433
|
+
const marker = action.required ? "-" : "- optional:";
|
|
11434
|
+
if (action.command) stream.write(`${marker} ${action.reason}: ${action.command}
|
|
11435
|
+
`);
|
|
11436
|
+
else if (action.external) stream.write(`${marker} ${action.reason}: ${action.external}
|
|
11437
|
+
`);
|
|
11438
|
+
else stream.write(`${marker} ${action.reason}
|
|
11439
|
+
`);
|
|
11440
|
+
}
|
|
11441
|
+
}
|
|
11442
|
+
function nextAction(id, reason, command, required = true) {
|
|
11443
|
+
return { id, required, reason, command };
|
|
11444
|
+
}
|
|
11445
|
+
function externalNextAction(id, reason, external, required = true) {
|
|
11446
|
+
return { id, required, reason, external };
|
|
11447
|
+
}
|
|
11448
|
+
function artifact(kind, path, target, digest) {
|
|
11449
|
+
return { kind, path, target, digest };
|
|
11450
|
+
}
|
|
11451
|
+
function nextAfterValidate(file, target) {
|
|
11452
|
+
if (target === "source") {
|
|
11453
|
+
return [
|
|
11454
|
+
nextAction(
|
|
11455
|
+
"compile-source",
|
|
11456
|
+
"Compile the source into runtime Markdown",
|
|
11457
|
+
`mda compile ${file} --target SKILL.md AGENTS.md --out-dir out --integrity`
|
|
11458
|
+
)
|
|
11459
|
+
];
|
|
11460
|
+
}
|
|
11461
|
+
return [nextAction("verify-integrity", "Verify declared integrity before use", `mda integrity verify ${file} --target ${target}`, false)];
|
|
11462
|
+
}
|
|
11463
|
+
function nextAfterCompile(paths) {
|
|
11464
|
+
const markdown = paths.find((path) => path.endsWith("SKILL.md") || path.endsWith("AGENTS.md") || path.endsWith("MCP-SERVER.md"));
|
|
11465
|
+
if (!markdown) return [];
|
|
11466
|
+
const target = targetForPath(markdown);
|
|
11467
|
+
if (!target) return [];
|
|
11468
|
+
return [
|
|
11469
|
+
nextAction("validate-output", "Validate the first compiled output", `mda validate ${markdown} --target ${target}`),
|
|
11470
|
+
nextAction(
|
|
11471
|
+
"verify-output-integrity",
|
|
11472
|
+
"Verify compiled output integrity before publishing",
|
|
11473
|
+
`mda integrity verify ${markdown} --target ${target}`,
|
|
11474
|
+
false
|
|
11475
|
+
)
|
|
11476
|
+
];
|
|
11477
|
+
}
|
|
11478
|
+
function targetForPath(path) {
|
|
11479
|
+
if (path.endsWith("SKILL.md")) return "SKILL.md";
|
|
11480
|
+
if (path.endsWith("AGENTS.md")) return "AGENTS.md";
|
|
11481
|
+
if (path.endsWith("MCP-SERVER.md")) return "MCP-SERVER.md";
|
|
11482
|
+
return void 0;
|
|
11483
|
+
}
|
|
10972
11484
|
function parseOptions(args) {
|
|
10973
11485
|
const positional = [];
|
|
10974
11486
|
const options = /* @__PURE__ */ new Map();
|
|
@@ -10979,7 +11491,47 @@ function parseOptions(args) {
|
|
|
10979
11491
|
positional.push(arg);
|
|
10980
11492
|
continue;
|
|
10981
11493
|
}
|
|
10982
|
-
if ([
|
|
11494
|
+
if ([
|
|
11495
|
+
"--out",
|
|
11496
|
+
"--target",
|
|
11497
|
+
"--out-dir",
|
|
11498
|
+
"--sidecar",
|
|
11499
|
+
"--algorithm",
|
|
11500
|
+
"--suite",
|
|
11501
|
+
"--level",
|
|
11502
|
+
"--policy",
|
|
11503
|
+
"--did-document",
|
|
11504
|
+
"--profile",
|
|
11505
|
+
"--did",
|
|
11506
|
+
"--key-id",
|
|
11507
|
+
"--key-file",
|
|
11508
|
+
"--method",
|
|
11509
|
+
"--key",
|
|
11510
|
+
"--identity",
|
|
11511
|
+
"--template",
|
|
11512
|
+
"--module",
|
|
11513
|
+
"--preset",
|
|
11514
|
+
"--provider",
|
|
11515
|
+
"--model",
|
|
11516
|
+
"--domain",
|
|
11517
|
+
"--min-signatures",
|
|
11518
|
+
"--repo",
|
|
11519
|
+
"--workflow",
|
|
11520
|
+
"--ref",
|
|
11521
|
+
"--offline-sigstore-fixture",
|
|
11522
|
+
"--source",
|
|
11523
|
+
"--registry-dir",
|
|
11524
|
+
"--registry-root",
|
|
11525
|
+
"--release-plan",
|
|
11526
|
+
"--manifest",
|
|
11527
|
+
"--format",
|
|
11528
|
+
"--snippet-format",
|
|
11529
|
+
"--snippet-out",
|
|
11530
|
+
"--expected-root-digest",
|
|
11531
|
+
"--minimum-revision",
|
|
11532
|
+
"--minimum-published-at",
|
|
11533
|
+
"--high-watermark"
|
|
11534
|
+
].includes(arg)) {
|
|
10983
11535
|
const values = [];
|
|
10984
11536
|
i += 1;
|
|
10985
11537
|
while (i < args.length && !args[i].startsWith("--")) {
|
|
@@ -10992,7 +11544,7 @@ function parseOptions(args) {
|
|
|
10992
11544
|
options.set(arg, [...options.get(arg) ?? [], ...values]);
|
|
10993
11545
|
continue;
|
|
10994
11546
|
}
|
|
10995
|
-
if (["--integrity", "--in-place", "--offline"].includes(arg)) {
|
|
11547
|
+
if (["--integrity", "--in-place", "--offline", "--write", "--rekor", "--derive-root-digest", "--strict-compat"].includes(arg)) {
|
|
10996
11548
|
flags.add(arg);
|
|
10997
11549
|
continue;
|
|
10998
11550
|
}
|
|
@@ -11015,10 +11567,15 @@ function unknownOptions(parsed, allowed) {
|
|
|
11015
11567
|
}
|
|
11016
11568
|
return null;
|
|
11017
11569
|
}
|
|
11570
|
+
|
|
11571
|
+
// src/cli/core-commands.ts
|
|
11018
11572
|
function runInit(args, globals) {
|
|
11019
11573
|
const parsed = parseOptions(args);
|
|
11020
|
-
const err = unknownOptions(parsed, ["--out"]);
|
|
11574
|
+
const err = unknownOptions(parsed, ["--out", "--template", "--module", "--preset", "--provider", "--model"]);
|
|
11021
11575
|
if (err) return usage("init", err);
|
|
11576
|
+
const template = oneOption(parsed.options, "--template");
|
|
11577
|
+
if (template && template !== "llmix-preset") return usage("init", `Unsupported template: ${template}`);
|
|
11578
|
+
if (template === "llmix-preset") return runInitLlmixPreset(parsed, globals);
|
|
11022
11579
|
if (parsed.positional.length !== 1) return usage("init", "Expected exactly one name: mda init <name>");
|
|
11023
11580
|
const name = parsed.positional[0];
|
|
11024
11581
|
const scaffold = makeScaffold(name);
|
|
@@ -11039,15 +11596,115 @@ function runInit(args, globals) {
|
|
|
11039
11596
|
}
|
|
11040
11597
|
}
|
|
11041
11598
|
if (globals.json) {
|
|
11042
|
-
return commandResult(true, "init", EXIT.ok, [], {
|
|
11599
|
+
return commandResult(true, "init", EXIT.ok, [], {
|
|
11600
|
+
summary: out ? `Created MDA source at ${out}` : "Generated MDA source scaffold",
|
|
11601
|
+
artifacts: out ? [artifact("mda-source", out, "source")] : [],
|
|
11602
|
+
nextActions: out ? [nextAction("validate-source", "Validate the source file", `mda validate ${out} --target source`)] : [externalNextAction("save-source", "Save this scaffold to a .mda file", "write the scaffold bytes to disk")],
|
|
11603
|
+
name,
|
|
11604
|
+
scaffold,
|
|
11605
|
+
out,
|
|
11606
|
+
written: Boolean(out)
|
|
11607
|
+
});
|
|
11043
11608
|
}
|
|
11044
11609
|
return commandResult(true, "init", EXIT.ok, [], {
|
|
11610
|
+
summary: out ? `Created MDA source at ${out}` : "Generated MDA source scaffold",
|
|
11611
|
+
artifacts: out ? [artifact("mda-source", out, "source")] : [],
|
|
11612
|
+
nextActions: out ? [nextAction("validate-source", "Validate the source file", `mda validate ${out} --target source`)] : [],
|
|
11045
11613
|
message: out ? `wrote ${out}` : scaffold,
|
|
11046
11614
|
name,
|
|
11047
11615
|
out,
|
|
11048
11616
|
written: Boolean(out)
|
|
11049
11617
|
});
|
|
11050
11618
|
}
|
|
11619
|
+
function runInitLlmixPreset(parsed, globals) {
|
|
11620
|
+
if (parsed.positional.length !== 0) return usage("init", "LLMix preset init does not take a positional name");
|
|
11621
|
+
const moduleName = oneOption(parsed.options, "--module");
|
|
11622
|
+
const presetName = oneOption(parsed.options, "--preset");
|
|
11623
|
+
const provider = oneOption(parsed.options, "--provider");
|
|
11624
|
+
const model = oneOption(parsed.options, "--model");
|
|
11625
|
+
const out = oneOption(parsed.options, "--out");
|
|
11626
|
+
const diagnostics = [];
|
|
11627
|
+
if (!moduleName || !LLMIX_MODULE_NAME2.test(moduleName))
|
|
11628
|
+
diagnostics.push(diag("llmix.invalid_identifier", "--module must be _default or a lowercase snake_case identifier"));
|
|
11629
|
+
if (!presetName || !LLMIX_PRESET_NAME2.test(presetName))
|
|
11630
|
+
diagnostics.push(diag("llmix.invalid_identifier", "--preset must be _base* or a lowercase snake_case identifier"));
|
|
11631
|
+
if (!provider || !LLMIX_PROVIDERS2.has(provider))
|
|
11632
|
+
diagnostics.push(diag("llmix.invalid_provider", "--provider must be a supported LLMix provider"));
|
|
11633
|
+
if (!model || model.trim().length === 0)
|
|
11634
|
+
diagnostics.push(diag("llmix.invalid_model", "--model must be a non-empty provider model identifier"));
|
|
11635
|
+
if (diagnostics.length > 0) {
|
|
11636
|
+
return commandResult(false, "init", EXIT.usage, diagnostics, {
|
|
11637
|
+
summary: "LLMix preset scaffold options are invalid",
|
|
11638
|
+
nextActions: [
|
|
11639
|
+
nextAction(
|
|
11640
|
+
"fix-llmix-init",
|
|
11641
|
+
"Fix the LLMix preset options and retry",
|
|
11642
|
+
"mda init --template llmix-preset --module search_summary --preset openai_fast --provider openai --model gpt-5-mini --out search_summary/openai_fast.mda"
|
|
11643
|
+
)
|
|
11644
|
+
]
|
|
11645
|
+
});
|
|
11646
|
+
}
|
|
11647
|
+
const scaffold = makeLlmixPresetScaffold(moduleName, presetName, provider, model);
|
|
11648
|
+
if (out) {
|
|
11649
|
+
if (existsSync2(out)) {
|
|
11650
|
+
return ioError("init", `Refusing to overwrite existing file: ${out}`, {
|
|
11651
|
+
module: moduleName,
|
|
11652
|
+
preset: presetName,
|
|
11653
|
+
scaffold: globals.json ? scaffold : void 0,
|
|
11654
|
+
out,
|
|
11655
|
+
written: false
|
|
11656
|
+
});
|
|
11657
|
+
}
|
|
11658
|
+
try {
|
|
11659
|
+
atomicWrite(out, scaffold);
|
|
11660
|
+
} catch (error) {
|
|
11661
|
+
return ioError("init", error instanceof Error ? error.message : String(error), {
|
|
11662
|
+
module: moduleName,
|
|
11663
|
+
preset: presetName,
|
|
11664
|
+
out,
|
|
11665
|
+
written: false
|
|
11666
|
+
});
|
|
11667
|
+
}
|
|
11668
|
+
}
|
|
11669
|
+
const nextActions = out ? [
|
|
11670
|
+
nextAction("validate-llmix-source", "Validate the LLMix source file", `mda validate ${out} --target source`),
|
|
11671
|
+
nextAction("write-integrity", "Record integrity before release", `mda integrity compute ${out} --target source --write`, false)
|
|
11672
|
+
] : [
|
|
11673
|
+
externalNextAction(
|
|
11674
|
+
"save-llmix-source",
|
|
11675
|
+
"Save this scaffold to a .mda file under the module directory",
|
|
11676
|
+
"write the scaffold bytes to disk"
|
|
11677
|
+
)
|
|
11678
|
+
];
|
|
11679
|
+
if (globals.json) {
|
|
11680
|
+
return commandResult(true, "init", EXIT.ok, [], {
|
|
11681
|
+
summary: out ? `Created LLMix preset source at ${out}` : "Generated LLMix preset scaffold",
|
|
11682
|
+
artifacts: out ? [artifact("mda-source", out, "source")] : [],
|
|
11683
|
+
nextActions,
|
|
11684
|
+
template: "llmix-preset",
|
|
11685
|
+
module: moduleName,
|
|
11686
|
+
preset: presetName,
|
|
11687
|
+
provider,
|
|
11688
|
+
model,
|
|
11689
|
+
scaffold,
|
|
11690
|
+
out,
|
|
11691
|
+
written: Boolean(out)
|
|
11692
|
+
});
|
|
11693
|
+
}
|
|
11694
|
+
return commandResult(true, "init", EXIT.ok, [], {
|
|
11695
|
+
summary: out ? `Created LLMix preset source at ${out}` : "Generated LLMix preset scaffold",
|
|
11696
|
+
artifacts: out ? [artifact("mda-source", out, "source")] : [],
|
|
11697
|
+
nextActions: out ? nextActions : [],
|
|
11698
|
+
message: out ? `wrote ${out}` : scaffold,
|
|
11699
|
+
template: "llmix-preset",
|
|
11700
|
+
module: moduleName,
|
|
11701
|
+
preset: presetName,
|
|
11702
|
+
provider,
|
|
11703
|
+
model,
|
|
11704
|
+
out,
|
|
11705
|
+
written: Boolean(out)
|
|
11706
|
+
});
|
|
11707
|
+
}
|
|
11051
11708
|
function runValidate(args) {
|
|
11052
11709
|
const parsed = parseOptions(args);
|
|
11053
11710
|
const err = unknownOptions(parsed, ["--target"]);
|
|
@@ -11060,6 +11717,15 @@ function runValidate(args) {
|
|
|
11060
11717
|
if (!targetResult.ok) return targetResult.result("validate", file);
|
|
11061
11718
|
const validation = validateArtifact(file, targetResult.target);
|
|
11062
11719
|
return commandResult(validation.ok, "validate", validation.ok ? EXIT.ok : EXIT.failure, validation.diagnostics, {
|
|
11720
|
+
summary: validation.ok ? `${targetResult.target} validation passed` : `${targetResult.target} validation failed`,
|
|
11721
|
+
artifacts: [artifact("validated-artifact", file, targetResult.target)],
|
|
11722
|
+
nextActions: validation.ok ? nextAfterValidate(file, targetResult.target) : [
|
|
11723
|
+
nextAction(
|
|
11724
|
+
"fix-validation",
|
|
11725
|
+
"Fix the reported diagnostics and re-run validation",
|
|
11726
|
+
`mda validate ${file} --target ${targetResult.target}`
|
|
11727
|
+
)
|
|
11728
|
+
],
|
|
11063
11729
|
file,
|
|
11064
11730
|
target: targetResult.target
|
|
11065
11731
|
});
|
|
@@ -11077,13 +11743,35 @@ function runCanonicalize(args, globals) {
|
|
|
11077
11743
|
const sidecar = oneOption(parsed.options, "--sidecar");
|
|
11078
11744
|
const can = canonicalizeFromFile(file, targetResult.target, sidecar);
|
|
11079
11745
|
if (!can.ok) {
|
|
11080
|
-
return commandResult(false, "canonicalize", can.exitCode, can.diagnostics, {
|
|
11746
|
+
return commandResult(false, "canonicalize", can.exitCode, can.diagnostics, {
|
|
11747
|
+
summary: "Canonicalization failed",
|
|
11748
|
+
nextActions: [
|
|
11749
|
+
nextAction(
|
|
11750
|
+
"fix-canonicalization",
|
|
11751
|
+
"Fix the reported diagnostics and retry",
|
|
11752
|
+
`mda canonicalize ${file} --target ${targetResult.target}`
|
|
11753
|
+
)
|
|
11754
|
+
],
|
|
11755
|
+
file,
|
|
11756
|
+
target: targetResult.target,
|
|
11757
|
+
files: can.files
|
|
11758
|
+
});
|
|
11081
11759
|
}
|
|
11082
11760
|
if (!globals.json) {
|
|
11083
11761
|
process.stdout.write(can.bytes);
|
|
11084
11762
|
return commandResult(true, "canonicalize", EXIT.ok, [], { suppressOutput: true });
|
|
11085
11763
|
}
|
|
11086
11764
|
return commandResult(true, "canonicalize", EXIT.ok, [], {
|
|
11765
|
+
summary: "Canonical bytes generated",
|
|
11766
|
+
artifacts: [artifact("canonical-bytes", file, targetResult.target)],
|
|
11767
|
+
nextActions: [
|
|
11768
|
+
nextAction(
|
|
11769
|
+
"compute-integrity",
|
|
11770
|
+
"Compute an integrity digest for these bytes",
|
|
11771
|
+
`mda integrity compute ${file} --target ${targetResult.target}`,
|
|
11772
|
+
false
|
|
11773
|
+
)
|
|
11774
|
+
],
|
|
11087
11775
|
file,
|
|
11088
11776
|
target: targetResult.target,
|
|
11089
11777
|
files: can.files,
|
|
@@ -11095,7 +11783,7 @@ function runIntegrity(args) {
|
|
|
11095
11783
|
const sub = args[0];
|
|
11096
11784
|
if (sub !== "compute" && sub !== "verify") return usage("integrity", "Expected subcommand: integrity compute|verify");
|
|
11097
11785
|
const parsed = parseOptions(args.slice(1));
|
|
11098
|
-
const err = unknownOptions(parsed, ["--target", "--sidecar", "--algorithm"]);
|
|
11786
|
+
const err = unknownOptions(parsed, ["--target", "--sidecar", "--algorithm", "--write"]);
|
|
11099
11787
|
if (err) return usage(`integrity ${sub}`, err);
|
|
11100
11788
|
if (parsed.positional.length !== 1) return usage(`integrity ${sub}`, `Expected one file: mda integrity ${sub} <file>`);
|
|
11101
11789
|
const file = parsed.positional[0];
|
|
@@ -11105,12 +11793,125 @@ function runIntegrity(args) {
|
|
|
11105
11793
|
if (!targetResult.ok) return targetResult.result(`integrity ${sub}`, file);
|
|
11106
11794
|
const sidecar = oneOption(parsed.options, "--sidecar");
|
|
11107
11795
|
const can = canonicalizeFromFile(file, targetResult.target, sidecar);
|
|
11108
|
-
if (!can.ok)
|
|
11796
|
+
if (!can.ok)
|
|
11797
|
+
return commandResult(false, `integrity ${sub}`, can.exitCode, can.diagnostics, {
|
|
11798
|
+
summary: `integrity ${sub} failed`,
|
|
11799
|
+
nextActions: [
|
|
11800
|
+
nextAction(
|
|
11801
|
+
"fix-integrity-input",
|
|
11802
|
+
"Fix the reported diagnostics and retry",
|
|
11803
|
+
`mda integrity ${sub} ${file} --target ${targetResult.target}`
|
|
11804
|
+
)
|
|
11805
|
+
],
|
|
11806
|
+
file,
|
|
11807
|
+
target: targetResult.target,
|
|
11808
|
+
files: can.files
|
|
11809
|
+
});
|
|
11109
11810
|
if (sub === "compute") {
|
|
11811
|
+
if (parsed.flags.has("--write") && parsed.options.has("--sidecar") && targetResult.target !== "MCP-SERVER.md") {
|
|
11812
|
+
return usage("integrity compute", "--sidecar is only valid with MCP-SERVER.md");
|
|
11813
|
+
}
|
|
11110
11814
|
const algorithm = oneOption(parsed.options, "--algorithm") ?? "sha256";
|
|
11111
11815
|
if (!DIGEST_ALGORITHMS.has(algorithm)) return usage("integrity compute", `Unsupported algorithm: ${algorithm}`);
|
|
11112
11816
|
const digest = computeDigest(can.bytes, algorithm);
|
|
11817
|
+
if (parsed.flags.has("--write")) {
|
|
11818
|
+
const ext2 = readArtifact(file);
|
|
11819
|
+
if (!ext2.ok || ext2.extract.kind !== "ok" || !isRecord(ext2.extract.frontmatter)) {
|
|
11820
|
+
return commandResult(
|
|
11821
|
+
false,
|
|
11822
|
+
"integrity compute",
|
|
11823
|
+
EXIT.failure,
|
|
11824
|
+
[diag("missing-required-frontmatter", "Integrity write requires frontmatter")],
|
|
11825
|
+
{
|
|
11826
|
+
summary: "Integrity write failed",
|
|
11827
|
+
nextActions: [
|
|
11828
|
+
nextAction(
|
|
11829
|
+
"fix-frontmatter",
|
|
11830
|
+
"Add frontmatter and retry integrity write",
|
|
11831
|
+
`mda validate ${file} --target ${targetResult.target}`
|
|
11832
|
+
)
|
|
11833
|
+
],
|
|
11834
|
+
file,
|
|
11835
|
+
target: targetResult.target,
|
|
11836
|
+
files: can.files
|
|
11837
|
+
}
|
|
11838
|
+
);
|
|
11839
|
+
}
|
|
11840
|
+
const existing = ext2.extract.frontmatter.integrity;
|
|
11841
|
+
if (isRecord(existing) && (existing.algorithm !== algorithm || existing.digest !== digest)) {
|
|
11842
|
+
return commandResult(
|
|
11843
|
+
false,
|
|
11844
|
+
"integrity compute",
|
|
11845
|
+
EXIT.failure,
|
|
11846
|
+
[
|
|
11847
|
+
diag(
|
|
11848
|
+
"integrity.existing_mismatch",
|
|
11849
|
+
"Existing integrity differs from the computed digest; remove it intentionally before rewriting"
|
|
11850
|
+
)
|
|
11851
|
+
],
|
|
11852
|
+
{
|
|
11853
|
+
summary: "Existing integrity does not match computed digest",
|
|
11854
|
+
nextActions: [
|
|
11855
|
+
nextAction(
|
|
11856
|
+
"inspect-integrity",
|
|
11857
|
+
"Inspect or remove the stale integrity field before retrying",
|
|
11858
|
+
`mda integrity verify ${file} --target ${targetResult.target}`
|
|
11859
|
+
)
|
|
11860
|
+
],
|
|
11861
|
+
file,
|
|
11862
|
+
target: targetResult.target,
|
|
11863
|
+
files: can.files,
|
|
11864
|
+
algorithm,
|
|
11865
|
+
digest,
|
|
11866
|
+
written: false
|
|
11867
|
+
}
|
|
11868
|
+
);
|
|
11869
|
+
}
|
|
11870
|
+
let written = false;
|
|
11871
|
+
if (!isRecord(existing)) {
|
|
11872
|
+
const frontmatter = { ...ext2.extract.frontmatter, integrity: { algorithm, digest } };
|
|
11873
|
+
try {
|
|
11874
|
+
atomicReplace(file, renderArtifact(frontmatter, ext2.extract.body));
|
|
11875
|
+
written = true;
|
|
11876
|
+
} catch (error) {
|
|
11877
|
+
return ioError("integrity compute", error instanceof Error ? error.message : String(error), {
|
|
11878
|
+
file,
|
|
11879
|
+
target: targetResult.target,
|
|
11880
|
+
algorithm,
|
|
11881
|
+
digest,
|
|
11882
|
+
written: false
|
|
11883
|
+
});
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
return commandResult(true, "integrity compute", EXIT.ok, [], {
|
|
11887
|
+
summary: written ? `Wrote ${algorithm} integrity` : "Integrity already matches",
|
|
11888
|
+
artifacts: [artifact("integrity-updated", file, targetResult.target, digest)],
|
|
11889
|
+
nextActions: [
|
|
11890
|
+
nextAction(
|
|
11891
|
+
"verify-integrity",
|
|
11892
|
+
"Verify the recorded integrity before release",
|
|
11893
|
+
`mda integrity verify ${file} --target ${targetResult.target}`
|
|
11894
|
+
)
|
|
11895
|
+
],
|
|
11896
|
+
message: written ? `wrote integrity to ${file}` : "integrity already matches",
|
|
11897
|
+
file,
|
|
11898
|
+
target: targetResult.target,
|
|
11899
|
+
files: can.files,
|
|
11900
|
+
algorithm,
|
|
11901
|
+
digest,
|
|
11902
|
+
written
|
|
11903
|
+
});
|
|
11904
|
+
}
|
|
11113
11905
|
return commandResult(true, "integrity compute", EXIT.ok, [], {
|
|
11906
|
+
summary: `Computed ${algorithm} digest`,
|
|
11907
|
+
artifacts: [artifact("canonical-digest", file, targetResult.target, digest)],
|
|
11908
|
+
nextActions: [
|
|
11909
|
+
externalNextAction(
|
|
11910
|
+
"record-integrity",
|
|
11911
|
+
"Record this digest in the artifact integrity field",
|
|
11912
|
+
"update frontmatter.integrity before publishing"
|
|
11913
|
+
)
|
|
11914
|
+
],
|
|
11114
11915
|
message: digest,
|
|
11115
11916
|
file,
|
|
11116
11917
|
target: targetResult.target,
|
|
@@ -11121,46 +11922,100 @@ function runIntegrity(args) {
|
|
|
11121
11922
|
}
|
|
11122
11923
|
const ext = readArtifact(file);
|
|
11123
11924
|
if (!ext.ok || ext.extract.kind !== "ok" || !isRecord(ext.extract.frontmatter)) {
|
|
11124
|
-
return commandResult(
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11925
|
+
return commandResult(
|
|
11926
|
+
false,
|
|
11927
|
+
"integrity verify",
|
|
11928
|
+
EXIT.failure,
|
|
11929
|
+
[diag("missing-required-frontmatter", "Integrity verification requires frontmatter")],
|
|
11930
|
+
{
|
|
11931
|
+
file,
|
|
11932
|
+
target: targetResult.target,
|
|
11933
|
+
files: can.files
|
|
11934
|
+
}
|
|
11935
|
+
);
|
|
11129
11936
|
}
|
|
11130
11937
|
const integrity = ext.extract.frontmatter.integrity;
|
|
11131
11938
|
if (!isRecord(integrity) || typeof integrity.algorithm !== "string" || typeof integrity.digest !== "string") {
|
|
11132
|
-
return commandResult(
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11939
|
+
return commandResult(
|
|
11940
|
+
false,
|
|
11941
|
+
"integrity verify",
|
|
11942
|
+
EXIT.failure,
|
|
11943
|
+
[diag("missing-required-integrity", "Artifact has no declared integrity")],
|
|
11944
|
+
{
|
|
11945
|
+
file,
|
|
11946
|
+
target: targetResult.target,
|
|
11947
|
+
files: can.files
|
|
11948
|
+
}
|
|
11949
|
+
);
|
|
11137
11950
|
}
|
|
11138
11951
|
if (!DIGEST_ALGORITHMS.has(integrity.algorithm)) {
|
|
11139
|
-
return commandResult(
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
algorithm: integrity.algorithm
|
|
11144
|
-
|
|
11952
|
+
return commandResult(
|
|
11953
|
+
false,
|
|
11954
|
+
"integrity verify",
|
|
11955
|
+
EXIT.failure,
|
|
11956
|
+
[diag("unsupported-integrity-algorithm", `Unsupported integrity algorithm: ${integrity.algorithm}`)],
|
|
11957
|
+
{
|
|
11958
|
+
file,
|
|
11959
|
+
target: targetResult.target,
|
|
11960
|
+
files: can.files,
|
|
11961
|
+
algorithm: integrity.algorithm
|
|
11962
|
+
}
|
|
11963
|
+
);
|
|
11145
11964
|
}
|
|
11146
11965
|
const expected = computeDigest(can.bytes, integrity.algorithm);
|
|
11147
11966
|
const ok = expected === integrity.digest;
|
|
11148
|
-
return commandResult(
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11967
|
+
return commandResult(
|
|
11968
|
+
ok,
|
|
11969
|
+
"integrity verify",
|
|
11970
|
+
ok ? EXIT.ok : EXIT.failure,
|
|
11971
|
+
ok ? [] : [diag("integrity-mismatch", `Declared digest ${integrity.digest} does not match recomputed ${expected}`)],
|
|
11972
|
+
{
|
|
11973
|
+
summary: ok ? "Integrity verification passed" : "Integrity verification failed",
|
|
11974
|
+
artifacts: [artifact("verified-artifact", file, targetResult.target, expected)],
|
|
11975
|
+
nextActions: ok ? [
|
|
11976
|
+
nextAction(
|
|
11977
|
+
"validate-artifact",
|
|
11978
|
+
"Validate the artifact before use",
|
|
11979
|
+
`mda validate ${file} --target ${targetResult.target}`,
|
|
11980
|
+
false
|
|
11981
|
+
)
|
|
11982
|
+
] : [
|
|
11983
|
+
nextAction(
|
|
11984
|
+
"recompute-integrity",
|
|
11985
|
+
"Recompute the digest after fixing the file",
|
|
11986
|
+
`mda integrity compute ${file} --target ${targetResult.target}`
|
|
11987
|
+
)
|
|
11988
|
+
],
|
|
11989
|
+
file,
|
|
11990
|
+
target: targetResult.target,
|
|
11991
|
+
files: can.files,
|
|
11992
|
+
algorithm: integrity.algorithm,
|
|
11993
|
+
expected,
|
|
11994
|
+
declared: integrity.digest
|
|
11995
|
+
}
|
|
11996
|
+
);
|
|
11158
11997
|
}
|
|
11159
11998
|
function runCompile(args) {
|
|
11160
11999
|
const parsed = parseOptions(args);
|
|
11161
|
-
const
|
|
12000
|
+
const signingOptions = [
|
|
12001
|
+
"--method",
|
|
12002
|
+
"--key",
|
|
12003
|
+
"--identity",
|
|
12004
|
+
"--profile",
|
|
12005
|
+
"--did",
|
|
12006
|
+
"--key-id",
|
|
12007
|
+
"--key-file",
|
|
12008
|
+
"--repo",
|
|
12009
|
+
"--workflow",
|
|
12010
|
+
"--ref",
|
|
12011
|
+
"--rekor",
|
|
12012
|
+
"--offline-sigstore-fixture",
|
|
12013
|
+
"--out",
|
|
12014
|
+
"--in-place"
|
|
12015
|
+
];
|
|
12016
|
+
const err = unknownOptions(parsed, ["--target", "--out-dir", "--integrity", "--manifest", "--strict-compat", ...signingOptions]);
|
|
11162
12017
|
if (err) return usage("compile", err);
|
|
11163
|
-
if (
|
|
12018
|
+
if (signingOptions.some((option) => parsed.options.has(option) || parsed.flags.has(option))) {
|
|
11164
12019
|
return usage("compile", "Compile does not sign artifacts. Run mda sign as a separate explicit step.");
|
|
11165
12020
|
}
|
|
11166
12021
|
if (parsed.positional.length !== 1) return usage("compile", "Expected one source file: mda compile <file.mda> --target <target...>");
|
|
@@ -11170,24 +12025,117 @@ function runCompile(args) {
|
|
|
11170
12025
|
const file = parsed.positional[0];
|
|
11171
12026
|
const sourceValidation = validateArtifact(file, "source");
|
|
11172
12027
|
if (!sourceValidation.ok) {
|
|
11173
|
-
return commandResult(false, "compile", EXIT.failure, sourceValidation.diagnostics, {
|
|
12028
|
+
return commandResult(false, "compile", EXIT.failure, sourceValidation.diagnostics, {
|
|
12029
|
+
summary: "Source validation failed before compile",
|
|
12030
|
+
nextActions: [
|
|
12031
|
+
nextAction("validate-source", "Fix source validation errors and re-run validation", `mda validate ${file} --target source`)
|
|
12032
|
+
],
|
|
12033
|
+
file,
|
|
12034
|
+
target: "source"
|
|
12035
|
+
});
|
|
11174
12036
|
}
|
|
11175
12037
|
const read = readArtifact(file);
|
|
11176
12038
|
if (!read.ok || read.extract.kind !== "ok" || !isRecord(read.extract.frontmatter)) {
|
|
11177
|
-
return commandResult(false, "compile", EXIT.failure, [diag("missing-required-frontmatter", "Source must contain frontmatter")], {
|
|
12039
|
+
return commandResult(false, "compile", EXIT.failure, [diag("missing-required-frontmatter", "Source must contain frontmatter")], {
|
|
12040
|
+
summary: "Source frontmatter is required before compile",
|
|
12041
|
+
nextActions: [
|
|
12042
|
+
nextAction("fix-source-frontmatter", "Add source frontmatter and re-run validation", `mda validate ${file} --target source`)
|
|
12043
|
+
],
|
|
12044
|
+
file,
|
|
12045
|
+
target: "source"
|
|
12046
|
+
});
|
|
11178
12047
|
}
|
|
11179
12048
|
const outDir = oneOption(parsed.options, "--out-dir") ?? process.cwd();
|
|
12049
|
+
const manifestPath = oneOption(parsed.options, "--manifest");
|
|
12050
|
+
const strictCompat = parsed.flags.has("--strict-compat");
|
|
11180
12051
|
const includeIntegrity = parsed.flags.has("--integrity") || isRecord(read.extract.frontmatter.integrity);
|
|
11181
12052
|
const staged = compileTargets(read.extract.frontmatter, read.extract.body, targets, outDir, includeIntegrity);
|
|
11182
|
-
if (!staged.ok)
|
|
12053
|
+
if (!staged.ok)
|
|
12054
|
+
return commandResult(false, "compile", EXIT.failure, staged.diagnostics, {
|
|
12055
|
+
summary: "Compile planning failed",
|
|
12056
|
+
nextActions: [
|
|
12057
|
+
nextAction(
|
|
12058
|
+
"fix-compile-input",
|
|
12059
|
+
"Fix the reported diagnostics and retry compile",
|
|
12060
|
+
`mda compile ${file} --target ${targets.join(" ")} --out-dir ${outDir}`
|
|
12061
|
+
)
|
|
12062
|
+
],
|
|
12063
|
+
file,
|
|
12064
|
+
outDir,
|
|
12065
|
+
planned: manifestPath ? [...staged.planned, manifestPath] : staged.planned
|
|
12066
|
+
});
|
|
12067
|
+
const compatibilityWarnings = compileCompatibilityWarnings(read.extract.frontmatter, targets);
|
|
12068
|
+
if (strictCompat && compatibilityWarnings.length > 0) {
|
|
12069
|
+
return commandResult(
|
|
12070
|
+
false,
|
|
12071
|
+
"compile",
|
|
12072
|
+
EXIT.failure,
|
|
12073
|
+
compatibilityWarnings.map((warning) => ({ ...warning, severity: "error" })),
|
|
12074
|
+
{
|
|
12075
|
+
summary: "Compile compatibility checks failed",
|
|
12076
|
+
nextActions: [
|
|
12077
|
+
nextAction(
|
|
12078
|
+
"fix-compile-compatibility",
|
|
12079
|
+
"Resolve compatibility diagnostics or rerun without --strict-compat",
|
|
12080
|
+
`mda compile ${file} --target ${targets.join(" ")} --out-dir ${outDir}`
|
|
12081
|
+
)
|
|
12082
|
+
],
|
|
12083
|
+
file,
|
|
12084
|
+
outDir,
|
|
12085
|
+
planned: manifestPath ? [...staged.outputs.map((o) => o.path), manifestPath] : staged.outputs.map((o) => o.path),
|
|
12086
|
+
written: []
|
|
12087
|
+
}
|
|
12088
|
+
);
|
|
12089
|
+
}
|
|
11183
12090
|
const existing = staged.outputs.find((o) => existsSync2(o.path));
|
|
11184
|
-
if (existing)
|
|
12091
|
+
if (existing)
|
|
12092
|
+
return ioError("compile", `Refusing to overwrite existing file: ${existing.path}`, {
|
|
12093
|
+
file,
|
|
12094
|
+
outDir,
|
|
12095
|
+
planned: staged.outputs.map((o) => o.path),
|
|
12096
|
+
written: []
|
|
12097
|
+
});
|
|
12098
|
+
if (manifestPath && staged.outputs.some((output) => resolve2(output.path) === resolve2(manifestPath))) {
|
|
12099
|
+
return ioError("compile", `Manifest path conflicts with a compiled output: ${manifestPath}`, {
|
|
12100
|
+
file,
|
|
12101
|
+
outDir,
|
|
12102
|
+
planned: staged.outputs.map((o) => o.path),
|
|
12103
|
+
written: []
|
|
12104
|
+
});
|
|
12105
|
+
}
|
|
12106
|
+
if (manifestPath && existsSync2(manifestPath)) {
|
|
12107
|
+
return ioError("compile", `Refusing to overwrite existing file: ${manifestPath}`, {
|
|
12108
|
+
file,
|
|
12109
|
+
outDir,
|
|
12110
|
+
planned: [...staged.outputs.map((o) => o.path), manifestPath],
|
|
12111
|
+
written: []
|
|
12112
|
+
});
|
|
12113
|
+
}
|
|
12114
|
+
const sourceDigest = canonicalizeFromFile(file, "source", null);
|
|
12115
|
+
if (!sourceDigest.ok) {
|
|
12116
|
+
return commandResult(false, "compile", sourceDigest.exitCode, sourceDigest.diagnostics, {
|
|
12117
|
+
summary: "Source canonicalization failed before compile manifest generation",
|
|
12118
|
+
nextActions: [
|
|
12119
|
+
nextAction("fix-source-canonicalization", "Fix the source and retry compile", `mda compile ${file} --target ${targets.join(" ")}`)
|
|
12120
|
+
],
|
|
12121
|
+
file,
|
|
12122
|
+
outDir,
|
|
12123
|
+
planned: staged.outputs.map((o) => o.path),
|
|
12124
|
+
written: []
|
|
12125
|
+
});
|
|
12126
|
+
}
|
|
12127
|
+
const manifest = manifestPath ? makeCompileManifest(file, sourceDigest.bytes, read.extract.frontmatter, targets, staged.outputs, compatibilityWarnings) : null;
|
|
11185
12128
|
const written = [];
|
|
11186
12129
|
try {
|
|
11187
12130
|
for (const output of staged.outputs) {
|
|
11188
12131
|
atomicWrite(output.path, output.bytes);
|
|
11189
12132
|
written.push(output.path);
|
|
11190
12133
|
}
|
|
12134
|
+
if (manifestPath && manifest) {
|
|
12135
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
12136
|
+
`);
|
|
12137
|
+
written.push(manifestPath);
|
|
12138
|
+
}
|
|
11191
12139
|
} catch (error) {
|
|
11192
12140
|
const rolledBack = [];
|
|
11193
12141
|
const rollbackDiagnostics = [];
|
|
@@ -11196,80 +12144,144 @@ function runCompile(args) {
|
|
|
11196
12144
|
rmSync2(path, { force: true });
|
|
11197
12145
|
rolledBack.push(path);
|
|
11198
12146
|
} catch (rollbackError) {
|
|
11199
|
-
rollbackDiagnostics.push(
|
|
12147
|
+
rollbackDiagnostics.push(
|
|
12148
|
+
diag("rollback-error", rollbackError instanceof Error ? rollbackError.message : String(rollbackError), { path })
|
|
12149
|
+
);
|
|
11200
12150
|
}
|
|
11201
12151
|
}
|
|
11202
|
-
return commandResult(
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
12152
|
+
return commandResult(
|
|
12153
|
+
false,
|
|
12154
|
+
"compile",
|
|
12155
|
+
EXIT.io,
|
|
12156
|
+
[diag("io-error", error instanceof Error ? error.message : String(error)), ...rollbackDiagnostics],
|
|
12157
|
+
{
|
|
12158
|
+
summary: "Compile failed while writing outputs",
|
|
12159
|
+
nextActions: [
|
|
12160
|
+
nextAction(
|
|
12161
|
+
"retry-compile",
|
|
12162
|
+
"Fix the filesystem error and retry compile",
|
|
12163
|
+
`mda compile ${file} --target ${targets.join(" ")} --out-dir ${outDir}`
|
|
12164
|
+
)
|
|
12165
|
+
],
|
|
12166
|
+
file,
|
|
12167
|
+
outDir,
|
|
12168
|
+
planned: manifestPath ? [...staged.outputs.map((o) => o.path), manifestPath] : staged.outputs.map((o) => o.path),
|
|
12169
|
+
written,
|
|
12170
|
+
rolledBack
|
|
12171
|
+
}
|
|
12172
|
+
);
|
|
11212
12173
|
}
|
|
11213
|
-
return commandResult(true, "compile", EXIT.ok,
|
|
12174
|
+
return commandResult(true, "compile", EXIT.ok, compatibilityWarnings, {
|
|
12175
|
+
summary: `Compiled ${staged.outputs.length} file(s)`,
|
|
12176
|
+
artifacts: [
|
|
12177
|
+
...staged.outputs.map((o) => artifact("compiled-output", o.path, targetForPath(o.path))),
|
|
12178
|
+
...manifestPath ? [artifact("compile-manifest", manifestPath)] : []
|
|
12179
|
+
],
|
|
12180
|
+
nextActions: nextAfterCompile(staged.outputs.map((o) => o.path)),
|
|
11214
12181
|
message: `wrote ${staged.outputs.length} file(s)`,
|
|
11215
12182
|
file,
|
|
11216
12183
|
target: "source",
|
|
11217
12184
|
outDir,
|
|
11218
|
-
planned: staged.outputs.map((o) => o.path),
|
|
11219
|
-
written
|
|
12185
|
+
planned: manifestPath ? [...staged.outputs.map((o) => o.path), manifestPath] : staged.outputs.map((o) => o.path),
|
|
12186
|
+
written
|
|
11220
12187
|
});
|
|
11221
12188
|
}
|
|
11222
|
-
function
|
|
11223
|
-
const
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
12189
|
+
function makeCompileManifest(file, sourceCanonicalBytes, frontmatter, targets, outputs, warnings) {
|
|
12190
|
+
const outputEntries = outputs.map((output) => ({
|
|
12191
|
+
path: output.path,
|
|
12192
|
+
target: targetForPath(output.path) ?? "mcp-server-sidecar",
|
|
12193
|
+
digest: computeDigest(Buffer.from(output.bytes), "sha256"),
|
|
12194
|
+
byteLength: Buffer.byteLength(output.bytes)
|
|
12195
|
+
}));
|
|
12196
|
+
return {
|
|
12197
|
+
kind: "mda-compile-manifest",
|
|
12198
|
+
version: 1,
|
|
12199
|
+
compiler: {
|
|
12200
|
+
name: "@markdown-ai/cli",
|
|
12201
|
+
version: CLI_VERSION
|
|
12202
|
+
},
|
|
12203
|
+
source: {
|
|
12204
|
+
path: file,
|
|
12205
|
+
target: "source",
|
|
12206
|
+
digest: computeDigest(sourceCanonicalBytes, "sha256")
|
|
12207
|
+
},
|
|
12208
|
+
targetProfile: targets,
|
|
12209
|
+
outputs: outputEntries,
|
|
12210
|
+
outputDigests: Object.fromEntries(outputEntries.map((output) => [output.path, output.digest])),
|
|
12211
|
+
emittedScripts: emittedScripts(frontmatter),
|
|
12212
|
+
emittedResources: outputEntries,
|
|
12213
|
+
capabilitySummary: capabilitySummary(frontmatter),
|
|
12214
|
+
signerIdentity: firstSignerIdentity(frontmatter),
|
|
12215
|
+
warnings: warnings.map(({ code, message, severity }) => ({ code, message, severity }))
|
|
12216
|
+
};
|
|
12217
|
+
}
|
|
12218
|
+
function compileCompatibilityWarnings(frontmatter, targets) {
|
|
12219
|
+
const warnings = [];
|
|
12220
|
+
const capabilities = capabilitySummary(frontmatter);
|
|
12221
|
+
const warn = (code, message) => warnings.push({ ...diag(code, message), severity: "warning" });
|
|
12222
|
+
if (frontmatter["allowed-tools"] !== void 0 && targets.some((target) => target !== "SKILL.md")) {
|
|
12223
|
+
warn("compat.target_feature_loss", "allowed-tools is native only for SKILL.md and is moved to a vendor namespace for other targets");
|
|
12224
|
+
warn("compat.script_permission_mismatch", "Compiled non-SKILL targets cannot enforce SKILL.md allowed-tools permissions directly");
|
|
12225
|
+
}
|
|
12226
|
+
if (capabilities.requires.network !== void 0)
|
|
12227
|
+
warn("compat.network_degradation", "Markdown targets cannot enforce declared network capability requirements");
|
|
12228
|
+
if (capabilities.requires.filesystem !== void 0)
|
|
12229
|
+
warn("compat.filesystem_degradation", "Markdown targets cannot enforce declared filesystem capability requirements");
|
|
12230
|
+
if (capabilities.requires.shell !== void 0 || capabilities.tools.some((tool) => /bash|shell/i.test(tool))) {
|
|
12231
|
+
warn("compat.shell_degradation", "Markdown targets cannot enforce declared shell capability requirements");
|
|
12232
|
+
}
|
|
12233
|
+
if (capabilities.llmix) {
|
|
12234
|
+
warn(
|
|
12235
|
+
"compat.unsupported_runtime_policy",
|
|
12236
|
+
"LLMix runtime policy must be enforced by LLMix deployment checks, not compiled Markdown alone"
|
|
12237
|
+
);
|
|
12238
|
+
warn("compat.llmix_namespace_not_consumed", "General Markdown runtimes do not consume metadata.snoai-llmix directly");
|
|
12239
|
+
}
|
|
12240
|
+
return warnings;
|
|
12241
|
+
}
|
|
12242
|
+
function capabilitySummary(frontmatter) {
|
|
12243
|
+
const metadata = isRecord(frontmatter.metadata) ? frontmatter.metadata : {};
|
|
12244
|
+
const mda = isRecord(metadata.mda) ? metadata.mda : {};
|
|
12245
|
+
const requires = firstRecord(frontmatter.requires, mda.requires);
|
|
12246
|
+
const dependsOn = Array.isArray(frontmatter["depends-on"]) ? frontmatter["depends-on"] : Array.isArray(mda["depends-on"]) ? mda["depends-on"] : [];
|
|
12247
|
+
const tools = [
|
|
12248
|
+
typeof frontmatter["allowed-tools"] === "string" ? frontmatter["allowed-tools"] : null,
|
|
12249
|
+
...stringArray(requires.tools)
|
|
12250
|
+
].filter((tool) => typeof tool === "string");
|
|
12251
|
+
return {
|
|
12252
|
+
requires,
|
|
12253
|
+
dependsOnCount: dependsOn.length,
|
|
12254
|
+
tools,
|
|
12255
|
+
llmix: isRecord(metadata["snoai-llmix"])
|
|
12256
|
+
};
|
|
12257
|
+
}
|
|
12258
|
+
function emittedScripts(frontmatter) {
|
|
12259
|
+
const capabilities = capabilitySummary(frontmatter);
|
|
12260
|
+
return capabilities.tools.filter((tool) => /bash|shell|python|node|tsx|ts-node/i.test(tool));
|
|
12261
|
+
}
|
|
12262
|
+
function firstSignerIdentity(frontmatter) {
|
|
12263
|
+
if (!Array.isArray(frontmatter.signatures)) return null;
|
|
12264
|
+
for (const signature of frontmatter.signatures) {
|
|
12265
|
+
if (!isRecord(signature) || typeof signature.signer !== "string") continue;
|
|
12266
|
+
return {
|
|
12267
|
+
signer: signature.signer,
|
|
12268
|
+
keyId: typeof signature.keyId === "string" ? signature.keyId : null,
|
|
12269
|
+
payloadDigest: typeof signature["payload-digest"] === "string" ? signature["payload-digest"] : null
|
|
12270
|
+
};
|
|
11249
12271
|
}
|
|
11250
|
-
|
|
11251
|
-
|
|
12272
|
+
return null;
|
|
12273
|
+
}
|
|
12274
|
+
function firstRecord(...values) {
|
|
12275
|
+
for (const value of values) {
|
|
12276
|
+
if (isRecord(value)) return value;
|
|
11252
12277
|
}
|
|
11253
|
-
return
|
|
11254
|
-
diag("signature-verification-unavailable", "Integrity and policy shape were checked, but did:web/Sigstore cryptographic verification is not implemented in this MVP")
|
|
11255
|
-
], { file, target: targetResult.target, policy: policyPath });
|
|
12278
|
+
return {};
|
|
11256
12279
|
}
|
|
11257
|
-
function
|
|
11258
|
-
|
|
11259
|
-
const err = unknownOptions(parsed, ["--method", "--key", "--identity", "--out", "--in-place"]);
|
|
11260
|
-
if (err) return usage("sign", err);
|
|
11261
|
-
if (parsed.positional.length !== 1) return usage("sign", "Expected one file: mda sign <file> --method did-web --key <path> --identity <domain> (--out <file>|--in-place)");
|
|
11262
|
-
const method = oneOption(parsed.options, "--method");
|
|
11263
|
-
if (method !== "did-web") return usage("sign", "--method did-web is required");
|
|
11264
|
-
if (!oneOption(parsed.options, "--key")) return usage("sign", "--key <path> is required");
|
|
11265
|
-
if (!oneOption(parsed.options, "--identity")) return usage("sign", "--identity <domain> is required");
|
|
11266
|
-
const out = oneOption(parsed.options, "--out");
|
|
11267
|
-
const inPlace = parsed.flags.has("--in-place");
|
|
11268
|
-
if (out && inPlace || !out && !inPlace) return usage("sign", "Choose exactly one output mode: --out <file> or --in-place");
|
|
11269
|
-
return commandResult(false, "sign", EXIT.failure, [
|
|
11270
|
-
diag("signing-unavailable", "did:web signing is intentionally unavailable until deterministic verification fixtures are implemented")
|
|
11271
|
-
], { file: parsed.positional[0], method });
|
|
12280
|
+
function stringArray(value) {
|
|
12281
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
11272
12282
|
}
|
|
12283
|
+
|
|
12284
|
+
// src/cli/conformance-command.ts
|
|
11273
12285
|
function runConformance(args) {
|
|
11274
12286
|
const parsed = parseOptions(args);
|
|
11275
12287
|
const err = unknownOptions(parsed, ["--suite", "--level"]);
|
|
@@ -11280,6 +12292,21 @@ function runConformance(args) {
|
|
|
11280
12292
|
if (level !== "V" && level !== "C") return usage("conformance", "--level must be V or C");
|
|
11281
12293
|
const report = runConformanceSuite(suite, level);
|
|
11282
12294
|
return commandResult(report.ok, "conformance", report.ok ? EXIT.ok : EXIT.failure, report.diagnostics, {
|
|
12295
|
+
summary: report.ok ? `Conformance Level ${level} passed` : `Conformance Level ${level} failed`,
|
|
12296
|
+
nextActions: report.ok && level === "V" ? [
|
|
12297
|
+
nextAction(
|
|
12298
|
+
"run-level-c",
|
|
12299
|
+
"Run compile conformance before release evidence",
|
|
12300
|
+
`mda conformance --suite ${suite} --level C`,
|
|
12301
|
+
false
|
|
12302
|
+
)
|
|
12303
|
+
] : report.ok ? [] : [
|
|
12304
|
+
nextAction(
|
|
12305
|
+
"fix-conformance",
|
|
12306
|
+
"Fix failing fixtures and re-run conformance",
|
|
12307
|
+
`mda conformance --suite ${suite} --level ${level}`
|
|
12308
|
+
)
|
|
12309
|
+
],
|
|
11283
12310
|
suite,
|
|
11284
12311
|
level,
|
|
11285
12312
|
passCount: report.passCount,
|
|
@@ -11288,6 +12315,2195 @@ function runConformance(args) {
|
|
|
11288
12315
|
});
|
|
11289
12316
|
}
|
|
11290
12317
|
|
|
12318
|
+
// src/cli/llmix-commands.ts
|
|
12319
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
12320
|
+
import { basename, dirname as dirname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
|
|
12321
|
+
|
|
12322
|
+
// src/cli/security-commands.ts
|
|
12323
|
+
import { createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
|
|
12324
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
12325
|
+
function integrityPayloadBytes(integrity) {
|
|
12326
|
+
return Buffer.from(jcs({ integrity: { algorithm: integrity.algorithm, digest: integrity.digest } }), "utf8");
|
|
12327
|
+
}
|
|
12328
|
+
function dssePae(payloadType, payload) {
|
|
12329
|
+
return Buffer.concat([
|
|
12330
|
+
Buffer.from(`DSSEv1 ${Buffer.byteLength(payloadType, "utf8")} ${payloadType} ${payload.length} `, "utf8"),
|
|
12331
|
+
payload
|
|
12332
|
+
]);
|
|
12333
|
+
}
|
|
12334
|
+
function didWebDomainFromDid(did) {
|
|
12335
|
+
if (!did.startsWith("did:web:")) return null;
|
|
12336
|
+
const domain = did.slice("did:web:".length).split(":")[0];
|
|
12337
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/.test(domain)) return null;
|
|
12338
|
+
return domain;
|
|
12339
|
+
}
|
|
12340
|
+
function signerDomain(signer) {
|
|
12341
|
+
return signer.startsWith("did-web:") ? signer.slice("did-web:".length) : null;
|
|
12342
|
+
}
|
|
12343
|
+
function sigstoreIssuer(signer) {
|
|
12344
|
+
return signer.startsWith("sigstore-oidc:") ? signer.slice("sigstore-oidc:".length) : null;
|
|
12345
|
+
}
|
|
12346
|
+
function keyAlgorithm(key) {
|
|
12347
|
+
return key.asymmetricKeyType === "ed25519" ? "ed25519" : null;
|
|
12348
|
+
}
|
|
12349
|
+
function publicKeyFingerprint(key) {
|
|
12350
|
+
return computeDigest(key.export({ format: "der", type: "spki" }), "sha256");
|
|
12351
|
+
}
|
|
12352
|
+
function policyAllowsDidWeb2(policy, domain) {
|
|
12353
|
+
return isRecord(policy) && Array.isArray(policy.trustedSigners) && policy.trustedSigners.some((entry) => isRecord(entry) && entry.type === "did-web" && entry.domain === domain);
|
|
12354
|
+
}
|
|
12355
|
+
function policyAllowsSigstore2(policy, fixture) {
|
|
12356
|
+
if (!isRecord(policy) || !Array.isArray(policy.trustedSigners)) return false;
|
|
12357
|
+
return policy.trustedSigners.some((entry) => {
|
|
12358
|
+
if (!isRecord(entry) || entry.type !== "sigstore-oidc") return false;
|
|
12359
|
+
if (entry.issuer !== fixture.issuer || entry.subject !== fixture.subject) return false;
|
|
12360
|
+
if (entry.repository !== void 0 && entry.repository !== fixture.repository) return false;
|
|
12361
|
+
if (entry.workflow !== void 0 && entry.workflow !== fixture.workflow) return false;
|
|
12362
|
+
if (entry.ref !== void 0 && entry.ref !== fixture.ref) return false;
|
|
12363
|
+
if (entry.environment !== void 0 && entry.environment !== fixture.environment) return false;
|
|
12364
|
+
if (entry.jobWorkflowRef !== void 0 && entry.jobWorkflowRef !== fixture.jobWorkflowRef) return false;
|
|
12365
|
+
return true;
|
|
12366
|
+
});
|
|
12367
|
+
}
|
|
12368
|
+
function trustPolicyDiagnostics(policy, diagnostics) {
|
|
12369
|
+
if (!isRecord(policy) || !Array.isArray(policy.trustedSigners)) return diagnostics;
|
|
12370
|
+
const hasEnvironmentOnlyGithubActions = policy.trustedSigners.some((entry) => {
|
|
12371
|
+
if (!isRecord(entry) || entry.type !== "sigstore-oidc") return false;
|
|
12372
|
+
if (entry.issuer !== GITHUB_ACTIONS_ISSUER || typeof entry.environment !== "string") return false;
|
|
12373
|
+
const hasRefBinding = typeof entry.repository === "string" && typeof entry.workflow === "string" && typeof entry.ref === "string";
|
|
12374
|
+
const hasJobWorkflowBinding = typeof entry.repository === "string" && typeof entry.jobWorkflowRef === "string";
|
|
12375
|
+
return !hasRefBinding && !hasJobWorkflowBinding;
|
|
12376
|
+
});
|
|
12377
|
+
if (!hasEnvironmentOnlyGithubActions) return diagnostics;
|
|
12378
|
+
return [
|
|
12379
|
+
diag(
|
|
12380
|
+
"trust_policy.environment_only_not_supported",
|
|
12381
|
+
"GitHub Actions trust policy must bind repository and exact ref or jobWorkflowRef; environment alone is not supported"
|
|
12382
|
+
),
|
|
12383
|
+
...diagnostics
|
|
12384
|
+
];
|
|
12385
|
+
}
|
|
12386
|
+
function policyMentionsSigstoreIssuer(policy, issuer) {
|
|
12387
|
+
return isRecord(policy) && Array.isArray(policy.trustedSigners) && policy.trustedSigners.some((entry) => isRecord(entry) && entry.type === "sigstore-oidc" && entry.issuer === issuer);
|
|
12388
|
+
}
|
|
12389
|
+
function trustPolicyMinSignatures(policy) {
|
|
12390
|
+
return isRecord(policy) && Number.isInteger(policy.minSignatures) ? Number(policy.minSignatures) : 1;
|
|
12391
|
+
}
|
|
12392
|
+
function asSignatureEntry(value) {
|
|
12393
|
+
if (!isRecord(value)) return null;
|
|
12394
|
+
if (typeof value.signer !== "string") return null;
|
|
12395
|
+
if (typeof value["key-id"] !== "string") return null;
|
|
12396
|
+
if (typeof value["payload-digest"] !== "string") return null;
|
|
12397
|
+
if (typeof value.algorithm !== "string") return null;
|
|
12398
|
+
if (typeof value.signature !== "string") return null;
|
|
12399
|
+
return value;
|
|
12400
|
+
}
|
|
12401
|
+
function payloadTypeDiagnostic(signature) {
|
|
12402
|
+
if (signature["payload-type"] === void 0 || signature["payload-type"] === INTEGRITY_PAYLOAD_TYPE) return null;
|
|
12403
|
+
return diag("signature.payload_type_mismatch", `Signature payload-type must be ${INTEGRITY_PAYLOAD_TYPE}`);
|
|
12404
|
+
}
|
|
12405
|
+
function verifySignatureEntries(signatures, integrity, policy, didDocumentPath, sigstoreFixturePath) {
|
|
12406
|
+
const payload = integrityPayloadBytes(integrity);
|
|
12407
|
+
const sigstoreFixture = sigstoreFixturePath ? readSigstoreFixture(sigstoreFixturePath, false) : null;
|
|
12408
|
+
const trusted = /* @__PURE__ */ new Set();
|
|
12409
|
+
const trustedSignerIdentities = [];
|
|
12410
|
+
const rejectedTrusted = [];
|
|
12411
|
+
for (const rawSignature of signatures) {
|
|
12412
|
+
const signature = asSignatureEntry(rawSignature);
|
|
12413
|
+
if (!signature) return { malformed: true, trusted, trustedSignerIdentities, rejectedTrusted };
|
|
12414
|
+
const domain = signerDomain(signature.signer);
|
|
12415
|
+
if (domain) {
|
|
12416
|
+
if (!policyAllowsDidWeb2(policy, domain)) continue;
|
|
12417
|
+
if (signature["payload-digest"] !== integrity.digest) {
|
|
12418
|
+
rejectedTrusted.push(diag("signature.digest_mismatch", "Signature payload digest does not match the expected integrity digest"));
|
|
12419
|
+
continue;
|
|
12420
|
+
}
|
|
12421
|
+
const payloadTypeError2 = payloadTypeDiagnostic(signature);
|
|
12422
|
+
if (payloadTypeError2) {
|
|
12423
|
+
rejectedTrusted.push(payloadTypeError2);
|
|
12424
|
+
continue;
|
|
12425
|
+
}
|
|
12426
|
+
const keyResult = publicKeyFromDidDocument(didDocumentPath, signature["key-id"], `did:web:${domain}`);
|
|
12427
|
+
if (!keyResult.ok) {
|
|
12428
|
+
rejectedTrusted.push(...keyResult.diagnostics);
|
|
12429
|
+
continue;
|
|
12430
|
+
}
|
|
12431
|
+
if (signature.algorithm !== keyAlgorithm(keyResult.key)) {
|
|
12432
|
+
rejectedTrusted.push(
|
|
12433
|
+
diag("signature.unsupported_algorithm", `Signature algorithm ${signature.algorithm} does not match the DID public key`)
|
|
12434
|
+
);
|
|
12435
|
+
continue;
|
|
12436
|
+
}
|
|
12437
|
+
const ok2 = cryptoVerify(null, dssePae(INTEGRITY_PAYLOAD_TYPE, payload), keyResult.key, Buffer.from(signature.signature, "base64"));
|
|
12438
|
+
if (!ok2) {
|
|
12439
|
+
rejectedTrusted.push(diag("signature.verification_failed", `Signature verification failed for ${signature["key-id"]}`));
|
|
12440
|
+
continue;
|
|
12441
|
+
}
|
|
12442
|
+
const identityKey2 = `did-web:${domain}
|
|
12443
|
+
${keyResult.keyFingerprint}`;
|
|
12444
|
+
if (!trusted.has(identityKey2)) {
|
|
12445
|
+
trusted.add(identityKey2);
|
|
12446
|
+
trustedSignerIdentities.push({
|
|
12447
|
+
type: "did-web",
|
|
12448
|
+
signer: `did-web:${domain}`,
|
|
12449
|
+
keyId: keyResult.methodId,
|
|
12450
|
+
payloadDigest: signature["payload-digest"]
|
|
12451
|
+
});
|
|
12452
|
+
}
|
|
12453
|
+
continue;
|
|
12454
|
+
}
|
|
12455
|
+
const issuer = sigstoreIssuer(signature.signer);
|
|
12456
|
+
if (!issuer || !policyMentionsSigstoreIssuer(policy, issuer)) continue;
|
|
12457
|
+
if (signature["payload-digest"] !== integrity.digest) {
|
|
12458
|
+
rejectedTrusted.push(diag("signature.digest_mismatch", "Signature payload digest does not match the expected integrity digest"));
|
|
12459
|
+
continue;
|
|
12460
|
+
}
|
|
12461
|
+
const payloadTypeError = payloadTypeDiagnostic(signature);
|
|
12462
|
+
if (payloadTypeError) {
|
|
12463
|
+
rejectedTrusted.push(payloadTypeError);
|
|
12464
|
+
continue;
|
|
12465
|
+
}
|
|
12466
|
+
if (!sigstoreFixture) {
|
|
12467
|
+
rejectedTrusted.push(
|
|
12468
|
+
diag("sigstore.fixture_required", "--offline-sigstore-fixture <path> is required for deterministic Sigstore verification")
|
|
12469
|
+
);
|
|
12470
|
+
continue;
|
|
12471
|
+
}
|
|
12472
|
+
if (!sigstoreFixture.ok) {
|
|
12473
|
+
rejectedTrusted.push(...sigstoreFixture.diagnostics);
|
|
12474
|
+
continue;
|
|
12475
|
+
}
|
|
12476
|
+
const fixture = sigstoreFixture.fixture;
|
|
12477
|
+
if (fixture.issuer !== issuer || fixture.keyId !== signature["key-id"] || !policyAllowsSigstore2(policy, fixture)) {
|
|
12478
|
+
rejectedTrusted.push(diag("sigstore.identity_mismatch", "Sigstore fixture identity does not match the signature and trust policy"));
|
|
12479
|
+
continue;
|
|
12480
|
+
}
|
|
12481
|
+
const policyRekor = isRecord(policy) && isRecord(policy.rekor) ? policy.rekor : null;
|
|
12482
|
+
if (!policyRekor || policyRekor.url !== fixture.rekor.url) {
|
|
12483
|
+
rejectedTrusted.push(diag("rekor.policy_mismatch", "Trust policy Rekor URL does not match the Sigstore fixture"));
|
|
12484
|
+
continue;
|
|
12485
|
+
}
|
|
12486
|
+
if (fixture.expectedPayloadDigest !== integrity.digest || fixture.rekor.payloadDigest !== integrity.digest) {
|
|
12487
|
+
rejectedTrusted.push(
|
|
12488
|
+
diag("sigstore.fixture_digest_mismatch", "Sigstore fixture digest does not match the expected integrity digest")
|
|
12489
|
+
);
|
|
12490
|
+
continue;
|
|
12491
|
+
}
|
|
12492
|
+
if (signature["rekor-log-id"] === void 0 || signature["rekor-log-index"] === void 0) {
|
|
12493
|
+
rejectedTrusted.push(diag("rekor.evidence_missing", "Signature is missing required Rekor evidence"));
|
|
12494
|
+
continue;
|
|
12495
|
+
}
|
|
12496
|
+
if (signature["rekor-log-id"] !== fixture.rekor.logId || signature["rekor-log-index"] !== fixture.rekor.logIndex) {
|
|
12497
|
+
rejectedTrusted.push(diag("rekor.evidence_mismatch", "Signature Rekor evidence does not match the Sigstore fixture"));
|
|
12498
|
+
continue;
|
|
12499
|
+
}
|
|
12500
|
+
let publicKey;
|
|
12501
|
+
try {
|
|
12502
|
+
publicKey = createPublicKey(fixture.publicKeyPem);
|
|
12503
|
+
} catch (error) {
|
|
12504
|
+
rejectedTrusted.push(diag("sigstore.fixture_invalid", error instanceof Error ? error.message : String(error)));
|
|
12505
|
+
continue;
|
|
12506
|
+
}
|
|
12507
|
+
if (signature.algorithm !== fixture.algorithm || signature.algorithm !== keyAlgorithm(publicKey)) {
|
|
12508
|
+
rejectedTrusted.push(diag("signature.unsupported_algorithm", "Signature algorithm does not match the Sigstore fixture public key"));
|
|
12509
|
+
continue;
|
|
12510
|
+
}
|
|
12511
|
+
const ok = cryptoVerify(null, dssePae(INTEGRITY_PAYLOAD_TYPE, payload), publicKey, Buffer.from(signature.signature, "base64"));
|
|
12512
|
+
if (!ok) {
|
|
12513
|
+
rejectedTrusted.push(diag("signature.verification_failed", `Signature verification failed for ${signature["key-id"]}`));
|
|
12514
|
+
continue;
|
|
12515
|
+
}
|
|
12516
|
+
const identityKey = `sigstore-oidc:${fixture.issuer}
|
|
12517
|
+
${fixture.subject}
|
|
12518
|
+
${signature["key-id"]}`;
|
|
12519
|
+
if (!trusted.has(identityKey)) {
|
|
12520
|
+
trusted.add(identityKey);
|
|
12521
|
+
trustedSignerIdentities.push({
|
|
12522
|
+
type: "sigstore-oidc",
|
|
12523
|
+
signer: `sigstore-oidc:${fixture.issuer}`,
|
|
12524
|
+
subject: fixture.subject,
|
|
12525
|
+
keyId: signature["key-id"],
|
|
12526
|
+
payloadDigest: signature["payload-digest"],
|
|
12527
|
+
rekorLogId: fixture.rekor.logId,
|
|
12528
|
+
rekorLogIndex: fixture.rekor.logIndex
|
|
12529
|
+
});
|
|
12530
|
+
}
|
|
12531
|
+
}
|
|
12532
|
+
return { malformed: false, trusted, trustedSignerIdentities, rejectedTrusted };
|
|
12533
|
+
}
|
|
12534
|
+
function readSigstoreFixture(path, requirePrivateKey) {
|
|
12535
|
+
const read = readJson(path);
|
|
12536
|
+
if (!read.ok) return { ok: false, diagnostics: [diag("sigstore.fixture_unavailable", read.message)] };
|
|
12537
|
+
const value = read.value;
|
|
12538
|
+
const errors = [];
|
|
12539
|
+
const requireString = (name) => {
|
|
12540
|
+
const field = isRecord(value) ? value[name] : void 0;
|
|
12541
|
+
if (typeof field !== "string" || field.length === 0)
|
|
12542
|
+
errors.push(diag("sigstore.fixture_invalid", `${name} must be a non-empty string`));
|
|
12543
|
+
return typeof field === "string" ? field : "";
|
|
12544
|
+
};
|
|
12545
|
+
if (!isRecord(value)) {
|
|
12546
|
+
return { ok: false, diagnostics: [diag("sigstore.fixture_invalid", "Sigstore fixture must be a JSON object")] };
|
|
12547
|
+
}
|
|
12548
|
+
const fixture = {
|
|
12549
|
+
issuer: requireString("issuer"),
|
|
12550
|
+
subject: requireString("subject"),
|
|
12551
|
+
repository: requireString("repository"),
|
|
12552
|
+
workflow: requireString("workflow"),
|
|
12553
|
+
ref: requireString("ref"),
|
|
12554
|
+
keyId: requireString("keyId"),
|
|
12555
|
+
algorithm: "ed25519",
|
|
12556
|
+
publicKeyPem: requireString("publicKeyPem"),
|
|
12557
|
+
expectedPayloadDigest: requireString("expectedPayloadDigest"),
|
|
12558
|
+
rekor: {
|
|
12559
|
+
url: "",
|
|
12560
|
+
logId: "",
|
|
12561
|
+
logIndex: -1,
|
|
12562
|
+
payloadDigest: ""
|
|
12563
|
+
}
|
|
12564
|
+
};
|
|
12565
|
+
if (typeof value.environment === "string" && value.environment.length > 0) fixture.environment = value.environment;
|
|
12566
|
+
if (typeof value.jobWorkflowRef === "string" && value.jobWorkflowRef.length > 0) fixture.jobWorkflowRef = value.jobWorkflowRef;
|
|
12567
|
+
if (typeof value.privateKeyPem === "string" && value.privateKeyPem.length > 0) fixture.privateKeyPem = value.privateKeyPem;
|
|
12568
|
+
if (value.algorithm !== "ed25519") errors.push(diag("sigstore.fixture_invalid", "algorithm must be ed25519"));
|
|
12569
|
+
if (!GITHUB_REPOSITORY.test(fixture.repository)) errors.push(diag("sigstore.fixture_invalid", "repository must be owner/repo"));
|
|
12570
|
+
if (!GITHUB_REF.test(fixture.ref)) errors.push(diag("sigstore.fixture_invalid", "ref must be an exact refs/heads/* or refs/tags/* ref"));
|
|
12571
|
+
if (!DIGEST_PATTERN.test(fixture.expectedPayloadDigest))
|
|
12572
|
+
errors.push(diag("sigstore.fixture_invalid", "expectedPayloadDigest must be a sha256/sha384/sha512 digest"));
|
|
12573
|
+
if (requirePrivateKey && !fixture.privateKeyPem) errors.push(diag("sigstore.fixture_invalid", "privateKeyPem is required for signing"));
|
|
12574
|
+
if (!isRecord(value.rekor)) {
|
|
12575
|
+
errors.push(diag("sigstore.fixture_invalid", "rekor must be an object"));
|
|
12576
|
+
} else {
|
|
12577
|
+
fixture.rekor = {
|
|
12578
|
+
url: typeof value.rekor.url === "string" ? value.rekor.url : "",
|
|
12579
|
+
logId: typeof value.rekor.logId === "string" ? value.rekor.logId : "",
|
|
12580
|
+
logIndex: Number(value.rekor.logIndex),
|
|
12581
|
+
payloadDigest: typeof value.rekor.payloadDigest === "string" ? value.rekor.payloadDigest : ""
|
|
12582
|
+
};
|
|
12583
|
+
if (!fixture.rekor.url) errors.push(diag("sigstore.fixture_invalid", "rekor.url must be a non-empty string"));
|
|
12584
|
+
if (!fixture.rekor.logId) errors.push(diag("sigstore.fixture_invalid", "rekor.logId must be a non-empty string"));
|
|
12585
|
+
if (!Number.isInteger(fixture.rekor.logIndex) || fixture.rekor.logIndex < 0)
|
|
12586
|
+
errors.push(diag("sigstore.fixture_invalid", "rekor.logIndex must be a non-negative integer"));
|
|
12587
|
+
if (!DIGEST_PATTERN.test(fixture.rekor.payloadDigest))
|
|
12588
|
+
errors.push(diag("sigstore.fixture_invalid", "rekor.payloadDigest must be a sha256/sha384/sha512 digest"));
|
|
12589
|
+
}
|
|
12590
|
+
return errors.length > 0 ? { ok: false, diagnostics: errors } : { ok: true, fixture };
|
|
12591
|
+
}
|
|
12592
|
+
function publicKeyFromDidDocument(didDocumentPath, keyId, expectedDid) {
|
|
12593
|
+
if (!didDocumentPath) {
|
|
12594
|
+
return {
|
|
12595
|
+
ok: false,
|
|
12596
|
+
diagnostics: [diag("did_web.document_unavailable", "--did-document <path> is required for local did:web verification")]
|
|
12597
|
+
};
|
|
12598
|
+
}
|
|
12599
|
+
const document = readJson(didDocumentPath);
|
|
12600
|
+
if (!document.ok) {
|
|
12601
|
+
return { ok: false, diagnostics: [diag("did_web.document_unavailable", document.message)] };
|
|
12602
|
+
}
|
|
12603
|
+
if (!isRecord(document.value) || !Array.isArray(document.value.verificationMethod)) {
|
|
12604
|
+
return { ok: false, diagnostics: [diag("did_web.document_invalid", "DID document must contain verificationMethod[]")] };
|
|
12605
|
+
}
|
|
12606
|
+
if (document.value.id !== expectedDid) {
|
|
12607
|
+
return { ok: false, diagnostics: [diag("did_web.document_invalid", `DID document id must be ${expectedDid}`)] };
|
|
12608
|
+
}
|
|
12609
|
+
if (keyId.startsWith("did:") && !keyId.startsWith(`${expectedDid}#`)) {
|
|
12610
|
+
return { ok: false, diagnostics: [diag("did_web.document_invalid", `DID key ${keyId} is not anchored to ${expectedDid}`)] };
|
|
12611
|
+
}
|
|
12612
|
+
const fragment = keyId.startsWith("#") ? keyId : keyId.includes("#") ? `#${keyId.split("#").pop()}` : keyId;
|
|
12613
|
+
const fullKeyId = fragment.startsWith("#") ? `${expectedDid}${fragment}` : null;
|
|
12614
|
+
const method = document.value.verificationMethod.find(
|
|
12615
|
+
(entry) => isRecord(entry) && (entry.id === keyId || entry.id === fragment || entry.id === fullKeyId)
|
|
12616
|
+
);
|
|
12617
|
+
if (!isRecord(method)) {
|
|
12618
|
+
return { ok: false, diagnostics: [diag("did_web.key_not_found", `DID document does not contain key id ${keyId}`)] };
|
|
12619
|
+
}
|
|
12620
|
+
if (typeof method.id !== "string" || !method.id.startsWith(`${expectedDid}#`) && !method.id.startsWith("#")) {
|
|
12621
|
+
return { ok: false, diagnostics: [diag("did_web.document_invalid", `DID key ${keyId} is not anchored to ${expectedDid}`)] };
|
|
12622
|
+
}
|
|
12623
|
+
if (method.controller !== expectedDid) {
|
|
12624
|
+
return { ok: false, diagnostics: [diag("did_web.document_invalid", `DID key ${keyId} controller must be ${expectedDid}`)] };
|
|
12625
|
+
}
|
|
12626
|
+
const methodId = method.id.startsWith("#") ? `${expectedDid}${method.id}` : method.id;
|
|
12627
|
+
try {
|
|
12628
|
+
if (isRecord(method.publicKeyJwk)) {
|
|
12629
|
+
const key = createPublicKey({ key: method.publicKeyJwk, format: "jwk" });
|
|
12630
|
+
return {
|
|
12631
|
+
ok: true,
|
|
12632
|
+
key,
|
|
12633
|
+
methodId,
|
|
12634
|
+
keyFingerprint: publicKeyFingerprint(key)
|
|
12635
|
+
};
|
|
12636
|
+
}
|
|
12637
|
+
if (typeof method.publicKeyPem === "string") {
|
|
12638
|
+
const key = createPublicKey(method.publicKeyPem);
|
|
12639
|
+
return { ok: true, key, methodId, keyFingerprint: publicKeyFingerprint(key) };
|
|
12640
|
+
}
|
|
12641
|
+
return { ok: false, diagnostics: [diag("did_web.key_not_found", `DID key ${keyId} has no supported public key material`)] };
|
|
12642
|
+
} catch (error) {
|
|
12643
|
+
return { ok: false, diagnostics: [diag("did_web.key_invalid", error instanceof Error ? error.message : String(error))] };
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
12646
|
+
function runVerify(args) {
|
|
12647
|
+
const parsed = parseOptions(args);
|
|
12648
|
+
const err = unknownOptions(parsed, ["--target", "--sidecar", "--policy", "--did-document", "--offline-sigstore-fixture", "--offline"]);
|
|
12649
|
+
if (err) return usage("verify", err);
|
|
12650
|
+
if (parsed.flags.has("--offline")) return usage("verify", "verify --offline is not a stable MVP option");
|
|
12651
|
+
if (parsed.positional.length !== 1) return usage("verify", "Expected one file: mda verify <file> --policy <path>");
|
|
12652
|
+
const policyPath = oneOption(parsed.options, "--policy");
|
|
12653
|
+
if (!policyPath) return usage("verify", "--policy <path> is required");
|
|
12654
|
+
const file = parsed.positional[0];
|
|
12655
|
+
const requestedTarget = parseTarget(oneOption(parsed.options, "--target") ?? "auto");
|
|
12656
|
+
if (!requestedTarget) return usage("verify", "--target must be source, SKILL.md, AGENTS.md, MCP-SERVER.md, or auto");
|
|
12657
|
+
const targetResult = resolveTarget(file, requestedTarget);
|
|
12658
|
+
if (!targetResult.ok) return targetResult.result("verify", file);
|
|
12659
|
+
const policy = readJson(policyPath);
|
|
12660
|
+
if (!policy.ok) return ioError("verify", policy.message, { file, policy: policyPath });
|
|
12661
|
+
const policyValidation = validateJsonAgainst(policy.value, "trustPolicy");
|
|
12662
|
+
if (!policyValidation.ok)
|
|
12663
|
+
return commandResult(false, "verify", EXIT.failure, trustPolicyDiagnostics(policy.value, policyValidation.diagnostics), {
|
|
12664
|
+
file,
|
|
12665
|
+
policy: policyPath
|
|
12666
|
+
});
|
|
12667
|
+
const validation = validateArtifact(file, targetResult.target);
|
|
12668
|
+
if (!validation.ok)
|
|
12669
|
+
return commandResult(false, "verify", EXIT.failure, validation.diagnostics, { file, target: targetResult.target, policy: policyPath });
|
|
12670
|
+
const integrityArgs = ["verify", file, "--target", targetResult.target];
|
|
12671
|
+
const sidecar = oneOption(parsed.options, "--sidecar");
|
|
12672
|
+
if (sidecar) integrityArgs.push("--sidecar", sidecar);
|
|
12673
|
+
const integrity = runIntegrity(integrityArgs);
|
|
12674
|
+
if (!integrity.ok)
|
|
12675
|
+
return commandResult(false, "verify", EXIT.failure, integrity.diagnostics, { file, target: targetResult.target, policy: policyPath });
|
|
12676
|
+
const signedArtifact = readArtifact(file);
|
|
12677
|
+
if (!signedArtifact.ok || signedArtifact.extract.kind !== "ok" || !isRecord(signedArtifact.extract.frontmatter)) {
|
|
12678
|
+
return commandResult(false, "verify", EXIT.failure, [diag("missing-required-frontmatter", "Verification requires frontmatter")], {
|
|
12679
|
+
file,
|
|
12680
|
+
target: targetResult.target,
|
|
12681
|
+
policy: policyPath
|
|
12682
|
+
});
|
|
12683
|
+
}
|
|
12684
|
+
if (!Array.isArray(signedArtifact.extract.frontmatter.signatures) || signedArtifact.extract.frontmatter.signatures.length === 0) {
|
|
12685
|
+
return commandResult(false, "verify", EXIT.failure, [diag("missing-required-signature", "Verification requires signatures[]")], {
|
|
12686
|
+
file,
|
|
12687
|
+
target: targetResult.target,
|
|
12688
|
+
policy: policyPath
|
|
12689
|
+
});
|
|
12690
|
+
}
|
|
12691
|
+
const integrityField = signedArtifact.extract.frontmatter.integrity;
|
|
12692
|
+
if (!isRecord(integrityField) || typeof integrityField.algorithm !== "string" || typeof integrityField.digest !== "string") {
|
|
12693
|
+
return commandResult(false, "verify", EXIT.failure, [diag("missing-required-integrity", "Verification requires integrity")], {
|
|
12694
|
+
file,
|
|
12695
|
+
target: targetResult.target,
|
|
12696
|
+
policy: policyPath
|
|
12697
|
+
});
|
|
12698
|
+
}
|
|
12699
|
+
const didDocumentPath = oneOption(parsed.options, "--did-document");
|
|
12700
|
+
const sigstoreFixturePath = oneOption(parsed.options, "--offline-sigstore-fixture");
|
|
12701
|
+
const signatureVerification = verifySignatureEntries(
|
|
12702
|
+
signedArtifact.extract.frontmatter.signatures,
|
|
12703
|
+
{ algorithm: integrityField.algorithm, digest: integrityField.digest },
|
|
12704
|
+
policy.value,
|
|
12705
|
+
didDocumentPath,
|
|
12706
|
+
sigstoreFixturePath
|
|
12707
|
+
);
|
|
12708
|
+
if (signatureVerification.malformed) {
|
|
12709
|
+
return commandResult(false, "verify", EXIT.failure, [diag("signature.invalid_entry", "Signature entry is malformed")], {
|
|
12710
|
+
file,
|
|
12711
|
+
target: targetResult.target,
|
|
12712
|
+
policy: policyPath
|
|
12713
|
+
});
|
|
12714
|
+
}
|
|
12715
|
+
const { trusted, trustedSignerIdentities, rejectedTrusted } = signatureVerification;
|
|
12716
|
+
if (trusted.size === 0) {
|
|
12717
|
+
const hasSigstoreRejection = rejectedTrusted.some((d) => d.code.startsWith("sigstore.") || d.code.startsWith("rekor."));
|
|
12718
|
+
return commandResult(
|
|
12719
|
+
false,
|
|
12720
|
+
"verify",
|
|
12721
|
+
EXIT.failure,
|
|
12722
|
+
rejectedTrusted.length > 0 ? rejectedTrusted : [diag("no-trusted-signature", "no signature matched the trust policy")],
|
|
12723
|
+
{
|
|
12724
|
+
summary: rejectedTrusted.length > 0 ? "No trusted signature could be verified" : "No trusted signature matched the policy",
|
|
12725
|
+
nextActions: rejectedTrusted.length > 0 ? hasSigstoreRejection ? [
|
|
12726
|
+
nextAction(
|
|
12727
|
+
"provide-sigstore-fixture",
|
|
12728
|
+
"Provide the explicit Sigstore/Rekor fixture matching this release identity",
|
|
12729
|
+
`mda verify ${file} --policy ${policyPath} --offline-sigstore-fixture <sigstore-fixture.json>`
|
|
12730
|
+
)
|
|
12731
|
+
] : [
|
|
12732
|
+
nextAction(
|
|
12733
|
+
"fix-did-document",
|
|
12734
|
+
"Provide the DID document containing the signature key",
|
|
12735
|
+
`mda verify ${file} --policy ${policyPath} --did-document <did-document.json>`
|
|
12736
|
+
)
|
|
12737
|
+
] : [
|
|
12738
|
+
nextAction(
|
|
12739
|
+
"sign-with-trusted-identity",
|
|
12740
|
+
"Sign the artifact with a did:web identity allowed by the policy",
|
|
12741
|
+
`mda sign ${file} --profile did-web --did did:web:<domain> --key-id did:web:<domain>#<key> --key-file <private-key> --out signed.mda`
|
|
12742
|
+
)
|
|
12743
|
+
],
|
|
12744
|
+
file,
|
|
12745
|
+
target: targetResult.target,
|
|
12746
|
+
policy: policyPath,
|
|
12747
|
+
rejectedSignatures: rejectedTrusted.length
|
|
12748
|
+
}
|
|
12749
|
+
);
|
|
12750
|
+
}
|
|
12751
|
+
const minSignatures = trustPolicyMinSignatures(policy.value);
|
|
12752
|
+
if (trusted.size < minSignatures) {
|
|
12753
|
+
return commandResult(
|
|
12754
|
+
false,
|
|
12755
|
+
"verify",
|
|
12756
|
+
EXIT.failure,
|
|
12757
|
+
[diag("insufficient-trusted-signatures", `${trusted.size} trusted signer identities < ${minSignatures}`)],
|
|
12758
|
+
{
|
|
12759
|
+
summary: "Trusted signature threshold was not met",
|
|
12760
|
+
nextActions: [
|
|
12761
|
+
nextAction(
|
|
12762
|
+
"add-signatures",
|
|
12763
|
+
"Add enough trusted signatures to satisfy minSignatures",
|
|
12764
|
+
`mda sign ${file} --profile did-web --did did:web:<domain> --key-id did:web:<domain>#<key> --key-file <private-key> --out signed.mda`
|
|
12765
|
+
)
|
|
12766
|
+
],
|
|
12767
|
+
file,
|
|
12768
|
+
target: targetResult.target,
|
|
12769
|
+
policy: policyPath,
|
|
12770
|
+
trustedSignatures: trusted.size,
|
|
12771
|
+
rejectedSignatures: rejectedTrusted.length,
|
|
12772
|
+
minSignatures,
|
|
12773
|
+
trustedSignerIdentities
|
|
12774
|
+
}
|
|
12775
|
+
);
|
|
12776
|
+
}
|
|
12777
|
+
return commandResult(true, "verify", EXIT.ok, [], {
|
|
12778
|
+
summary: "Signature verification passed",
|
|
12779
|
+
artifacts: [artifact("verified-signature", file, targetResult.target, String(integrityField.digest))],
|
|
12780
|
+
nextActions: [
|
|
12781
|
+
externalNextAction(
|
|
12782
|
+
"publish-artifact",
|
|
12783
|
+
"Use this verified artifact in the release flow",
|
|
12784
|
+
"continue to release-plan or trust-manifest generation",
|
|
12785
|
+
false
|
|
12786
|
+
)
|
|
12787
|
+
],
|
|
12788
|
+
message: `verified ${trusted.size} trusted signature(s)`,
|
|
12789
|
+
file,
|
|
12790
|
+
target: targetResult.target,
|
|
12791
|
+
policy: policyPath,
|
|
12792
|
+
trustedSignatures: trusted.size,
|
|
12793
|
+
rejectedSignatures: rejectedTrusted.length,
|
|
12794
|
+
minSignatures,
|
|
12795
|
+
trustedSignerIdentities,
|
|
12796
|
+
payloadDigest: integrityField.digest
|
|
12797
|
+
});
|
|
12798
|
+
}
|
|
12799
|
+
function runSign(args) {
|
|
12800
|
+
const parsed = parseOptions(args);
|
|
12801
|
+
const err = unknownOptions(parsed, [
|
|
12802
|
+
"--target",
|
|
12803
|
+
"--sidecar",
|
|
12804
|
+
"--profile",
|
|
12805
|
+
"--did",
|
|
12806
|
+
"--key-id",
|
|
12807
|
+
"--key-file",
|
|
12808
|
+
"--method",
|
|
12809
|
+
"--key",
|
|
12810
|
+
"--identity",
|
|
12811
|
+
"--repo",
|
|
12812
|
+
"--workflow",
|
|
12813
|
+
"--ref",
|
|
12814
|
+
"--offline-sigstore-fixture",
|
|
12815
|
+
"--out",
|
|
12816
|
+
"--in-place",
|
|
12817
|
+
"--rekor"
|
|
12818
|
+
]);
|
|
12819
|
+
if (err) return usage("sign", err);
|
|
12820
|
+
if (parsed.positional.length !== 1)
|
|
12821
|
+
return usage(
|
|
12822
|
+
"sign",
|
|
12823
|
+
"Expected one file: mda sign <file> --profile did-web --did <did> --key-id <key-id> --key-file <path> (--out <file>|--in-place)"
|
|
12824
|
+
);
|
|
12825
|
+
const file = parsed.positional[0];
|
|
12826
|
+
const profile = oneOption(parsed.options, "--profile");
|
|
12827
|
+
const method = oneOption(parsed.options, "--method");
|
|
12828
|
+
if (profile === "github-actions") return runGithubActionsSign(parsed);
|
|
12829
|
+
if (profile && profile !== "did-web") return usage("sign", `Unsupported signing profile: ${profile}`);
|
|
12830
|
+
if (method && method !== "did-web") return usage("sign", "--method did-web is the only compatibility alias");
|
|
12831
|
+
if (!profile && !method) return usage("sign", "--profile did-web is required");
|
|
12832
|
+
if (parsed.options.has("--repo") || parsed.options.has("--workflow") || parsed.options.has("--ref") || parsed.options.has("--offline-sigstore-fixture") || parsed.flags.has("--rekor")) {
|
|
12833
|
+
return usage("sign", "GitHub Actions signing options require --profile github-actions");
|
|
12834
|
+
}
|
|
12835
|
+
const identity = oneOption(parsed.options, "--identity");
|
|
12836
|
+
const did = oneOption(parsed.options, "--did") ?? (identity ? `did:web:${identity}` : null);
|
|
12837
|
+
if (!did) return usage("sign", "--did <did> is required");
|
|
12838
|
+
const domain = didWebDomainFromDid(did);
|
|
12839
|
+
if (!domain) return usage("sign", "--did must be a did:web DID with a valid domain");
|
|
12840
|
+
const keyId = oneOption(parsed.options, "--key-id") ?? `${did}#default`;
|
|
12841
|
+
const keyFile = oneOption(parsed.options, "--key-file") ?? oneOption(parsed.options, "--key");
|
|
12842
|
+
if (!keyFile)
|
|
12843
|
+
return commandResult(false, "sign", EXIT.usage, [diag("did_web.key_input_missing", "--key-file <path> is required")], {
|
|
12844
|
+
summary: "sign usage error",
|
|
12845
|
+
nextActions: [
|
|
12846
|
+
nextAction(
|
|
12847
|
+
"provide-did-web-key",
|
|
12848
|
+
"Provide the explicit did:web private key file",
|
|
12849
|
+
`mda sign ${file} --profile did-web --did ${did} --key-id ${keyId} --key-file <private-key> --out signed.mda`
|
|
12850
|
+
)
|
|
12851
|
+
],
|
|
12852
|
+
file,
|
|
12853
|
+
written: false
|
|
12854
|
+
});
|
|
12855
|
+
const out = oneOption(parsed.options, "--out");
|
|
12856
|
+
const inPlace = parsed.flags.has("--in-place");
|
|
12857
|
+
if (out && inPlace || !out && !inPlace) return usage("sign", "Choose exactly one output mode: --out <file> or --in-place");
|
|
12858
|
+
if (out && existsSync3(out)) return ioError("sign", `Refusing to overwrite existing file: ${out}`, { file, out, written: false });
|
|
12859
|
+
const requestedTarget = parseTarget(oneOption(parsed.options, "--target") ?? "auto");
|
|
12860
|
+
if (!requestedTarget) return usage("sign", "--target must be source, SKILL.md, AGENTS.md, MCP-SERVER.md, or auto");
|
|
12861
|
+
const targetResult = resolveTarget(file, requestedTarget);
|
|
12862
|
+
if (!targetResult.ok) return targetResult.result("sign", file);
|
|
12863
|
+
const sidecar = oneOption(parsed.options, "--sidecar");
|
|
12864
|
+
const integrityArgs = ["verify", file, "--target", targetResult.target];
|
|
12865
|
+
if (sidecar) integrityArgs.push("--sidecar", sidecar);
|
|
12866
|
+
const integrityResult = runIntegrity(integrityArgs);
|
|
12867
|
+
if (!integrityResult.ok) {
|
|
12868
|
+
return commandResult(false, "sign", EXIT.failure, integrityResult.diagnostics, {
|
|
12869
|
+
summary: "Signing requires valid recorded integrity",
|
|
12870
|
+
nextActions: [
|
|
12871
|
+
nextAction(
|
|
12872
|
+
"write-integrity",
|
|
12873
|
+
"Record integrity before signing",
|
|
12874
|
+
`mda integrity compute ${file} --target ${targetResult.target} --write`
|
|
12875
|
+
)
|
|
12876
|
+
],
|
|
12877
|
+
file,
|
|
12878
|
+
target: targetResult.target,
|
|
12879
|
+
written: false
|
|
12880
|
+
});
|
|
12881
|
+
}
|
|
12882
|
+
const input = readArtifact(file);
|
|
12883
|
+
if (!input.ok || input.extract.kind !== "ok" || !isRecord(input.extract.frontmatter)) {
|
|
12884
|
+
return commandResult(false, "sign", EXIT.failure, [diag("missing-required-frontmatter", "Signing requires frontmatter")], {
|
|
12885
|
+
file,
|
|
12886
|
+
target: targetResult.target,
|
|
12887
|
+
written: false
|
|
12888
|
+
});
|
|
12889
|
+
}
|
|
12890
|
+
const integrity = input.extract.frontmatter.integrity;
|
|
12891
|
+
if (!isRecord(integrity) || typeof integrity.digest !== "string") {
|
|
12892
|
+
return commandResult(false, "sign", EXIT.failure, [diag("missing-required-integrity", "Signing requires integrity")], {
|
|
12893
|
+
file,
|
|
12894
|
+
target: targetResult.target,
|
|
12895
|
+
written: false
|
|
12896
|
+
});
|
|
12897
|
+
}
|
|
12898
|
+
if (input.extract.frontmatter.signatures !== void 0 && !Array.isArray(input.extract.frontmatter.signatures)) {
|
|
12899
|
+
return commandResult(false, "sign", EXIT.failure, [diag("signature.invalid_entry", "Existing signatures must be an array")], {
|
|
12900
|
+
file,
|
|
12901
|
+
target: targetResult.target,
|
|
12902
|
+
written: false
|
|
12903
|
+
});
|
|
12904
|
+
}
|
|
12905
|
+
let privateKey;
|
|
12906
|
+
try {
|
|
12907
|
+
privateKey = createPrivateKey(readFileSync2(keyFile));
|
|
12908
|
+
} catch (error) {
|
|
12909
|
+
return ioError("sign", error instanceof Error ? error.message : String(error), { file, keyFile, written: false });
|
|
12910
|
+
}
|
|
12911
|
+
const algorithm = keyAlgorithm(privateKey);
|
|
12912
|
+
if (!algorithm) {
|
|
12913
|
+
return commandResult(
|
|
12914
|
+
false,
|
|
12915
|
+
"sign",
|
|
12916
|
+
EXIT.failure,
|
|
12917
|
+
[diag("signature.unsupported_algorithm", "Only Ed25519 did:web keys are supported in this release")],
|
|
12918
|
+
{ file, keyFile, written: false }
|
|
12919
|
+
);
|
|
12920
|
+
}
|
|
12921
|
+
const payload = integrityPayloadBytes(integrity);
|
|
12922
|
+
const signatureBytes = cryptoSign(null, dssePae(INTEGRITY_PAYLOAD_TYPE, payload), privateKey);
|
|
12923
|
+
const signature = {
|
|
12924
|
+
signer: `did-web:${domain}`,
|
|
12925
|
+
"key-id": keyId,
|
|
12926
|
+
"payload-digest": integrity.digest,
|
|
12927
|
+
algorithm,
|
|
12928
|
+
signature: signatureBytes.toString("base64"),
|
|
12929
|
+
"payload-type": INTEGRITY_PAYLOAD_TYPE
|
|
12930
|
+
};
|
|
12931
|
+
const frontmatter = {
|
|
12932
|
+
...input.extract.frontmatter,
|
|
12933
|
+
signatures: [...Array.isArray(input.extract.frontmatter.signatures) ? input.extract.frontmatter.signatures : [], signature]
|
|
12934
|
+
};
|
|
12935
|
+
const destination = inPlace ? file : out;
|
|
12936
|
+
try {
|
|
12937
|
+
if (inPlace) atomicReplace(file, renderArtifact(frontmatter, input.extract.body));
|
|
12938
|
+
else atomicWrite(destination, renderArtifact(frontmatter, input.extract.body));
|
|
12939
|
+
} catch (error) {
|
|
12940
|
+
return ioError("sign", error instanceof Error ? error.message : String(error), { file, out: destination, written: false });
|
|
12941
|
+
}
|
|
12942
|
+
return commandResult(true, "sign", EXIT.ok, [], {
|
|
12943
|
+
summary: "did:web signature written",
|
|
12944
|
+
artifacts: [artifact("signed-artifact", destination, targetResult.target, String(integrity.digest))],
|
|
12945
|
+
nextActions: [
|
|
12946
|
+
nextAction(
|
|
12947
|
+
"verify-signature",
|
|
12948
|
+
"Verify the signed artifact with a trust policy",
|
|
12949
|
+
`mda verify ${destination} --policy <policy.json> --did-document <did-document.json>`
|
|
12950
|
+
)
|
|
12951
|
+
],
|
|
12952
|
+
message: `signed ${destination}`,
|
|
12953
|
+
file,
|
|
12954
|
+
target: targetResult.target,
|
|
12955
|
+
profile: "did-web",
|
|
12956
|
+
signer: `did-web:${domain}`,
|
|
12957
|
+
keyId,
|
|
12958
|
+
payloadDigest: integrity.digest,
|
|
12959
|
+
out: destination,
|
|
12960
|
+
written: true
|
|
12961
|
+
});
|
|
12962
|
+
}
|
|
12963
|
+
function runGithubActionsSign(parsed) {
|
|
12964
|
+
if (parsed.options.has("--method") || parsed.options.has("--did") || parsed.options.has("--key-id") || parsed.options.has("--key-file") || parsed.options.has("--key") || parsed.options.has("--identity")) {
|
|
12965
|
+
return usage("sign", "did:web signing options cannot be combined with --profile github-actions");
|
|
12966
|
+
}
|
|
12967
|
+
const file = parsed.positional[0];
|
|
12968
|
+
const repo = oneOption(parsed.options, "--repo");
|
|
12969
|
+
const workflow = oneOption(parsed.options, "--workflow");
|
|
12970
|
+
const ref = oneOption(parsed.options, "--ref");
|
|
12971
|
+
const fixturePath = oneOption(parsed.options, "--offline-sigstore-fixture");
|
|
12972
|
+
if (!repo || !GITHUB_REPOSITORY.test(repo)) return usage("sign", "--repo must be a GitHub repository in owner/repo form");
|
|
12973
|
+
if (!workflow || workflow.trim() !== workflow || workflow.length === 0)
|
|
12974
|
+
return usage("sign", "--workflow must be a non-empty workflow file or identity");
|
|
12975
|
+
if (!ref || !GITHUB_REF.test(ref)) return usage("sign", "--ref must be an exact Git ref such as refs/heads/main or refs/tags/v1.1.0");
|
|
12976
|
+
if (!parsed.flags.has("--rekor")) return usage("sign", "--rekor is required for --profile github-actions");
|
|
12977
|
+
if (!fixturePath) return usage("sign", "--offline-sigstore-fixture <path> is required for --profile github-actions");
|
|
12978
|
+
const out = oneOption(parsed.options, "--out");
|
|
12979
|
+
const inPlace = parsed.flags.has("--in-place");
|
|
12980
|
+
if (out && inPlace || !out && !inPlace) return usage("sign", "Choose exactly one output mode: --out <file> or --in-place");
|
|
12981
|
+
if (out && existsSync3(out)) return ioError("sign", `Refusing to overwrite existing file: ${out}`, { file, out, written: false });
|
|
12982
|
+
const requestedTarget = parseTarget(oneOption(parsed.options, "--target") ?? "auto");
|
|
12983
|
+
if (!requestedTarget) return usage("sign", "--target must be source, SKILL.md, AGENTS.md, MCP-SERVER.md, or auto");
|
|
12984
|
+
const targetResult = resolveTarget(file, requestedTarget);
|
|
12985
|
+
if (!targetResult.ok) return targetResult.result("sign", file);
|
|
12986
|
+
const sidecar = oneOption(parsed.options, "--sidecar");
|
|
12987
|
+
const integrityArgs = ["verify", file, "--target", targetResult.target];
|
|
12988
|
+
if (sidecar) integrityArgs.push("--sidecar", sidecar);
|
|
12989
|
+
const integrityResult = runIntegrity(integrityArgs);
|
|
12990
|
+
if (!integrityResult.ok) {
|
|
12991
|
+
return commandResult(false, "sign", EXIT.failure, integrityResult.diagnostics, {
|
|
12992
|
+
summary: "Signing requires valid recorded integrity",
|
|
12993
|
+
nextActions: [
|
|
12994
|
+
nextAction(
|
|
12995
|
+
"write-integrity",
|
|
12996
|
+
"Record integrity before signing",
|
|
12997
|
+
`mda integrity compute ${file} --target ${targetResult.target} --write`
|
|
12998
|
+
)
|
|
12999
|
+
],
|
|
13000
|
+
file,
|
|
13001
|
+
target: targetResult.target,
|
|
13002
|
+
written: false
|
|
13003
|
+
});
|
|
13004
|
+
}
|
|
13005
|
+
const input = readArtifact(file);
|
|
13006
|
+
if (!input.ok || input.extract.kind !== "ok" || !isRecord(input.extract.frontmatter)) {
|
|
13007
|
+
return commandResult(false, "sign", EXIT.failure, [diag("missing-required-frontmatter", "Signing requires frontmatter")], {
|
|
13008
|
+
file,
|
|
13009
|
+
target: targetResult.target,
|
|
13010
|
+
written: false
|
|
13011
|
+
});
|
|
13012
|
+
}
|
|
13013
|
+
const integrity = input.extract.frontmatter.integrity;
|
|
13014
|
+
if (!isRecord(integrity) || typeof integrity.digest !== "string") {
|
|
13015
|
+
return commandResult(false, "sign", EXIT.failure, [diag("missing-required-integrity", "Signing requires integrity")], {
|
|
13016
|
+
file,
|
|
13017
|
+
target: targetResult.target,
|
|
13018
|
+
written: false
|
|
13019
|
+
});
|
|
13020
|
+
}
|
|
13021
|
+
if (input.extract.frontmatter.signatures !== void 0 && !Array.isArray(input.extract.frontmatter.signatures)) {
|
|
13022
|
+
return commandResult(false, "sign", EXIT.failure, [diag("signature.invalid_entry", "Existing signatures must be an array")], {
|
|
13023
|
+
file,
|
|
13024
|
+
target: targetResult.target,
|
|
13025
|
+
written: false
|
|
13026
|
+
});
|
|
13027
|
+
}
|
|
13028
|
+
const fixtureResult = readSigstoreFixture(fixturePath, true);
|
|
13029
|
+
if (!fixtureResult.ok) {
|
|
13030
|
+
return commandResult(false, "sign", EXIT.failure, fixtureResult.diagnostics, {
|
|
13031
|
+
summary: "Sigstore fixture is invalid",
|
|
13032
|
+
file,
|
|
13033
|
+
target: targetResult.target,
|
|
13034
|
+
fixture: fixturePath,
|
|
13035
|
+
written: false
|
|
13036
|
+
});
|
|
13037
|
+
}
|
|
13038
|
+
const fixture = fixtureResult.fixture;
|
|
13039
|
+
const expectedSubject = `repo:${repo}:ref:${ref}`;
|
|
13040
|
+
if (fixture.issuer !== GITHUB_ACTIONS_ISSUER || fixture.subject !== expectedSubject || fixture.repository !== repo || fixture.workflow !== workflow || fixture.ref !== ref) {
|
|
13041
|
+
return commandResult(
|
|
13042
|
+
false,
|
|
13043
|
+
"sign",
|
|
13044
|
+
EXIT.failure,
|
|
13045
|
+
[diag("sigstore.identity_mismatch", "Sigstore fixture identity does not match the requested GitHub Actions release identity")],
|
|
13046
|
+
{ file, target: targetResult.target, fixture: fixturePath, written: false }
|
|
13047
|
+
);
|
|
13048
|
+
}
|
|
13049
|
+
if (fixture.expectedPayloadDigest !== integrity.digest || fixture.rekor.payloadDigest !== integrity.digest) {
|
|
13050
|
+
return commandResult(
|
|
13051
|
+
false,
|
|
13052
|
+
"sign",
|
|
13053
|
+
EXIT.failure,
|
|
13054
|
+
[diag("sigstore.fixture_digest_mismatch", "Sigstore fixture digest does not match artifact integrity digest")],
|
|
13055
|
+
{ file, target: targetResult.target, fixture: fixturePath, written: false }
|
|
13056
|
+
);
|
|
13057
|
+
}
|
|
13058
|
+
let privateKey;
|
|
13059
|
+
let publicKey;
|
|
13060
|
+
try {
|
|
13061
|
+
privateKey = createPrivateKey(fixture.privateKeyPem);
|
|
13062
|
+
publicKey = createPublicKey(fixture.publicKeyPem);
|
|
13063
|
+
} catch (error) {
|
|
13064
|
+
return commandResult(
|
|
13065
|
+
false,
|
|
13066
|
+
"sign",
|
|
13067
|
+
EXIT.failure,
|
|
13068
|
+
[diag("sigstore.fixture_invalid", error instanceof Error ? error.message : String(error))],
|
|
13069
|
+
{
|
|
13070
|
+
file,
|
|
13071
|
+
target: targetResult.target,
|
|
13072
|
+
fixture: fixturePath,
|
|
13073
|
+
written: false
|
|
13074
|
+
}
|
|
13075
|
+
);
|
|
13076
|
+
}
|
|
13077
|
+
if (keyAlgorithm(privateKey) !== fixture.algorithm || keyAlgorithm(publicKey) !== fixture.algorithm) {
|
|
13078
|
+
return commandResult(
|
|
13079
|
+
false,
|
|
13080
|
+
"sign",
|
|
13081
|
+
EXIT.failure,
|
|
13082
|
+
[diag("signature.unsupported_algorithm", "Only Ed25519 GitHub Actions fixture keys are supported in this release")],
|
|
13083
|
+
{ file, target: targetResult.target, fixture: fixturePath, written: false }
|
|
13084
|
+
);
|
|
13085
|
+
}
|
|
13086
|
+
const payload = integrityPayloadBytes(integrity);
|
|
13087
|
+
const signatureBytes = cryptoSign(null, dssePae(INTEGRITY_PAYLOAD_TYPE, payload), privateKey);
|
|
13088
|
+
if (!cryptoVerify(null, dssePae(INTEGRITY_PAYLOAD_TYPE, payload), publicKey, signatureBytes)) {
|
|
13089
|
+
return commandResult(
|
|
13090
|
+
false,
|
|
13091
|
+
"sign",
|
|
13092
|
+
EXIT.failure,
|
|
13093
|
+
[diag("sigstore.fixture_invalid", "Sigstore fixture public key does not match private key")],
|
|
13094
|
+
{
|
|
13095
|
+
file,
|
|
13096
|
+
target: targetResult.target,
|
|
13097
|
+
fixture: fixturePath,
|
|
13098
|
+
written: false
|
|
13099
|
+
}
|
|
13100
|
+
);
|
|
13101
|
+
}
|
|
13102
|
+
const signature = {
|
|
13103
|
+
signer: `sigstore-oidc:${fixture.issuer}`,
|
|
13104
|
+
"key-id": fixture.keyId,
|
|
13105
|
+
"payload-digest": integrity.digest,
|
|
13106
|
+
algorithm: fixture.algorithm,
|
|
13107
|
+
signature: signatureBytes.toString("base64"),
|
|
13108
|
+
"payload-type": INTEGRITY_PAYLOAD_TYPE,
|
|
13109
|
+
"rekor-log-id": fixture.rekor.logId,
|
|
13110
|
+
"rekor-log-index": fixture.rekor.logIndex
|
|
13111
|
+
};
|
|
13112
|
+
const frontmatter = {
|
|
13113
|
+
...input.extract.frontmatter,
|
|
13114
|
+
signatures: [...Array.isArray(input.extract.frontmatter.signatures) ? input.extract.frontmatter.signatures : [], signature]
|
|
13115
|
+
};
|
|
13116
|
+
const destination = inPlace ? file : out;
|
|
13117
|
+
try {
|
|
13118
|
+
if (inPlace) atomicReplace(file, renderArtifact(frontmatter, input.extract.body));
|
|
13119
|
+
else atomicWrite(destination, renderArtifact(frontmatter, input.extract.body));
|
|
13120
|
+
} catch (error) {
|
|
13121
|
+
return ioError("sign", error instanceof Error ? error.message : String(error), { file, out: destination, written: false });
|
|
13122
|
+
}
|
|
13123
|
+
return commandResult(true, "sign", EXIT.ok, [], {
|
|
13124
|
+
summary: "GitHub Actions Sigstore/Rekor signature written",
|
|
13125
|
+
artifacts: [artifact("signed-artifact", destination, targetResult.target, String(integrity.digest))],
|
|
13126
|
+
nextActions: [
|
|
13127
|
+
nextAction(
|
|
13128
|
+
"verify-github-actions-signature",
|
|
13129
|
+
"Verify the signed release artifact with the pinned trust policy and fixture",
|
|
13130
|
+
`mda verify ${destination} --policy <policy.json> --offline-sigstore-fixture ${fixturePath}`
|
|
13131
|
+
)
|
|
13132
|
+
],
|
|
13133
|
+
message: `signed ${destination}`,
|
|
13134
|
+
file,
|
|
13135
|
+
target: targetResult.target,
|
|
13136
|
+
profile: "github-actions",
|
|
13137
|
+
signer: `sigstore-oidc:${fixture.issuer}`,
|
|
13138
|
+
keyId: fixture.keyId,
|
|
13139
|
+
payloadDigest: integrity.digest,
|
|
13140
|
+
rekorLogId: fixture.rekor.logId,
|
|
13141
|
+
rekorLogIndex: fixture.rekor.logIndex,
|
|
13142
|
+
out: destination,
|
|
13143
|
+
written: true
|
|
13144
|
+
});
|
|
13145
|
+
}
|
|
13146
|
+
|
|
13147
|
+
// src/cli/llmix-commands.ts
|
|
13148
|
+
var LLMIX_REGISTRY_TARGET = "llmix-registry";
|
|
13149
|
+
function runLlmix(args) {
|
|
13150
|
+
return migratedCommand("llmix", llmixMigrationReplacement(args));
|
|
13151
|
+
}
|
|
13152
|
+
function runRelease(args) {
|
|
13153
|
+
if (args[0] === "trust" && args[1] === "policy") return runLlmixTrustPolicy(args.slice(2), "release trust policy");
|
|
13154
|
+
if (args[0] === "prepare") return runLlmixReleasePlan(args.slice(1), "release prepare");
|
|
13155
|
+
if (args[0] === "finalize") {
|
|
13156
|
+
if (args.includes("--manifest") || args.includes("--snippet-format") || args.includes("--snippet-out")) {
|
|
13157
|
+
return runLlmixTrustSnippets(args.slice(1), "release finalize");
|
|
13158
|
+
}
|
|
13159
|
+
return runLlmixTrustManifest(args.slice(1), "release finalize");
|
|
13160
|
+
}
|
|
13161
|
+
return usage(
|
|
13162
|
+
"release",
|
|
13163
|
+
"Expected subcommand: release trust policy --target llmix-registry | release prepare --target llmix-registry | release finalize --target llmix-registry"
|
|
13164
|
+
);
|
|
13165
|
+
}
|
|
13166
|
+
function runLlmixTrustPolicy(args, command = "release trust policy") {
|
|
13167
|
+
const parsed = parseOptions(args);
|
|
13168
|
+
const err = unknownOptions(parsed, ["--target", "--profile", "--domain", "--min-signatures", "--out", "--repo", "--workflow", "--ref"]);
|
|
13169
|
+
if (err) return usage(command, err);
|
|
13170
|
+
const targetErr = releaseTargetError(command, parsed);
|
|
13171
|
+
if (targetErr) return targetErr;
|
|
13172
|
+
if (parsed.positional.length !== 0) return usage(command, `${command} takes no positional arguments`);
|
|
13173
|
+
const profile = oneOption(parsed.options, "--profile");
|
|
13174
|
+
if (!profile) return usage(command, "--profile <profile> is required");
|
|
13175
|
+
if (profile === "did-web") return runDidWebTrustPolicy(parsed.options, command);
|
|
13176
|
+
if (profile === "github-actions") return runGithubActionsTrustPolicy(parsed.options, command);
|
|
13177
|
+
return commandResult(
|
|
13178
|
+
false,
|
|
13179
|
+
command,
|
|
13180
|
+
EXIT.failure,
|
|
13181
|
+
[diag("trust_policy.unsupported_profile", `Unsupported trust policy profile: ${profile}`)],
|
|
13182
|
+
{
|
|
13183
|
+
summary: "Trust policy profile is not supported",
|
|
13184
|
+
nextActions: [
|
|
13185
|
+
nextAction(
|
|
13186
|
+
"use-did-web-policy-profile",
|
|
13187
|
+
"Use did:web for local deterministic signing",
|
|
13188
|
+
"mda release trust policy --target llmix-registry --profile did-web --domain example.com --out release-trust-policy.json"
|
|
13189
|
+
),
|
|
13190
|
+
nextAction(
|
|
13191
|
+
"use-github-actions-policy-profile",
|
|
13192
|
+
"Use GitHub Actions Sigstore/Rekor for CI release signing",
|
|
13193
|
+
"mda release trust policy --target llmix-registry --profile github-actions --repo owner/repo --workflow release.yml --ref refs/heads/main --out release-trust-policy.json"
|
|
13194
|
+
)
|
|
13195
|
+
]
|
|
13196
|
+
}
|
|
13197
|
+
);
|
|
13198
|
+
}
|
|
13199
|
+
function runLlmixReleasePlan(args, command = "release prepare") {
|
|
13200
|
+
const parsed = parseOptions(args);
|
|
13201
|
+
const err = unknownOptions(parsed, [
|
|
13202
|
+
"--target",
|
|
13203
|
+
"--source",
|
|
13204
|
+
"--registry-dir",
|
|
13205
|
+
"--policy",
|
|
13206
|
+
"--out",
|
|
13207
|
+
"--did-document",
|
|
13208
|
+
"--offline-sigstore-fixture"
|
|
13209
|
+
]);
|
|
13210
|
+
if (err) return usage(command, err);
|
|
13211
|
+
const targetErr = releaseTargetError(command, parsed);
|
|
13212
|
+
if (targetErr) return targetErr;
|
|
13213
|
+
if (parsed.positional.length !== 0) return usage(command, `${command} takes no positional arguments`);
|
|
13214
|
+
const sourceDir = oneOption(parsed.options, "--source");
|
|
13215
|
+
const registryDir = oneOption(parsed.options, "--registry-dir");
|
|
13216
|
+
const policyPath = oneOption(parsed.options, "--policy");
|
|
13217
|
+
const out = oneOption(parsed.options, "--out");
|
|
13218
|
+
if (!sourceDir) return usage(command, "--source <dir> is required");
|
|
13219
|
+
if (!registryDir) return usage(command, "--registry-dir <dir> is required");
|
|
13220
|
+
if (!policyPath) return usage(command, "--policy <path> is required");
|
|
13221
|
+
if (!out) return usage(command, "--out <file> is required");
|
|
13222
|
+
if (existsSync4(out))
|
|
13223
|
+
return ioError(command, `Refusing to overwrite existing file: ${out}`, { sourceDir, registryDir, policy: policyPath });
|
|
13224
|
+
const policy = readJson(policyPath);
|
|
13225
|
+
if (!policy.ok) return ioError(command, policy.message, { sourceDir, registryDir, policy: policyPath });
|
|
13226
|
+
const policyValidation = validateJsonAgainst(policy.value, "trustPolicy");
|
|
13227
|
+
if (!policyValidation.ok)
|
|
13228
|
+
return commandResult(false, command, EXIT.failure, policyValidation.diagnostics, {
|
|
13229
|
+
sourceDir,
|
|
13230
|
+
registryDir,
|
|
13231
|
+
policy: policyPath
|
|
13232
|
+
});
|
|
13233
|
+
const sourceRoot = resolve3(sourceDir);
|
|
13234
|
+
const scanned = scanMdaSources(sourceRoot);
|
|
13235
|
+
if (!scanned.ok) return ioError(command, scanned.message, { sourceDir, registryDir, policy: policyPath });
|
|
13236
|
+
if (scanned.files.length === 0) {
|
|
13237
|
+
return commandResult(false, command, EXIT.failure, [diag("llmix.no_sources", "No .mda sources found under --source")], {
|
|
13238
|
+
summary: "Release prepare blocked",
|
|
13239
|
+
nextActions: [
|
|
13240
|
+
nextAction(
|
|
13241
|
+
"add-llmix-source",
|
|
13242
|
+
"Add signed LLMix .mda preset sources and retry",
|
|
13243
|
+
`mda release prepare --target llmix-registry --source ${sourceDir} --registry-dir ${registryDir} --policy ${policyPath} --out ${out}`
|
|
13244
|
+
)
|
|
13245
|
+
],
|
|
13246
|
+
sourceDir,
|
|
13247
|
+
registryDir,
|
|
13248
|
+
policy: policyPath,
|
|
13249
|
+
written: false
|
|
13250
|
+
});
|
|
13251
|
+
}
|
|
13252
|
+
const diagnostics = [];
|
|
13253
|
+
const sources = [];
|
|
13254
|
+
const plannedRegistryEntries = /* @__PURE__ */ new Map();
|
|
13255
|
+
const didDocument = oneOption(parsed.options, "--did-document");
|
|
13256
|
+
const sigstoreFixture = oneOption(parsed.options, "--offline-sigstore-fixture");
|
|
13257
|
+
for (const file of scanned.files) {
|
|
13258
|
+
const sourceRelativePath = relativePath(sourceRoot, file);
|
|
13259
|
+
const validation = validateArtifact(file, "source");
|
|
13260
|
+
if (!validation.ok) {
|
|
13261
|
+
diagnostics.push(...validation.diagnostics.map((d) => ({ ...d, path: file })));
|
|
13262
|
+
continue;
|
|
13263
|
+
}
|
|
13264
|
+
const integrity = runIntegrity(["verify", file, "--target", "source"]);
|
|
13265
|
+
if (!integrity.ok) {
|
|
13266
|
+
diagnostics.push(...integrity.diagnostics.map((d) => ({ ...d, path: file })));
|
|
13267
|
+
continue;
|
|
13268
|
+
}
|
|
13269
|
+
const verifyArgs = [file, "--target", "source", "--policy", policyPath];
|
|
13270
|
+
if (didDocument) verifyArgs.push("--did-document", didDocument);
|
|
13271
|
+
if (sigstoreFixture) verifyArgs.push("--offline-sigstore-fixture", sigstoreFixture);
|
|
13272
|
+
const verified = runVerify(verifyArgs);
|
|
13273
|
+
if (!verified.ok) {
|
|
13274
|
+
diagnostics.push(...verified.diagnostics.map((d) => ({ ...d, path: file })));
|
|
13275
|
+
continue;
|
|
13276
|
+
}
|
|
13277
|
+
const artifactRead = readArtifact(file);
|
|
13278
|
+
if (!artifactRead.ok || artifactRead.extract.kind !== "ok" || !isRecord(artifactRead.extract.frontmatter)) {
|
|
13279
|
+
diagnostics.push(diag("llmix.source_unreadable", "Source frontmatter could not be read after verification", { path: file }));
|
|
13280
|
+
continue;
|
|
13281
|
+
}
|
|
13282
|
+
const identity = llmixPresetIdentity(artifactRead.extract.frontmatter);
|
|
13283
|
+
if (!identity) {
|
|
13284
|
+
diagnostics.push(
|
|
13285
|
+
diag("llmix.release_identity_missing", "Source is missing a valid metadata.snoai-llmix module and preset", { path: file })
|
|
13286
|
+
);
|
|
13287
|
+
continue;
|
|
13288
|
+
}
|
|
13289
|
+
const canonical = canonicalizeFromFile(file, "source", null);
|
|
13290
|
+
if (!canonical.ok) {
|
|
13291
|
+
diagnostics.push(...canonical.diagnostics.map((d) => ({ ...d, path: file })));
|
|
13292
|
+
continue;
|
|
13293
|
+
}
|
|
13294
|
+
const signerIdentity = firstTrustedSignerIdentity(verified.trustedSignerIdentities);
|
|
13295
|
+
if (!signerIdentity) {
|
|
13296
|
+
diagnostics.push(
|
|
13297
|
+
diag("trust_policy.no_trusted_signature", "Verified source did not return a trusted signer identity", { path: file })
|
|
13298
|
+
);
|
|
13299
|
+
continue;
|
|
13300
|
+
}
|
|
13301
|
+
const expectedRegistryEntryIdentity = `${identity.module}/${identity.preset}`;
|
|
13302
|
+
const existingSourcePath = plannedRegistryEntries.get(expectedRegistryEntryIdentity);
|
|
13303
|
+
if (existingSourcePath) {
|
|
13304
|
+
diagnostics.push(
|
|
13305
|
+
diag(
|
|
13306
|
+
"llmix.duplicate_registry_entry",
|
|
13307
|
+
`Multiple sources target registry entry ${expectedRegistryEntryIdentity}; first source is ${existingSourcePath}`,
|
|
13308
|
+
{ path: file }
|
|
13309
|
+
)
|
|
13310
|
+
);
|
|
13311
|
+
continue;
|
|
13312
|
+
}
|
|
13313
|
+
plannedRegistryEntries.set(expectedRegistryEntryIdentity, sourceRelativePath);
|
|
13314
|
+
sources.push({
|
|
13315
|
+
module: identity.module,
|
|
13316
|
+
preset: identity.preset,
|
|
13317
|
+
sourcePath: sourceRelativePath,
|
|
13318
|
+
canonicalSourceDigest: computeDigest(canonical.bytes, "sha256"),
|
|
13319
|
+
signaturePayloadDigest: signerIdentity.payloadDigest,
|
|
13320
|
+
signerIdentity,
|
|
13321
|
+
expectedRegistryEntryIdentity,
|
|
13322
|
+
expectedRegistryEntryPath: `${identity.module}/${identity.preset}.json`
|
|
13323
|
+
});
|
|
13324
|
+
}
|
|
13325
|
+
if (diagnostics.length > 0) {
|
|
13326
|
+
return commandResult(false, command, EXIT.failure, diagnostics, {
|
|
13327
|
+
summary: "Release prepare blocked",
|
|
13328
|
+
nextActions: [
|
|
13329
|
+
nextAction(
|
|
13330
|
+
"fix-source-validation",
|
|
13331
|
+
"Fix validation, integrity, and signature diagnostics before release planning",
|
|
13332
|
+
`mda validate <source.mda> --target source`
|
|
13333
|
+
),
|
|
13334
|
+
nextAction(
|
|
13335
|
+
"verify-source-signature",
|
|
13336
|
+
"Verify each signed preset with the selected trust policy",
|
|
13337
|
+
`mda verify <signed.mda> --target source --policy ${policyPath}`
|
|
13338
|
+
)
|
|
13339
|
+
],
|
|
13340
|
+
sourceDir,
|
|
13341
|
+
registryDir,
|
|
13342
|
+
policy: policyPath,
|
|
13343
|
+
sourceCount: scanned.files.length,
|
|
13344
|
+
written: false
|
|
13345
|
+
});
|
|
13346
|
+
}
|
|
13347
|
+
sources.sort(
|
|
13348
|
+
(left, right) => String(left.expectedRegistryEntryIdentity).localeCompare(String(right.expectedRegistryEntryIdentity)) || String(left.sourcePath).localeCompare(String(right.sourcePath))
|
|
13349
|
+
);
|
|
13350
|
+
const sourceSetDigest = computeDigest(Buffer.from(jcs({ sources }), "utf8"), "sha256");
|
|
13351
|
+
const releasePlan = {
|
|
13352
|
+
version: 1,
|
|
13353
|
+
kind: "llmix-release-plan",
|
|
13354
|
+
sourceDir,
|
|
13355
|
+
registryDir,
|
|
13356
|
+
policy: policyPath,
|
|
13357
|
+
sourceSetDigest,
|
|
13358
|
+
sources,
|
|
13359
|
+
checklist: [
|
|
13360
|
+
{ id: "source-validation", ok: true, count: sources.length },
|
|
13361
|
+
{ id: "integrity-verification", ok: true, count: sources.length },
|
|
13362
|
+
{ id: "signature-verification", ok: true, count: sources.length },
|
|
13363
|
+
{
|
|
13364
|
+
id: "registry-publish",
|
|
13365
|
+
ok: false,
|
|
13366
|
+
external: true,
|
|
13367
|
+
reason: "Publish with LLMix trustedRuntime=true using this verified source set."
|
|
13368
|
+
}
|
|
13369
|
+
],
|
|
13370
|
+
publish: {
|
|
13371
|
+
external: true,
|
|
13372
|
+
trustedRuntime: true,
|
|
13373
|
+
next: "Run the LLMix registry publisher with this verified source set, then sign the registry root before generating the trust manifest."
|
|
13374
|
+
}
|
|
13375
|
+
};
|
|
13376
|
+
try {
|
|
13377
|
+
atomicWrite(out, `${JSON.stringify(releasePlan, null, 2)}
|
|
13378
|
+
`);
|
|
13379
|
+
} catch (error) {
|
|
13380
|
+
return ioError(command, error instanceof Error ? error.message : String(error), {
|
|
13381
|
+
sourceDir,
|
|
13382
|
+
registryDir,
|
|
13383
|
+
policy: policyPath,
|
|
13384
|
+
out,
|
|
13385
|
+
written: false
|
|
13386
|
+
});
|
|
13387
|
+
}
|
|
13388
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
13389
|
+
summary: `Prepared LLMix registry release plan for ${sources.length} source(s)`,
|
|
13390
|
+
artifacts: [artifact("llmix-release-plan", out, void 0, sourceSetDigest)],
|
|
13391
|
+
nextActions: [
|
|
13392
|
+
externalNextAction(
|
|
13393
|
+
"publish-llmix-registry",
|
|
13394
|
+
"Publish the verified source set with LLMix trustedRuntime=true",
|
|
13395
|
+
"use the LLMix registry publisher, then sign the registry root"
|
|
13396
|
+
),
|
|
13397
|
+
externalNextAction(
|
|
13398
|
+
"prepare-trust-manifest-inputs",
|
|
13399
|
+
"After registry publication, collect the signed registry root and root trust policy for trust manifest generation",
|
|
13400
|
+
"wait for the signed registry root evidence before running the trust manifest step",
|
|
13401
|
+
false
|
|
13402
|
+
)
|
|
13403
|
+
],
|
|
13404
|
+
message: `wrote ${out}`,
|
|
13405
|
+
sourceDir,
|
|
13406
|
+
registryDir,
|
|
13407
|
+
policy: policyPath,
|
|
13408
|
+
out,
|
|
13409
|
+
sourceSetDigest,
|
|
13410
|
+
sourceCount: sources.length,
|
|
13411
|
+
releasePlan,
|
|
13412
|
+
written: true
|
|
13413
|
+
});
|
|
13414
|
+
}
|
|
13415
|
+
function runLlmixTrustManifest(args, command = "release finalize") {
|
|
13416
|
+
const parsed = parseOptions(args);
|
|
13417
|
+
const err = unknownOptions(parsed, [
|
|
13418
|
+
"--target",
|
|
13419
|
+
"--registry-dir",
|
|
13420
|
+
"--registry-root",
|
|
13421
|
+
"--release-plan",
|
|
13422
|
+
"--policy",
|
|
13423
|
+
"--expected-root-digest",
|
|
13424
|
+
"--derive-root-digest",
|
|
13425
|
+
"--minimum-revision",
|
|
13426
|
+
"--minimum-published-at",
|
|
13427
|
+
"--high-watermark",
|
|
13428
|
+
"--out",
|
|
13429
|
+
"--did-document",
|
|
13430
|
+
"--offline-sigstore-fixture"
|
|
13431
|
+
]);
|
|
13432
|
+
if (err) return usage(command, err);
|
|
13433
|
+
const targetErr = releaseTargetError(command, parsed);
|
|
13434
|
+
if (targetErr) return targetErr;
|
|
13435
|
+
if (parsed.positional.length !== 0) return usage(command, `${command} takes no positional arguments`);
|
|
13436
|
+
const registryDir = oneOption(parsed.options, "--registry-dir");
|
|
13437
|
+
const registryRootPath = oneOption(parsed.options, "--registry-root");
|
|
13438
|
+
const releasePlanPath = oneOption(parsed.options, "--release-plan");
|
|
13439
|
+
const policyPath = oneOption(parsed.options, "--policy");
|
|
13440
|
+
const out = oneOption(parsed.options, "--out");
|
|
13441
|
+
const expectedRootDigestOption = oneOption(parsed.options, "--expected-root-digest");
|
|
13442
|
+
const deriveRootDigest = parsed.flags.has("--derive-root-digest");
|
|
13443
|
+
if (!registryDir) return usage(command, "--registry-dir <dir> is required");
|
|
13444
|
+
if (!registryRootPath) return usage(command, "--registry-root <file> is required");
|
|
13445
|
+
if (!releasePlanPath) return usage(command, "--release-plan <file> is required");
|
|
13446
|
+
if (!policyPath) return usage(command, "--policy <path> is required");
|
|
13447
|
+
if (!out) return usage(command, "--out <file> is required");
|
|
13448
|
+
if (Boolean(expectedRootDigestOption) === deriveRootDigest)
|
|
13449
|
+
return usage(command, "Choose exactly one: --expected-root-digest <digest> or --derive-root-digest");
|
|
13450
|
+
if (expectedRootDigestOption && !DIGEST_PATTERN.test(expectedRootDigestOption))
|
|
13451
|
+
return usage(command, "--expected-root-digest must be a sha256/sha384/sha512 digest");
|
|
13452
|
+
if (existsSync4(out))
|
|
13453
|
+
return ioError(command, `Refusing to overwrite existing file: ${out}`, {
|
|
13454
|
+
registryDir,
|
|
13455
|
+
registryRoot: registryRootPath,
|
|
13456
|
+
out
|
|
13457
|
+
});
|
|
13458
|
+
if (pathResolvesInsideDir(out, registryDir)) {
|
|
13459
|
+
return commandResult(
|
|
13460
|
+
false,
|
|
13461
|
+
command,
|
|
13462
|
+
EXIT.failure,
|
|
13463
|
+
[diag("release.trust_artifact_inside_registry", "--out must resolve outside --registry-dir")],
|
|
13464
|
+
{
|
|
13465
|
+
summary: "Release trust artifact output must be outside the registry directory",
|
|
13466
|
+
nextActions: [
|
|
13467
|
+
nextAction(
|
|
13468
|
+
"write-external-manifest",
|
|
13469
|
+
"Write the deployment trust manifest outside config/llm or the selected registry directory",
|
|
13470
|
+
`mda release finalize --target llmix-registry --registry-dir ${registryDir} --registry-root ${registryRootPath} --release-plan ${releasePlanPath} --policy ${policyPath} --derive-root-digest --out release/llmix-trust.json`
|
|
13471
|
+
)
|
|
13472
|
+
],
|
|
13473
|
+
registryDir,
|
|
13474
|
+
out,
|
|
13475
|
+
written: false
|
|
13476
|
+
}
|
|
13477
|
+
);
|
|
13478
|
+
}
|
|
13479
|
+
if (!pathResolvesInsideDir(registryRootPath, registryDir)) {
|
|
13480
|
+
return commandResult(
|
|
13481
|
+
false,
|
|
13482
|
+
command,
|
|
13483
|
+
EXIT.failure,
|
|
13484
|
+
[diag("release.registry_root_outside_registry", "--registry-root must resolve inside --registry-dir")],
|
|
13485
|
+
{
|
|
13486
|
+
summary: "Registry-root evidence must belong to the selected registry directory",
|
|
13487
|
+
nextActions: [
|
|
13488
|
+
nextAction(
|
|
13489
|
+
"use-registry-root-from-registry",
|
|
13490
|
+
"Pass the signed registry-root evidence from the selected registry directory",
|
|
13491
|
+
`mda release finalize --target llmix-registry --registry-dir ${registryDir} --registry-root ${registryDir}/snapshots/current/registry-root.json --release-plan ${releasePlanPath} --policy ${policyPath} --derive-root-digest --out ${out}`
|
|
13492
|
+
)
|
|
13493
|
+
],
|
|
13494
|
+
registryDir,
|
|
13495
|
+
registryRoot: registryRootPath,
|
|
13496
|
+
out,
|
|
13497
|
+
written: false
|
|
13498
|
+
}
|
|
13499
|
+
);
|
|
13500
|
+
}
|
|
13501
|
+
const policy = readJson(policyPath);
|
|
13502
|
+
if (!policy.ok) return ioError(command, policy.message, { registryDir, registryRoot: registryRootPath, policy: policyPath });
|
|
13503
|
+
const policyValidation = validateJsonAgainst(policy.value, "trustPolicy");
|
|
13504
|
+
if (!policyValidation.ok)
|
|
13505
|
+
return commandResult(false, command, EXIT.failure, policyValidation.diagnostics, {
|
|
13506
|
+
registryDir,
|
|
13507
|
+
registryRoot: registryRootPath,
|
|
13508
|
+
policy: policyPath,
|
|
13509
|
+
written: false
|
|
13510
|
+
});
|
|
13511
|
+
const releasePlan = readJson(releasePlanPath);
|
|
13512
|
+
if (!releasePlan.ok) return ioError(command, releasePlan.message, { releasePlan: releasePlanPath, written: false });
|
|
13513
|
+
const registryRoot = readJson(registryRootPath);
|
|
13514
|
+
if (!registryRoot.ok) return ioError(command, registryRoot.message, { registryRoot: registryRootPath, written: false });
|
|
13515
|
+
const diagnostics = [];
|
|
13516
|
+
const rootEvidence = validateRegistryRootEvidence(registryRoot.value);
|
|
13517
|
+
diagnostics.push(...rootEvidence.diagnostics);
|
|
13518
|
+
const releasePlanEvidence = validateReleasePlanEvidence(releasePlan.value);
|
|
13519
|
+
diagnostics.push(...releasePlanEvidence.diagnostics);
|
|
13520
|
+
if (rootEvidence.ok && releasePlanEvidence.ok) {
|
|
13521
|
+
const expectedRootDigest = expectedRootDigestOption ?? rootEvidence.rootDigest;
|
|
13522
|
+
if (expectedRootDigest !== rootEvidence.rootDigest) {
|
|
13523
|
+
diagnostics.push(diag("llmix.root_digest_mismatch", "Expected root digest does not match signed registry-root evidence"));
|
|
13524
|
+
}
|
|
13525
|
+
diagnostics.push(
|
|
13526
|
+
...freshnessDiagnostics(rootEvidence.root, {
|
|
13527
|
+
minimumRevision: oneOption(parsed.options, "--minimum-revision"),
|
|
13528
|
+
minimumPublishedAt: oneOption(parsed.options, "--minimum-published-at"),
|
|
13529
|
+
highWatermark: oneOption(parsed.options, "--high-watermark")
|
|
13530
|
+
})
|
|
13531
|
+
);
|
|
13532
|
+
diagnostics.push(...sourceSetDiagnostics(rootEvidence.root, releasePlanEvidence.releasePlan));
|
|
13533
|
+
const signatureVerification = verifySignatureEntries(
|
|
13534
|
+
rootEvidence.root.signatures,
|
|
13535
|
+
rootEvidence.root.integrity,
|
|
13536
|
+
policy.value,
|
|
13537
|
+
oneOption(parsed.options, "--did-document"),
|
|
13538
|
+
oneOption(parsed.options, "--offline-sigstore-fixture")
|
|
13539
|
+
);
|
|
13540
|
+
if (signatureVerification.malformed) {
|
|
13541
|
+
diagnostics.push(diag("signature.invalid_entry", "Registry-root signature entry is malformed"));
|
|
13542
|
+
} else if (signatureVerification.trusted.size === 0) {
|
|
13543
|
+
diagnostics.push(
|
|
13544
|
+
...signatureVerification.rejectedTrusted.length > 0 ? signatureVerification.rejectedTrusted : [diag("trust_policy.no_trusted_signature", "No registry-root signature matched the trust policy")]
|
|
13545
|
+
);
|
|
13546
|
+
} else {
|
|
13547
|
+
const minSignatures = trustPolicyMinSignatures(policy.value);
|
|
13548
|
+
if (signatureVerification.trusted.size < minSignatures) {
|
|
13549
|
+
diagnostics.push(
|
|
13550
|
+
diag(
|
|
13551
|
+
"trust_policy.insufficient_trusted_signatures",
|
|
13552
|
+
`${signatureVerification.trusted.size} trusted signer identities < ${minSignatures}`
|
|
13553
|
+
)
|
|
13554
|
+
);
|
|
13555
|
+
}
|
|
13556
|
+
}
|
|
13557
|
+
if (diagnostics.length === 0) {
|
|
13558
|
+
const trustedSignerIdentity = firstTrustedSignerIdentity(signatureVerification.trustedSignerIdentities);
|
|
13559
|
+
const manifest = {
|
|
13560
|
+
version: 1,
|
|
13561
|
+
kind: "llmix-trust-manifest",
|
|
13562
|
+
expectedRootDigest,
|
|
13563
|
+
sourceSetDigest: rootEvidence.root.sourceSetDigest,
|
|
13564
|
+
releasePlanDigest: computeDigest(Buffer.from(jcs(releasePlan.value), "utf8"), "sha256"),
|
|
13565
|
+
registryRootTrustPolicy: policy.value,
|
|
13566
|
+
rekorPolicy: isRecord(policy.value) && isRecord(policy.value.rekor) ? policy.value.rekor : null,
|
|
13567
|
+
minimumRevision: oneOption(parsed.options, "--minimum-revision") ?? null,
|
|
13568
|
+
minimumPublishedAt: oneOption(parsed.options, "--minimum-published-at") ?? null,
|
|
13569
|
+
highWatermark: oneOption(parsed.options, "--high-watermark") ?? rootEvidence.root.highWatermark,
|
|
13570
|
+
registryRootSignerIdentity: trustedSignerIdentity,
|
|
13571
|
+
registryRoot: {
|
|
13572
|
+
path: registryRootPath,
|
|
13573
|
+
revision: rootEvidence.root.revision,
|
|
13574
|
+
publishedAt: rootEvidence.root.publishedAt,
|
|
13575
|
+
highWatermark: rootEvidence.root.highWatermark
|
|
13576
|
+
},
|
|
13577
|
+
releasePlan: { path: releasePlanPath, sourceCount: releasePlanEvidence.releasePlan.sources.length }
|
|
13578
|
+
};
|
|
13579
|
+
try {
|
|
13580
|
+
atomicWrite(out, `${JSON.stringify(manifest, null, 2)}
|
|
13581
|
+
`);
|
|
13582
|
+
} catch (error) {
|
|
13583
|
+
return ioError(command, error instanceof Error ? error.message : String(error), { out, written: false });
|
|
13584
|
+
}
|
|
13585
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
13586
|
+
summary: "Finalized external LLMix deployment trust manifest",
|
|
13587
|
+
artifacts: [artifact("llmix-trust-manifest", out, void 0, expectedRootDigest)],
|
|
13588
|
+
nextActions: [
|
|
13589
|
+
externalNextAction(
|
|
13590
|
+
"install-external-trust-manifest",
|
|
13591
|
+
"Install this external trust manifest in the deployment configuration outside config/llm",
|
|
13592
|
+
`copy ${out} into the deployment config path consumed by secure LLMix startup`,
|
|
13593
|
+
false
|
|
13594
|
+
),
|
|
13595
|
+
externalNextAction(
|
|
13596
|
+
"deploy-signed-registry",
|
|
13597
|
+
"Deploy only the signed registry files covered by this manifest and release plan",
|
|
13598
|
+
`publish ${registryDir} with registry root ${registryRootPath} and release plan ${releasePlanPath}`,
|
|
13599
|
+
false
|
|
13600
|
+
)
|
|
13601
|
+
],
|
|
13602
|
+
message: `wrote ${out}`,
|
|
13603
|
+
registryDir,
|
|
13604
|
+
registryRoot: registryRootPath,
|
|
13605
|
+
releasePlan: releasePlanPath,
|
|
13606
|
+
out,
|
|
13607
|
+
expectedRootDigest,
|
|
13608
|
+
sourceSetDigest: rootEvidence.root.sourceSetDigest,
|
|
13609
|
+
written: true
|
|
13610
|
+
});
|
|
13611
|
+
}
|
|
13612
|
+
}
|
|
13613
|
+
return commandResult(false, command, EXIT.failure, diagnostics, {
|
|
13614
|
+
summary: "Release finalize blocked",
|
|
13615
|
+
nextActions: [
|
|
13616
|
+
nextAction(
|
|
13617
|
+
"fix-registry-root",
|
|
13618
|
+
"Fix registry-root evidence, signature, freshness, or source-set mismatches before writing deployment anchors",
|
|
13619
|
+
`mda release finalize --target llmix-registry --registry-dir ${registryDir} --registry-root ${registryRootPath} --release-plan ${releasePlanPath} --policy ${policyPath} --derive-root-digest --out ${out}`
|
|
13620
|
+
)
|
|
13621
|
+
],
|
|
13622
|
+
registryDir,
|
|
13623
|
+
registryRoot: registryRootPath,
|
|
13624
|
+
releasePlan: releasePlanPath,
|
|
13625
|
+
out,
|
|
13626
|
+
written: false
|
|
13627
|
+
});
|
|
13628
|
+
}
|
|
13629
|
+
function runLlmixTrustSnippets(args, command = "release finalize") {
|
|
13630
|
+
const parsed = parseOptions(args);
|
|
13631
|
+
const err = unknownOptions(parsed, ["--target", "--registry-dir", "--manifest", "--snippet-format", "--snippet-out"]);
|
|
13632
|
+
if (err) return usage(command, err);
|
|
13633
|
+
const targetErr = releaseTargetError(command, parsed);
|
|
13634
|
+
if (targetErr) return targetErr;
|
|
13635
|
+
if (parsed.positional.length !== 0) return usage(command, `${command} takes no positional arguments`);
|
|
13636
|
+
const registryDir = oneOption(parsed.options, "--registry-dir");
|
|
13637
|
+
const manifestPath = oneOption(parsed.options, "--manifest");
|
|
13638
|
+
const format = oneOption(parsed.options, "--snippet-format");
|
|
13639
|
+
const out = oneOption(parsed.options, "--snippet-out");
|
|
13640
|
+
if (!registryDir) {
|
|
13641
|
+
return commandResult(
|
|
13642
|
+
false,
|
|
13643
|
+
command,
|
|
13644
|
+
EXIT.failure,
|
|
13645
|
+
[diag("release.registry_dir_required", "--registry-dir <dir> is required for snippet output")],
|
|
13646
|
+
{
|
|
13647
|
+
summary: "Release snippet generation blocked",
|
|
13648
|
+
nextActions: [
|
|
13649
|
+
nextAction(
|
|
13650
|
+
"add-registry-dir",
|
|
13651
|
+
"Pass the registry directory so MDA can prove trust artifacts remain outside it",
|
|
13652
|
+
`mda release finalize --target llmix-registry --registry-dir <registry-dir> --manifest ${manifestPath ?? "<manifest>"} --snippet-format ${format ?? "env"} --snippet-out ${out ?? "release/trust.env"}`
|
|
13653
|
+
)
|
|
13654
|
+
],
|
|
13655
|
+
manifest: manifestPath,
|
|
13656
|
+
format,
|
|
13657
|
+
out,
|
|
13658
|
+
written: false
|
|
13659
|
+
}
|
|
13660
|
+
);
|
|
13661
|
+
}
|
|
13662
|
+
if (!manifestPath) return usage(command, "--manifest <path> is required");
|
|
13663
|
+
if (!format) return usage(command, "--snippet-format <format> is required");
|
|
13664
|
+
if (!LLMIX_SNIPPET_FORMATS.has(format))
|
|
13665
|
+
return usage(command, "--snippet-format must be json, env, kubernetes, github-actions, terraform, typescript, python, or rust");
|
|
13666
|
+
if (!out) return usage(command, "--snippet-out <file> is required");
|
|
13667
|
+
if (existsSync4(out)) {
|
|
13668
|
+
return ioError(command, `Refusing to overwrite existing file: ${out}`, {
|
|
13669
|
+
manifest: manifestPath,
|
|
13670
|
+
format,
|
|
13671
|
+
out,
|
|
13672
|
+
written: false
|
|
13673
|
+
});
|
|
13674
|
+
}
|
|
13675
|
+
if (pathResolvesInsideDir(manifestPath, registryDir) || pathResolvesInsideDir(out, registryDir)) {
|
|
13676
|
+
return commandResult(
|
|
13677
|
+
false,
|
|
13678
|
+
command,
|
|
13679
|
+
EXIT.failure,
|
|
13680
|
+
[diag("release.trust_artifact_inside_registry", "--manifest and --snippet-out must resolve outside --registry-dir")],
|
|
13681
|
+
{
|
|
13682
|
+
summary: "Release trust artifacts must be outside the registry directory",
|
|
13683
|
+
nextActions: [
|
|
13684
|
+
nextAction(
|
|
13685
|
+
"write-external-snippet",
|
|
13686
|
+
"Write deployment snippets outside config/llm or the selected registry directory",
|
|
13687
|
+
`mda release finalize --target llmix-registry --registry-dir ${registryDir} --manifest release/llmix-trust.json --snippet-format ${format} --snippet-out release/trust.${format}`
|
|
13688
|
+
)
|
|
13689
|
+
],
|
|
13690
|
+
registryDir,
|
|
13691
|
+
manifest: manifestPath,
|
|
13692
|
+
format,
|
|
13693
|
+
out,
|
|
13694
|
+
written: false
|
|
13695
|
+
}
|
|
13696
|
+
);
|
|
13697
|
+
}
|
|
13698
|
+
const manifestRead = readJson(manifestPath);
|
|
13699
|
+
if (!manifestRead.ok) return ioError(command, manifestRead.message, { manifest: manifestPath, out, written: false });
|
|
13700
|
+
const manifest = validateTrustManifestEvidence(manifestRead.value);
|
|
13701
|
+
if (!manifest.ok) {
|
|
13702
|
+
return commandResult(false, command, EXIT.failure, manifest.diagnostics, {
|
|
13703
|
+
summary: "Release snippet generation blocked",
|
|
13704
|
+
nextActions: [
|
|
13705
|
+
nextAction(
|
|
13706
|
+
"regenerate-trust-manifest",
|
|
13707
|
+
"Regenerate a valid external trust manifest before producing deployment snippets",
|
|
13708
|
+
"mda release finalize --target llmix-registry --registry-dir <registry> --registry-root <registry-root.json> --release-plan <release-plan.json> --policy <policy.json> --derive-root-digest --out release/llmix-trust.json"
|
|
13709
|
+
)
|
|
13710
|
+
],
|
|
13711
|
+
manifest: manifestPath,
|
|
13712
|
+
format,
|
|
13713
|
+
out,
|
|
13714
|
+
written: false
|
|
13715
|
+
});
|
|
13716
|
+
}
|
|
13717
|
+
const content = renderLlmixTrustSnippet(format, manifestPath, manifest.manifest);
|
|
13718
|
+
try {
|
|
13719
|
+
atomicWrite(out, content);
|
|
13720
|
+
} catch (error) {
|
|
13721
|
+
return ioError(command, error instanceof Error ? error.message : String(error), {
|
|
13722
|
+
manifest: manifestPath,
|
|
13723
|
+
format,
|
|
13724
|
+
out,
|
|
13725
|
+
written: false
|
|
13726
|
+
});
|
|
13727
|
+
}
|
|
13728
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
13729
|
+
summary: `Wrote ${format} LLMix deployment trust snippet`,
|
|
13730
|
+
artifacts: [artifact("llmix-trust-snippet", out)],
|
|
13731
|
+
nextActions: [
|
|
13732
|
+
externalNextAction(
|
|
13733
|
+
"install-deployment-snippet",
|
|
13734
|
+
"Install this snippet in deployment configuration outside config/llm",
|
|
13735
|
+
`wire ${out} into the deployment environment that starts secure LLMix`,
|
|
13736
|
+
false
|
|
13737
|
+
),
|
|
13738
|
+
nextAction(
|
|
13739
|
+
"run-release-doctor",
|
|
13740
|
+
"Check the source, registry, and manifest before deployment",
|
|
13741
|
+
`mda doctor release --target llmix-registry --source <source-dir> --registry-dir <registry-dir> --release-plan ${manifest.manifest.releasePlan.path} --manifest ${manifestPath}`,
|
|
13742
|
+
false
|
|
13743
|
+
)
|
|
13744
|
+
],
|
|
13745
|
+
message: `wrote ${out}`,
|
|
13746
|
+
manifest: manifestPath,
|
|
13747
|
+
format,
|
|
13748
|
+
out,
|
|
13749
|
+
written: true
|
|
13750
|
+
});
|
|
13751
|
+
}
|
|
13752
|
+
function runDoctor(args) {
|
|
13753
|
+
if (args[0] === "llmix") return migratedCommand("doctor llmix", "mda doctor release --target llmix-registry");
|
|
13754
|
+
if (args[0] !== "release") return usage("doctor", "Expected subcommand: doctor release --target llmix-registry");
|
|
13755
|
+
return runDoctorRelease(args.slice(1));
|
|
13756
|
+
}
|
|
13757
|
+
function runDoctorRelease(args) {
|
|
13758
|
+
const command = "doctor release";
|
|
13759
|
+
const parsed = parseOptions(args);
|
|
13760
|
+
const err = unknownOptions(parsed, [
|
|
13761
|
+
"--target",
|
|
13762
|
+
"--source",
|
|
13763
|
+
"--registry-dir",
|
|
13764
|
+
"--release-plan",
|
|
13765
|
+
"--manifest",
|
|
13766
|
+
"--did-document",
|
|
13767
|
+
"--offline-sigstore-fixture"
|
|
13768
|
+
]);
|
|
13769
|
+
if (err) return usage(command, err);
|
|
13770
|
+
const targetErr = releaseTargetError(command, parsed);
|
|
13771
|
+
if (targetErr) return targetErr;
|
|
13772
|
+
if (parsed.positional.length !== 0) return usage(command, `${command} takes no positional arguments`);
|
|
13773
|
+
const sourceDir = oneOption(parsed.options, "--source");
|
|
13774
|
+
const registryDir = oneOption(parsed.options, "--registry-dir");
|
|
13775
|
+
const releasePlanPath = oneOption(parsed.options, "--release-plan");
|
|
13776
|
+
const manifestPath = oneOption(parsed.options, "--manifest");
|
|
13777
|
+
const didDocumentPath = oneOption(parsed.options, "--did-document");
|
|
13778
|
+
const sigstoreFixturePath = oneOption(parsed.options, "--offline-sigstore-fixture");
|
|
13779
|
+
if (!sourceDir) return usage(command, "--source <dir> is required");
|
|
13780
|
+
if (!registryDir) return usage(command, "--registry-dir <dir> is required");
|
|
13781
|
+
if (!releasePlanPath) return usage(command, "--release-plan <path> is required");
|
|
13782
|
+
if (!manifestPath) return usage(command, "--manifest <path> is required");
|
|
13783
|
+
if (!existsSync4(releasePlanPath)) {
|
|
13784
|
+
return commandResult(false, command, EXIT.failure, [diag("llmix.release_plan_missing", "Release plan is missing")], {
|
|
13785
|
+
summary: "Release doctor found readiness issues",
|
|
13786
|
+
nextActions: [
|
|
13787
|
+
nextAction(
|
|
13788
|
+
"create-release-plan",
|
|
13789
|
+
"Prepare the verified release plan before final deployment checks",
|
|
13790
|
+
`mda release prepare --target llmix-registry --source ${sourceDir} --registry-dir ${registryDir} --policy <policy.json> --out ${releasePlanPath}`
|
|
13791
|
+
)
|
|
13792
|
+
],
|
|
13793
|
+
sourceDir,
|
|
13794
|
+
registryDir,
|
|
13795
|
+
releasePlan: releasePlanPath,
|
|
13796
|
+
manifest: manifestPath,
|
|
13797
|
+
readOnly: true,
|
|
13798
|
+
written: false
|
|
13799
|
+
});
|
|
13800
|
+
}
|
|
13801
|
+
if (!existsSync4(manifestPath)) {
|
|
13802
|
+
return commandResult(false, command, EXIT.failure, [diag("llmix.manifest_missing", "Trust manifest is missing")], {
|
|
13803
|
+
summary: "Release doctor found readiness issues",
|
|
13804
|
+
nextActions: [
|
|
13805
|
+
nextAction(
|
|
13806
|
+
"create-trust-manifest",
|
|
13807
|
+
"Generate the external deployment trust manifest before release",
|
|
13808
|
+
`mda release finalize --target llmix-registry --registry-dir ${registryDir} --registry-root <registry-root.json> --release-plan ${releasePlanPath} --policy <policy.json> --derive-root-digest --out ${manifestPath}`
|
|
13809
|
+
)
|
|
13810
|
+
],
|
|
13811
|
+
sourceDir,
|
|
13812
|
+
registryDir,
|
|
13813
|
+
releasePlan: releasePlanPath,
|
|
13814
|
+
manifest: manifestPath,
|
|
13815
|
+
readOnly: true,
|
|
13816
|
+
written: false
|
|
13817
|
+
});
|
|
13818
|
+
}
|
|
13819
|
+
const manifestRead = readJson(manifestPath);
|
|
13820
|
+
if (!manifestRead.ok)
|
|
13821
|
+
return ioError(command, manifestRead.message, {
|
|
13822
|
+
sourceDir,
|
|
13823
|
+
registryDir,
|
|
13824
|
+
releasePlan: releasePlanPath,
|
|
13825
|
+
manifest: manifestPath,
|
|
13826
|
+
readOnly: true,
|
|
13827
|
+
written: false
|
|
13828
|
+
});
|
|
13829
|
+
const diagnostics = [];
|
|
13830
|
+
const checks = [];
|
|
13831
|
+
if (pathResolvesInsideDir(manifestPath, registryDir)) {
|
|
13832
|
+
diagnostics.push(diag("release.trust_artifact_inside_registry", "--manifest must resolve outside --registry-dir"));
|
|
13833
|
+
checks.push({ id: "manifest-placement", ok: false });
|
|
13834
|
+
} else {
|
|
13835
|
+
checks.push({ id: "manifest-placement", ok: true });
|
|
13836
|
+
}
|
|
13837
|
+
const requestedReleasePlanRead = readJson(releasePlanPath);
|
|
13838
|
+
if (!requestedReleasePlanRead.ok) diagnostics.push(diag("filesystem.io", requestedReleasePlanRead.message, { path: releasePlanPath }));
|
|
13839
|
+
const requestedReleasePlanEvidence = requestedReleasePlanRead.ok ? validateReleasePlanEvidence(requestedReleasePlanRead.value) : null;
|
|
13840
|
+
if (requestedReleasePlanEvidence) diagnostics.push(...requestedReleasePlanEvidence.diagnostics);
|
|
13841
|
+
checks.push({ id: "release-plan-input", ok: Boolean(requestedReleasePlanEvidence?.ok) });
|
|
13842
|
+
const manifest = validateTrustManifestEvidence(manifestRead.value);
|
|
13843
|
+
diagnostics.push(...manifest.diagnostics);
|
|
13844
|
+
checks.push({ id: "trust-manifest-shape", ok: manifest.ok });
|
|
13845
|
+
if (manifest.ok) {
|
|
13846
|
+
const registryRootInsideRegistry = pathResolvesInsideDir(manifest.manifest.registryRoot.path, registryDir);
|
|
13847
|
+
if (!registryRootInsideRegistry) {
|
|
13848
|
+
diagnostics.push(
|
|
13849
|
+
diag("release.registry_root_outside_registry", "Trust manifest registryRoot.path must resolve inside --registry-dir")
|
|
13850
|
+
);
|
|
13851
|
+
}
|
|
13852
|
+
checks.push({ id: "registry-root-placement", ok: registryRootInsideRegistry });
|
|
13853
|
+
const registryRootRead = registryRootInsideRegistry ? readJson(manifest.manifest.registryRoot.path) : null;
|
|
13854
|
+
if (registryRootRead && !registryRootRead.ok)
|
|
13855
|
+
diagnostics.push(diag("filesystem.io", registryRootRead.message, { path: manifest.manifest.registryRoot.path }));
|
|
13856
|
+
if (manifest.manifest.releasePlan.path !== releasePlanPath)
|
|
13857
|
+
diagnostics.push(diag("llmix.release_plan_digest_mismatch", "Trust manifest releasePlan.path does not match --release-plan"));
|
|
13858
|
+
const releasePlanRead = readJson(releasePlanPath);
|
|
13859
|
+
if (!releasePlanRead.ok) diagnostics.push(diag("filesystem.io", releasePlanRead.message, { path: manifest.manifest.releasePlan.path }));
|
|
13860
|
+
const rootEvidence = registryRootRead?.ok ? validateRegistryRootEvidence(registryRootRead.value) : null;
|
|
13861
|
+
const releasePlanEvidence = releasePlanRead.ok ? validateReleasePlanEvidence(releasePlanRead.value) : null;
|
|
13862
|
+
if (rootEvidence) diagnostics.push(...rootEvidence.diagnostics);
|
|
13863
|
+
if (releasePlanEvidence) diagnostics.push(...releasePlanEvidence.diagnostics);
|
|
13864
|
+
checks.push({ id: "registry-root-evidence", ok: Boolean(rootEvidence?.ok) });
|
|
13865
|
+
checks.push({ id: "release-plan-evidence", ok: Boolean(releasePlanEvidence?.ok) });
|
|
13866
|
+
if (rootEvidence?.ok && releasePlanEvidence?.ok) {
|
|
13867
|
+
if (manifest.manifest.expectedRootDigest !== rootEvidence.rootDigest)
|
|
13868
|
+
diagnostics.push(diag("llmix.root_digest_mismatch", "Trust manifest expectedRootDigest does not match registry-root evidence"));
|
|
13869
|
+
if (manifest.manifest.sourceSetDigest !== rootEvidence.root.sourceSetDigest)
|
|
13870
|
+
diagnostics.push(diag("llmix.source_set_digest_mismatch", "Trust manifest sourceSetDigest does not match registry-root evidence"));
|
|
13871
|
+
if (manifest.manifest.releasePlanDigest !== computeDigest(Buffer.from(jcs(releasePlanRead.value), "utf8"), "sha256"))
|
|
13872
|
+
diagnostics.push(diag("llmix.release_plan_digest_mismatch", "Trust manifest releasePlanDigest does not match release plan"));
|
|
13873
|
+
diagnostics.push(...sourceSetDiagnostics(rootEvidence.root, releasePlanEvidence.releasePlan));
|
|
13874
|
+
diagnostics.push(
|
|
13875
|
+
...freshnessDiagnostics(rootEvidence.root, {
|
|
13876
|
+
minimumRevision: manifest.manifest.minimumRevision,
|
|
13877
|
+
minimumPublishedAt: manifest.manifest.minimumPublishedAt,
|
|
13878
|
+
highWatermark: manifest.manifest.highWatermark
|
|
13879
|
+
})
|
|
13880
|
+
);
|
|
13881
|
+
const signatureVerification = verifySignatureEntries(
|
|
13882
|
+
rootEvidence.root.signatures,
|
|
13883
|
+
rootEvidence.root.integrity,
|
|
13884
|
+
manifest.manifest.registryRootTrustPolicy,
|
|
13885
|
+
didDocumentPath,
|
|
13886
|
+
sigstoreFixturePath
|
|
13887
|
+
);
|
|
13888
|
+
if (signatureVerification.malformed) {
|
|
13889
|
+
diagnostics.push(diag("signature.invalid_entry", "Registry-root signature entry is malformed"));
|
|
13890
|
+
} else if (signatureVerification.trusted.size < trustPolicyMinSignatures(manifest.manifest.registryRootTrustPolicy)) {
|
|
13891
|
+
diagnostics.push(
|
|
13892
|
+
...signatureVerification.rejectedTrusted.length > 0 ? signatureVerification.rejectedTrusted : [diag("trust_policy.no_trusted_signature", "Registry-root signatures do not match the embedded trust policy")]
|
|
13893
|
+
);
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13896
|
+
checks.push({
|
|
13897
|
+
id: "registry-root-trust",
|
|
13898
|
+
ok: diagnostics.length === 0 && Boolean(rootEvidence?.ok && releasePlanEvidence?.ok)
|
|
13899
|
+
});
|
|
13900
|
+
}
|
|
13901
|
+
const sourceReadiness = doctorSourceReadiness(sourceDir);
|
|
13902
|
+
diagnostics.push(...sourceReadiness.diagnostics);
|
|
13903
|
+
checks.push({ id: "source-readiness", ok: sourceReadiness.ok });
|
|
13904
|
+
if (diagnostics.length > 0) {
|
|
13905
|
+
return commandResult(false, command, EXIT.failure, diagnostics, {
|
|
13906
|
+
summary: "Release doctor found readiness issues",
|
|
13907
|
+
nextActions: [
|
|
13908
|
+
nextAction(
|
|
13909
|
+
"fix-release-state",
|
|
13910
|
+
"Fix the reported source, registry, manifest, freshness, or placement issue and run doctor again",
|
|
13911
|
+
`mda doctor release --target llmix-registry --source ${sourceDir} --registry-dir ${registryDir} --release-plan ${releasePlanPath} --manifest ${manifestPath}`
|
|
13912
|
+
)
|
|
13913
|
+
],
|
|
13914
|
+
sourceDir,
|
|
13915
|
+
registryDir,
|
|
13916
|
+
releasePlan: releasePlanPath,
|
|
13917
|
+
manifest: manifestPath,
|
|
13918
|
+
checks,
|
|
13919
|
+
readOnly: true,
|
|
13920
|
+
written: false
|
|
13921
|
+
});
|
|
13922
|
+
}
|
|
13923
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
13924
|
+
summary: "Release state is ready for secure LLMix deployment",
|
|
13925
|
+
nextActions: [
|
|
13926
|
+
externalNextAction(
|
|
13927
|
+
"deploy-secure-llmix",
|
|
13928
|
+
"Deploy the signed registry with the external trust manifest and generated deployment snippet",
|
|
13929
|
+
"use the deployment system that mounts the manifest outside config/llm",
|
|
13930
|
+
false
|
|
13931
|
+
)
|
|
13932
|
+
],
|
|
13933
|
+
sourceDir,
|
|
13934
|
+
registryDir,
|
|
13935
|
+
releasePlan: releasePlanPath,
|
|
13936
|
+
manifest: manifestPath,
|
|
13937
|
+
checks,
|
|
13938
|
+
sourceCount: sourceReadiness.sourceCount,
|
|
13939
|
+
readOnly: true,
|
|
13940
|
+
written: false
|
|
13941
|
+
});
|
|
13942
|
+
}
|
|
13943
|
+
function validateTrustManifestEvidence(value) {
|
|
13944
|
+
const diagnostics = [];
|
|
13945
|
+
if (!isRecord(value)) {
|
|
13946
|
+
return { ok: false, diagnostics: [diag("llmix.trust_manifest_invalid", "Trust manifest must be a JSON object")] };
|
|
13947
|
+
}
|
|
13948
|
+
if (value.kind !== "llmix-trust-manifest")
|
|
13949
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest kind must be llmix-trust-manifest"));
|
|
13950
|
+
if (value.version !== 1) diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest version must be 1"));
|
|
13951
|
+
if (typeof value.expectedRootDigest !== "string" || !DIGEST_PATTERN.test(value.expectedRootDigest))
|
|
13952
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest expectedRootDigest must be a digest"));
|
|
13953
|
+
if (typeof value.sourceSetDigest !== "string" || !DIGEST_PATTERN.test(value.sourceSetDigest))
|
|
13954
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest sourceSetDigest must be a digest"));
|
|
13955
|
+
if (typeof value.releasePlanDigest !== "string" || !DIGEST_PATTERN.test(value.releasePlanDigest))
|
|
13956
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest releasePlanDigest must be a digest"));
|
|
13957
|
+
if (!isNullableString(value.minimumRevision))
|
|
13958
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest minimumRevision must be a string or null"));
|
|
13959
|
+
if (!isNullableString(value.minimumPublishedAt))
|
|
13960
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest minimumPublishedAt must be a string or null"));
|
|
13961
|
+
if (!isNullableString(value.highWatermark))
|
|
13962
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest highWatermark must be a string or null"));
|
|
13963
|
+
if (!isRecord(value.registryRoot)) {
|
|
13964
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest registryRoot must be an object"));
|
|
13965
|
+
} else {
|
|
13966
|
+
if (typeof value.registryRoot.path !== "string" || value.registryRoot.path.length === 0)
|
|
13967
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest registryRoot.path must be a non-empty string"));
|
|
13968
|
+
if (typeof value.registryRoot.revision !== "string" || value.registryRoot.revision.length === 0)
|
|
13969
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest registryRoot.revision must be a non-empty string"));
|
|
13970
|
+
if (typeof value.registryRoot.publishedAt !== "string" || Number.isNaN(Date.parse(value.registryRoot.publishedAt)))
|
|
13971
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest registryRoot.publishedAt must be an ISO timestamp"));
|
|
13972
|
+
if (typeof value.registryRoot.highWatermark !== "string" || value.registryRoot.highWatermark.length === 0)
|
|
13973
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest registryRoot.highWatermark must be a non-empty string"));
|
|
13974
|
+
}
|
|
13975
|
+
if (!isRecord(value.releasePlan)) {
|
|
13976
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest releasePlan must be an object"));
|
|
13977
|
+
} else {
|
|
13978
|
+
if (typeof value.releasePlan.path !== "string" || value.releasePlan.path.length === 0)
|
|
13979
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest releasePlan.path must be a non-empty string"));
|
|
13980
|
+
const sourceCount = value.releasePlan.sourceCount;
|
|
13981
|
+
if (typeof sourceCount !== "number" || !Number.isInteger(sourceCount) || sourceCount < 0)
|
|
13982
|
+
diagnostics.push(diag("llmix.trust_manifest_invalid", "Trust manifest releasePlan.sourceCount must be a non-negative integer"));
|
|
13983
|
+
}
|
|
13984
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
|
13985
|
+
return { ok: true, diagnostics, manifest: value };
|
|
13986
|
+
}
|
|
13987
|
+
function isNullableString(value) {
|
|
13988
|
+
return value === null || typeof value === "string";
|
|
13989
|
+
}
|
|
13990
|
+
function renderLlmixTrustSnippet(format, manifestPath, manifest) {
|
|
13991
|
+
const vars = {
|
|
13992
|
+
LLMIX_TRUST_MANIFEST: manifestPath,
|
|
13993
|
+
LLMIX_EXPECTED_ROOT_DIGEST: manifest.expectedRootDigest,
|
|
13994
|
+
LLMIX_SOURCE_SET_DIGEST: manifest.sourceSetDigest,
|
|
13995
|
+
LLMIX_RELEASE_PLAN_DIGEST: manifest.releasePlanDigest,
|
|
13996
|
+
LLMIX_REGISTRY_ROOT: manifest.registryRoot.path,
|
|
13997
|
+
LLMIX_RELEASE_PLAN: manifest.releasePlan.path,
|
|
13998
|
+
LLMIX_HIGH_WATERMARK: manifest.highWatermark ?? ""
|
|
13999
|
+
};
|
|
14000
|
+
if (format === "json") return `${JSON.stringify(vars, null, 2)}
|
|
14001
|
+
`;
|
|
14002
|
+
if (format === "env")
|
|
14003
|
+
return `${Object.entries(vars).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n")}
|
|
14004
|
+
`;
|
|
14005
|
+
if (format === "kubernetes") {
|
|
14006
|
+
return [
|
|
14007
|
+
"apiVersion: v1",
|
|
14008
|
+
"kind: ConfigMap",
|
|
14009
|
+
"metadata:",
|
|
14010
|
+
" name: llmix-trust",
|
|
14011
|
+
"data:",
|
|
14012
|
+
...Object.entries(vars).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
|
|
14013
|
+
""
|
|
14014
|
+
].join("\n");
|
|
14015
|
+
}
|
|
14016
|
+
if (format === "github-actions") {
|
|
14017
|
+
return ["env:", ...Object.entries(vars).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`), ""].join("\n");
|
|
14018
|
+
}
|
|
14019
|
+
if (format === "terraform") {
|
|
14020
|
+
return [
|
|
14021
|
+
"locals {",
|
|
14022
|
+
" llmix_trust = {",
|
|
14023
|
+
...Object.entries(vars).map(([key, value]) => ` ${key} = ${JSON.stringify(value)}`),
|
|
14024
|
+
" }",
|
|
14025
|
+
"}",
|
|
14026
|
+
""
|
|
14027
|
+
].join("\n");
|
|
14028
|
+
}
|
|
14029
|
+
if (format === "typescript") return `export const llmixTrust = ${JSON.stringify(vars, null, 2)} as const;
|
|
14030
|
+
`;
|
|
14031
|
+
if (format === "python") return `LLMIX_TRUST = ${JSON.stringify(vars, null, 2)}
|
|
14032
|
+
`;
|
|
14033
|
+
return `${Object.entries(vars).map(([key, value]) => `pub const ${key}: &str = ${JSON.stringify(value)};`).join("\n")}
|
|
14034
|
+
`;
|
|
14035
|
+
}
|
|
14036
|
+
function doctorSourceReadiness(sourceDir) {
|
|
14037
|
+
const diagnostics = [];
|
|
14038
|
+
const scanned = scanMdaSources(resolve3(sourceDir));
|
|
14039
|
+
if (!scanned.ok) return { ok: false, diagnostics: [diag("filesystem.io", scanned.message)], sourceCount: 0 };
|
|
14040
|
+
if (scanned.files.length === 0) {
|
|
14041
|
+
return {
|
|
14042
|
+
ok: false,
|
|
14043
|
+
diagnostics: [diag("llmix.no_sources", "No .mda sources found under --source")],
|
|
14044
|
+
sourceCount: 0
|
|
14045
|
+
};
|
|
14046
|
+
}
|
|
14047
|
+
for (const file of scanned.files) {
|
|
14048
|
+
const validation = validateArtifact(file, "source");
|
|
14049
|
+
if (!validation.ok) diagnostics.push(...validation.diagnostics.map((d) => ({ ...d, path: file })));
|
|
14050
|
+
const integrity = runIntegrity(["verify", file, "--target", "source"]);
|
|
14051
|
+
if (!integrity.ok) diagnostics.push(...integrity.diagnostics.map((d) => ({ ...d, path: file })));
|
|
14052
|
+
}
|
|
14053
|
+
return { ok: diagnostics.length === 0, diagnostics, sourceCount: scanned.files.length };
|
|
14054
|
+
}
|
|
14055
|
+
function validateRegistryRootEvidence(value) {
|
|
14056
|
+
const diagnostics = [];
|
|
14057
|
+
if (!isRecord(value)) {
|
|
14058
|
+
return { ok: false, diagnostics: [diag("llmix.registry_root_invalid", "Registry-root evidence must be a JSON object")] };
|
|
14059
|
+
}
|
|
14060
|
+
if (value.kind !== "llmix-registry-root")
|
|
14061
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root kind must be llmix-registry-root"));
|
|
14062
|
+
if (value.version !== 1) diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root version must be 1"));
|
|
14063
|
+
if (typeof value.revision !== "string" || value.revision.length === 0)
|
|
14064
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root revision must be a non-empty string"));
|
|
14065
|
+
if (typeof value.publishedAt !== "string" || Number.isNaN(Date.parse(value.publishedAt)))
|
|
14066
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root publishedAt must be an ISO timestamp"));
|
|
14067
|
+
if (typeof value.highWatermark !== "string" || value.highWatermark.length === 0)
|
|
14068
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root highWatermark must be a non-empty string"));
|
|
14069
|
+
if (typeof value.sourceSetDigest !== "string" || !DIGEST_PATTERN.test(value.sourceSetDigest))
|
|
14070
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root sourceSetDigest must be a digest"));
|
|
14071
|
+
const sources = Array.isArray(value.sources) ? value.sources : null;
|
|
14072
|
+
if (!sources) {
|
|
14073
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root sources must be an array"));
|
|
14074
|
+
} else {
|
|
14075
|
+
for (const [index, source] of sources.entries()) {
|
|
14076
|
+
if (!isRecord(source)) diagnostics.push(diag("llmix.registry_root_invalid", `Registry-root sources[${index}] must be a JSON object`));
|
|
14077
|
+
}
|
|
14078
|
+
}
|
|
14079
|
+
if (!isRecord(value.integrity) || value.integrity.algorithm !== "sha256" || typeof value.integrity.digest !== "string") {
|
|
14080
|
+
diagnostics.push(diag("llmix.registry_root_invalid", "Registry-root integrity must contain sha256 digest"));
|
|
14081
|
+
}
|
|
14082
|
+
if (!Array.isArray(value.signatures) || value.signatures.length === 0)
|
|
14083
|
+
diagnostics.push(diag("missing-required-signature", "Registry-root evidence requires signatures[]"));
|
|
14084
|
+
const rootDigest = computeDigest(Buffer.from(jcs(unsignedRegistryRoot(value)), "utf8"), "sha256");
|
|
14085
|
+
if (isRecord(value.integrity) && typeof value.integrity.digest === "string" && value.integrity.digest !== rootDigest) {
|
|
14086
|
+
diagnostics.push(diag("integrity.mismatch", "Registry-root integrity digest does not match canonical evidence bytes"));
|
|
14087
|
+
}
|
|
14088
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
|
14089
|
+
const root = {
|
|
14090
|
+
revision: value.revision,
|
|
14091
|
+
publishedAt: value.publishedAt,
|
|
14092
|
+
highWatermark: value.highWatermark,
|
|
14093
|
+
sourceSetDigest: value.sourceSetDigest,
|
|
14094
|
+
sources,
|
|
14095
|
+
integrity: value.integrity,
|
|
14096
|
+
signatures: value.signatures
|
|
14097
|
+
};
|
|
14098
|
+
return {
|
|
14099
|
+
ok: true,
|
|
14100
|
+
diagnostics,
|
|
14101
|
+
rootDigest,
|
|
14102
|
+
root
|
|
14103
|
+
};
|
|
14104
|
+
}
|
|
14105
|
+
function unsignedRegistryRoot(root) {
|
|
14106
|
+
const unsigned = {};
|
|
14107
|
+
for (const [key, value] of Object.entries(root)) {
|
|
14108
|
+
if (key !== "integrity" && key !== "signatures") unsigned[key] = value;
|
|
14109
|
+
}
|
|
14110
|
+
return unsigned;
|
|
14111
|
+
}
|
|
14112
|
+
function validateReleasePlanEvidence(value) {
|
|
14113
|
+
const diagnostics = [];
|
|
14114
|
+
if (!isRecord(value)) {
|
|
14115
|
+
return { ok: false, diagnostics: [diag("llmix.release_plan_invalid", "Release plan must be a JSON object")] };
|
|
14116
|
+
}
|
|
14117
|
+
if (value.kind !== "llmix-release-plan")
|
|
14118
|
+
diagnostics.push(diag("llmix.release_plan_invalid", "Release plan kind must be llmix-release-plan"));
|
|
14119
|
+
if (typeof value.sourceSetDigest !== "string" || !DIGEST_PATTERN.test(value.sourceSetDigest))
|
|
14120
|
+
diagnostics.push(diag("llmix.release_plan_invalid", "Release plan sourceSetDigest must be a digest"));
|
|
14121
|
+
const sources = Array.isArray(value.sources) ? value.sources : null;
|
|
14122
|
+
if (!sources) {
|
|
14123
|
+
diagnostics.push(diag("llmix.release_plan_invalid", "Release plan sources must be an array"));
|
|
14124
|
+
} else {
|
|
14125
|
+
for (const [index, source] of sources.entries()) {
|
|
14126
|
+
if (!isRecord(source)) diagnostics.push(diag("llmix.release_plan_invalid", `Release plan sources[${index}] must be a JSON object`));
|
|
14127
|
+
}
|
|
14128
|
+
}
|
|
14129
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
|
14130
|
+
const releasePlan = {
|
|
14131
|
+
sourceSetDigest: value.sourceSetDigest,
|
|
14132
|
+
sources
|
|
14133
|
+
};
|
|
14134
|
+
return {
|
|
14135
|
+
ok: true,
|
|
14136
|
+
diagnostics,
|
|
14137
|
+
releasePlan
|
|
14138
|
+
};
|
|
14139
|
+
}
|
|
14140
|
+
function freshnessDiagnostics(root, requirements) {
|
|
14141
|
+
const diagnostics = [];
|
|
14142
|
+
if (requirements.minimumRevision) {
|
|
14143
|
+
const comparison = compareMonotonicRequirement(root.revision, requirements.minimumRevision, "revision");
|
|
14144
|
+
if (!comparison.ok) diagnostics.push(...comparison.diagnostics);
|
|
14145
|
+
else if (comparison.value < 0)
|
|
14146
|
+
diagnostics.push(diag("llmix.freshness_revision_rollback", "Registry-root revision is below minimumRevision"));
|
|
14147
|
+
}
|
|
14148
|
+
if (requirements.minimumPublishedAt) {
|
|
14149
|
+
const publishedAt = Date.parse(root.publishedAt);
|
|
14150
|
+
const minimumPublishedAt = Date.parse(requirements.minimumPublishedAt);
|
|
14151
|
+
if (Number.isNaN(minimumPublishedAt))
|
|
14152
|
+
diagnostics.push(diag("llmix.freshness_invalid", "--minimum-published-at must be an ISO timestamp"));
|
|
14153
|
+
else if (publishedAt < minimumPublishedAt)
|
|
14154
|
+
diagnostics.push(diag("llmix.freshness_published_at_rollback", "Registry-root publishedAt is below minimumPublishedAt"));
|
|
14155
|
+
}
|
|
14156
|
+
if (requirements.highWatermark) {
|
|
14157
|
+
const comparison = compareMonotonicRequirement(root.highWatermark, requirements.highWatermark, "highWatermark");
|
|
14158
|
+
if (!comparison.ok) diagnostics.push(...comparison.diagnostics);
|
|
14159
|
+
else if (comparison.value < 0)
|
|
14160
|
+
diagnostics.push(diag("llmix.high_watermark_rollback", "Registry-root highWatermark is below the required high-watermark"));
|
|
14161
|
+
}
|
|
14162
|
+
return diagnostics;
|
|
14163
|
+
}
|
|
14164
|
+
function compareMonotonicRequirement(actual, requirement, label) {
|
|
14165
|
+
const actualValue = parseMonotonicValue(actual, `registry-root ${label}`);
|
|
14166
|
+
const requiredValue = parseMonotonicValue(requirement, `required ${label}`);
|
|
14167
|
+
const diagnostics = [...actualValue.diagnostics, ...requiredValue.diagnostics];
|
|
14168
|
+
if (!actualValue.ok || !requiredValue.ok) return { ok: false, diagnostics };
|
|
14169
|
+
if (actualValue.value.kind !== requiredValue.value.kind) {
|
|
14170
|
+
return {
|
|
14171
|
+
ok: false,
|
|
14172
|
+
diagnostics: [diag("llmix.freshness_invalid", `${label} and required ${label} must use the same monotonic format`)]
|
|
14173
|
+
};
|
|
14174
|
+
}
|
|
14175
|
+
return { ok: true, value: compareMonotonicValues(actualValue.value, requiredValue.value) };
|
|
14176
|
+
}
|
|
14177
|
+
function parseMonotonicValue(value, label) {
|
|
14178
|
+
if (/^(0|[1-9][0-9]*)$/.test(value)) {
|
|
14179
|
+
return { ok: true, diagnostics: [], value: { kind: "integer", value } };
|
|
14180
|
+
}
|
|
14181
|
+
const compactUtc = value.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$/);
|
|
14182
|
+
const millis = compactUtc ? Date.UTC(
|
|
14183
|
+
Number(compactUtc[1]),
|
|
14184
|
+
Number(compactUtc[2]) - 1,
|
|
14185
|
+
Number(compactUtc[3]),
|
|
14186
|
+
Number(compactUtc[4]),
|
|
14187
|
+
Number(compactUtc[5]),
|
|
14188
|
+
Number(compactUtc[6])
|
|
14189
|
+
) : Date.parse(value);
|
|
14190
|
+
if (!Number.isNaN(millis)) {
|
|
14191
|
+
return { ok: true, diagnostics: [], value: { kind: "timestamp", value: millis } };
|
|
14192
|
+
}
|
|
14193
|
+
return {
|
|
14194
|
+
ok: false,
|
|
14195
|
+
diagnostics: [
|
|
14196
|
+
diag(
|
|
14197
|
+
"llmix.freshness_invalid",
|
|
14198
|
+
`${label} must be a decimal integer, ISO timestamp, or compact UTC timestamp like 2026-05-09T120000Z`
|
|
14199
|
+
)
|
|
14200
|
+
]
|
|
14201
|
+
};
|
|
14202
|
+
}
|
|
14203
|
+
function compareMonotonicValues(actual, requirement) {
|
|
14204
|
+
if (actual.kind === "timestamp" && requirement.kind === "timestamp") return actual.value - requirement.value;
|
|
14205
|
+
if (actual.kind === "integer" && requirement.kind === "integer") {
|
|
14206
|
+
if (actual.value.length !== requirement.value.length) return actual.value.length - requirement.value.length;
|
|
14207
|
+
return actual.value.localeCompare(requirement.value);
|
|
14208
|
+
}
|
|
14209
|
+
return 0;
|
|
14210
|
+
}
|
|
14211
|
+
function sourceSetDiagnostics(root, releasePlan) {
|
|
14212
|
+
const diagnostics = [];
|
|
14213
|
+
if (root.sourceSetDigest !== releasePlan.sourceSetDigest) {
|
|
14214
|
+
diagnostics.push(diag("llmix.source_set_digest_mismatch", "Registry-root sourceSetDigest does not match the release plan"));
|
|
14215
|
+
}
|
|
14216
|
+
const rootSources = /* @__PURE__ */ new Map();
|
|
14217
|
+
for (const source of root.sources) {
|
|
14218
|
+
const identity = registryEntryIdentity(source);
|
|
14219
|
+
if (!identity) {
|
|
14220
|
+
diagnostics.push(diag("llmix.registry_root_identity_mismatch", "Registry-root source is missing registry entry identity"));
|
|
14221
|
+
continue;
|
|
14222
|
+
}
|
|
14223
|
+
if (rootSources.has(identity)) {
|
|
14224
|
+
diagnostics.push(diag("llmix.registry_root_duplicate_preset", `Registry-root has duplicate preset ${identity}`));
|
|
14225
|
+
continue;
|
|
14226
|
+
}
|
|
14227
|
+
rootSources.set(identity, source);
|
|
14228
|
+
}
|
|
14229
|
+
const releaseIdentities = /* @__PURE__ */ new Set();
|
|
14230
|
+
for (const source of releasePlan.sources) {
|
|
14231
|
+
const identity = registryEntryIdentity(source);
|
|
14232
|
+
if (!identity) {
|
|
14233
|
+
diagnostics.push(diag("llmix.release_plan_invalid", "Release plan source is missing registry entry identity"));
|
|
14234
|
+
continue;
|
|
14235
|
+
}
|
|
14236
|
+
if (releaseIdentities.has(identity)) {
|
|
14237
|
+
diagnostics.push(diag("llmix.release_plan_duplicate_preset", `Release plan has duplicate preset ${identity}`));
|
|
14238
|
+
continue;
|
|
14239
|
+
}
|
|
14240
|
+
releaseIdentities.add(identity);
|
|
14241
|
+
const rootSource = rootSources.get(identity);
|
|
14242
|
+
if (!rootSource) {
|
|
14243
|
+
diagnostics.push(diag("llmix.registry_root_missing_preset", `Registry-root is missing preset ${identity}`));
|
|
14244
|
+
continue;
|
|
14245
|
+
}
|
|
14246
|
+
if (rootSource.canonicalSourceDigest !== source.canonicalSourceDigest) {
|
|
14247
|
+
diagnostics.push(diag("llmix.registry_root_stale_digest", `Registry-root digest for ${identity} does not match the release plan`));
|
|
14248
|
+
}
|
|
14249
|
+
if (rootSource.registryEntryPath !== source.expectedRegistryEntryPath) {
|
|
14250
|
+
diagnostics.push(diag("llmix.registry_root_identity_mismatch", `Registry-root path for ${identity} does not match the release plan`));
|
|
14251
|
+
}
|
|
14252
|
+
}
|
|
14253
|
+
for (const identity of rootSources.keys()) {
|
|
14254
|
+
if (!releaseIdentities.has(identity))
|
|
14255
|
+
diagnostics.push(diag("llmix.registry_root_extra_preset", `Registry-root has extra preset ${identity}`));
|
|
14256
|
+
}
|
|
14257
|
+
return diagnostics;
|
|
14258
|
+
}
|
|
14259
|
+
function registryEntryIdentity(source) {
|
|
14260
|
+
const identity = source.registryEntryIdentity ?? source.expectedRegistryEntryIdentity;
|
|
14261
|
+
return typeof identity === "string" && identity.length > 0 ? identity : null;
|
|
14262
|
+
}
|
|
14263
|
+
function pathResolvesInsideDir(candidate, rootDir) {
|
|
14264
|
+
const root = realPathIfPossible(rootDir);
|
|
14265
|
+
const resolvedCandidate = realPathCandidate(candidate);
|
|
14266
|
+
return resolvedCandidate === root || resolvedCandidate.startsWith(`${root}${sep}`);
|
|
14267
|
+
}
|
|
14268
|
+
function realPathCandidate(candidate) {
|
|
14269
|
+
const resolved = resolve3(candidate);
|
|
14270
|
+
try {
|
|
14271
|
+
return realpathSync(resolved);
|
|
14272
|
+
} catch {
|
|
14273
|
+
const parent = realPathIfPossible(dirname2(resolved));
|
|
14274
|
+
return join2(parent, basename(resolved));
|
|
14275
|
+
}
|
|
14276
|
+
}
|
|
14277
|
+
function realPathIfPossible(path) {
|
|
14278
|
+
try {
|
|
14279
|
+
return realpathSync(path);
|
|
14280
|
+
} catch {
|
|
14281
|
+
return resolve3(path);
|
|
14282
|
+
}
|
|
14283
|
+
}
|
|
14284
|
+
function scanMdaSources(root) {
|
|
14285
|
+
const files = [];
|
|
14286
|
+
try {
|
|
14287
|
+
const visit = (dir) => {
|
|
14288
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
14289
|
+
const path = join2(dir, entry.name);
|
|
14290
|
+
if (entry.isDirectory()) visit(path);
|
|
14291
|
+
else if (entry.isFile() && entry.name.endsWith(".mda")) files.push(path);
|
|
14292
|
+
}
|
|
14293
|
+
};
|
|
14294
|
+
visit(root);
|
|
14295
|
+
return { ok: true, files };
|
|
14296
|
+
} catch (error) {
|
|
14297
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
14300
|
+
function relativePath(root, file) {
|
|
14301
|
+
return relative2(root, file).split(sep).join("/");
|
|
14302
|
+
}
|
|
14303
|
+
function llmixPresetIdentity(frontmatter) {
|
|
14304
|
+
const metadata = isRecord(frontmatter.metadata) ? frontmatter.metadata : null;
|
|
14305
|
+
const namespace = metadata && isRecord(metadata["snoai-llmix"]) ? metadata["snoai-llmix"] : null;
|
|
14306
|
+
if (!namespace || typeof namespace.module !== "string" || typeof namespace.preset !== "string") return null;
|
|
14307
|
+
if (!LLMIX_MODULE_NAME2.test(namespace.module) || !LLMIX_PRESET_NAME2.test(namespace.preset)) return null;
|
|
14308
|
+
return { module: namespace.module, preset: namespace.preset };
|
|
14309
|
+
}
|
|
14310
|
+
function firstTrustedSignerIdentity(value) {
|
|
14311
|
+
if (!Array.isArray(value)) return null;
|
|
14312
|
+
for (const entry of value) {
|
|
14313
|
+
if (!isRecord(entry)) continue;
|
|
14314
|
+
if (entry.type !== "did-web" && entry.type !== "sigstore-oidc") continue;
|
|
14315
|
+
if (typeof entry.signer !== "string" || typeof entry.keyId !== "string" || typeof entry.payloadDigest !== "string") continue;
|
|
14316
|
+
const identity = {
|
|
14317
|
+
type: entry.type,
|
|
14318
|
+
signer: entry.signer,
|
|
14319
|
+
keyId: entry.keyId,
|
|
14320
|
+
payloadDigest: entry.payloadDigest
|
|
14321
|
+
};
|
|
14322
|
+
if (typeof entry.subject === "string") identity.subject = entry.subject;
|
|
14323
|
+
if (typeof entry.rekorLogId === "string") identity.rekorLogId = entry.rekorLogId;
|
|
14324
|
+
if (typeof entry.rekorLogIndex === "number") identity.rekorLogIndex = entry.rekorLogIndex;
|
|
14325
|
+
return identity;
|
|
14326
|
+
}
|
|
14327
|
+
return null;
|
|
14328
|
+
}
|
|
14329
|
+
function releaseTargetError(command, parsed) {
|
|
14330
|
+
if ("error" in parsed && parsed.error) return usage(command, parsed.error);
|
|
14331
|
+
const target = oneOption(parsed.options, "--target");
|
|
14332
|
+
if (!target) return usage(command, "--target llmix-registry is required");
|
|
14333
|
+
if (target === LLMIX_REGISTRY_TARGET) return null;
|
|
14334
|
+
return commandResult(false, command, EXIT.failure, [diag("release.unsupported_target", `Unsupported release target: ${target}`)], {
|
|
14335
|
+
summary: "Release target is not supported",
|
|
14336
|
+
nextActions: [
|
|
14337
|
+
nextAction(
|
|
14338
|
+
"use-llmix-registry-target",
|
|
14339
|
+
"Use the supported LLMix registry release target",
|
|
14340
|
+
`mda ${command} --target ${LLMIX_REGISTRY_TARGET}`
|
|
14341
|
+
)
|
|
14342
|
+
],
|
|
14343
|
+
target,
|
|
14344
|
+
supportedTargets: [LLMIX_REGISTRY_TARGET]
|
|
14345
|
+
});
|
|
14346
|
+
}
|
|
14347
|
+
function migratedCommand(command, replacement) {
|
|
14348
|
+
return commandResult(false, command, EXIT.failure, [diag("release.command_migrated", `Use ${replacement} instead of mda ${command}`)], {
|
|
14349
|
+
summary: "Command moved to the generic release workflow",
|
|
14350
|
+
nextActions: [nextAction("use-release-command", "Use the generic release command surface", replacement)]
|
|
14351
|
+
});
|
|
14352
|
+
}
|
|
14353
|
+
function llmixMigrationReplacement(args) {
|
|
14354
|
+
if (args[0] === "release" && args[1] === "plan") return "mda release prepare --target llmix-registry";
|
|
14355
|
+
if (args[0] === "trust" && args[1] === "policy") return "mda release trust policy --target llmix-registry";
|
|
14356
|
+
if (args[0] === "trust" && args[1] === "manifest") return "mda release finalize --target llmix-registry";
|
|
14357
|
+
if (args[0] === "trust" && args[1] === "snippets") {
|
|
14358
|
+
return "mda release finalize --target llmix-registry --registry-dir <registry-dir> --manifest <manifest> --snippet-format <format> --snippet-out <path>";
|
|
14359
|
+
}
|
|
14360
|
+
return "mda release --help";
|
|
14361
|
+
}
|
|
14362
|
+
function runDidWebTrustPolicy(options, command) {
|
|
14363
|
+
const profile = "did-web";
|
|
14364
|
+
const domain = oneOption(options, "--domain");
|
|
14365
|
+
if (!domain || didWebDomainFromDid(`did:web:${domain}`) !== domain) return usage(command, "--domain must be a valid did:web domain");
|
|
14366
|
+
const minRaw = oneOption(options, "--min-signatures") ?? "1";
|
|
14367
|
+
const minSignatures = Number(minRaw);
|
|
14368
|
+
if (!Number.isInteger(minSignatures) || minSignatures < 1) return usage(command, "--min-signatures must be a positive integer");
|
|
14369
|
+
const policy = { version: 1, minSignatures, trustedSigners: [{ type: "did-web", domain }] };
|
|
14370
|
+
const validation = validateJsonAgainst(policy, "trustPolicy");
|
|
14371
|
+
if (!validation.ok) return commandResult(false, command, EXIT.failure, validation.diagnostics, { profile, domain, minSignatures });
|
|
14372
|
+
const out = oneOption(options, "--out");
|
|
14373
|
+
const write = writeTrustPolicy(command, out, policy, { profile, domain, minSignatures });
|
|
14374
|
+
if (!write.ok) return write;
|
|
14375
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
14376
|
+
summary: out ? `Wrote did:web trust policy to ${out}` : "Generated did:web trust policy",
|
|
14377
|
+
artifacts: out ? [artifact("trust-policy", out)] : [],
|
|
14378
|
+
nextActions: out ? [
|
|
14379
|
+
nextAction(
|
|
14380
|
+
"verify-signed-preset",
|
|
14381
|
+
"Verify a signed preset with this policy",
|
|
14382
|
+
`mda verify signed.mda --target source --policy ${out} --did-document did-web-document.json`
|
|
14383
|
+
)
|
|
14384
|
+
] : [externalNextAction("save-trust-policy", "Save this policy JSON before release verification", "write the policy bytes to disk")],
|
|
14385
|
+
message: out ? `wrote ${out}` : JSON.stringify(policy, null, 2),
|
|
14386
|
+
profile,
|
|
14387
|
+
domain,
|
|
14388
|
+
minSignatures,
|
|
14389
|
+
policy,
|
|
14390
|
+
out,
|
|
14391
|
+
written: Boolean(out)
|
|
14392
|
+
});
|
|
14393
|
+
}
|
|
14394
|
+
function runGithubActionsTrustPolicy(options, command) {
|
|
14395
|
+
const profile = "github-actions";
|
|
14396
|
+
const repo = oneOption(options, "--repo");
|
|
14397
|
+
const workflow = oneOption(options, "--workflow");
|
|
14398
|
+
const ref = oneOption(options, "--ref");
|
|
14399
|
+
if (!repo || !GITHUB_REPOSITORY.test(repo)) return usage(command, "--repo must be a GitHub repository in owner/repo form");
|
|
14400
|
+
if (!workflow || workflow.trim() !== workflow || workflow.length === 0)
|
|
14401
|
+
return usage(command, "--workflow must be a non-empty workflow file or identity");
|
|
14402
|
+
if (!ref || !GITHUB_REF.test(ref)) return usage(command, "--ref must be an exact Git ref such as refs/heads/main or refs/tags/v1.1.0");
|
|
14403
|
+
const subject = `repo:${repo}:ref:${ref}`;
|
|
14404
|
+
const policy = {
|
|
14405
|
+
version: 1,
|
|
14406
|
+
trustedSigners: [
|
|
14407
|
+
{
|
|
14408
|
+
type: "sigstore-oidc",
|
|
14409
|
+
issuer: GITHUB_ACTIONS_ISSUER,
|
|
14410
|
+
subject,
|
|
14411
|
+
repository: repo,
|
|
14412
|
+
workflow,
|
|
14413
|
+
ref
|
|
14414
|
+
}
|
|
14415
|
+
],
|
|
14416
|
+
rekor: { url: SIGSTORE_REKOR_URL }
|
|
14417
|
+
};
|
|
14418
|
+
const validation = validateJsonAgainst(policy, "trustPolicy");
|
|
14419
|
+
if (!validation.ok) return commandResult(false, command, EXIT.failure, validation.diagnostics, { profile, repo, workflow, ref });
|
|
14420
|
+
const out = oneOption(options, "--out");
|
|
14421
|
+
const write = writeTrustPolicy(command, out, policy, { profile, repo, workflow, ref });
|
|
14422
|
+
if (!write.ok) return write;
|
|
14423
|
+
return commandResult(true, command, EXIT.ok, [], {
|
|
14424
|
+
summary: out ? `Wrote GitHub Actions trust policy to ${out}` : "Generated GitHub Actions trust policy",
|
|
14425
|
+
artifacts: out ? [artifact("trust-policy", out)] : [],
|
|
14426
|
+
nextActions: out ? [
|
|
14427
|
+
nextAction(
|
|
14428
|
+
"sign-github-actions-release",
|
|
14429
|
+
"Sign the release artifact with GitHub Actions Sigstore/Rekor evidence",
|
|
14430
|
+
`mda sign release.mda --profile github-actions --repo ${repo} --workflow ${workflow} --ref ${ref} --rekor --offline-sigstore-fixture sigstore-fixture.json --out signed-release.mda`
|
|
14431
|
+
),
|
|
14432
|
+
nextAction(
|
|
14433
|
+
"verify-github-actions-release",
|
|
14434
|
+
"Verify the signed release with this pinned policy",
|
|
14435
|
+
`mda verify signed-release.mda --policy ${out} --offline-sigstore-fixture sigstore-fixture.json`
|
|
14436
|
+
)
|
|
14437
|
+
] : [externalNextAction("save-trust-policy", "Save this policy JSON before release verification", "write the policy bytes to disk")],
|
|
14438
|
+
message: out ? `wrote ${out}` : JSON.stringify(policy, null, 2),
|
|
14439
|
+
profile,
|
|
14440
|
+
repo,
|
|
14441
|
+
workflow,
|
|
14442
|
+
ref,
|
|
14443
|
+
policy,
|
|
14444
|
+
out,
|
|
14445
|
+
written: Boolean(out)
|
|
14446
|
+
});
|
|
14447
|
+
}
|
|
14448
|
+
function writeTrustPolicy(command, out, policy, context) {
|
|
14449
|
+
if (!out) {
|
|
14450
|
+
return commandResult(true, command, EXIT.ok, [], { written: false });
|
|
14451
|
+
}
|
|
14452
|
+
if (existsSync4(out))
|
|
14453
|
+
return commandResult(false, command, EXIT.io, [diag("filesystem.io", `Refusing to overwrite existing file: ${out}`)], {
|
|
14454
|
+
...context,
|
|
14455
|
+
out,
|
|
14456
|
+
written: false
|
|
14457
|
+
});
|
|
14458
|
+
try {
|
|
14459
|
+
atomicWrite(out, `${JSON.stringify(policy, null, 2)}
|
|
14460
|
+
`);
|
|
14461
|
+
} catch (error) {
|
|
14462
|
+
return ioError(command, error instanceof Error ? error.message : String(error), {
|
|
14463
|
+
...context,
|
|
14464
|
+
out,
|
|
14465
|
+
written: false
|
|
14466
|
+
});
|
|
14467
|
+
}
|
|
14468
|
+
return commandResult(true, command, EXIT.ok, [], { ...context, out, written: true });
|
|
14469
|
+
}
|
|
14470
|
+
|
|
14471
|
+
// src/cli/main.ts
|
|
14472
|
+
async function main() {
|
|
14473
|
+
const { globals, args } = splitGlobals(process.argv.slice(2));
|
|
14474
|
+
try {
|
|
14475
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
14476
|
+
process.stdout.write(HELP);
|
|
14477
|
+
process.exit(EXIT.ok);
|
|
14478
|
+
}
|
|
14479
|
+
const command = args[0];
|
|
14480
|
+
const rest = args.slice(1);
|
|
14481
|
+
const result = await runCommand(command, rest, globals);
|
|
14482
|
+
writeResult(result, globals);
|
|
14483
|
+
process.exit(result.exitCode);
|
|
14484
|
+
} catch (error) {
|
|
14485
|
+
const result = commandResult(false, "internal", EXIT.internal, [
|
|
14486
|
+
diag("internal-error", error instanceof Error ? error.message : String(error))
|
|
14487
|
+
]);
|
|
14488
|
+
writeResult(result, globals);
|
|
14489
|
+
process.exit(result.exitCode);
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
async function runCommand(command, args, globals) {
|
|
14493
|
+
if (command === "init") return runInit(args, globals);
|
|
14494
|
+
if (command === "validate") return runValidate(args);
|
|
14495
|
+
if (command === "compile") return runCompile(args);
|
|
14496
|
+
if (command === "canonicalize") return runCanonicalize(args, globals);
|
|
14497
|
+
if (command === "integrity") return runIntegrity(args);
|
|
14498
|
+
if (command === "verify") return runVerify(args);
|
|
14499
|
+
if (command === "sign") return runSign(args);
|
|
14500
|
+
if (command === "release") return runRelease(args);
|
|
14501
|
+
if (command === "llmix") return runLlmix(args);
|
|
14502
|
+
if (command === "doctor") return runDoctor(args);
|
|
14503
|
+
if (command === "conformance") return runConformance(args);
|
|
14504
|
+
return usage("root", `Unknown command: ${command}`);
|
|
14505
|
+
}
|
|
14506
|
+
|
|
11291
14507
|
// src/index.ts
|
|
11292
14508
|
void main();
|
|
11293
14509
|
/*! Bundled license information:
|