@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.
Files changed (42) hide show
  1. package/bin/aiwg.mjs +2 -0
  2. package/dist/src/api/index.d.ts +6 -0
  3. package/dist/src/api/index.js +6 -0
  4. package/dist/src/cli/handlers/artifact-verify.js +171 -0
  5. package/dist/src/cli/handlers/index.js +3 -1
  6. package/dist/src/cli/handlers/setup-manifest.js +52 -3
  7. package/dist/src/cli/handlers/setup.js +15 -2
  8. package/dist/src/cli/handlers/use.js +71 -6
  9. package/dist/src/cli/scope-resolver.js +6 -1
  10. package/dist/src/cli/services/deployment-verification.js +65 -7
  11. package/dist/src/config/aiwg-config.js +4 -3
  12. package/dist/src/config/cli.js +3 -1
  13. package/dist/src/config/gitignore.js +67 -21
  14. package/dist/src/config/workspace.js +8 -1
  15. package/dist/src/extensions/commands/definitions.js +19 -0
  16. package/dist/src/extensions/project-quickref.js +9 -0
  17. package/dist/src/marketplace/artifact-attestation.js +195 -0
  18. package/dist/src/marketplace/exchange.js +437 -79
  19. package/dist/src/marketplace/provenance-types.js +1 -0
  20. package/dist/src/marketplace/provenance.js +7 -1
  21. package/dist/src/providers/hermes-home.js +20 -0
  22. package/dist/src/providers/provider-definitions.js +5 -4
  23. package/dist/src/providers/transformation-receipt-integration.js +448 -0
  24. package/dist/src/providers/transformation-receipt.js +215 -0
  25. package/dist/src/resources/web-release.d.ts +11 -0
  26. package/dist/src/resources/web-release.js +61 -6
  27. package/dist/src/security/artifact-attestation.js +117 -0
  28. package/dist/src/security/artifact-trust.js +557 -0
  29. package/dist/src/security/artifact-verifier.js +478 -0
  30. package/dist/src/skills/deployer.js +5 -1
  31. package/dist/src/tracker/capability-protocol.js +7 -2
  32. package/package.json +5 -1
  33. package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
  34. package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
  35. package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
  36. package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
  37. package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
  38. package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
  39. package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
  40. package/tools/agents/deploy-agents.mjs +8 -2
  41. package/tools/agents/providers/base.mjs +29 -2
  42. 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 = Buffer.from(file.contentBase64, 'base64');
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 bundle = {
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 !== MARKETPLACE_BUNDLE_SCHEMA)
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
- const allowed = new Set(['schemaVersion', 'envelope', 'lock', 'receipts', 'fortemiShardBase64', 'files']);
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
- validatePortableFiles(bundle);
376
- const expectedLock = createPackageLock(bundle.envelope, bundle.lock.createdAt);
377
- if (canonicalJson(expectedLock) !== canonicalJson(bundle.lock))
378
- throw new Error('Portable bundle lock does not match its provenance envelope');
379
- const shard = new Uint8Array(Buffer.from(bundle.fortemiShardBase64, 'base64'));
380
- await verifyFortemiBundle(bundle.envelope, shard);
381
- const verification = await verifyProvenanceEnvelope({
382
- envelope: bundle.envelope,
383
- trustStore: options.trustStore,
384
- policy: options.verify ? { requireSignature: true, allowIntegrityOnly: false, ...options.policy } : options.policy,
385
- });
386
- if (!verification.ok)
387
- throw new Error(`Portable package verification failed: ${verification.errors.join('; ')}`);
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
- if (!await pathExists(destination)) {
393
- const stage = await mkdtemp(path.join(cacheParent, '.import-'));
394
- try {
395
- await writePortableFiles(stage, bundle.files);
396
- const stagedInventory = await inventoryDirectory(stage);
397
- if (inventorySha256(stagedInventory) !== bundle.envelope.source.artifactSha256)
398
- throw new Error('Staged import changed package bytes');
399
- await rename(stage, destination);
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
- catch (error) {
402
- await rm(stage, { recursive: true, force: true });
403
- throw error;
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
- else {
407
- const existing = await inventoryDirectory(destination);
408
- if (inventorySha256(existing) !== bundle.envelope.source.artifactSha256)
409
- throw new Error(`Existing cache content for ${bundle.lock.lockId} is inconsistent`);
410
- }
411
- let conformance = { profile: '2.0.0/full-v1', lossless: true, contractValid: true, shardSha256: sha256(shard) };
412
- for (let index = bundle.receipts.length - 1; index >= 0; index--) {
413
- const candidate = bundle.receipts[index]?.conformance;
414
- if (candidate?.lossless) {
415
- conformance = candidate;
416
- break;
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
- const receipt = createOperationReceipt({
420
- operation: 'import',
421
- lock: bundle.lock,
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
- }, configDir);
454
- return { entry, verification, receipt };
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 && item.lockId)) {
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