@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
|
@@ -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],
|
|
246
|
+
const resolved = resolveProviderPathValue(artifactPaths[type], deploymentRoot);
|
|
195
247
|
counts[type] = await countEntries(resolved);
|
|
196
248
|
}
|
|
197
|
-
const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills,
|
|
249
|
+
const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, deploymentRoot);
|
|
198
250
|
const kernelPath = options.scope === 'user'
|
|
199
251
|
? ''
|
|
200
|
-
: resolveProviderPathValue(definition.paths.kernelSkills,
|
|
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.
|
|
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
|
-
:
|
|
412
|
-
|
|
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(
|
|
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
|
|
452
|
-
*
|
|
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
|
-
//
|
|
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,
|
package/dist/src/config/cli.js
CHANGED
|
@@ -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
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
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,
|
|
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
|