@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
@@ -4,6 +4,7 @@ import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
4
4
  import { readAiwgConfig } from '../../config/aiwg-config.js';
5
5
  import { readUserRegistry } from '../../config/user-registry.js';
6
6
  import { getProviderDefinition, normalizeProviderDefinitionId, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
7
+ import { diagnoseIntegratedProviderTransformationReceipt } from '../../providers/transformation-receipt-integration.js';
7
8
  import { diagnoseWorkspaceContext, providerContextContract, } from '../../smiths/context-pipeline/workspace-context.js';
8
9
  import { USER_SCOPE_PATHS } from '../scope-resolver.js';
9
10
  const RESTART_NOTICES = {
@@ -120,6 +121,56 @@ function classifyOutcome(findings, restartRequired) {
120
121
  return 'degraded';
121
122
  return restartRequired ? 'ready-restart-required' : 'ready';
122
123
  }
124
+ const RECEIPT_DRIFT_POLICY = {
125
+ 'source-verification-failure': {
126
+ severity: 'blocking',
127
+ remediation: 'Restore or reverify the canonical source before regenerating provider outputs.',
128
+ },
129
+ 'transformation-mismatch': {
130
+ severity: 'blocking',
131
+ remediation: 'Review the active provider adapter change, then re-run the same aiwg use command.',
132
+ },
133
+ 'user-modification': {
134
+ severity: 'blocking',
135
+ remediation: 'Back up the changed managed output if needed, then re-run the same aiwg use command.',
136
+ },
137
+ 'stale-output': {
138
+ severity: 'blocking',
139
+ remediation: 'Re-run the same aiwg use command to complete a verified regeneration.',
140
+ },
141
+ 'missing-receipt': {
142
+ severity: 'advisory',
143
+ remediation: 'Re-run the same aiwg use command to establish provider transformation evidence.',
144
+ },
145
+ };
146
+ async function collectProviderReceiptFindings(options, provider) {
147
+ try {
148
+ const diagnosis = await diagnoseIntegratedProviderTransformationReceipt({
149
+ projectRoot: options.projectRoot,
150
+ outputRoot: options.outputRoot,
151
+ frameworkRoot: options.frameworkRoot,
152
+ provider,
153
+ scope: options.scope,
154
+ requestedBundles: options.requestedBundles,
155
+ });
156
+ return diagnosis.findings
157
+ .filter((drift) => options.reportMissingReceipt !== false || drift.kind !== 'missing-receipt')
158
+ .map((drift, index) => {
159
+ const policy = RECEIPT_DRIFT_POLICY[drift.kind];
160
+ return finding(provider, `provider-drift:${drift.kind}:${index}`, policy.severity, drift.message, policy.remediation, {
161
+ driftClass: drift.kind,
162
+ receiptPath: diagnosis.receiptPath,
163
+ checkedOutputs: diagnosis.checkedOutputs,
164
+ ...(drift.path ? { path: drift.path } : {}),
165
+ ...(drift.expected ? { expected: drift.expected } : {}),
166
+ ...(drift.actual ? { actual: drift.actual } : {}),
167
+ });
168
+ });
169
+ }
170
+ catch (error) {
171
+ return [finding(provider, 'provider-drift:missing-receipt:0', 'advisory', `Provider transformation evidence could not be evaluated: ${error instanceof Error ? error.message : String(error)}`, RECEIPT_DRIFT_POLICY['missing-receipt'].remediation, { driftClass: 'missing-receipt' })];
172
+ }
173
+ }
123
174
  function indexContainsRequestedBundles(index, requestedBundles) {
124
175
  if (requestedBundles.includes('all'))
125
176
  return Object.keys(index.entries).length > 0;
@@ -187,17 +238,18 @@ export async function verifyProviderDeployment(options) {
187
238
  findings.push(finding(normalized, 'project-root-missing', 'blocking', `Resolved project root does not exist: ${options.projectRoot}`, 'Select an existing project root and run aiwg use again.'));
188
239
  }
189
240
  if (definition) {
241
+ const deploymentRoot = options.outputRoot ?? options.projectRoot;
190
242
  const artifactPaths = options.scope === 'user'
191
243
  ? USER_SCOPE_PATHS[normalized] ?? definition.paths.artifacts
192
244
  : definition.paths.artifacts;
193
245
  for (const type of ['agents', 'commands', 'skills', 'rules', 'behaviors']) {
194
- const resolved = resolveProviderPathValue(artifactPaths[type], options.projectRoot);
246
+ const resolved = resolveProviderPathValue(artifactPaths[type], deploymentRoot);
195
247
  counts[type] = await countEntries(resolved);
196
248
  }
197
- const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, options.projectRoot);
249
+ const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, deploymentRoot);
198
250
  const kernelPath = options.scope === 'user'
199
251
  ? ''
200
- : resolveProviderPathValue(definition.paths.kernelSkills, options.projectRoot);
252
+ : resolveProviderPathValue(definition.paths.kernelSkills, deploymentRoot);
201
253
  const kernelCount = await countEntries(kernelPath);
202
254
  if (kernelPath && kernelPath !== resolvedSkillsPath)
203
255
  counts.skills += kernelCount;
@@ -207,6 +259,7 @@ export async function verifyProviderDeployment(options) {
207
259
  }
208
260
  }
209
261
  findings.push(...await collectRegistryFindings(options, normalized, counts));
262
+ findings.push(...await collectProviderReceiptFindings(options, normalized));
210
263
  const projectConfig = await readAiwgConfig(options.projectRoot);
211
264
  const scopedRegistry = options.scope === 'user'
212
265
  ? await readUserRegistry()
@@ -300,7 +353,7 @@ export async function verifyProviderDeployment(options) {
300
353
  const indexFailed = findings.some((item) => item.id.startsWith('index-'));
301
354
  const contextFailed = findings.some((item) => item.id.startsWith('context-') && item.severity === 'blocking');
302
355
  const deployFailed = findings.some((item) => item.severity === 'blocking'
303
- && (item.id.startsWith('deployment-') || item.id.startsWith('provider-') || item.id.startsWith('registry-')));
356
+ && (item.id.startsWith('deployment-') || item.id === 'provider-unknown' || item.id === 'provider-artifacts-missing' || item.id.startsWith('registry-')));
304
357
  const phases = [
305
358
  phase('resolve', 'passed', true, `Resolved ${options.projectRoot}, ${normalized}, ${options.scope} scope.`),
306
359
  phase('deploy', deployFailed ? 'failed' : 'passed', true, deployFailed ? 'Deployment invariants failed.' : 'Provider artifacts and installed state verified.', { counts }),
@@ -406,13 +459,18 @@ export function aggregateUseDeploymentResult(options) {
406
459
  }
407
460
  export async function verifyConfiguredDeployments(projectRoot, filters = {}, frameworkRoot = process.env.AIWG_ROOT || projectRoot) {
408
461
  const config = await readAiwgConfig(projectRoot);
462
+ const userRegistry = filters.scope === 'user' ? await readUserRegistry() : null;
463
+ const installed = userRegistry?.installed ?? config?.installed ?? {};
464
+ const registeredProviders = [...new Set(Object.values(installed).flatMap((entry) => Object.keys(entry.deployedTo ?? {})))];
409
465
  const providers = filters.provider
410
466
  ? [filters.provider]
411
- : config?.providers?.length ? config.providers : [];
412
- const bundles = filters.bundle ? [filters.bundle] : Object.keys(config?.installed ?? {});
467
+ : filters.scope === 'user'
468
+ ? registeredProviders
469
+ : config?.providers?.length ? config.providers : registeredProviders;
470
+ const bundles = filters.bundle ? [filters.bundle] : Object.keys(installed);
413
471
  const results = [];
414
472
  for (const provider of providers) {
415
- const providerBundles = bundles.filter((bundle) => Boolean(config?.installed[bundle]?.deployedTo[provider]));
473
+ const providerBundles = bundles.filter((bundle) => Boolean(installed[bundle]?.deployedTo[provider]));
416
474
  if (providerBundles.length === 0)
417
475
  continue;
418
476
  results.push(await verifyProviderDeployment({
@@ -448,8 +448,8 @@ export async function readIndexConfig(projectDir) {
448
448
  * - any host containing 'gitea' (or matching the typical Gitea path shape) → 'gitea'
449
449
  *
450
450
  * Returns 'unknown' for self-hosted instances we can't classify by host alone —
451
- * callers should then prompt the operator or fall back to the configured
452
- * AIWG provider list.
451
+ * callers should then prompt the operator or use `remotes.issue_provider`
452
+ * when the project has declared one.
453
453
  *
454
454
  * @implements #997
455
455
  */
@@ -466,7 +466,7 @@ export function resolveRemoteProvider(remoteUrl) {
466
466
  // gitea — identified by hostname token. Self-hosted Gitea instances often
467
467
  // don't include 'gitea' in their hostname (e.g. corporate git servers), so
468
468
  // 'unknown' is the honest answer there — callers should consult the
469
- // configured AIWG provider list rather than guess.
469
+ // explicit remotes.issue_provider hint rather than guess.
470
470
  if (lower.includes('gitea'))
471
471
  return 'gitea';
472
472
  return 'unknown';
@@ -487,6 +487,7 @@ export function resolveRemotes(remotes) {
487
487
  return {
488
488
  primary,
489
489
  issue_tracker: remotes?.issue_tracker ?? primary,
490
+ issue_provider: remotes?.issue_provider,
490
491
  ci: remotes?.ci ?? primary,
491
492
  tracker_actor: remotes?.tracker_actor,
492
493
  transport: remotes?.transport,
@@ -140,7 +140,8 @@ async function handleSet(config, args) {
140
140
  // aiwg config get --project remotes.primary
141
141
  //
142
142
  // Set validates enum membership for known fields (delivery.mode,
143
- // delivery.merge_style, delivery.force_push_policy) before writing.
143
+ // delivery.merge_style, delivery.force_push_policy, remotes.issue_provider)
144
+ // before writing.
144
145
  const ENUM_RULES = {
145
146
  'delivery.mode': ['direct', 'feature-branch', 'pr-required'],
146
147
  'delivery.merge_style': ['rebase-merge', 'squash', 'merge', 'fast-forward-only'],
@@ -149,6 +150,7 @@ const ENUM_RULES = {
149
150
  'delivery.signing.enforce': ['commits', 'tags', 'all'],
150
151
  'delivery.release_signing.format': ['openpgp', 'ssh', 'x509'],
151
152
  'delivery.release_signing.enforce': ['commits', 'tags', 'all'],
153
+ 'remotes.issue_provider': ['gitea', 'github', 'local'],
152
154
  'remotes.tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
153
155
  'remotes.transport.protocol': ['ssh', 'https'],
154
156
  'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
@@ -9,6 +9,10 @@
9
9
  */
10
10
  import * as fs from 'fs/promises';
11
11
  import * as path from 'path';
12
+ import { execFile } from 'child_process';
13
+ import { promisify } from 'util';
14
+ const execFileAsync = promisify(execFile);
15
+ const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
12
16
  // ===========================
13
17
  // Recommended Patterns
14
18
  // ===========================
@@ -62,6 +66,63 @@ export const ALL_RECOMMENDED_PATTERNS = [
62
66
  // ===========================
63
67
  // Core Functions
64
68
  // ===========================
69
+ function isTextuallyCovered(pattern, lines) {
70
+ if (lines.includes(pattern))
71
+ return true;
72
+ if (lines.includes(pattern.replace(/\/$/, '')))
73
+ return true;
74
+ const parts = pattern.split('/').filter(Boolean);
75
+ for (let i = 1; i < parts.length; i++) {
76
+ const parent = parts.slice(0, i).join('/') + '/';
77
+ if (lines.includes(parent) || lines.includes(parent.replace(/\/$/, ''))) {
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ function probePath(pattern) {
84
+ return pattern.endsWith('/') ? `${pattern}${GITIGNORE_PROBE_BASENAME}` : pattern;
85
+ }
86
+ async function isGitWorktree(projectRoot) {
87
+ try {
88
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
89
+ cwd: projectRoot,
90
+ windowsHide: true,
91
+ });
92
+ return stdout.trim() === 'true';
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /**
99
+ * Resolve effective ignore coverage through Git when possible. This honors
100
+ * negations, .git/info/exclude, and core.excludesFile. A textual fallback keeps
101
+ * the helper deterministic outside Git worktrees or when Git is unavailable.
102
+ *
103
+ * @implements #2106
104
+ */
105
+ async function resolveCoveredPatterns(projectRoot, patterns, lines) {
106
+ if (!(await isGitWorktree(projectRoot))) {
107
+ return new Set(patterns.filter(pattern => isTextuallyCovered(pattern, lines)));
108
+ }
109
+ const covered = await Promise.all(patterns.map(async (pattern) => {
110
+ try {
111
+ await execFileAsync('git', ['check-ignore', '--quiet', '--', probePath(pattern)], {
112
+ cwd: projectRoot,
113
+ windowsHide: true,
114
+ });
115
+ return true;
116
+ }
117
+ catch (error) {
118
+ const code = error.code;
119
+ if (code === 1)
120
+ return false;
121
+ return isTextuallyCovered(pattern, lines);
122
+ }
123
+ }));
124
+ return new Set(patterns.filter((_, index) => covered[index]));
125
+ }
65
126
  /**
66
127
  * Check which recommended patterns are missing from the project's .gitignore.
67
128
  */
@@ -77,26 +138,10 @@ export async function checkGitignore(projectRoot) {
77
138
  // File doesn't exist — all patterns are missing
78
139
  }
79
140
  const lines = content.split('\n').map(l => l.trim());
80
- const isCovered = (pattern) => {
81
- // Exact match
82
- if (lines.includes(pattern))
83
- return true;
84
- // Pattern without trailing slash is also sufficient
85
- if (lines.includes(pattern.replace(/\/$/, '')))
86
- return true;
87
- // Parent directory is ignored (covers all children)
88
- const parts = pattern.split('/').filter(Boolean);
89
- for (let i = 1; i < parts.length; i++) {
90
- const parent = parts.slice(0, i).join('/') + '/';
91
- if (lines.includes(parent) || lines.includes(parent.replace(/\/$/, ''))) {
92
- return true;
93
- }
94
- }
95
- return false;
96
- };
97
- const missingRuntime = AIWG_RUNTIME_PATTERNS.filter(p => !isCovered(p));
98
- const missingSession = CLAUDE_SESSION_PATTERNS.filter(p => !isCovered(p));
99
- const missingProvider = PROVIDER_CONVENTIONAL_PATTERNS.filter(p => !isCovered(p));
141
+ const covered = await resolveCoveredPatterns(projectRoot, ALL_RECOMMENDED_PATTERNS, lines);
142
+ const missingRuntime = AIWG_RUNTIME_PATTERNS.filter(p => !covered.has(p));
143
+ const missingSession = CLAUDE_SESSION_PATTERNS.filter(p => !covered.has(p));
144
+ const missingProvider = PROVIDER_CONVENTIONAL_PATTERNS.filter(p => !covered.has(p));
100
145
  const missing = [...missingRuntime, ...missingSession, ...missingProvider];
101
146
  return { exists, missing, missingRuntime, missingSession, missingProvider };
102
147
  }
@@ -116,10 +161,11 @@ export async function appendGitignore(projectRoot, patterns) {
116
161
  // File doesn't exist, will create
117
162
  }
118
163
  const existingLines = existing.split('\n').map(l => l.trim());
164
+ const covered = await resolveCoveredPatterns(projectRoot, patterns, existingLines);
119
165
  const toAdd = [];
120
166
  const alreadyPresent = [];
121
167
  for (const pattern of patterns) {
122
- if (existingLines.includes(pattern) || existingLines.includes(pattern.replace(/\/$/, ''))) {
168
+ if (covered.has(pattern)) {
123
169
  alreadyPresent.push(pattern);
124
170
  }
125
171
  else {
@@ -119,14 +119,21 @@ async function resolveEndpoint(repoPath, remote, providerHint) {
119
119
  : 'unknown',
120
120
  };
121
121
  }
122
+ function trackerProviderHint(remotes, fallback) {
123
+ const configured = remotes.issue_provider;
124
+ if (configured === 'gitea' || configured === 'github')
125
+ return configured;
126
+ return fallback;
127
+ }
122
128
  async function resolveMember(entry, workspaceRoot) {
123
129
  const memberPath = resolveFrom(workspaceRoot, entry.path);
124
130
  const configPath = getConfigPath(memberPath);
125
131
  const config = await readAiwgConfig(memberPath);
126
132
  const remotes = resolveRemotes(config?.remotes);
133
+ const issueProviderHint = trackerProviderHint(remotes, entry.provider);
127
134
  const [primary, issueTracker, ci] = await Promise.all([
128
135
  resolveEndpoint(memberPath, remotes.primary, entry.provider),
129
- resolveEndpoint(memberPath, remotes.issue_tracker, entry.provider),
136
+ resolveEndpoint(memberPath, remotes.issue_tracker, issueProviderHint),
130
137
  resolveEndpoint(memberPath, remotes.ci, entry.provider),
131
138
  ]);
132
139
  const drift = [];
@@ -2478,6 +2478,24 @@ export const evidenceCommand = {
2478
2478
  },
2479
2479
  },
2480
2480
  };
2481
+ export const artifactVerifyCommand = {
2482
+ id: 'verify',
2483
+ type: 'command',
2484
+ name: 'Artifact Verification',
2485
+ description: 'Verify cross-asset DSSE provenance and manage versioned trust roots',
2486
+ version: '1.0.0',
2487
+ capabilities: ['cli', 'security', 'dsse', 'sigstore', 'provenance', 'trust-policy'],
2488
+ keywords: ['verify', 'artifact', 'attestation', 'dsse', 'sigstore', 'trust-root', 'revocation'],
2489
+ category: 'utility',
2490
+ platforms: { claude: 'full', generic: 'full' },
2491
+ deployment: { pathTemplate: '.{platform}/commands/{id}.md', core: true },
2492
+ metadata: {
2493
+ type: 'command',
2494
+ template: 'utility',
2495
+ argumentHint: '<artifact> --attestation <path> --policy <root.json> [--offline] [--json] | trust <bootstrap|update|status>',
2496
+ allowedTools: ['Read', 'Bash'],
2497
+ },
2498
+ };
2481
2499
  export const costReportCommand = {
2482
2500
  id: 'cost-report',
2483
2501
  type: 'skill',
@@ -3614,6 +3632,7 @@ export const commandDefinitions = [
3614
3632
  teamCommand,
3615
3633
  // Metrics (4)
3616
3634
  evidenceCommand,
3635
+ artifactVerifyCommand,
3617
3636
  costReportCommand,
3618
3637
  costHistoryCommand,
3619
3638
  metricsTokensCommand,
@@ -11,6 +11,7 @@ import { basename, dirname, isAbsolute, join, resolve } from 'path';
11
11
  import { homedir } from 'os';
12
12
  import { z } from 'zod';
13
13
  import { getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
14
+ import { resolveHermesHome } from '../providers/hermes-home.js';
14
15
  import { OPERATIONAL_SHOW_TYPES } from '../artifacts/types.js';
15
16
  import { projectAiwgPath } from '../config/project-artifacts.js';
16
17
  import { appendAiwgSourceTrackBlock } from './project-local-gitignore.js';
@@ -339,6 +340,14 @@ function resolveProviderSkillsRoot(provider, projectDir, homeDir) {
339
340
  const definition = getProviderDefinition(normalized);
340
341
  if (!definition)
341
342
  throw new Error(`Provider definition unavailable for '${provider}'`);
343
+ if (normalized === 'hermes') {
344
+ return {
345
+ provider: normalized,
346
+ root: resolve(resolveHermesHome(homeDir), 'skills'),
347
+ emulated: false,
348
+ global: true,
349
+ };
350
+ }
342
351
  const configured = definition.paths.kernelSkills ?? definition.paths.artifacts.skills;
343
352
  if (!configured)
344
353
  throw new Error(`Provider '${normalized}' has no supported skill or aggregation target`);
@@ -0,0 +1,195 @@
1
+ import { createArtifactAttestation, serializeArtifactAttestation, } from '../security/artifact-attestation.js';
2
+ import { verifyArtifact, } from '../security/artifact-verifier.js';
3
+ import { canonicalJson as artifactCanonicalJson, parseTrustRoot, sha256 as artifactSha256, } from '../security/artifact-trust.js';
4
+ import { canonicalJson, createPackageLock, verifyProvenanceEnvelope, } from './provenance.js';
5
+ export const MARKETPLACE_ENVELOPE_SUBJECT = 'aiwg-marketplace-envelope.json';
6
+ export function serializeMarketplaceEnvelope(envelope) {
7
+ return Buffer.from(`${canonicalJson(envelope)}\n`, 'utf8');
8
+ }
9
+ export function marketplaceArtifactScope(envelope) {
10
+ return {
11
+ assetType: 'marketplace-envelope',
12
+ namespace: envelope.package.namespace,
13
+ channel: `marketplace:${envelope.package.name}`,
14
+ };
15
+ }
16
+ function requiredMaterialNames(materials) {
17
+ const names = new Set(materials.map(material => material.name));
18
+ for (const required of ['lock', 'inventory', 'git-tree', 'fortemi-shard', 'receipt', 'sbom', 'license']) {
19
+ if (!names.has(required))
20
+ throw new Error(`Marketplace attestation requires '${required}' material`);
21
+ }
22
+ }
23
+ export function createMarketplaceArtifactAttestation(options) {
24
+ requiredMaterialNames(options.materials);
25
+ const scope = marketplaceArtifactScope(options.envelope);
26
+ const envelopeBytes = serializeMarketplaceEnvelope(options.envelope);
27
+ const provenanceBytes = Buffer.from(artifactCanonicalJson(options.envelope.provenance), 'utf8');
28
+ const dependencies = options.envelope.package.dependencies
29
+ .filter(dependency => !dependency.optional)
30
+ .map(dependency => {
31
+ if (!dependency.lockId)
32
+ throw new Error(`Required dependency '${dependency.identity}' has no immutable lockId`);
33
+ return {
34
+ name: dependency.identity,
35
+ uri: `aiwg:marketplace-lock:${dependency.lockId}`,
36
+ mediaType: 'application/vnd.aiwg.marketplace-lock.v1+json',
37
+ digest: { sha256: dependency.lockId.replace(/^sha256:/, '') },
38
+ };
39
+ });
40
+ const attestation = createArtifactAttestation({
41
+ artifact: {
42
+ name: MARKETPLACE_ENVELOPE_SUBJECT,
43
+ bytes: envelopeBytes,
44
+ mediaType: 'application/vnd.aiwg.marketplace-envelope.v1+json',
45
+ },
46
+ assetType: scope.assetType,
47
+ publisher: {
48
+ id: options.publisherIdentity ?? options.envelope.publisher.id,
49
+ namespace: scope.namespace,
50
+ role: 'marketplace-publisher',
51
+ },
52
+ publication: {
53
+ version: options.envelope.package.version,
54
+ channel: scope.channel,
55
+ sequence: options.envelope.publication.sequence,
56
+ sourceUri: options.envelope.source.canonicalRemote,
57
+ },
58
+ issuedAt: options.issuedAt ?? options.envelope.publication.publishedAt,
59
+ expiresAt: options.expiresAt,
60
+ derivation: {
61
+ builder: options.builder,
62
+ materials: options.materials.map(material => ({
63
+ name: material.name,
64
+ uri: material.uri,
65
+ ...(material.mediaType ? { mediaType: material.mediaType } : {}),
66
+ digest: { sha256: artifactSha256(material.bytes) },
67
+ })),
68
+ reproducible: true,
69
+ },
70
+ provenanceGraph: {
71
+ standard: 'W3C-PROV',
72
+ uri: 'aiwg:marketplace-provenance:w3c-prov',
73
+ sha256: artifactSha256(provenanceBytes),
74
+ },
75
+ dependencies,
76
+ privateKey: options.privateKey,
77
+ });
78
+ return { attestation, materials: options.materials };
79
+ }
80
+ export function serializeMarketplaceAttestation(evidence) {
81
+ return serializeArtifactAttestation(evidence.attestation);
82
+ }
83
+ function signedMarketplacePolicy(root) {
84
+ return root.signed.policy.marketplace ?? {
85
+ evidenceMode: 'marketplace-only',
86
+ legacySignatureMigrationGate: false,
87
+ recursiveDependencies: 'if-present',
88
+ };
89
+ }
90
+ export async function verifyMarketplaceEvidence(options) {
91
+ const authenticatedRoot = options.artifact ? parseTrustRoot(options.artifact.rootBytes) : undefined;
92
+ const signedPolicy = authenticatedRoot ? signedMarketplacePolicy(authenticatedRoot) : {
93
+ evidenceMode: 'marketplace-only',
94
+ legacySignatureMigrationGate: false,
95
+ recursiveDependencies: 'if-present',
96
+ };
97
+ const legacyRequired = signedPolicy.evidenceMode !== 'cross-asset-required'
98
+ || !signedPolicy.legacySignatureMigrationGate;
99
+ const marketplace = await verifyProvenanceEnvelope({
100
+ envelope: options.envelope,
101
+ contentRoot: options.contentRoot,
102
+ checkoutPath: options.checkoutPath,
103
+ trustStore: options.trustStore,
104
+ installedLocks: options.installedLocks,
105
+ at: options.artifact?.now ? new Date(options.artifact.now) : undefined,
106
+ policy: legacyRequired
107
+ ? {
108
+ requireSignature: true,
109
+ allowIntegrityOnly: false,
110
+ requireDependencyLocks: signedPolicy.recursiveDependencies === 'required',
111
+ ...options.marketplacePolicy,
112
+ }
113
+ : {
114
+ requireDependencyLocks: signedPolicy.recursiveDependencies === 'required',
115
+ ...options.marketplacePolicy,
116
+ },
117
+ });
118
+ const errors = marketplace.ok ? [] : marketplace.errors.map(error => `marketplace: ${error}`);
119
+ let crossAsset;
120
+ const crossRequired = signedPolicy.evidenceMode !== 'marketplace-only';
121
+ if (options.artifact) {
122
+ const scope = marketplaceArtifactScope(options.envelope);
123
+ crossAsset = await verifyArtifact({
124
+ artifactBytes: serializeMarketplaceEnvelope(options.envelope),
125
+ artifactName: MARKETPLACE_ENVELOPE_SUBJECT,
126
+ attestation: options.artifact.evidence.attestation,
127
+ rootBytes: options.artifact.rootBytes,
128
+ state: options.artifact.state,
129
+ materials: new Map(options.artifact.evidence.materials.map(material => [material.uri, material.bytes])),
130
+ expectedScope: scope,
131
+ offline: options.artifact.offline,
132
+ now: options.artifact.now,
133
+ });
134
+ if (crossAsset.status !== 'verified')
135
+ errors.push(`cross-asset: ${crossAsset.status}`);
136
+ }
137
+ else if (crossRequired) {
138
+ errors.push('cross-asset: required evidence is unavailable');
139
+ }
140
+ if (legacyRequired && marketplace.status !== 'verified')
141
+ errors.push('marketplace: a trusted legacy publisher signature remains mandatory');
142
+ if (crossAsset?.status === 'verified' && marketplace.signer && options.trustStore) {
143
+ const legacyKey = options.trustStore.keys.find(key => key.keyId === marketplace.signer);
144
+ if (!legacyKey?.artifactIdentityId || !crossAsset.identities.includes(legacyKey.artifactIdentityId)) {
145
+ errors.push('trust-parity: marketplace and cross-asset verification resolved different publisher authorities');
146
+ }
147
+ }
148
+ return {
149
+ ok: errors.length === 0,
150
+ status: errors.length === 0 ? 'verified' : 'failed',
151
+ marketplace,
152
+ ...(crossAsset ? { crossAsset } : {}),
153
+ errors: [...new Set(errors)],
154
+ };
155
+ }
156
+ export async function verifyMarketplaceDependencyClosure(options) {
157
+ const errors = [];
158
+ const verified = new Set();
159
+ const active = new Set();
160
+ const visit = async (envelope) => {
161
+ const lock = createPackageLock(envelope, envelope.publication.publishedAt);
162
+ if (verified.has(lock.lockId))
163
+ return;
164
+ if (active.has(lock.lockId)) {
165
+ errors.push(`dependency cycle detected at ${lock.lockId}`);
166
+ return;
167
+ }
168
+ active.add(lock.lockId);
169
+ if (!await options.verify(envelope))
170
+ errors.push(`dependency evidence failed for ${lock.lockId}`);
171
+ for (const dependency of envelope.package.dependencies.filter(item => !item.optional)) {
172
+ if (!dependency.lockId) {
173
+ if (options.required)
174
+ errors.push(`required dependency '${dependency.identity}' has no immutable lockId`);
175
+ continue;
176
+ }
177
+ const child = options.packages.get(dependency.lockId);
178
+ if (!child) {
179
+ errors.push(`required dependency '${dependency.identity}' is unavailable at ${dependency.lockId}`);
180
+ continue;
181
+ }
182
+ const actual = createPackageLock(child, child.publication.publishedAt).lockId;
183
+ if (actual !== dependency.lockId) {
184
+ errors.push(`dependency substitution for '${dependency.identity}': expected ${dependency.lockId}, got ${actual}`);
185
+ continue;
186
+ }
187
+ await visit(child);
188
+ }
189
+ active.delete(lock.lockId);
190
+ verified.add(lock.lockId);
191
+ };
192
+ await visit(options.root);
193
+ return { ok: errors.length === 0, errors, verifiedLocks: [...verified].sort() };
194
+ }
195
+ //# sourceMappingURL=artifact-attestation.js.map