@aiwg/cli 2026.8.11 → 2026.8.13
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/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +6 -0
- package/dist/src/api/index.js +6 -0
- package/dist/src/cli/handlers/artifact-verify.js +171 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/setup-manifest.js +52 -3
- package/dist/src/cli/handlers/setup.js +15 -2
- package/dist/src/cli/handlers/use.js +71 -6
- package/dist/src/cli/scope-resolver.js +6 -1
- package/dist/src/cli/services/deployment-verification.js +65 -7
- package/dist/src/config/aiwg-config.js +4 -3
- package/dist/src/config/cli.js +3 -1
- package/dist/src/config/gitignore.js +67 -21
- package/dist/src/config/workspace.js +8 -1
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-quickref.js +9 -0
- package/dist/src/marketplace/artifact-attestation.js +195 -0
- package/dist/src/marketplace/exchange.js +437 -79
- package/dist/src/marketplace/provenance-types.js +1 -0
- package/dist/src/marketplace/provenance.js +7 -1
- package/dist/src/providers/hermes-home.js +20 -0
- package/dist/src/providers/provider-definitions.js +5 -4
- package/dist/src/providers/transformation-receipt-integration.js +448 -0
- package/dist/src/providers/transformation-receipt.js +215 -0
- package/dist/src/resources/web-release.d.ts +11 -0
- package/dist/src/resources/web-release.js +61 -6
- package/dist/src/security/artifact-attestation.js +117 -0
- package/dist/src/security/artifact-trust.js +557 -0
- package/dist/src/security/artifact-verifier.js +478 -0
- package/dist/src/skills/deployer.js +5 -1
- package/dist/src/tracker/capability-protocol.js +7 -2
- package/package.json +5 -1
- package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
- package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
- package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
- package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
- package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
- package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
- package/tools/agents/deploy-agents.mjs +8 -2
- package/tools/agents/providers/base.mjs +29 -2
- package/tools/agents/providers/hermes.mjs +163 -19
|
@@ -11,8 +11,10 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { setPackageEntry } from '../packages/package-registry.js';
|
|
13
13
|
import { discoverInstallablePackage } from '../packages/package-discovery.js';
|
|
14
|
+
import { parseTrustRoot, sha256 as artifactSha256, } from '../security/artifact-trust.js';
|
|
15
|
+
import { serializeMarketplaceAttestation, verifyMarketplaceEvidence, } from './artifact-attestation.js';
|
|
14
16
|
import { buildFortemiEnvelopeShard, canonicalJson, createOperationReceipt, createPackageLock, createProvenanceEnvelope, inventoryDirectory, inventorySha256, sha256, signProvenanceEnvelope, signCanonicalDocument, validateProvenanceEnvelope, verifyDocumentTrust, verifyFortemiEnvelopeShard, verifyProvenanceEnvelope, } from './provenance.js';
|
|
15
|
-
import { MARKETPLACE_BUNDLE_SCHEMA, MARKETPLACE_CATALOG_REGISTRY_SCHEMA, MARKETPLACE_CATALOG_SCHEMA, MARKETPLACE_INDEX_SCHEMA, MARKETPLACE_TRUST_SCHEMA, } from './provenance-types.js';
|
|
17
|
+
import { MARKETPLACE_BUNDLE_SCHEMA, MARKETPLACE_BUNDLE_V2_SCHEMA, MARKETPLACE_CATALOG_REGISTRY_SCHEMA, MARKETPLACE_CATALOG_SCHEMA, MARKETPLACE_INDEX_SCHEMA, MARKETPLACE_TRUST_SCHEMA, } from './provenance-types.js';
|
|
16
18
|
const STATE_DIR = 'marketplace';
|
|
17
19
|
const INDEX_FILE = 'index.json';
|
|
18
20
|
const TRUST_FILE = 'trust.json';
|
|
@@ -146,11 +148,23 @@ export async function recordInstalledPackage(options) {
|
|
|
146
148
|
const lockPath = path.join(packageDir, 'lock.json');
|
|
147
149
|
const receiptPath = path.join(packageDir, 'receipts', `${safeDigestName(options.receipt.receiptId)}.json`);
|
|
148
150
|
const shardPath = options.fortemiShard ? path.join(packageDir, 'provenance.full-v1.shard') : undefined;
|
|
151
|
+
const attestationPath = options.crossAsset ? path.join(packageDir, 'cross-asset', 'attestation.json') : undefined;
|
|
152
|
+
const materialPaths = options.crossAsset
|
|
153
|
+
? Object.fromEntries(options.crossAsset.materials.map((material, index) => [
|
|
154
|
+
material.uri,
|
|
155
|
+
path.join(packageDir, 'cross-asset', 'materials', `${String(index).padStart(4, '0')}-${artifactSha256(material.bytes)}.material`),
|
|
156
|
+
]))
|
|
157
|
+
: undefined;
|
|
158
|
+
if (options.crossAsset && Object.keys(materialPaths).length !== options.crossAsset.materials.length) {
|
|
159
|
+
throw new Error('Cross-asset material URIs must be unique');
|
|
160
|
+
}
|
|
149
161
|
await Promise.all([
|
|
150
162
|
atomicWrite(envelopePath, `${canonicalJson(options.envelope)}\n`),
|
|
151
163
|
atomicWrite(lockPath, `${canonicalJson(options.lock)}\n`),
|
|
152
164
|
atomicWrite(receiptPath, `${canonicalJson(options.receipt)}\n`),
|
|
153
165
|
...(shardPath && options.fortemiShard ? [atomicWrite(shardPath, options.fortemiShard)] : []),
|
|
166
|
+
...(attestationPath && options.crossAsset ? [atomicWrite(attestationPath, serializeMarketplaceAttestation(options.crossAsset))] : []),
|
|
167
|
+
...((options.crossAsset && materialPaths) ? options.crossAsset.materials.map(material => atomicWrite(materialPaths[material.uri], material.bytes)) : []),
|
|
154
168
|
]);
|
|
155
169
|
const index = await readMarketplaceIndex(options);
|
|
156
170
|
const existing = index.packages[options.lock.lockId];
|
|
@@ -159,6 +173,11 @@ export async function recordInstalledPackage(options) {
|
|
|
159
173
|
envelopePath,
|
|
160
174
|
receiptPaths: [...new Set([...(existing?.receiptPaths ?? []), receiptPath])].sort(),
|
|
161
175
|
...(shardPath ? { fortemiShardPath: shardPath } : existing?.fortemiShardPath ? { fortemiShardPath: existing.fortemiShardPath } : {}),
|
|
176
|
+
...(attestationPath ? { attestationPath } : existing?.attestationPath ? { attestationPath: existing.attestationPath } : {}),
|
|
177
|
+
...(materialPaths ? { materialPaths } : existing?.materialPaths ? { materialPaths: existing.materialPaths } : {}),
|
|
178
|
+
...(options.dependencyLockIds
|
|
179
|
+
? { dependencyLockIds: [...new Set(options.dependencyLockIds)].sort() }
|
|
180
|
+
: existing?.dependencyLockIds ? { dependencyLockIds: existing.dependencyLockIds } : {}),
|
|
162
181
|
cachePath: options.cachePath,
|
|
163
182
|
artifactPath: options.artifactPath,
|
|
164
183
|
installedAt: options.receipt.occurredAt,
|
|
@@ -277,6 +296,16 @@ async function portableFiles(root) {
|
|
|
277
296
|
contentBase64: (await readFile(path.join(root, ...entry.path.split('/')))).toString('base64'),
|
|
278
297
|
})));
|
|
279
298
|
}
|
|
299
|
+
function decodeStrictBase64(value, label, allowEmpty = false) {
|
|
300
|
+
if ((!allowEmpty && value.length === 0)
|
|
301
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
302
|
+
throw new Error(`${label} is not canonical base64`);
|
|
303
|
+
}
|
|
304
|
+
const bytes = Buffer.from(value, 'base64');
|
|
305
|
+
if (bytes.toString('base64') !== value)
|
|
306
|
+
throw new Error(`${label} is not canonical base64`);
|
|
307
|
+
return bytes;
|
|
308
|
+
}
|
|
280
309
|
function validatePortableFiles(bundle) {
|
|
281
310
|
if (!Array.isArray(bundle.files) || bundle.files.length === 0)
|
|
282
311
|
throw new Error('Portable marketplace bundle has no package files');
|
|
@@ -285,7 +314,9 @@ function validatePortableFiles(bundle) {
|
|
|
285
314
|
throw new Error('Portable bundle file inventory diverges from the signed envelope');
|
|
286
315
|
}
|
|
287
316
|
for (const file of bundle.files) {
|
|
288
|
-
const bytes =
|
|
317
|
+
const bytes = bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA
|
|
318
|
+
? decodeStrictBase64(file.contentBase64, `Portable file '${file.path}' content`, true)
|
|
319
|
+
: Buffer.from(file.contentBase64, 'base64');
|
|
289
320
|
if (bytes.byteLength !== file.bytes || sha256(bytes) !== file.sha256) {
|
|
290
321
|
throw new Error(`Portable bundle content digest mismatch for '${file.path}'`);
|
|
291
322
|
}
|
|
@@ -294,6 +325,111 @@ function validatePortableFiles(bundle) {
|
|
|
294
325
|
throw new Error('Portable bundle artifact digest does not match the provenance envelope');
|
|
295
326
|
}
|
|
296
327
|
}
|
|
328
|
+
function toPortableCrossAsset(evidence) {
|
|
329
|
+
return {
|
|
330
|
+
attestation: evidence.attestation,
|
|
331
|
+
materials: evidence.materials.map(material => ({
|
|
332
|
+
name: material.name,
|
|
333
|
+
uri: material.uri,
|
|
334
|
+
...(material.mediaType ? { mediaType: material.mediaType } : {}),
|
|
335
|
+
bytes: material.bytes.byteLength,
|
|
336
|
+
sha256: artifactSha256(material.bytes),
|
|
337
|
+
contentBase64: Buffer.from(material.bytes).toString('base64'),
|
|
338
|
+
})),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function fromPortableCrossAsset(crossAsset) {
|
|
342
|
+
return {
|
|
343
|
+
attestation: crossAsset.attestation,
|
|
344
|
+
materials: crossAsset.materials.map(material => ({
|
|
345
|
+
name: material.name,
|
|
346
|
+
uri: material.uri,
|
|
347
|
+
...(material.mediaType ? { mediaType: material.mediaType } : {}),
|
|
348
|
+
bytes: new Uint8Array(decodeStrictBase64(material.contentBase64, `Portable material '${material.uri}' content`)),
|
|
349
|
+
})),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function canonicalMaterial(value) {
|
|
353
|
+
return Buffer.from(`${canonicalJson(value)}\n`, 'utf8');
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Produce the only material byte representation accepted by portable v2.
|
|
357
|
+
* The Git/SBOM/license entries use explicit deterministic descriptors when
|
|
358
|
+
* their source bytes are not themselves an archive member.
|
|
359
|
+
*/
|
|
360
|
+
export function marketplacePortableBindingMaterials(options) {
|
|
361
|
+
if (options.receipts.length === 0)
|
|
362
|
+
throw new Error('Marketplace binding requires at least one receipt');
|
|
363
|
+
const boundReceipt = options.receipts.find(receipt => receipt.lockId === options.lock.lockId);
|
|
364
|
+
if (!boundReceipt)
|
|
365
|
+
throw new Error(`Marketplace binding has no receipt for ${options.lock.lockId}`);
|
|
366
|
+
const sbom = options.envelope.package.sbom;
|
|
367
|
+
let sbomBytes;
|
|
368
|
+
if (sbom) {
|
|
369
|
+
const file = options.files.find(candidate => candidate.path === sbom.path);
|
|
370
|
+
if (!file)
|
|
371
|
+
throw new Error(`Marketplace SBOM '${sbom.path}' is absent from the portable inventory`);
|
|
372
|
+
sbomBytes = new Uint8Array(Buffer.from(file.contentBase64, 'base64'));
|
|
373
|
+
if (sha256(sbomBytes) !== sbom.sha256)
|
|
374
|
+
throw new Error(`Marketplace SBOM '${sbom.path}' does not match its envelope digest`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
sbomBytes = canonicalMaterial({ present: false });
|
|
378
|
+
}
|
|
379
|
+
if (artifactSha256(options.gitTreeBytes) !== options.envelope.source.treeSha256) {
|
|
380
|
+
throw new Error('Marketplace Git tree bytes do not match the envelope treeSha256');
|
|
381
|
+
}
|
|
382
|
+
return [
|
|
383
|
+
{ name: 'lock', uri: 'aiwg:marketplace-material:lock', mediaType: 'application/vnd.aiwg.marketplace-lock.v1+json', bytes: canonicalMaterial(options.lock) },
|
|
384
|
+
{ name: 'inventory', uri: 'aiwg:marketplace-material:inventory', mediaType: 'application/json', bytes: canonicalMaterial(options.envelope.package.inventory) },
|
|
385
|
+
{
|
|
386
|
+
name: 'git-tree',
|
|
387
|
+
uri: 'aiwg:marketplace-material:git-tree',
|
|
388
|
+
mediaType: 'application/vnd.aiwg.git-ls-tree.v1',
|
|
389
|
+
bytes: options.gitTreeBytes,
|
|
390
|
+
},
|
|
391
|
+
{ name: 'fortemi-shard', uri: 'aiwg:marketplace-material:fortemi-shard', mediaType: 'application/vnd.fortemi.index.full-v1', bytes: options.fortemiShard },
|
|
392
|
+
{ name: 'receipt', uri: 'aiwg:marketplace-material:receipt', mediaType: 'application/vnd.aiwg.marketplace-receipt.v1+json', bytes: canonicalMaterial(boundReceipt) },
|
|
393
|
+
{
|
|
394
|
+
name: 'sbom',
|
|
395
|
+
uri: 'aiwg:marketplace-material:sbom',
|
|
396
|
+
mediaType: sbom?.format ?? 'application/vnd.aiwg.absent-descriptor.v1+json',
|
|
397
|
+
bytes: sbomBytes,
|
|
398
|
+
},
|
|
399
|
+
{ name: 'license', uri: 'aiwg:marketplace-material:license', mediaType: 'application/vnd.aiwg.license-descriptor.v1+json', bytes: canonicalMaterial({ license: options.envelope.package.license }) },
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
function validatePortableMaterialBindings(bundle, evidence, shard) {
|
|
403
|
+
const gitTree = evidence.materials.find(material => material.name === 'git-tree');
|
|
404
|
+
if (!gitTree)
|
|
405
|
+
throw new Error("Portable crossAsset evidence requires 'git-tree' material");
|
|
406
|
+
const expected = marketplacePortableBindingMaterials({
|
|
407
|
+
envelope: bundle.envelope,
|
|
408
|
+
lock: bundle.lock,
|
|
409
|
+
receipts: bundle.receipts,
|
|
410
|
+
fortemiShard: shard,
|
|
411
|
+
gitTreeBytes: gitTree.bytes,
|
|
412
|
+
files: bundle.files,
|
|
413
|
+
});
|
|
414
|
+
const actual = new Map(evidence.materials.map(material => [material.name, material]));
|
|
415
|
+
for (const binding of expected) {
|
|
416
|
+
const material = actual.get(binding.name);
|
|
417
|
+
if (!material)
|
|
418
|
+
throw new Error(`Portable crossAsset evidence requires '${binding.name}' material`);
|
|
419
|
+
if (material.uri !== binding.uri || material.mediaType !== binding.mediaType) {
|
|
420
|
+
throw new Error(`Portable crossAsset '${binding.name}' material descriptor is not canonical`);
|
|
421
|
+
}
|
|
422
|
+
if (binding.name === 'receipt') {
|
|
423
|
+
const matchesReceipt = bundle.receipts.some(receipt => (receipt.lockId === bundle.lock.lockId && Buffer.from(material.bytes).equals(canonicalMaterial(receipt))));
|
|
424
|
+
if (!matchesReceipt)
|
|
425
|
+
throw new Error('Portable crossAsset receipt material does not bind a bundled receipt');
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (!Buffer.from(material.bytes).equals(Buffer.from(binding.bytes))) {
|
|
429
|
+
throw new Error(`Portable crossAsset '${binding.name}' material does not bind the bundle evidence`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
297
433
|
export async function exportPortablePackage(options) {
|
|
298
434
|
const indexed = await findIndexedPackage(options.query, options);
|
|
299
435
|
if (!indexed)
|
|
@@ -317,14 +453,33 @@ export async function exportPortablePackage(options) {
|
|
|
317
453
|
conformance: fortemi.conformance,
|
|
318
454
|
});
|
|
319
455
|
const receipts = await Promise.all(indexed.receiptPaths.map(readReceipt));
|
|
320
|
-
const
|
|
321
|
-
schemaVersion: MARKETPLACE_BUNDLE_SCHEMA,
|
|
456
|
+
const common = {
|
|
322
457
|
envelope,
|
|
323
458
|
lock: indexed.lock,
|
|
324
459
|
receipts: [...receipts, receipt],
|
|
325
460
|
fortemiShardBase64: Buffer.from(fortemi.archive).toString('base64'),
|
|
326
461
|
files: await portableFiles(indexed.artifactPath),
|
|
327
462
|
};
|
|
463
|
+
const bundle = options.crossAsset
|
|
464
|
+
? {
|
|
465
|
+
schemaVersion: MARKETPLACE_BUNDLE_V2_SCHEMA,
|
|
466
|
+
...common,
|
|
467
|
+
crossAsset: toPortableCrossAsset(options.crossAsset),
|
|
468
|
+
dependencies: options.dependencies ?? [],
|
|
469
|
+
}
|
|
470
|
+
: { schemaVersion: MARKETPLACE_BUNDLE_SCHEMA, ...common };
|
|
471
|
+
let nextArtifactTrustState;
|
|
472
|
+
if (bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA) {
|
|
473
|
+
if (!options.artifactTrust)
|
|
474
|
+
throw new Error('v2 portable export requires scoped cross-asset trust root and state');
|
|
475
|
+
validatePortableBundle(bundle);
|
|
476
|
+
const verifiedTree = await verifyPortableBundleTree(bundle, {
|
|
477
|
+
artifactTrust: options.artifactTrust,
|
|
478
|
+
trustStore: options.trustStore,
|
|
479
|
+
requireLegacySignature: true,
|
|
480
|
+
});
|
|
481
|
+
nextArtifactTrustState = verifiedTree.nextState;
|
|
482
|
+
}
|
|
328
483
|
const output = path.resolve(options.output);
|
|
329
484
|
await atomicWrite(output, `${canonicalJson(bundle)}\n`);
|
|
330
485
|
await recordInstalledPackage({
|
|
@@ -336,13 +491,28 @@ export async function exportPortablePackage(options) {
|
|
|
336
491
|
artifactPath: indexed.artifactPath,
|
|
337
492
|
verificationStatus: indexed.verificationStatus,
|
|
338
493
|
fortemiShard: fortemi.archive,
|
|
494
|
+
...(options.crossAsset ? { crossAsset: options.crossAsset } : {}),
|
|
495
|
+
...(bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA
|
|
496
|
+
? { dependencyLockIds: bundle.dependencies.map(dependency => dependency.lock.lockId) }
|
|
497
|
+
: {}),
|
|
339
498
|
});
|
|
340
|
-
return { bundle, output, receipt };
|
|
499
|
+
return { bundle, output, receipt, ...(nextArtifactTrustState ? { nextArtifactTrustState } : {}) };
|
|
500
|
+
}
|
|
501
|
+
function exactPortableKeys(value, allowed, label) {
|
|
502
|
+
const accepted = new Set(allowed);
|
|
503
|
+
const extras = Object.keys(value).filter(key => !accepted.has(key));
|
|
504
|
+
if (extras.length)
|
|
505
|
+
throw new Error(`${label} contains unknown required field(s): ${extras.join(', ')}`);
|
|
341
506
|
}
|
|
342
507
|
function validatePortableBundle(value) {
|
|
343
|
-
if (!isRecord(value) || value.schemaVersion
|
|
508
|
+
if (!isRecord(value) || ![MARKETPLACE_BUNDLE_SCHEMA, MARKETPLACE_BUNDLE_V2_SCHEMA].includes(value.schemaVersion)) {
|
|
344
509
|
throw new Error('Unsupported portable marketplace bundle schema');
|
|
345
|
-
|
|
510
|
+
}
|
|
511
|
+
const isV2 = value.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA;
|
|
512
|
+
const allowed = new Set([
|
|
513
|
+
'schemaVersion', 'envelope', 'lock', 'receipts', 'fortemiShardBase64', 'files',
|
|
514
|
+
...(isV2 ? ['crossAsset', 'dependencies'] : []),
|
|
515
|
+
]);
|
|
346
516
|
const extras = Object.keys(value).filter((key) => !allowed.has(key));
|
|
347
517
|
if (extras.length)
|
|
348
518
|
throw new Error(`Portable bundle contains unknown required field(s): ${extras.join(', ')}`);
|
|
@@ -351,10 +521,151 @@ function validatePortableBundle(value) {
|
|
|
351
521
|
throw new Error('Portable bundle lock is invalid');
|
|
352
522
|
if (!Array.isArray(value.receipts) || !Array.isArray(value.files) || typeof value.fortemiShardBase64 !== 'string')
|
|
353
523
|
throw new Error('Portable bundle is incomplete');
|
|
524
|
+
if (!isV2)
|
|
525
|
+
return;
|
|
526
|
+
decodeStrictBase64(value.fortemiShardBase64, 'v2 portable Fortemi shard');
|
|
527
|
+
if (!isRecord(value.crossAsset) || !Array.isArray(value.crossAsset.materials) || !isRecord(value.crossAsset.attestation)) {
|
|
528
|
+
throw new Error('v2 portable bundle crossAsset evidence is incomplete');
|
|
529
|
+
}
|
|
530
|
+
exactPortableKeys(value.crossAsset, ['attestation', 'materials'], 'Portable crossAsset evidence');
|
|
531
|
+
const uris = new Set();
|
|
532
|
+
const materialNames = new Set();
|
|
533
|
+
for (const [index, material] of value.crossAsset.materials.entries()) {
|
|
534
|
+
if (!isRecord(material))
|
|
535
|
+
throw new Error(`Portable crossAsset material ${index} is malformed`);
|
|
536
|
+
exactPortableKeys(material, ['name', 'uri', 'mediaType', 'bytes', 'sha256', 'contentBase64'], `Portable crossAsset material ${index}`);
|
|
537
|
+
if (typeof material.name !== 'string' || !material.name
|
|
538
|
+
|| typeof material.uri !== 'string' || !material.uri
|
|
539
|
+
|| (material.mediaType !== undefined && (typeof material.mediaType !== 'string' || !material.mediaType))
|
|
540
|
+
|| !Number.isSafeInteger(material.bytes) || Number(material.bytes) < 0
|
|
541
|
+
|| typeof material.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(material.sha256)
|
|
542
|
+
|| typeof material.contentBase64 !== 'string') {
|
|
543
|
+
throw new Error(`Portable crossAsset material ${index} is incomplete`);
|
|
544
|
+
}
|
|
545
|
+
decodeStrictBase64(material.contentBase64, `Portable crossAsset material ${index}`);
|
|
546
|
+
if (uris.has(material.uri))
|
|
547
|
+
throw new Error(`Portable crossAsset material URI '${material.uri}' is duplicated`);
|
|
548
|
+
if (materialNames.has(material.name))
|
|
549
|
+
throw new Error(`Portable crossAsset material name '${material.name}' is duplicated`);
|
|
550
|
+
uris.add(material.uri);
|
|
551
|
+
materialNames.add(material.name);
|
|
552
|
+
}
|
|
553
|
+
const names = new Set(value.crossAsset.materials.map(material => material.name));
|
|
554
|
+
for (const required of ['lock', 'inventory', 'git-tree', 'fortemi-shard', 'receipt', 'sbom', 'license']) {
|
|
555
|
+
if (!names.has(required))
|
|
556
|
+
throw new Error(`v2 portable crossAsset evidence requires '${required}' material`);
|
|
557
|
+
}
|
|
558
|
+
if (!Array.isArray(value.dependencies))
|
|
559
|
+
throw new Error('v2 portable bundle dependencies must be an array');
|
|
560
|
+
value.dependencies.forEach(validatePortableBundle);
|
|
354
561
|
}
|
|
355
562
|
async function verifyFortemiBundle(envelope, bytes) {
|
|
356
563
|
await verifyFortemiEnvelopeShard(envelope, bytes);
|
|
357
564
|
}
|
|
565
|
+
async function verifyPortableBundleTree(root, options) {
|
|
566
|
+
const artifactRoot = parseTrustRoot(options.artifactTrust.rootBytes);
|
|
567
|
+
const marketplacePolicy = artifactRoot.signed.policy.marketplace ?? {
|
|
568
|
+
evidenceMode: 'marketplace-only',
|
|
569
|
+
legacySignatureMigrationGate: false,
|
|
570
|
+
recursiveDependencies: 'if-present',
|
|
571
|
+
};
|
|
572
|
+
const results = new Map();
|
|
573
|
+
const visiting = new Set();
|
|
574
|
+
let nextState = options.artifactTrust.state;
|
|
575
|
+
const visit = async (bundle) => {
|
|
576
|
+
validatePortableFiles(bundle);
|
|
577
|
+
const expectedLock = createPackageLock(bundle.envelope, bundle.lock.createdAt);
|
|
578
|
+
if (canonicalJson(expectedLock) !== canonicalJson(bundle.lock))
|
|
579
|
+
throw new Error('Portable bundle lock does not match its provenance envelope');
|
|
580
|
+
const shard = new Uint8Array(Buffer.from(bundle.fortemiShardBase64, 'base64'));
|
|
581
|
+
await verifyFortemiBundle(bundle.envelope, shard);
|
|
582
|
+
if (results.has(bundle.lock.lockId))
|
|
583
|
+
return;
|
|
584
|
+
if (visiting.has(bundle.lock.lockId))
|
|
585
|
+
throw new Error(`Portable dependency cycle detected at ${bundle.lock.lockId}`);
|
|
586
|
+
visiting.add(bundle.lock.lockId);
|
|
587
|
+
let verification;
|
|
588
|
+
if (bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA) {
|
|
589
|
+
const evidence = fromPortableCrossAsset(bundle.crossAsset);
|
|
590
|
+
for (const material of bundle.crossAsset.materials) {
|
|
591
|
+
const bytes = evidence.materials.find(candidate => candidate.uri === material.uri).bytes;
|
|
592
|
+
if (bytes.byteLength !== material.bytes || artifactSha256(bytes) !== material.sha256) {
|
|
593
|
+
throw new Error(`Portable crossAsset material digest mismatch for '${material.uri}'`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
validatePortableMaterialBindings(bundle, evidence, shard);
|
|
597
|
+
const composite = await verifyMarketplaceEvidence({
|
|
598
|
+
envelope: bundle.envelope,
|
|
599
|
+
trustStore: options.trustStore,
|
|
600
|
+
// Legacy verification authenticates the signed lock declarations here;
|
|
601
|
+
// the recursive closure below proves the declared children are present
|
|
602
|
+
// and unsubstituted before any persistence.
|
|
603
|
+
installedLocks: Object.fromEntries(bundle.envelope.package.dependencies
|
|
604
|
+
.filter(dependency => dependency.lockId)
|
|
605
|
+
.map(dependency => [dependency.identity, dependency.lockId])),
|
|
606
|
+
artifact: {
|
|
607
|
+
evidence,
|
|
608
|
+
rootBytes: options.artifactTrust.rootBytes,
|
|
609
|
+
state: nextState,
|
|
610
|
+
now: options.artifactTrust.now,
|
|
611
|
+
offline: true,
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
if (!composite.ok)
|
|
615
|
+
throw new Error(`Portable package verification failed: ${composite.errors.join('; ')}`);
|
|
616
|
+
if (!composite.crossAsset?.nextState)
|
|
617
|
+
throw new Error('Cross-asset verifier did not return advanced freshness state');
|
|
618
|
+
nextState = composite.crossAsset.nextState;
|
|
619
|
+
verification = composite.marketplace;
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
if (marketplacePolicy.evidenceMode !== 'marketplace-only') {
|
|
623
|
+
throw new Error(`Portable dependency ${bundle.lock.lockId} lacks cross-asset evidence required by '${marketplacePolicy.evidenceMode}' policy`);
|
|
624
|
+
}
|
|
625
|
+
verification = await verifyProvenanceEnvelope({
|
|
626
|
+
envelope: bundle.envelope,
|
|
627
|
+
trustStore: options.trustStore,
|
|
628
|
+
policy: options.requireLegacySignature ? { requireSignature: true, allowIntegrityOnly: false } : undefined,
|
|
629
|
+
});
|
|
630
|
+
if (!verification.ok)
|
|
631
|
+
throw new Error(`Portable package verification failed: ${verification.errors.join('; ')}`);
|
|
632
|
+
}
|
|
633
|
+
const children = bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA ? bundle.dependencies : [];
|
|
634
|
+
const childByLock = new Map(children.map(child => [child.lock.lockId, child]));
|
|
635
|
+
if (childByLock.size !== children.length)
|
|
636
|
+
throw new Error(`Portable bundle ${bundle.lock.lockId} contains duplicate dependencies`);
|
|
637
|
+
const requiredLocks = new Set();
|
|
638
|
+
for (const dependency of bundle.envelope.package.dependencies.filter(candidate => !candidate.optional)) {
|
|
639
|
+
if (!dependency.lockId) {
|
|
640
|
+
if (marketplacePolicy.recursiveDependencies === 'required') {
|
|
641
|
+
throw new Error(`Required dependency '${dependency.identity}' has no immutable lockId`);
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
requiredLocks.add(dependency.lockId);
|
|
646
|
+
const child = childByLock.get(dependency.lockId);
|
|
647
|
+
if (!child) {
|
|
648
|
+
const substituted = children.find(candidate => candidate.lock.identity === dependency.identity);
|
|
649
|
+
if (substituted) {
|
|
650
|
+
throw new Error(`Dependency substitution for '${dependency.identity}': expected ${dependency.lockId}, got ${substituted.lock.lockId}`);
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Required dependency '${dependency.identity}' is unavailable at ${dependency.lockId}`);
|
|
653
|
+
}
|
|
654
|
+
if (child.lock.identity !== dependency.identity) {
|
|
655
|
+
throw new Error(`Dependency substitution for '${dependency.identity}' at ${dependency.lockId}`);
|
|
656
|
+
}
|
|
657
|
+
await visit(child);
|
|
658
|
+
}
|
|
659
|
+
for (const child of children) {
|
|
660
|
+
if (!requiredLocks.has(child.lock.lockId))
|
|
661
|
+
throw new Error(`Portable bundle contains undeclared dependency ${child.lock.lockId}`);
|
|
662
|
+
}
|
|
663
|
+
visiting.delete(bundle.lock.lockId);
|
|
664
|
+
results.set(bundle.lock.lockId, verification);
|
|
665
|
+
};
|
|
666
|
+
await visit(root);
|
|
667
|
+
return { verifications: results, nextState };
|
|
668
|
+
}
|
|
358
669
|
async function writePortableFiles(stage, files) {
|
|
359
670
|
for (const file of files) {
|
|
360
671
|
const relative = file.path.replaceAll('\\', '/');
|
|
@@ -372,86 +683,133 @@ export async function importPortablePackage(options) {
|
|
|
372
683
|
const value = await readJson(input, MAX_PORTABLE_BUNDLE_BYTES);
|
|
373
684
|
validatePortableBundle(value);
|
|
374
685
|
const bundle = value;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
686
|
+
let verifications;
|
|
687
|
+
let nextArtifactTrustState;
|
|
688
|
+
if (bundle.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA) {
|
|
689
|
+
if (!options.artifactTrust)
|
|
690
|
+
throw new Error('v2 portable import requires scoped cross-asset trust root and state');
|
|
691
|
+
const verifiedTree = await verifyPortableBundleTree(bundle, {
|
|
692
|
+
artifactTrust: options.artifactTrust,
|
|
693
|
+
trustStore: options.trustStore,
|
|
694
|
+
requireLegacySignature: true,
|
|
695
|
+
});
|
|
696
|
+
verifications = verifiedTree.verifications;
|
|
697
|
+
nextArtifactTrustState = verifiedTree.nextState;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
validatePortableFiles(bundle);
|
|
701
|
+
const expectedLock = createPackageLock(bundle.envelope, bundle.lock.createdAt);
|
|
702
|
+
if (canonicalJson(expectedLock) !== canonicalJson(bundle.lock))
|
|
703
|
+
throw new Error('Portable bundle lock does not match its provenance envelope');
|
|
704
|
+
await verifyFortemiBundle(bundle.envelope, new Uint8Array(Buffer.from(bundle.fortemiShardBase64, 'base64')));
|
|
705
|
+
const verification = await verifyProvenanceEnvelope({
|
|
706
|
+
envelope: bundle.envelope,
|
|
707
|
+
trustStore: options.trustStore,
|
|
708
|
+
policy: options.verify ? { requireSignature: true, allowIntegrityOnly: false, ...options.policy } : options.policy,
|
|
709
|
+
});
|
|
710
|
+
if (!verification.ok)
|
|
711
|
+
throw new Error(`Portable package verification failed: ${verification.errors.join('; ')}`);
|
|
712
|
+
verifications = new Map([[bundle.lock.lockId, verification]]);
|
|
713
|
+
}
|
|
714
|
+
const ordered = [];
|
|
715
|
+
const collect = (candidate) => {
|
|
716
|
+
if (candidate.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA)
|
|
717
|
+
candidate.dependencies.forEach(collect);
|
|
718
|
+
if (!ordered.some(existing => existing.lock.lockId === candidate.lock.lockId))
|
|
719
|
+
ordered.push(candidate);
|
|
720
|
+
};
|
|
721
|
+
collect(bundle);
|
|
388
722
|
const state = marketplaceStateDir(options);
|
|
389
723
|
const cacheParent = path.join(state, 'cache');
|
|
390
|
-
const destination = path.join(cacheParent, safeDigestName(bundle.lock.lockId));
|
|
391
724
|
await mkdir(cacheParent, { recursive: true });
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
await
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
725
|
+
for (const candidate of ordered) {
|
|
726
|
+
const destination = path.join(cacheParent, safeDigestName(candidate.lock.lockId));
|
|
727
|
+
if (!await pathExists(destination)) {
|
|
728
|
+
const stage = await mkdtemp(path.join(cacheParent, '.import-'));
|
|
729
|
+
try {
|
|
730
|
+
await writePortableFiles(stage, candidate.files);
|
|
731
|
+
const stagedInventory = await inventoryDirectory(stage);
|
|
732
|
+
if (inventorySha256(stagedInventory) !== candidate.envelope.source.artifactSha256)
|
|
733
|
+
throw new Error('Staged import changed package bytes');
|
|
734
|
+
await rename(stage, destination);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
await rm(stage, { recursive: true, force: true });
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
400
740
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
741
|
+
else {
|
|
742
|
+
const existing = await inventoryDirectory(destination);
|
|
743
|
+
if (inventorySha256(existing) !== candidate.envelope.source.artifactSha256)
|
|
744
|
+
throw new Error(`Existing cache content for ${candidate.lock.lockId} is inconsistent`);
|
|
404
745
|
}
|
|
405
746
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
747
|
+
let rootEntry;
|
|
748
|
+
let rootReceipt;
|
|
749
|
+
for (const candidate of ordered) {
|
|
750
|
+
const verification = verifications.get(candidate.lock.lockId);
|
|
751
|
+
const shard = new Uint8Array(Buffer.from(candidate.fortemiShardBase64, 'base64'));
|
|
752
|
+
let conformance = { profile: '2.0.0/full-v1', lossless: true, contractValid: true, shardSha256: sha256(shard) };
|
|
753
|
+
for (let index = candidate.receipts.length - 1; index >= 0; index--) {
|
|
754
|
+
const receiptConformance = candidate.receipts[index]?.conformance;
|
|
755
|
+
if (receiptConformance?.lossless) {
|
|
756
|
+
conformance = receiptConformance;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
417
759
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
actor: options.actor ?? 'aiwg',
|
|
423
|
-
verificationStatus: verification.status,
|
|
424
|
-
evidence: { input, offline: true, importedFiles: bundle.files.length },
|
|
425
|
-
conformance,
|
|
426
|
-
});
|
|
427
|
-
const entry = await recordInstalledPackage({
|
|
428
|
-
...options,
|
|
429
|
-
envelope: bundle.envelope,
|
|
430
|
-
lock: bundle.lock,
|
|
431
|
-
receipt,
|
|
432
|
-
cachePath: destination,
|
|
433
|
-
artifactPath: destination,
|
|
434
|
-
verificationStatus: verification.status,
|
|
435
|
-
fortemiShard: shard,
|
|
436
|
-
});
|
|
437
|
-
const configDir = marketplaceConfigDir(options);
|
|
438
|
-
await setPackageEntry(bundle.lock.identity, {
|
|
439
|
-
version: bundle.lock.version,
|
|
440
|
-
source: bundle.lock.canonicalRemote,
|
|
441
|
-
type: bundle.envelope.package.type === 'plugin' ? 'extension' : bundle.envelope.package.type,
|
|
442
|
-
cachePath: destination,
|
|
443
|
-
installedAt: receipt.occurredAt,
|
|
444
|
-
deployedTo: [],
|
|
445
|
-
provenance: {
|
|
446
|
-
lockId: bundle.lock.lockId,
|
|
447
|
-
resolvedCommit: bundle.lock.resolvedCommit,
|
|
448
|
-
treeSha256: bundle.lock.treeSha256,
|
|
449
|
-
artifactSha256: bundle.lock.artifactSha256,
|
|
450
|
-
envelopeSha256: bundle.lock.envelopeSha256,
|
|
760
|
+
const receipt = createOperationReceipt({
|
|
761
|
+
operation: 'import',
|
|
762
|
+
lock: candidate.lock,
|
|
763
|
+
actor: options.actor ?? 'aiwg',
|
|
451
764
|
verificationStatus: verification.status,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
765
|
+
evidence: { input, offline: true, importedFiles: candidate.files.length },
|
|
766
|
+
conformance,
|
|
767
|
+
});
|
|
768
|
+
const destination = path.join(cacheParent, safeDigestName(candidate.lock.lockId));
|
|
769
|
+
const entry = await recordInstalledPackage({
|
|
770
|
+
...options,
|
|
771
|
+
envelope: candidate.envelope,
|
|
772
|
+
lock: candidate.lock,
|
|
773
|
+
receipt,
|
|
774
|
+
cachePath: destination,
|
|
775
|
+
artifactPath: destination,
|
|
776
|
+
verificationStatus: verification.status,
|
|
777
|
+
fortemiShard: shard,
|
|
778
|
+
...(candidate.schemaVersion === MARKETPLACE_BUNDLE_V2_SCHEMA
|
|
779
|
+
? {
|
|
780
|
+
crossAsset: fromPortableCrossAsset(candidate.crossAsset),
|
|
781
|
+
dependencyLockIds: candidate.dependencies.map(dependency => dependency.lock.lockId),
|
|
782
|
+
}
|
|
783
|
+
: {}),
|
|
784
|
+
});
|
|
785
|
+
const configDir = marketplaceConfigDir(options);
|
|
786
|
+
await setPackageEntry(candidate.lock.identity, {
|
|
787
|
+
version: candidate.lock.version,
|
|
788
|
+
source: candidate.lock.canonicalRemote,
|
|
789
|
+
type: candidate.envelope.package.type === 'plugin' ? 'extension' : candidate.envelope.package.type,
|
|
790
|
+
cachePath: destination,
|
|
791
|
+
installedAt: receipt.occurredAt,
|
|
792
|
+
deployedTo: [],
|
|
793
|
+
provenance: {
|
|
794
|
+
lockId: candidate.lock.lockId,
|
|
795
|
+
resolvedCommit: candidate.lock.resolvedCommit,
|
|
796
|
+
treeSha256: candidate.lock.treeSha256,
|
|
797
|
+
artifactSha256: candidate.lock.artifactSha256,
|
|
798
|
+
envelopeSha256: candidate.lock.envelopeSha256,
|
|
799
|
+
verificationStatus: verification.status,
|
|
800
|
+
},
|
|
801
|
+
}, configDir);
|
|
802
|
+
if (candidate.lock.lockId === bundle.lock.lockId) {
|
|
803
|
+
rootEntry = entry;
|
|
804
|
+
rootReceipt = receipt;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
entry: rootEntry,
|
|
809
|
+
verification: verifications.get(bundle.lock.lockId),
|
|
810
|
+
receipt: rootReceipt,
|
|
811
|
+
...(nextArtifactTrustState ? { nextArtifactTrustState } : {}),
|
|
812
|
+
};
|
|
455
813
|
}
|
|
456
814
|
function emptyCatalogRegistry() {
|
|
457
815
|
return { schemaVersion: MARKETPLACE_CATALOG_REGISTRY_SCHEMA, catalogs: [] };
|
|
@@ -15,5 +15,6 @@ export const MARKETPLACE_TRUST_SCHEMA = 'aiwg.marketplace.trust-store.v1';
|
|
|
15
15
|
export const MARKETPLACE_CATALOG_SCHEMA = 'aiwg.marketplace.catalog.v1';
|
|
16
16
|
export const MARKETPLACE_CATALOG_REGISTRY_SCHEMA = 'aiwg.marketplace.catalog-registry.v1';
|
|
17
17
|
export const MARKETPLACE_BUNDLE_SCHEMA = 'aiwg.marketplace.portable-bundle.v1';
|
|
18
|
+
export const MARKETPLACE_BUNDLE_V2_SCHEMA = 'aiwg.marketplace.portable-bundle.v2';
|
|
18
19
|
export const MARKETPLACE_INDEX_SCHEMA = 'aiwg.marketplace.local-index.v1';
|
|
19
20
|
//# sourceMappingURL=provenance-types.js.map
|
|
@@ -476,6 +476,7 @@ export function keyDelegationStatement(key) {
|
|
|
476
476
|
publisher: key.publisher,
|
|
477
477
|
validFrom: key.validFrom,
|
|
478
478
|
...(key.validUntil ? { validUntil: key.validUntil } : {}),
|
|
479
|
+
...(key.artifactIdentityId ? { artifactIdentityId: key.artifactIdentityId } : {}),
|
|
479
480
|
};
|
|
480
481
|
}
|
|
481
482
|
export function signKeyDelegation(key, parentPrivateKeyPem) {
|
|
@@ -772,7 +773,12 @@ export async function verifyProvenanceEnvelope(options) {
|
|
|
772
773
|
&& options.previousLock.resolvedCommit !== lock.resolvedCommit) {
|
|
773
774
|
errors.push(`Mutable ref '${lock.requestedRef}' moved from ${options.previousLock.resolvedCommit} to ${lock.resolvedCommit}`);
|
|
774
775
|
}
|
|
775
|
-
for (const dependency of envelope.package.dependencies.filter((item) => !item.optional
|
|
776
|
+
for (const dependency of envelope.package.dependencies.filter((item) => !item.optional)) {
|
|
777
|
+
if (!dependency.lockId) {
|
|
778
|
+
if (policy.requireDependencyLocks)
|
|
779
|
+
errors.push(`Required dependency '${dependency.identity}' has no immutable lockId`);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
776
782
|
const actual = options.installedLocks?.[dependency.identity];
|
|
777
783
|
const ok = actual === dependency.lockId;
|
|
778
784
|
checks.push({ check: `dependency:${dependency.identity}`, ok, detail: actual ?? 'not installed' });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/** Match Hermes Agent's process-level HERMES_HOME resolution contract. */
|
|
4
|
+
export function resolveHermesHome(userHome = homedir()) {
|
|
5
|
+
const configured = (process.env.HERMES_HOME || '').trim();
|
|
6
|
+
if (configured)
|
|
7
|
+
return configured;
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
const localAppData = (process.env.LOCALAPPDATA || '').trim();
|
|
10
|
+
return localAppData
|
|
11
|
+
? path.join(localAppData, 'hermes')
|
|
12
|
+
: path.join(userHome, 'AppData', 'Local', 'hermes');
|
|
13
|
+
}
|
|
14
|
+
return path.join(userHome, '.hermes');
|
|
15
|
+
}
|
|
16
|
+
/** Resolve a path exactly as a Hermes process would consume HERMES_HOME. */
|
|
17
|
+
export function resolveHermesHomePath(...segments) {
|
|
18
|
+
return path.resolve(resolveHermesHome(), ...segments);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=hermes-home.js.map
|