@aiwg/cli 2026.8.10 → 2026.8.12

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 (38) hide show
  1. package/bin/aiwg.mjs +2 -0
  2. package/dist/src/agents/packaged-agent-inventory.js +37 -1
  3. package/dist/src/api/index.d.ts +6 -0
  4. package/dist/src/api/index.js +6 -0
  5. package/dist/src/artifacts/fortemi-core-sync.js +23 -8
  6. package/dist/src/cli/handlers/artifact-verify.js +171 -0
  7. package/dist/src/cli/handlers/index.js +3 -1
  8. package/dist/src/cli/handlers/refresh.js +43 -9
  9. package/dist/src/cli/handlers/setup-manifest.js +52 -3
  10. package/dist/src/cli/handlers/setup.js +15 -2
  11. package/dist/src/cli/handlers/use.js +80 -0
  12. package/dist/src/cli/services/deployment-verification.js +65 -7
  13. package/dist/src/config/aiwg-config.js +4 -3
  14. package/dist/src/config/cli.js +3 -1
  15. package/dist/src/config/gitignore.js +67 -21
  16. package/dist/src/config/workspace.js +8 -1
  17. package/dist/src/extensions/commands/definitions.js +19 -0
  18. package/dist/src/marketplace/artifact-attestation.js +195 -0
  19. package/dist/src/marketplace/exchange.js +437 -79
  20. package/dist/src/marketplace/provenance-types.js +1 -0
  21. package/dist/src/marketplace/provenance.js +7 -1
  22. package/dist/src/providers/transformation-receipt-integration.js +448 -0
  23. package/dist/src/providers/transformation-receipt.js +215 -0
  24. package/dist/src/resources/web-release.d.ts +11 -0
  25. package/dist/src/resources/web-release.js +61 -6
  26. package/dist/src/security/artifact-attestation.js +117 -0
  27. package/dist/src/security/artifact-trust.js +557 -0
  28. package/dist/src/security/artifact-verifier.js +478 -0
  29. package/dist/src/tracker/capability-protocol.js +7 -2
  30. package/package.json +5 -1
  31. package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
  32. package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
  33. package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
  34. package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
  35. package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
  36. package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
  37. package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
  38. package/tools/agents/providers/base.mjs +14 -2
package/bin/aiwg.mjs CHANGED
@@ -124,6 +124,8 @@ const FAST_HELP_TEXT = `
124
124
 
125
125
  VALIDATION
126
126
  validate-metadata [path] Validate AIWG component metadata (defaults to agentic/code)
127
+ verify <artifact> Verify DSSE provenance using an explicit versioned trust root
128
+ verify trust <action> Bootstrap, update, or inspect artifact trust state
127
129
  context-firewall [scan] Audit provider context, trust, drift, poisoning signals, and budget
128
130
  context-firewall baseline Plan or explicitly write the reviewed context baseline
129
131
 
@@ -1,4 +1,5 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  const MANAGED_MARKER_RE = /(?:^|\n)(?:#|<!--)\s*aiwg:managed\s+v?([^\s]+)\s+([^\s>]+)(?:\s*-->)?/;
4
5
  /** Normalize provider-specific deployed filenames to the source agent id. */
@@ -12,6 +13,31 @@ export function parseManagedArtifactMarker(content) {
12
13
  const match = MANAGED_MARKER_RE.exec(content);
13
14
  return match ? { version: match[1], source: match[2] } : null;
14
15
  }
16
+ /** Extract the developer-instruction body from a canonical Markdown agent. */
17
+ export function extractAgentInstructionBody(content) {
18
+ if (!content.startsWith('---'))
19
+ return content.trim();
20
+ const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
21
+ return withoutFrontmatter.trim();
22
+ }
23
+ function instructionHash(content) {
24
+ return createHash('sha256').update(content).digest('hex');
25
+ }
26
+ function extractDeployedInstructions(filename, content) {
27
+ if (!filename.toLowerCase().endsWith('.toml')) {
28
+ return extractAgentInstructionBody(content);
29
+ }
30
+ const match = content.match(/^developer_instructions\s*=\s*(.+)$/m);
31
+ if (!match)
32
+ return null;
33
+ try {
34
+ const value = JSON.parse(match[1]);
35
+ return typeof value === 'string' ? value.trim() : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
15
41
  async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdownFiles) {
16
42
  let entries;
17
43
  try {
@@ -31,8 +57,12 @@ async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdow
31
57
  if (!allMarkdownFiles && path.basename(rootDir) !== 'agents')
32
58
  return;
33
59
  let stat;
60
+ let content;
34
61
  try {
35
- stat = await fs.stat(absolute);
62
+ [stat, content] = await Promise.all([
63
+ fs.stat(absolute),
64
+ fs.readFile(absolute, 'utf8'),
65
+ ]);
36
66
  }
37
67
  catch {
38
68
  return;
@@ -42,6 +72,7 @@ async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdow
42
72
  sources.push({
43
73
  path: path.relative(frameworkRoot, absolute),
44
74
  size: stat.size,
75
+ instructionHash: instructionHash(extractAgentInstructionBody(content)),
45
76
  });
46
77
  inventory.set(name, sources);
47
78
  }));
@@ -67,6 +98,11 @@ export function diagnoseOversizedAgent(filename, content, inventory, ceilingByte
67
98
  const packaged = inventory.get(normalizeAgentArtifactName(filename)) ?? [];
68
99
  if (packaged.some((source) => source.size > ceilingBytes))
69
100
  return 'current-package';
101
+ const deployedInstructions = extractDeployedInstructions(filename, content);
102
+ if (deployedInstructions !== null
103
+ && packaged.some((source) => source.instructionHash === instructionHash(deployedInstructions))) {
104
+ return 'current-package';
105
+ }
70
106
  const marker = parseManagedArtifactMarker(content);
71
107
  if (marker?.source === 'bundled')
72
108
  return 'stale-deployment';
@@ -10,4 +10,10 @@ export * from '../resources/index.js';
10
10
  export * from '../sessions/index.js';
11
11
  export * from '../memory/index.js';
12
12
  export * from '../security/threat-assessment-config.js';
13
+ export * from '../security/artifact-verifier.js';
14
+ export * from '../security/artifact-attestation.js';
15
+ export * from '../providers/transformation-receipt.js';
16
+ export * from '../providers/transformation-receipt-integration.js';
17
+ export * from '../marketplace/artifact-attestation.js';
18
+ export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, type ArtifactTrustRoot, type ArtifactTrustState, type RootBootstrapResult, type RootTransitionResult, type ArtifactTrustPolicySettings, type TrustedChannelState, } from '../security/artifact-trust.js';
13
19
  //# sourceMappingURL=index.d.ts.map
@@ -10,4 +10,10 @@ export * from '../resources/index.js';
10
10
  export * from '../sessions/index.js';
11
11
  export * from '../memory/index.js';
12
12
  export * from '../security/threat-assessment-config.js';
13
+ export * from '../security/artifact-verifier.js';
14
+ export * from '../security/artifact-attestation.js';
15
+ export * from '../providers/transformation-receipt.js';
16
+ export * from '../providers/transformation-receipt-integration.js';
17
+ export * from '../marketplace/artifact-attestation.js';
18
+ export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, } from '../security/artifact-trust.js';
13
19
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
5
6
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
6
7
  import { loadGraphIndexFile } from "./index-reader.js";
@@ -25,7 +26,7 @@ function findPackageRoot(startDir) {
25
26
  }
26
27
  }
27
28
  function prebuiltDir(graph) {
28
- const moduleDir = path.dirname(new URL(import.meta.url).pathname);
29
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
29
30
  const packageRoot = findPackageRoot(moduleDir);
30
31
  if (!packageRoot)
31
32
  return null;
@@ -215,17 +216,31 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
215
216
  reason: manifestReadReason,
216
217
  };
217
218
  }
219
+ const prebuiltManifest = manifest;
218
220
  const exportExists = fs.existsSync(exportPath);
219
221
  let reason = null;
220
222
  if (exportExists) {
221
223
  try {
222
224
  const exportText = fs.readFileSync(exportPath, "utf-8");
223
225
  const exported = JSON.parse(exportText);
224
- if (sha256(exportText) !== manifest.export_checksum) {
226
+ if (prebuiltManifest.schema_version !== "aiwg.fortemi.prebuilt.v1" ||
227
+ prebuiltManifest.backend !== "fortemi-core" ||
228
+ prebuiltManifest.graph !== graph ||
229
+ prebuiltManifest.export_path !== "aiwg-fortemi-index-v2.json" ||
230
+ prebuiltManifest.export_schema_version !== "aiwg.fortemi.index.export.v2") {
231
+ reason = "prebuilt manifest is incompatible with the requested graph";
232
+ }
233
+ else if (sha256(exportText) !== prebuiltManifest.export_checksum) {
225
234
  reason = "prebuilt export checksum does not match manifest";
226
235
  }
227
- else if (exported.schema_version !== manifest.export_schema_version) {
228
- reason = `prebuilt export schema '${exported.schema_version}' does not match manifest '${manifest.export_schema_version}'`;
236
+ else if (exported.schema_version !== prebuiltManifest.export_schema_version) {
237
+ reason = `prebuilt export schema '${exported.schema_version}' does not match manifest '${prebuiltManifest.export_schema_version}'`;
238
+ }
239
+ else if (exported.source?.graph !== graph) {
240
+ reason = `prebuilt export graph '${exported.source?.graph ?? "unknown"}' does not match requested graph '${graph}'`;
241
+ }
242
+ else if (!Array.isArray(exported.items) || exported.items.length !== prebuiltManifest.item_count) {
243
+ reason = "prebuilt export item count does not match manifest";
229
244
  }
230
245
  }
231
246
  catch (err) {
@@ -241,10 +256,10 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
241
256
  exportPath,
242
257
  built: exportExists,
243
258
  stale: !exportExists || reason !== null,
244
- itemCount: manifest.item_count,
245
- exportChecksum: manifest.export_checksum,
246
- generatedAt: manifest.generated_at,
247
- sourceIndexBuiltAt: manifest.source_index_built_at,
259
+ itemCount: prebuiltManifest.item_count,
260
+ exportChecksum: prebuiltManifest.export_checksum,
261
+ generatedAt: prebuiltManifest.generated_at,
262
+ sourceIndexBuiltAt: prebuiltManifest.source_index_built_at,
248
263
  reason: !exportExists ? "prebuilt manifest exists but export file is missing" : reason,
249
264
  };
250
265
  }
@@ -0,0 +1,171 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { bootstrapTrustRoot, parseTrustState, verifyRootTransition, writeTrustState, } from '../../security/artifact-trust.js';
4
+ import { verifyArtifact } from '../../security/artifact-verifier.js';
5
+ const MAX_BYTES = 32 * 1024 * 1024;
6
+ const DEFAULT_STATE = '.aiwg/security/artifact-trust-state.json';
7
+ function usage() {
8
+ return [
9
+ 'aiwg verify — Verify cross-asset provenance using explicit trust policy',
10
+ '',
11
+ 'Usage:',
12
+ ' aiwg verify <artifact> --attestation <file-or-https-url> --policy <root.json>',
13
+ ' [--state <file>] [--root-fingerprint <sha256>] [--material <uri>=<file>]...',
14
+ ' [--asset-type <type> --namespace <name> --channel <name>]',
15
+ ' [--offline] [--json] [--allow-policy-exempt] [--no-write-state]',
16
+ ' aiwg verify trust bootstrap --root <file> --fingerprint <sha256> [--state <file>]',
17
+ ' aiwg verify trust update --current <file> --next <file> [--state <file>]',
18
+ ' aiwg verify trust status [--state <file>] [--json]',
19
+ '',
20
+ 'Trust is never inferred from DSSE keyid or embedded outer public keys.',
21
+ ].join('\n');
22
+ }
23
+ function valueAfter(args, flag) {
24
+ const index = args.indexOf(flag);
25
+ return index >= 0 ? args[index + 1] : undefined;
26
+ }
27
+ function resolve(cwd, file) {
28
+ return path.isAbsolute(file) ? file : path.resolve(cwd, file);
29
+ }
30
+ function checkedRead(file) {
31
+ const bytes = readFileSync(file);
32
+ if (bytes.length > MAX_BYTES)
33
+ throw new Error(`${file} exceeds the ${MAX_BYTES}-byte safety limit`);
34
+ return bytes;
35
+ }
36
+ async function readLocation(location, cwd, offline, signal) {
37
+ if (!/^https?:\/\//i.test(location))
38
+ return checkedRead(resolve(cwd, location));
39
+ if (offline)
40
+ throw new Error('offline mode forbids network locations');
41
+ if (!location.startsWith('https://'))
42
+ throw new Error('remote verification inputs must use HTTPS');
43
+ const response = await fetch(location, { redirect: 'follow', signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000) });
44
+ if (!response.ok || !response.url.startsWith('https://'))
45
+ throw new Error(`HTTPS input failed closed (${response.status})`);
46
+ const contentLength = Number(response.headers.get('content-length') ?? 0);
47
+ if (contentLength > MAX_BYTES)
48
+ throw new Error('remote input exceeds the safety limit');
49
+ const bytes = Buffer.from(await response.arrayBuffer());
50
+ if (bytes.length > MAX_BYTES)
51
+ throw new Error('remote input exceeds the safety limit');
52
+ return bytes;
53
+ }
54
+ function renderHuman(outcome) {
55
+ return [
56
+ `${outcome.status}: ${outcome.artifact.name}`,
57
+ ` SHA-256: ${outcome.artifact.sha256}`,
58
+ ` Policy: ${outcome.policy ?? 'unavailable'}`,
59
+ ` Identities: ${outcome.identities.join(', ') || 'none'}`,
60
+ ` Root version: ${outcome.rootVersion ?? 'unavailable'}`,
61
+ ...outcome.diagnostics.map(diagnostic => ` ${diagnostic.code}: ${diagnostic.message}`),
62
+ ].join('\n');
63
+ }
64
+ async function trustCommand(ctx) {
65
+ const action = ctx.args[1];
66
+ const stateFile = resolve(ctx.cwd, valueAfter(ctx.args, '--state') ?? DEFAULT_STATE);
67
+ if (action === 'status') {
68
+ if (!existsSync(stateFile))
69
+ return { exitCode: 1, message: `Trust state not found: ${stateFile}` };
70
+ const state = parseTrustState(checkedRead(stateFile));
71
+ return {
72
+ exitCode: 0,
73
+ message: ctx.args.includes('--json') ? JSON.stringify(state, null, 2) : `Root v${state.rootVersion} ${state.rootSha256}\nTrusted time: ${state.trustedTime}\nChannels: ${Object.keys(state.channels).length}`,
74
+ rawOutput: ctx.args.includes('--json'),
75
+ };
76
+ }
77
+ if (action === 'bootstrap') {
78
+ const rootFile = valueAfter(ctx.args, '--root');
79
+ const fingerprint = valueAfter(ctx.args, '--fingerprint');
80
+ if (!rootFile || !fingerprint)
81
+ return { exitCode: 1, message: 'trust bootstrap requires --root and --fingerprint' };
82
+ const bootstrapped = bootstrapTrustRoot(checkedRead(resolve(ctx.cwd, rootFile)), fingerprint);
83
+ writeTrustState(stateFile, bootstrapped.state);
84
+ return { exitCode: 0, message: `Bootstrapped trust root v${bootstrapped.root.signed.version} at ${stateFile}` };
85
+ }
86
+ if (action === 'update') {
87
+ const current = valueAfter(ctx.args, '--current');
88
+ const next = valueAfter(ctx.args, '--next');
89
+ if (!current || !next)
90
+ return { exitCode: 1, message: 'trust update requires --current and --next' };
91
+ if (!existsSync(stateFile))
92
+ return { exitCode: 1, message: `Trust state not found: ${stateFile}` };
93
+ const transition = verifyRootTransition(checkedRead(resolve(ctx.cwd, current)), checkedRead(resolve(ctx.cwd, next)), parseTrustState(checkedRead(stateFile)));
94
+ writeTrustState(stateFile, transition.state);
95
+ return { exitCode: 0, message: `Updated trust root to v${transition.state.rootVersion} at ${stateFile}` };
96
+ }
97
+ return { exitCode: 1, message: usage() };
98
+ }
99
+ export const artifactVerifyHandler = {
100
+ id: 'verify',
101
+ name: 'Artifact Verification',
102
+ description: 'Verify cross-asset DSSE provenance and manage trust roots',
103
+ category: 'utility',
104
+ aliases: [],
105
+ async execute(ctx) {
106
+ if (ctx.args.includes('--help') || ctx.args.includes('-h') || ctx.args.length === 0)
107
+ return { exitCode: 0, message: usage() };
108
+ try {
109
+ if (ctx.args[0] === 'trust')
110
+ return await trustCommand(ctx);
111
+ const artifactLocation = ctx.args[0];
112
+ const rootFile = valueAfter(ctx.args, '--policy');
113
+ if (!rootFile)
114
+ return { exitCode: 1, message: `--policy <root.json> is required\n\n${usage()}` };
115
+ const offline = ctx.args.includes('--offline');
116
+ const attestationLocation = valueAfter(ctx.args, '--attestation') ?? `${artifactLocation}.aiwg-attestation.json`;
117
+ const [artifactBytes, attestationBytes] = await Promise.all([
118
+ readLocation(artifactLocation, ctx.cwd, offline, ctx.signal),
119
+ readLocation(attestationLocation, ctx.cwd, offline, ctx.signal),
120
+ ]);
121
+ const rootBytes = checkedRead(resolve(ctx.cwd, rootFile));
122
+ const stateFile = resolve(ctx.cwd, valueAfter(ctx.args, '--state') ?? DEFAULT_STATE);
123
+ let state = existsSync(stateFile) ? parseTrustState(checkedRead(stateFile)) : undefined;
124
+ if (!state && !offline) {
125
+ const fingerprint = valueAfter(ctx.args, '--root-fingerprint');
126
+ if (fingerprint)
127
+ state = bootstrapTrustRoot(rootBytes, fingerprint).state;
128
+ }
129
+ const materials = new Map();
130
+ for (let index = 0; index < ctx.args.length; index += 1) {
131
+ if (ctx.args[index] !== '--material')
132
+ continue;
133
+ const spec = ctx.args[index + 1] ?? '';
134
+ const separator = spec.lastIndexOf('=');
135
+ if (separator <= 0)
136
+ throw new Error('--material must use <uri>=<file>');
137
+ materials.set(spec.slice(0, separator), checkedRead(resolve(ctx.cwd, spec.slice(separator + 1))));
138
+ }
139
+ const outcome = await verifyArtifact({
140
+ artifactBytes,
141
+ artifactName: /^https:\/\//.test(artifactLocation) ? new URL(artifactLocation).pathname.split('/').pop() ?? artifactLocation : path.basename(artifactLocation),
142
+ attestation: JSON.parse(attestationBytes.toString('utf8')),
143
+ rootBytes,
144
+ state,
145
+ materials,
146
+ ...(valueAfter(ctx.args, '--asset-type') && valueAfter(ctx.args, '--namespace') && valueAfter(ctx.args, '--channel')
147
+ ? { expectedScope: {
148
+ assetType: valueAfter(ctx.args, '--asset-type'),
149
+ namespace: valueAfter(ctx.args, '--namespace'),
150
+ channel: valueAfter(ctx.args, '--channel'),
151
+ } }
152
+ : {}),
153
+ offline,
154
+ });
155
+ if (outcome.status === 'verified' && outcome.nextState && !ctx.args.includes('--no-write-state'))
156
+ writeTrustState(stateFile, outcome.nextState);
157
+ const json = ctx.args.includes('--json');
158
+ const processExit = outcome.status === 'policy-exempt' && ctx.args.includes('--allow-policy-exempt') ? 0 : outcome.exitCode;
159
+ return { exitCode: processExit, message: json ? JSON.stringify(outcome, null, 2) : renderHuman(outcome), rawOutput: json };
160
+ }
161
+ catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ return { exitCode: 27, message: ctx.args.includes('--json') ? JSON.stringify({
164
+ schemaVersion: 'aiwg.verify.result.v1', status: 'malformed', exitCode: 27,
165
+ artifact: { name: ctx.args[0] ? path.basename(ctx.args[0]) : 'unknown', sha256: '0'.repeat(64) },
166
+ identities: [], diagnostics: [{ code: 'CLI_INPUT_ERROR', message }],
167
+ }, null, 2) : `malformed: ${message}`, rawOutput: ctx.args.includes('--json') };
168
+ }
169
+ },
170
+ };
171
+ //# sourceMappingURL=artifact-verify.js.map
@@ -61,12 +61,13 @@ import { versionsHandler } from './resource-versions.js';
61
61
  import { jobHandler } from './job.js';
62
62
  import { costReportHandler } from './cost-report.js';
63
63
  import { evidenceHandler } from './evidence.js';
64
+ import { artifactVerifyHandler } from './artifact-verify.js';
64
65
  // Re-export individual handlers
65
66
  export {
66
67
  // Maintenance
67
68
  helpHandler, versionHandler, authHandler, doctorHandler, contextFirewallHandler, updateHandler, refreshHandler, regenerateHandler, workspaceContextHandler,
68
69
  // Framework management
69
- useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler,
70
+ useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler, artifactVerifyHandler,
70
71
  // Project
71
72
  newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
72
73
  // Workspace
@@ -148,6 +149,7 @@ export const allHandlers = [
148
149
  jobHandler,
149
150
  costReportHandler,
150
151
  evidenceHandler,
152
+ artifactVerifyHandler,
151
153
  // Workspace management
152
154
  ...workspaceHandlers,
153
155
  // Subcommand handlers (MCP, catalog, index, skills)
@@ -15,6 +15,7 @@
15
15
  import { promises as fs } from 'fs';
16
16
  import path from 'path';
17
17
  import { createScriptRunner } from './script-runner.js';
18
+ import { createUseHandler } from './use.js';
18
19
  import { getFrameworkRoot } from '../../channel/manager.mjs';
19
20
  import { refreshAllPackages } from '../../packages/registry.js';
20
21
  import { resolveActiveProvider } from '../provider-resolution.js';
@@ -97,7 +98,14 @@ export async function pruneStaleManagedAgentFiles(options) {
97
98
  continue;
98
99
  const artifactName = normalizeAgentArtifactName(entry.name);
99
100
  const missingFromCurrentPackage = !desired.has(artifactName);
100
- const fromOlderPackage = currentVersion !== null && isOlderManagedVersion(marker.version, currentVersion);
101
+ // Addons have independent manifest versions. Comparing their managed
102
+ // marker to the top-level package version makes a successful refresh
103
+ // delete freshly restored addon agents. Version-based cleanup remains
104
+ // valid for other provider trees that were not refreshed, while the
105
+ // active provider removes only artifacts absent from current sources.
106
+ const fromOlderPackage = provider !== options.provider
107
+ && currentVersion !== null
108
+ && isOlderManagedVersion(marker.version, currentVersion);
101
109
  if (!missingFromCurrentPackage && !fromOlderPackage)
102
110
  continue;
103
111
  const relFile = path.relative(options.projectRoot, file);
@@ -163,6 +171,7 @@ export const refreshHandler = {
163
171
  const modelDeployArgs = collectModelDeployArgs(ctx.args);
164
172
  const frameworkRoot = await getFrameworkRoot();
165
173
  const runner = createScriptRunner(frameworkRoot);
174
+ const activeUseHandler = createUseHandler();
166
175
  if (!quiet) {
167
176
  ui.blank();
168
177
  // Deprecation notice when invoked as 'sync'
@@ -195,6 +204,7 @@ export const refreshHandler = {
195
204
  // Step 2.5: Refresh remote packages (always, unless --packages-only skips npm)
196
205
  if (!quiet)
197
206
  ui.info(dryRun ? 'Would refresh remote packages...' : 'Refreshing remote packages...');
207
+ const deploymentFailures = [];
198
208
  if (!dryRun) {
199
209
  try {
200
210
  const refreshed = await refreshAllPackages();
@@ -259,13 +269,30 @@ export const refreshHandler = {
259
269
  ui.dim(' No installed frameworks or addons to re-deploy');
260
270
  }
261
271
  for (const fw of frameworks) {
262
- const providerArgs = ['--provider', detectedProvider, ...modelDeployArgs];
263
- const useResult = await runner.run('tools/cli/deploy.mjs', [fw, ...providerArgs], { capture: quiet });
272
+ // Invoke the active installation's handler directly. The historical
273
+ // deploy.mjs bridge shells out to the first `aiwg` on PATH, which can
274
+ // be a different version/root and therefore cannot safely refresh
275
+ // addons installed by this package (#143/#2102).
276
+ const useResult = await activeUseHandler.execute({
277
+ ...ctx,
278
+ cwd: ctx.cwd,
279
+ frameworkRoot,
280
+ args: [
281
+ fw,
282
+ '--provider', detectedProvider,
283
+ '--target', ctx.cwd,
284
+ '--yes',
285
+ '--json',
286
+ ...modelDeployArgs,
287
+ ],
288
+ rawArgs: ['use', fw],
289
+ });
264
290
  if (useResult.exitCode === 0) {
265
291
  if (!quiet)
266
292
  ui.success(`Deployed: ${fw}`);
267
293
  }
268
294
  else {
295
+ deploymentFailures.push(fw);
269
296
  if (!quiet)
270
297
  ui.warn(`Deploy issue: ${fw} (exit ${useResult.exitCode})`);
271
298
  }
@@ -281,11 +308,10 @@ export const refreshHandler = {
281
308
  }
282
309
  }
283
310
  // Step 4.25: Report planned project-local deploys (#1035).
284
- // The actual deploy is performed by `aiwg use` underneath via deploy.mjs;
285
- // this block surfaces what *would* happen during dry-run and what was
286
- // covered during a real refresh.
311
+ // The active use handler performs the actual project-local deploy during
312
+ // framework refresh; this block surfaces dry-run and completion details.
287
313
  try {
288
- const plDiscovery = await discoverProjectLocalBundles(process.cwd());
314
+ const plDiscovery = await discoverProjectLocalBundles(ctx.cwd);
289
315
  const plCount = plDiscovery.bundles.length;
290
316
  if (plCount > 0) {
291
317
  if (dryRun) {
@@ -312,11 +338,12 @@ export const refreshHandler = {
312
338
  if (!quiet)
313
339
  ui.info('Checking for stale deployments...');
314
340
  let staleAgentRemovals = [];
315
- if (!dryRun) {
341
+ if (!dryRun && deploymentFailures.length === 0) {
316
342
  try {
317
343
  staleAgentRemovals = await pruneStaleManagedAgentFiles({
318
344
  projectRoot: ctx.cwd,
319
345
  frameworkRoot,
346
+ provider: detectedProvider,
320
347
  });
321
348
  if (staleAgentRemovals.length > 0 && !quiet) {
322
349
  const total = staleAgentRemovals.reduce((sum, item) => sum + item.paths.length, 0);
@@ -435,10 +462,17 @@ export const refreshHandler = {
435
462
  skipUpdate,
436
463
  channel: channel || undefined,
437
464
  staleAgentRemovals,
465
+ deploymentFailures,
438
466
  });
439
467
  console.log(output);
440
468
  }
441
- return { exitCode: dryRun ? 0 : 0 };
469
+ if (deploymentFailures.length > 0) {
470
+ return {
471
+ exitCode: 1,
472
+ message: `Failed to re-deploy installed bundle(s): ${deploymentFailures.join(', ')}`,
473
+ };
474
+ }
475
+ return { exitCode: 0 };
442
476
  },
443
477
  };
444
478
  //# sourceMappingURL=refresh.js.map
@@ -4,6 +4,8 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { parseDocument, stringify } from 'yaml';
6
6
  import { findPackageRoot } from '../find-package-root.js';
7
+ import { sha256 } from '../../security/artifact-trust.js';
8
+ import { artifactVerifyHandler } from './artifact-verify.js';
7
9
  const SETUP_SCHEMA_REL = 'agentic/code/addons/agentic-installer/schemas/v1/setup-manifest.schema.json';
8
10
  const GENERATE_HELP = `aiwg setup-generate - generate a starter setup.aiwg.io/v1 SetupManifest
9
11
 
@@ -40,7 +42,8 @@ Usage:
40
42
  aiwg setup-run [manifest-path] [--manifest PATH] [--dry-run] [--platform OS]
41
43
  [--distro NAME] [--params-file PATH] [--param KEY=VALUE]
42
44
  [--step STEP_ID] [--skip A,B] [--type user|developer|ci]
43
- [--yes|--confirm]
45
+ [--yes|--confirm] [--attestation PATH] [--policy ROOT]
46
+ [--state PATH] [--offline]
44
47
 
45
48
  Options:
46
49
  --manifest PATH Manifest path. Defaults to ./setup.manifest.yaml.
@@ -53,11 +56,17 @@ Options:
53
56
  --skip A,B Comma-separated step IDs to skip.
54
57
  --type TYPE Select default manifest by install type when no path is given.
55
58
  --yes, --confirm Explicitly authorize mutating step execution and recovery.
59
+ --attestation PATH Adjacent AIWG attestation for provider-orchestrated handoff.
60
+ --policy ROOT Explicit cross-asset trust root; required for agent handoff.
61
+ --state PATH Persisted trust/freshness state used by verification.
62
+ --offline Forbid network retrieval and require portable evidence.
56
63
  --help, -h Show this help.
57
64
 
58
65
  Safety:
59
66
  setup-run always runs setup-validate before platform detection or execution.
60
67
  Mutating execution refuses to run without explicit confirmation.
68
+ Provider-orchestrated manifests are never handed to an agent unless their
69
+ exact bytes have status verified under the explicit trust policy.
61
70
  `;
62
71
  function flagValue(args, name) {
63
72
  for (let i = 0; i < args.length; i += 1) {
@@ -85,6 +94,12 @@ function positionalManifest(args) {
85
94
  '--type',
86
95
  '--output',
87
96
  '--name',
97
+ '--attestation',
98
+ '--policy',
99
+ '--state',
100
+ '--asset-type',
101
+ '--namespace',
102
+ '--channel',
88
103
  ]);
89
104
  for (let i = 0; i < args.length; i += 1) {
90
105
  const arg = args[i];
@@ -689,9 +704,17 @@ export function runSetupManifest(options) {
689
704
  }
690
705
  const manifest = validation.manifest;
691
706
  if (manifest.metadata.execution_mode === 'provider-orchestrated') {
707
+ const expectedDigest = sha256(readFileSync(validation.manifestPath));
708
+ if (options.artifactVerification?.status !== 'verified'
709
+ || options.artifactVerification.artifact.sha256 !== expectedDigest) {
710
+ return {
711
+ exitCode: 29,
712
+ message: 'setup-run: provider-orchestrated handoff blocked; verify these exact manifest bytes with an explicit trust root and adjacent attestation first',
713
+ };
714
+ }
692
715
  return {
693
716
  exitCode: 2,
694
- message: 'setup-run: this manifest is provider-orchestrated; give its URL or contents to a supported AI provider instead of executing it as a deterministic CLI manifest',
717
+ message: `setup-run: verified provider-orchestrated manifest (${options.artifactVerification.artifact.sha256}); give these exact contents to a supported AI provider instead of executing them as a deterministic CLI manifest`,
695
718
  };
696
719
  }
697
720
  const target = detectPlatform(options);
@@ -801,7 +824,33 @@ export const setupRunHandler = {
801
824
  process.stdout.write(RUN_HELP);
802
825
  return { exitCode: 0 };
803
826
  }
804
- return runSetupManifest(parseRunOptions(ctx));
827
+ const options = parseRunOptions(ctx);
828
+ const manifestPath = options.manifestPath ?? 'setup.manifest.yaml';
829
+ let artifactVerification;
830
+ if (flagValue(ctx.args, '--policy')) {
831
+ const verificationArgs = [
832
+ manifestPath,
833
+ '--attestation', flagValue(ctx.args, '--attestation') ?? `${manifestPath}.aiwg-attestation.json`,
834
+ '--policy', flagValue(ctx.args, '--policy'),
835
+ '--asset-type', flagValue(ctx.args, '--asset-type') ?? 'setup-manifest',
836
+ '--namespace', flagValue(ctx.args, '--namespace') ?? 'aiwg',
837
+ '--channel', flagValue(ctx.args, '--channel') ?? 'stable',
838
+ '--json',
839
+ ...(flagValue(ctx.args, '--state') ? ['--state', flagValue(ctx.args, '--state')] : []),
840
+ ...(hasFlag(ctx.args, '--offline') ? ['--offline'] : []),
841
+ ];
842
+ const verification = await artifactVerifyHandler.execute({ ...ctx, args: verificationArgs, rawArgs: verificationArgs });
843
+ try {
844
+ artifactVerification = JSON.parse(verification.message ?? '');
845
+ }
846
+ catch {
847
+ return { exitCode: verification.exitCode || 27, message: `setup-run: artifact verification failed: ${verification.message ?? 'invalid verifier output'}` };
848
+ }
849
+ if (artifactVerification.status !== 'verified') {
850
+ return { exitCode: artifactVerification.exitCode, message: `setup-run: provider handoff blocked by artifact verification status '${artifactVerification.status}'` };
851
+ }
852
+ }
853
+ return runSetupManifest({ ...options, artifactVerification });
805
854
  },
806
855
  };
807
856
  //# sourceMappingURL=setup-manifest.js.map
@@ -153,6 +153,13 @@ function secondaryRemotes(remotes, primary, issueTracker, ci) {
153
153
  push_on_release: false,
154
154
  }));
155
155
  }
156
+ function normalizeForcePushPolicy(value, warnings) {
157
+ if (value === 'main-only-blocked') {
158
+ warnings.push('delivery.force_push_policy=main-only-blocked is a legacy alias; setup normalized it to own-branch-only.');
159
+ return 'own-branch-only';
160
+ }
161
+ return value;
162
+ }
156
163
  function cloneConfig(config) {
157
164
  return JSON.parse(JSON.stringify(config));
158
165
  }
@@ -191,6 +198,9 @@ function validateSetupConfig(config, remotes, issueProvider) {
191
198
  else {
192
199
  checkRemote('remotes.issue_tracker', config.remotes?.issue_tracker);
193
200
  }
201
+ if (config.remotes?.issue_provider && !ISSUE_PROVIDERS.includes(config.remotes.issue_provider)) {
202
+ errors.push('remotes.issue_provider is invalid');
203
+ }
194
204
  checkRemote('remotes.ci', config.remotes?.ci);
195
205
  if (!DELIVERY_MODES.includes(config.delivery?.mode))
196
206
  errors.push('delivery.mode is invalid');
@@ -222,6 +232,7 @@ export async function buildSetupProjectPlan(options) {
222
232
  const primaryRemote = remotes.find(r => r.name === primary);
223
233
  const hasLocalIssues = existsSync(projectAiwgPath(options.projectDir, 'issues', 'config.json'));
224
234
  const issueProvider = options.issueProvider ?? chooseIssueProvider(primaryRemote, hasLocalIssues);
235
+ const warnings = [];
225
236
  const issueTracker = issueProvider === 'local'
226
237
  ? 'local'
227
238
  : options.issueTracker ?? base.remotes?.issue_tracker ?? primary;
@@ -229,6 +240,7 @@ export async function buildSetupProjectPlan(options) {
229
240
  const remotesConfig = {
230
241
  primary,
231
242
  issue_tracker: issueTracker,
243
+ issue_provider: issueProvider,
232
244
  ci,
233
245
  secondary: base.remotes?.secondary ?? secondaryRemotes(remotes, primary, issueTracker, ci),
234
246
  };
@@ -249,9 +261,11 @@ export async function buildSetupProjectPlan(options) {
249
261
  require_ci_green: options.requireCiGreen ?? existingDelivery.require_ci_green ?? true,
250
262
  auto_close_issues: options.autoCloseIssues ?? existingDelivery.auto_close_issues ?? true,
251
263
  issue_comment_on_cycle: options.issueCommentOnCycle ?? existingDelivery.issue_comment_on_cycle ?? true,
252
- force_push_policy: options.forcePushPolicy ?? existingDelivery.force_push_policy ?? 'never',
253
264
  require_signed_commits: options.requireSignedCommits ?? existingDelivery.require_signed_commits ?? false,
254
265
  };
266
+ delivery.force_push_policy = (options.forcePushPolicy
267
+ ?? normalizeForcePushPolicy(existingDelivery.force_push_policy, warnings)
268
+ ?? 'never');
255
269
  const committerName = options.committerName ?? existingDelivery.committer?.name ?? gitConfig(options.projectDir, 'user.name');
256
270
  const committerEmail = options.committerEmail ?? existingDelivery.committer?.email ?? gitConfig(options.projectDir, 'user.email');
257
271
  if (committerName || committerEmail) {
@@ -273,7 +287,6 @@ export async function buildSetupProjectPlan(options) {
273
287
  }
274
288
  base.remotes = remotesConfig;
275
289
  base.delivery = delivery;
276
- const warnings = [];
277
290
  if (primaryRemote?.provider === 'unknown' && issueProvider !== 'local') {
278
291
  warnings.push(`Remote '${primary}' is self-hosted or unknown; provider '${issueProvider}' was selected explicitly/defaulted.`);
279
292
  }