@nexart/cli 0.6.0 → 0.8.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/README.md +169 -11
- package/dist/index.d.ts +130 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +440 -126
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,27 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @nexart/cli v0.
|
|
3
|
+
* @nexart/cli v0.8.0 — NexArt CLI
|
|
4
4
|
*
|
|
5
5
|
* Code Mode commands:
|
|
6
|
-
* - nexart run <file>
|
|
6
|
+
* - nexart run <file> — Execute via remote renderer and create snapshot
|
|
7
7
|
* - nexart replay <snapshot> — Re-execute from snapshot
|
|
8
8
|
* - nexart verify <snapshot> — Verify snapshot hash
|
|
9
9
|
*
|
|
10
10
|
* AI Certification commands:
|
|
11
|
-
* - nexart ai create <file>
|
|
12
|
-
* - nexart ai certify <file>
|
|
13
|
-
* - nexart ai
|
|
11
|
+
* - nexart ai create <file> — Create a CER bundle via node API
|
|
12
|
+
* - nexart ai certify <file> — Certify an AI execution and attest
|
|
13
|
+
* - nexart ai seal <file> — Seal a CER bundle locally (offline, no node)
|
|
14
|
+
* - nexart ai verify <file> — Verify a CER bundle or CER package locally
|
|
15
|
+
* - nexart ai project-verify <file> — Verify a cer.project.bundle.v1 artifact locally
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* Sealed vs Certified:
|
|
18
|
+
* - Sealed = integrity only. Bundle is locally produced via SDK sealCer().
|
|
19
|
+
* No node receipt, no signature, no envelope. Use for offline
|
|
20
|
+
* workflows, CI, or pre-certification staging. `ai seal` builds
|
|
21
|
+
* these.
|
|
22
|
+
* - Certified = integrity + node attestation receipt. Bundle has been
|
|
23
|
+
* attested by a NexArt node and includes a signed receipt.
|
|
24
|
+
* `ai certify` builds these.
|
|
25
|
+
* `ai verify` works on both. For sealed bundles, the Receipt and Envelope
|
|
26
|
+
* layers report SKIPPED — that is correct, not a failure.
|
|
27
|
+
*
|
|
28
|
+
* VERIFICATION ALIGNMENT (v0.8.0):
|
|
29
|
+
* `ai verify` and `ai project-verify` both delegate ALL verification logic to
|
|
30
|
+
* @nexart/ai-execution (>=0.16.1). The CLI implements ZERO independent
|
|
31
|
+
* cryptographic logic — no canonicalization, no hash recomputation, no
|
|
32
|
+
* integrity checks. This guarantees byte-for-byte parity with:
|
|
33
|
+
* - the SDK (`verifyAiCerBundleDetailed`, `verifyProjectBundle`)
|
|
34
|
+
* - NexArt Node (`/v1/cer/verify`)
|
|
35
|
+
* - verify.nexart.io
|
|
36
|
+
*
|
|
37
|
+
* `ai verify` reports the three SDK protocol layers independently:
|
|
38
|
+
* - bundleIntegrity (Integrity: certificate hash recomputation)
|
|
39
|
+
* - nodeSignature (Receipt: node attestation signature validity)
|
|
40
|
+
* - receiptConsistency (Envelope: bundle/receipt envelope consistency)
|
|
41
|
+
* Each layer is reported as PASS / FAIL / SKIPPED with no aggregation, and
|
|
42
|
+
* the SDK reason codes are passed through unmodified.
|
|
43
|
+
*
|
|
44
|
+
* Context signals:
|
|
16
45
|
* Pass --signals-file <path> to ai create / ai certify to attach upstream
|
|
17
|
-
* context signals to the CER. Signals are evidence
|
|
18
|
-
* the certificateHash. ai verify
|
|
46
|
+
* context signals to the CER. Signals are evidence and are bound by the
|
|
47
|
+
* SDK into the certificateHash. ai verify validates them via the SDK.
|
|
19
48
|
*
|
|
20
|
-
* CER packages
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
49
|
+
* CER packages:
|
|
50
|
+
* ai verify accepts raw CER bundles and CER packages. For packages, the
|
|
51
|
+
* inner CER bundle is unwrapped and passed to the SDK verifier.
|
|
52
|
+
*
|
|
53
|
+
* Project bundles:
|
|
54
|
+
* ai project-verify delegates to verifyProjectBundle() from @nexart/ai-execution.
|
|
55
|
+
* No network required. The CLI provides command surface and output formatting only.
|
|
25
56
|
*/
|
|
26
57
|
import yargs from 'yargs';
|
|
27
58
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -32,12 +63,44 @@ import * as http from 'http';
|
|
|
32
63
|
import * as https from 'https';
|
|
33
64
|
import { fileURLToPath } from 'url';
|
|
34
65
|
import { realpathSync } from 'fs';
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
import { createRequire } from 'module';
|
|
67
|
+
import { verifyProjectBundle, verifyAiCerBundleDetailed, hasAttestation as sdkHasAttestation, isCerPackage as sdkIsCerPackage, getCerFromPackage, createSnapshot as sdkCreateSnapshot, sealCer as sdkSealCer, } from '@nexart/ai-execution';
|
|
68
|
+
const CLI_VERSION = '0.8.0';
|
|
69
|
+
/**
|
|
70
|
+
* Code Mode renderer protocol version. This is the wire-protocol version the
|
|
71
|
+
* CLI sends to the remote renderer for snapshot creation. It is unrelated to
|
|
72
|
+
* the AI CER protocol (which is owned and versioned by @nexart/ai-execution).
|
|
73
|
+
*/
|
|
37
74
|
const PROTOCOL_VERSION = '1.2.0';
|
|
75
|
+
/**
|
|
76
|
+
* Stable fallback identifier used when computing a Code Mode `runtimeHash`
|
|
77
|
+
* locally (e.g. when the remote renderer doesn't supply one). This is NOT a
|
|
78
|
+
* CER hash and is NOT part of any verification path — it just needs to be
|
|
79
|
+
* stable across replays of the same CLI version.
|
|
80
|
+
*/
|
|
81
|
+
const CODE_MODE_RUNTIME_TAG = `nexart-cli/${CLI_VERSION}`;
|
|
82
|
+
/**
|
|
83
|
+
* Read the installed @nexart/ai-execution version at runtime from its
|
|
84
|
+
* package.json. Used by --version output so what the CLI reports always
|
|
85
|
+
* matches what is actually loaded.
|
|
86
|
+
*/
|
|
87
|
+
function getSdkVersion() {
|
|
88
|
+
try {
|
|
89
|
+
const req = createRequire(import.meta.url);
|
|
90
|
+
const pkg = req('@nexart/ai-execution/package.json');
|
|
91
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return 'unknown';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
38
97
|
const DEFAULT_ENDPOINT = process.env.NEXART_RENDERER_ENDPOINT || 'http://localhost:5000';
|
|
39
98
|
const DEFAULT_API_KEY = process.env.NEXART_API_KEY || '';
|
|
40
99
|
const DEFAULT_NODE_ENDPOINT = process.env.NEXART_NODE_ENDPOINT || 'https://node.nexart.art';
|
|
100
|
+
// NOTE: A local `CerBundle` shape used to live here. It has been removed in
|
|
101
|
+
// v0.8.0 — the canonical CER bundle type is `CerAiExecutionBundle` from
|
|
102
|
+
// @nexart/ai-execution, which the CLI imports directly. Keeping a parallel
|
|
103
|
+
// type would risk drift from the SDK schema.
|
|
41
104
|
function sha256(data) {
|
|
42
105
|
const buffer = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
|
|
43
106
|
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
@@ -186,7 +249,7 @@ async function callRenderer(endpoint, code, seed, VAR, width, height, apiKey) {
|
|
|
186
249
|
return;
|
|
187
250
|
}
|
|
188
251
|
if (contentType.includes('image/png')) {
|
|
189
|
-
const runtimeHash = res.headers['x-runtime-hash'] || sha256(
|
|
252
|
+
const runtimeHash = res.headers['x-runtime-hash'] || sha256(CODE_MODE_RUNTIME_TAG);
|
|
190
253
|
resolve({ pngBytes: responseBuffer, runtimeHash });
|
|
191
254
|
return;
|
|
192
255
|
}
|
|
@@ -198,7 +261,7 @@ async function callRenderer(endpoint, code, seed, VAR, width, height, apiKey) {
|
|
|
198
261
|
}
|
|
199
262
|
if (json.pngBase64) {
|
|
200
263
|
const pngBytes = Buffer.from(json.pngBase64, 'base64');
|
|
201
|
-
const runtimeHash = json.runtimeHash || sha256(
|
|
264
|
+
const runtimeHash = json.runtimeHash || sha256(CODE_MODE_RUNTIME_TAG);
|
|
202
265
|
resolve({ pngBytes, runtimeHash });
|
|
203
266
|
return;
|
|
204
267
|
}
|
|
@@ -269,7 +332,7 @@ async function runCommand(file, options) {
|
|
|
269
332
|
else {
|
|
270
333
|
console.log(`[nexart] WARNING: Local renderer NOT implemented yet — outputs placeholder image only`);
|
|
271
334
|
pngBytes = createPlaceholderPng();
|
|
272
|
-
runtimeHash = options.runtimeHash || sha256(
|
|
335
|
+
runtimeHash = options.runtimeHash || sha256(CODE_MODE_RUNTIME_TAG);
|
|
273
336
|
}
|
|
274
337
|
const outputHash = sha256(pngBytes);
|
|
275
338
|
const snapshot = {
|
|
@@ -546,48 +609,19 @@ export async function callNodeApi(endpoint, apiPath, body, apiKey, fetchFn = glo
|
|
|
546
609
|
return { status: response.status, data };
|
|
547
610
|
}
|
|
548
611
|
/**
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return '[' + value.map(canonicalJson).join(',') + ']';
|
|
561
|
-
}
|
|
562
|
-
const obj = value;
|
|
563
|
-
const keys = Object.keys(obj).sort();
|
|
564
|
-
const pairs = keys
|
|
565
|
-
.filter(k => obj[k] !== undefined)
|
|
566
|
-
.map(k => JSON.stringify(k) + ':' + canonicalJson(obj[k]));
|
|
567
|
-
return '{' + pairs.join(',') + '}';
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Recompute the certificateHash for a CER v1 bundle.
|
|
571
|
-
* Protected set: { bundleType, version, createdAt, snapshot }
|
|
572
|
-
* v0.11.0+: context is included in the protected set when present (non-null/non-undefined),
|
|
573
|
-
* ensuring signals are tamper-evident. When absent, the hash is identical to pre-v0.11.0.
|
|
612
|
+
* NOTE: As of v0.8.0, the CLI implements ZERO independent cryptographic logic.
|
|
613
|
+
*
|
|
614
|
+
* Removed in v0.8.0 (drift hazard — canonical CER semantics live in the SDK):
|
|
615
|
+
* - canonicalJson() → use @nexart/ai-execution canonicalization
|
|
616
|
+
* - computeBundleHash() → use verifyAiCerBundleDetailed() / verifyCer()
|
|
617
|
+
* - hasAttestation() → re-exported below from @nexart/ai-execution
|
|
618
|
+
*
|
|
619
|
+
* The CLI MUST NOT recompute certificateHash, canonicalize CER JSON, or compare
|
|
620
|
+
* hashes by hand. All such logic is owned by @nexart/ai-execution. This is what
|
|
621
|
+
* guarantees CLI verification matches the SDK, NexArt Node (`/v1/cer/verify`),
|
|
622
|
+
* and verify.nexart.io exactly.
|
|
574
623
|
*/
|
|
575
|
-
export
|
|
576
|
-
const { bundleType, version, createdAt, snapshot } = bundle;
|
|
577
|
-
const protectedSet = { bundleType, version, createdAt, snapshot };
|
|
578
|
-
const context = bundle['context'];
|
|
579
|
-
if (context !== undefined && context !== null) {
|
|
580
|
-
protectedSet['context'] = context;
|
|
581
|
-
}
|
|
582
|
-
return 'sha256:' + sha256(canonicalJson(protectedSet));
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Check whether a CER bundle has a valid-looking attestation.
|
|
586
|
-
*/
|
|
587
|
-
export function hasAttestation(bundle) {
|
|
588
|
-
return (typeof bundle['attestationId'] === 'string' &&
|
|
589
|
-
bundle['attestationId'].length > 0);
|
|
590
|
-
}
|
|
624
|
+
export const hasAttestation = sdkHasAttestation;
|
|
591
625
|
/**
|
|
592
626
|
* Load a JSON array of context signals from a file path.
|
|
593
627
|
* Exits with an error message if the file is missing, invalid JSON, or not an array.
|
|
@@ -619,6 +653,111 @@ export function loadSignalsFile(signalsPath) {
|
|
|
619
653
|
}
|
|
620
654
|
return parsed;
|
|
621
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Internal: validate that `raw` looks like a CreateSnapshotParams payload and
|
|
658
|
+
* return a typed object (plus optional seal-time inputs). The CLI owns input
|
|
659
|
+
* shape validation only — it does NOT touch crypto or canonicalization. All
|
|
660
|
+
* structural CER semantics belong to the SDK.
|
|
661
|
+
*
|
|
662
|
+
* Required fields: executionId, provider, model, prompt, input, output, parameters
|
|
663
|
+
* Seal-time optionals (top-level on the payload): createdAt, signals, meta, declaration
|
|
664
|
+
*/
|
|
665
|
+
function buildSealInputs(raw) {
|
|
666
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
667
|
+
console.error('[nexart] Error: Input must be a JSON object describing an AI execution.');
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
const obj = raw;
|
|
671
|
+
const required = ['executionId', 'provider', 'model', 'prompt', 'input', 'output', 'parameters'];
|
|
672
|
+
const missing = required.filter((k) => obj[k] === undefined || obj[k] === null);
|
|
673
|
+
if (missing.length > 0) {
|
|
674
|
+
console.error(`[nexart] Error: Missing required field(s) for ai seal: ${missing.join(', ')}`);
|
|
675
|
+
console.error('[nexart] Required fields: executionId, provider, model, prompt, input, output, parameters');
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
if (typeof obj.parameters !== 'object' || obj.parameters === null || Array.isArray(obj.parameters)) {
|
|
679
|
+
console.error('[nexart] Error: `parameters` must be an object (AiExecutionParameters).');
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
// Pass the payload through to createSnapshot. The SDK is the schema authority
|
|
683
|
+
// and will throw structured errors on anything it does not accept.
|
|
684
|
+
const snapshotParams = obj;
|
|
685
|
+
const sealOptions = {};
|
|
686
|
+
if (typeof obj.createdAt === 'string')
|
|
687
|
+
sealOptions.createdAt = obj.createdAt;
|
|
688
|
+
if (Array.isArray(obj.signals))
|
|
689
|
+
sealOptions.signals = obj.signals;
|
|
690
|
+
if (obj.meta && typeof obj.meta === 'object' && !Array.isArray(obj.meta)) {
|
|
691
|
+
sealOptions.meta = obj.meta;
|
|
692
|
+
}
|
|
693
|
+
if (obj.declaration && typeof obj.declaration === 'object' && !Array.isArray(obj.declaration)) {
|
|
694
|
+
sealOptions.declaration = obj.declaration;
|
|
695
|
+
}
|
|
696
|
+
return { snapshotParams, sealOptions };
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* nexart ai seal — Build a Certified Execution Record locally.
|
|
700
|
+
*
|
|
701
|
+
* This command is offline-only. It performs ZERO network I/O and ZERO local
|
|
702
|
+
* cryptography: all snapshot construction, canonicalization, and certificate
|
|
703
|
+
* hashing are delegated to @nexart/ai-execution via createSnapshot() and
|
|
704
|
+
* sealCer(). The CLI's job is parsing input, calling the SDK, and formatting
|
|
705
|
+
* output.
|
|
706
|
+
*
|
|
707
|
+
* Output contract:
|
|
708
|
+
* - stdout: the CER bundle JSON (pipe-friendly into `nexart ai verify`)
|
|
709
|
+
* - stderr: a status banner (suppress with --quiet)
|
|
710
|
+
* - --out: also writes the bundle JSON to the given file
|
|
711
|
+
*
|
|
712
|
+
* The sealed bundle is integrity-only. It does NOT contain a node attestation
|
|
713
|
+
* receipt, a node signature, or an envelope. Use `nexart ai certify` (or
|
|
714
|
+
* `attest()` from the SDK) to upgrade a sealed bundle to a certified one.
|
|
715
|
+
*
|
|
716
|
+
* Compatibility guarantee: a sealed bundle from this command verifies as
|
|
717
|
+
* VERIFIED by `nexart ai verify`, by SDK `verifyAiCerBundleDetailed`, and by
|
|
718
|
+
* NexArt Node `/v1/cer/verify` (Integrity layer PASS, Receipt and Envelope
|
|
719
|
+
* layers SKIPPED — that is by design, not a failure).
|
|
720
|
+
*
|
|
721
|
+
* To merge upstream context signals into the bundle, include `signals: [...]`
|
|
722
|
+
* in the input JSON. Signals are bound by the SDK into certificateHash via
|
|
723
|
+
* context.contextHash + signalCount + contextSummary.
|
|
724
|
+
*/
|
|
725
|
+
export async function aiSealCommand(file, opts, stdinReader = readStdinText) {
|
|
726
|
+
const raw = await readInputJson(file, stdinReader);
|
|
727
|
+
const { snapshotParams, sealOptions } = buildSealInputs(raw);
|
|
728
|
+
// Single source of truth: SDK builds the snapshot and seals it.
|
|
729
|
+
let bundle;
|
|
730
|
+
try {
|
|
731
|
+
const snapshot = sdkCreateSnapshot(snapshotParams);
|
|
732
|
+
bundle = sdkSealCer(snapshot, sealOptions);
|
|
733
|
+
}
|
|
734
|
+
catch (e) {
|
|
735
|
+
console.error(`[nexart] Error: SDK seal failed: ${e.message}`);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
// Defensive invariant: a freshly sealed bundle must NOT carry an attestation.
|
|
739
|
+
// If the SDK ever returns one, surface it loudly rather than silently shipping
|
|
740
|
+
// a misclassified artifact.
|
|
741
|
+
if (sdkHasAttestation(bundle)) {
|
|
742
|
+
console.error('[nexart] Error: SDK returned a bundle with an attestation; refusing to label as sealed.');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
const bundleJson = JSON.stringify(bundle, null, 2);
|
|
746
|
+
process.stdout.write(bundleJson + '\n');
|
|
747
|
+
if (opts.out) {
|
|
748
|
+
const outPath = writeFileAndVerify(opts.out, bundleJson);
|
|
749
|
+
console.error(`[nexart] Saved to: ${outPath}`);
|
|
750
|
+
}
|
|
751
|
+
if (!opts.quiet) {
|
|
752
|
+
const certHash = bundle.certificateHash ?? '(missing)';
|
|
753
|
+
console.error('');
|
|
754
|
+
console.error('[nexart] Local CER created');
|
|
755
|
+
console.error('[nexart] State: Sealed (integrity only)');
|
|
756
|
+
console.error('[nexart] No node attestation present');
|
|
757
|
+
console.error('[nexart] This record is not certified — use `nexart ai certify` to attest via a node');
|
|
758
|
+
console.error(`[nexart] certificateHash: ${certHash}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
622
761
|
/**
|
|
623
762
|
* nexart ai create — Read execution input, call POST /v1/cer/ai/create,
|
|
624
763
|
* print CER bundle JSON to stdout, optionally save to --out file.
|
|
@@ -730,94 +869,200 @@ export async function aiCertifyCommand(file, opts, fetchFn = globalThis.fetch, s
|
|
|
730
869
|
}
|
|
731
870
|
}
|
|
732
871
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
872
|
+
* nexart ai verify — Read a CER bundle or CER package JSON from file or stdin,
|
|
873
|
+
* delegate verification to verifyAiCerBundleDetailed() from @nexart/ai-execution,
|
|
874
|
+
* and report the protocol layer results.
|
|
875
|
+
*
|
|
876
|
+
* The CLI does ZERO crypto here. It only:
|
|
877
|
+
* 1. parses input
|
|
878
|
+
* 2. unwraps a CER package to its inner CER bundle (via SDK's isCerPackage / getCerFromPackage)
|
|
879
|
+
* 3. calls verifyAiCerBundleDetailed(bundle)
|
|
880
|
+
* 4. formats the SDK's CerVerificationResult for human / JSON output
|
|
735
881
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
882
|
+
* The three SDK protocol layers are reported INDEPENDENTLY (no aggregation):
|
|
883
|
+
* - bundleIntegrity (Integrity layer: certificate hash recomputation)
|
|
884
|
+
* - nodeSignature (Receipt layer: node attestation signature validity)
|
|
885
|
+
* - receiptConsistency (Envelope layer: bundle/receipt envelope consistency)
|
|
886
|
+
*
|
|
887
|
+
* Each layer is PASS / FAIL / SKIPPED. SDK reason codes are passed through
|
|
888
|
+
* unmodified. Exit code is driven by the SDK's `status` field (VERIFIED → 0,
|
|
889
|
+
* anything else → 1).
|
|
890
|
+
*
|
|
891
|
+
* --json prints the SDK's CerVerificationResult verbatim, plus a minimal
|
|
892
|
+
* `cli` envelope describing input handling. The SDK fields are never rewritten
|
|
893
|
+
* or reinterpreted.
|
|
738
894
|
*/
|
|
739
|
-
function
|
|
740
|
-
const
|
|
741
|
-
if (typeof
|
|
742
|
-
|
|
743
|
-
|
|
895
|
+
export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
896
|
+
const raw = await readInputJson(file, stdinReader);
|
|
897
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
898
|
+
console.error('[nexart] Error: Input is not a valid CER bundle or CER package object');
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
// Detect package vs raw bundle and unwrap. Both helpers are from the SDK,
|
|
902
|
+
// so the CLI stays in sync with however the SDK defines a "package".
|
|
903
|
+
const isPackage = sdkIsCerPackage(raw);
|
|
904
|
+
const inputType = isPackage ? 'package' : 'bundle';
|
|
905
|
+
let bundleForVerification;
|
|
906
|
+
if (isPackage) {
|
|
907
|
+
bundleForVerification = getCerFromPackage(raw);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
bundleForVerification = raw;
|
|
911
|
+
}
|
|
912
|
+
// Delegate ALL verification to the SDK. The CLI does not interpret, fall
|
|
913
|
+
// back, or aggregate. The result object is the protocol's source of truth.
|
|
914
|
+
const result = verifyAiCerBundleDetailed(bundleForVerification);
|
|
915
|
+
if (opts.json) {
|
|
916
|
+
// Pass the SDK result through verbatim. Add only a small `cli` envelope
|
|
917
|
+
// so callers can tell whether the input was a bundle or a package and
|
|
918
|
+
// which CLI version produced the output. SDK fields are NOT modified.
|
|
919
|
+
const output = {
|
|
920
|
+
...result,
|
|
921
|
+
cli: {
|
|
922
|
+
version: CLI_VERSION,
|
|
923
|
+
sdkVersion: getSdkVersion(),
|
|
924
|
+
inputType,
|
|
925
|
+
...(isPackage
|
|
926
|
+
? {
|
|
927
|
+
verifiedInnerCer: true,
|
|
928
|
+
packageTrustLayersVerified: false,
|
|
929
|
+
}
|
|
930
|
+
: {}),
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
if (isPackage) {
|
|
937
|
+
console.log('Input type: CER package');
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
console.log('Input type: CER bundle');
|
|
941
|
+
}
|
|
942
|
+
console.log(`Verification result: ${result.status}`);
|
|
943
|
+
console.log(`bundleType: ${result.bundleType || '(missing)'}`);
|
|
944
|
+
console.log(`certificateHash: ${result.certificateHash || '(missing)'}`);
|
|
945
|
+
console.log('');
|
|
946
|
+
console.log('Layers:');
|
|
947
|
+
console.log(` bundleIntegrity (Integrity): ${result.checks.bundleIntegrity}`);
|
|
948
|
+
console.log(` nodeSignature (Receipt): ${result.checks.nodeSignature}`);
|
|
949
|
+
console.log(` receiptConsistency (Envelope): ${result.checks.receiptConsistency}`);
|
|
950
|
+
if (result.reasonCodes.length > 0) {
|
|
951
|
+
console.log('');
|
|
952
|
+
console.log('Reason codes:');
|
|
953
|
+
for (const code of result.reasonCodes) {
|
|
954
|
+
console.log(` - ${code}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
console.log('');
|
|
958
|
+
console.log(`Verifier: ${result.verifier} (via @nexart/cli ${CLI_VERSION})`);
|
|
959
|
+
console.log(`VerifiedAt: ${result.verifiedAt}`);
|
|
960
|
+
if (isPackage) {
|
|
961
|
+
console.log('');
|
|
962
|
+
console.log('Note: Verified inner CER bundle only. Package-level receipt/signature/envelope not verified by this command.');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
process.exit(result.status === 'VERIFIED' ? 0 : 1);
|
|
744
966
|
}
|
|
745
967
|
/**
|
|
746
|
-
* nexart ai verify — Read a
|
|
747
|
-
*
|
|
748
|
-
*
|
|
968
|
+
* nexart ai project-verify — Read a cer.project.bundle.v1 JSON from file or stdin,
|
|
969
|
+
* delegate all verification to verifyProjectBundle() from @nexart/ai-execution,
|
|
970
|
+
* and report VERIFIED/FAILED.
|
|
971
|
+
*
|
|
972
|
+
* CLI responsibilities:
|
|
973
|
+
* - file/stdin input handling
|
|
974
|
+
* - artifact type routing (early discriminant check for clear user-facing errors)
|
|
975
|
+
* - calling verifyProjectBundle() from @nexart/ai-execution (canonical source of truth)
|
|
976
|
+
* - formatting ProjectBundleVerifyResult for human-readable terminal output
|
|
977
|
+
* - emitting the structured result in JSON mode
|
|
749
978
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
979
|
+
* The CLI does NOT own project bundle schema, project hash computation,
|
|
980
|
+
* per-step verification logic, or any competing implementation of those semantics.
|
|
981
|
+
*
|
|
982
|
+
* Exit code: 0 on VERIFIED, 1 on FAILED or structural error.
|
|
983
|
+
* --json outputs a machine-readable result object.
|
|
753
984
|
*/
|
|
754
|
-
export async function
|
|
985
|
+
export async function aiProjectVerifyCommand(file, opts, stdinReader = readStdinText) {
|
|
755
986
|
const raw = await readInputJson(file, stdinReader);
|
|
756
987
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
757
|
-
console.error('[nexart] Error: Input is not a valid
|
|
988
|
+
console.error('[nexart] Error: Input is not a valid project bundle object');
|
|
758
989
|
process.exit(1);
|
|
759
990
|
}
|
|
760
991
|
const input = raw;
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const bundle = isPackage
|
|
767
|
-
? input['cer']
|
|
768
|
-
: input;
|
|
769
|
-
const bundleType = typeof bundle['bundleType'] === 'string' ? bundle['bundleType'] : '';
|
|
770
|
-
const certificateHash = typeof bundle['certificateHash'] === 'string' ? bundle['certificateHash'] : '';
|
|
771
|
-
if (bundleType !== 'cer.ai.execution.v1') {
|
|
772
|
-
console.error(`[nexart] Error: Unsupported bundleType: ${bundleType}`);
|
|
773
|
-
console.error('[nexart] Expected: cer.ai.execution.v1');
|
|
992
|
+
// Early discriminant check — provides a clear user-facing error before delegating.
|
|
993
|
+
if (input['bundleType'] !== 'cer.project.bundle.v1') {
|
|
994
|
+
const got = typeof input['bundleType'] === 'string' ? input['bundleType'] : '(missing)';
|
|
995
|
+
console.error(`[nexart] Error: Unsupported bundleType: ${got}`);
|
|
996
|
+
console.error('[nexart] Expected: cer.project.bundle.v1');
|
|
774
997
|
process.exit(1);
|
|
775
998
|
}
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
999
|
+
// Delegate all verification logic to @nexart/ai-execution — the canonical source of truth.
|
|
1000
|
+
const result = verifyProjectBundle(raw);
|
|
1001
|
+
const verifiedAt = new Date().toISOString();
|
|
1002
|
+
emitProjectVerifyResult(result, verifiedAt, opts.json, input);
|
|
1003
|
+
process.exit(result.ok ? 0 : 1);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Format a ProjectBundleVerifyResult returned by verifyProjectBundle() from
|
|
1007
|
+
* @nexart/ai-execution into human-readable or JSON terminal output.
|
|
1008
|
+
*
|
|
1009
|
+
* The raw input bundle is accepted only for output-only display fields that
|
|
1010
|
+
* are not carried in ProjectBundleVerifyResult: projectBundleId, projectTitle.
|
|
1011
|
+
* All verification logic and semantic fields come from the result object.
|
|
1012
|
+
*/
|
|
1013
|
+
function emitProjectVerifyResult(result, verifiedAt, json, input) {
|
|
1014
|
+
const status = result.ok ? 'VERIFIED' : 'FAILED';
|
|
1015
|
+
const passedSteps = result.passedSteps ?? result.steps.filter(s => s.ok).length;
|
|
1016
|
+
const failedSteps = result.failedSteps ?? result.steps.filter(s => !s.ok).length;
|
|
1017
|
+
const structuralValid = result.structuralValid ?? result.ok;
|
|
1018
|
+
const projectHashIntegrity = result.projectHashValid === true ? 'PASS' :
|
|
1019
|
+
result.projectHashValid === false ? 'FAIL' :
|
|
1020
|
+
'SKIPPED';
|
|
1021
|
+
const stepIntegrity = result.steps.length === 0 ? 'SKIPPED' :
|
|
1022
|
+
result.steps.every(s => s.ok) ? 'PASS' : 'FAIL';
|
|
1023
|
+
const totalSteps = result.totalSteps ?? (typeof input['totalSteps'] === 'number' ? input['totalSteps'] : null);
|
|
1024
|
+
if (json) {
|
|
784
1025
|
const output = {
|
|
785
1026
|
status,
|
|
1027
|
+
bundleType: 'cer.project.bundle.v1',
|
|
1028
|
+
projectBundleId: typeof input['projectBundleId'] === 'string' ? input['projectBundleId'] : null,
|
|
1029
|
+
projectTitle: typeof input['projectTitle'] === 'string' ? input['projectTitle'] : null,
|
|
1030
|
+
totalSteps,
|
|
1031
|
+
passedSteps,
|
|
1032
|
+
failedSteps,
|
|
1033
|
+
projectHashValid: result.projectHashValid ?? null,
|
|
786
1034
|
checks: {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1035
|
+
structuralValid,
|
|
1036
|
+
projectHashIntegrity,
|
|
1037
|
+
stepIntegrity,
|
|
790
1038
|
},
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
bundleType,
|
|
794
|
-
inputType,
|
|
1039
|
+
steps: result.steps,
|
|
1040
|
+
errors: result.errors,
|
|
795
1041
|
verifiedAt,
|
|
796
1042
|
verifier: '@nexart/cli',
|
|
797
1043
|
};
|
|
798
|
-
if (isPackage) {
|
|
799
|
-
output['verifiedInnerCer'] = true;
|
|
800
|
-
output['packageTrustLayersVerified'] = false;
|
|
801
|
-
}
|
|
802
1044
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
803
1045
|
}
|
|
804
1046
|
else {
|
|
805
|
-
if (isPackage) {
|
|
806
|
-
console.log('Input type: CER package');
|
|
807
|
-
}
|
|
808
1047
|
console.log(`Verification result: ${status}`);
|
|
809
|
-
console.log(`
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1048
|
+
console.log(`bundleType: cer.project.bundle.v1`);
|
|
1049
|
+
if (typeof input['projectBundleId'] === 'string') {
|
|
1050
|
+
console.log(`projectBundleId: ${input['projectBundleId']}`);
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof input['projectTitle'] === 'string') {
|
|
1053
|
+
console.log(`projectTitle: ${input['projectTitle']}`);
|
|
1054
|
+
}
|
|
1055
|
+
if (totalSteps !== null) {
|
|
1056
|
+
console.log(`totalSteps: ${totalSteps}`);
|
|
814
1057
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1058
|
+
console.log(`passedSteps: ${passedSteps}`);
|
|
1059
|
+
console.log(`failedSteps: ${failedSteps}`);
|
|
1060
|
+
console.log(`projectHashIntegrity: ${projectHashIntegrity}`);
|
|
1061
|
+
console.log(`stepIntegrity: ${stepIntegrity}`);
|
|
1062
|
+
for (const err of result.errors) {
|
|
1063
|
+
console.error(`[nexart] Error: ${err}`);
|
|
818
1064
|
}
|
|
819
1065
|
}
|
|
820
|
-
process.exit(integrityPass ? 0 : 1);
|
|
821
1066
|
}
|
|
822
1067
|
// ─── CLI Setup ─────────────────────────────────────────────────────────────
|
|
823
1068
|
const _isMainModule = (() => {
|
|
@@ -1054,7 +1299,38 @@ if (_isMainModule) {
|
|
|
1054
1299
|
}, async (argv) => {
|
|
1055
1300
|
await aiCertifyCommand(argv.file, { out: argv.out, json: argv.json, endpoint: argv.endpoint, apiKey: argv['api-key'], signalsFile: argv['signals-file'] });
|
|
1056
1301
|
})
|
|
1057
|
-
.command('
|
|
1302
|
+
.command('seal [file]', 'Create a Certified Execution Record locally (no node required) — produces an integrity-only bundle for offline workflows, CI, or pre-certification', (yargs) => {
|
|
1303
|
+
return yargs
|
|
1304
|
+
.positional('file', {
|
|
1305
|
+
describe: 'Path to execution input JSON (omit to read from stdin)',
|
|
1306
|
+
type: 'string',
|
|
1307
|
+
})
|
|
1308
|
+
.option('out', {
|
|
1309
|
+
alias: 'o',
|
|
1310
|
+
describe: 'Save sealed CER bundle to this file',
|
|
1311
|
+
type: 'string',
|
|
1312
|
+
})
|
|
1313
|
+
.option('quiet', {
|
|
1314
|
+
alias: 'q',
|
|
1315
|
+
describe: 'Suppress the human-readable status banner on stderr (clean pipe output)',
|
|
1316
|
+
type: 'boolean',
|
|
1317
|
+
default: false,
|
|
1318
|
+
})
|
|
1319
|
+
.example('$0 ai seal execution.json', 'Seal locally and print bundle JSON')
|
|
1320
|
+
.example('cat execution.json | $0 ai seal --quiet | $0 ai verify', 'Seal then verify in a pipe')
|
|
1321
|
+
.example('$0 ai seal execution.json --out cer.json', 'Save sealed bundle to file')
|
|
1322
|
+
.example('$0 ai seal execution.json --quiet > cer.json', 'Redirect bundle JSON only')
|
|
1323
|
+
.epilogue([
|
|
1324
|
+
'Sealed (this command): integrity only. No node receipt, no signature, no envelope.',
|
|
1325
|
+
'Certified (ai certify): integrity + node attestation receipt + signature.',
|
|
1326
|
+
'',
|
|
1327
|
+
'A sealed bundle verifies as VERIFIED with Receipt and Envelope reported as SKIPPED.',
|
|
1328
|
+
'That is correct, not a failure: only the Integrity layer applies to local artifacts.',
|
|
1329
|
+
].join('\n'));
|
|
1330
|
+
}, async (argv) => {
|
|
1331
|
+
await aiSealCommand(argv.file, { out: argv.out, quiet: argv.quiet });
|
|
1332
|
+
})
|
|
1333
|
+
.command('verify [file]', 'Verify a CER bundle or CER package locally via @nexart/ai-execution (no network required)', (yargs) => {
|
|
1058
1334
|
return yargs
|
|
1059
1335
|
.positional('file', {
|
|
1060
1336
|
describe: 'Path to CER bundle or CER package JSON (omit to read from stdin)',
|
|
@@ -1073,23 +1349,55 @@ if (_isMainModule) {
|
|
|
1073
1349
|
}, async (argv) => {
|
|
1074
1350
|
await aiVerifyCommand(argv.file, { json: argv.json });
|
|
1075
1351
|
})
|
|
1076
|
-
.
|
|
1352
|
+
.command('project-verify [file]', 'Verify a project bundle (cer.project.bundle.v1) locally (no network required)', (yargs) => {
|
|
1353
|
+
return yargs
|
|
1354
|
+
.positional('file', {
|
|
1355
|
+
describe: 'Path to project bundle JSON (omit to read from stdin)',
|
|
1356
|
+
type: 'string',
|
|
1357
|
+
})
|
|
1358
|
+
.option('json', {
|
|
1359
|
+
alias: 'j',
|
|
1360
|
+
describe: 'Print machine-readable JSON result',
|
|
1361
|
+
type: 'boolean',
|
|
1362
|
+
default: false,
|
|
1363
|
+
})
|
|
1364
|
+
.example('$0 ai project-verify bundle.json', 'Verify a project bundle file')
|
|
1365
|
+
.example('cat bundle.json | $0 ai project-verify', 'Verify from stdin')
|
|
1366
|
+
.example('$0 ai project-verify bundle.json --json', 'Machine-readable output');
|
|
1367
|
+
}, async (argv) => {
|
|
1368
|
+
await aiProjectVerifyCommand(argv.file, { json: argv.json });
|
|
1369
|
+
})
|
|
1370
|
+
.demandCommand(1, 'You must provide a subcommand: create, certify, verify, or project-verify')
|
|
1077
1371
|
.help();
|
|
1078
1372
|
}, () => { })
|
|
1079
1373
|
.demandCommand(1, 'You must provide a command')
|
|
1080
1374
|
.help()
|
|
1081
|
-
.version(CLI_VERSION
|
|
1375
|
+
.version(`@nexart/cli ${CLI_VERSION}
|
|
1376
|
+
@nexart/ai-execution ${getSdkVersion()}
|
|
1377
|
+
codemode protocol ${PROTOCOL_VERSION}`)
|
|
1082
1378
|
.epilog(`
|
|
1083
1379
|
Environment Variables:
|
|
1084
1380
|
NEXART_RENDERER_ENDPOINT Remote renderer URL (default: http://localhost:5000)
|
|
1085
1381
|
NEXART_NODE_ENDPOINT NexArt node API URL (default: https://node.nexart.art)
|
|
1086
1382
|
NEXART_API_KEY API key for authenticated requests
|
|
1087
1383
|
|
|
1088
|
-
Output Files:
|
|
1384
|
+
Output Files (Code Mode):
|
|
1089
1385
|
--out ./out.png creates:
|
|
1090
1386
|
./out.png - The rendered PNG image
|
|
1091
1387
|
./out.snapshot.json - Snapshot for replay/verify
|
|
1092
1388
|
|
|
1389
|
+
AI verification (ai verify, ai project-verify):
|
|
1390
|
+
All cryptographic verification is delegated to @nexart/ai-execution
|
|
1391
|
+
(verifyAiCerBundleDetailed, verifyProjectBundle). The CLI itself implements
|
|
1392
|
+
no hashing or canonicalization and produces results that match the SDK,
|
|
1393
|
+
NexArt Node (/v1/cer/verify), and verify.nexart.io exactly.
|
|
1394
|
+
|
|
1395
|
+
ai verify reports the three SDK protocol layers independently:
|
|
1396
|
+
- bundleIntegrity (Integrity: certificate hash recomputation)
|
|
1397
|
+
- nodeSignature (Receipt: node attestation signature validity)
|
|
1398
|
+
- receiptConsistency (Envelope: bundle/receipt envelope consistency)
|
|
1399
|
+
No aggregation, no reinterpretation, SDK reason codes preserved verbatim.
|
|
1400
|
+
|
|
1093
1401
|
Examples:
|
|
1094
1402
|
# Run with remote renderer
|
|
1095
1403
|
npx @nexart/cli run ./sketch.js --seed 123 --include-code --out ./out.png
|
|
@@ -1109,11 +1417,17 @@ Examples:
|
|
|
1109
1417
|
# AI: Certify with context signals
|
|
1110
1418
|
npx @nexart/cli ai certify execution.json --signals-file signals.json
|
|
1111
1419
|
|
|
1112
|
-
# AI: Verify a raw CER bundle locally (
|
|
1420
|
+
# AI: Verify a raw CER bundle locally (delegates to SDK)
|
|
1113
1421
|
npx @nexart/cli ai verify cer.json
|
|
1114
1422
|
|
|
1115
|
-
# AI: Verify a CER package (inner bundle verified;
|
|
1423
|
+
# AI: Verify a CER package (inner bundle verified via SDK; package envelope not)
|
|
1116
1424
|
npx @nexart/cli ai verify cer-package.json
|
|
1425
|
+
|
|
1426
|
+
# AI: Verify a project bundle locally (no network required)
|
|
1427
|
+
npx @nexart/cli ai project-verify project-bundle.json
|
|
1428
|
+
|
|
1429
|
+
# AI: Verify a project bundle with machine-readable JSON output
|
|
1430
|
+
npx @nexart/cli ai project-verify project-bundle.json --json
|
|
1117
1431
|
`)
|
|
1118
1432
|
.parse();
|
|
1119
1433
|
}
|