@aiwg/cli 2026.8.11 → 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 (34) 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 +64 -0
  9. package/dist/src/cli/services/deployment-verification.js +65 -7
  10. package/dist/src/config/aiwg-config.js +4 -3
  11. package/dist/src/config/cli.js +3 -1
  12. package/dist/src/config/gitignore.js +67 -21
  13. package/dist/src/config/workspace.js +8 -1
  14. package/dist/src/extensions/commands/definitions.js +19 -0
  15. package/dist/src/marketplace/artifact-attestation.js +195 -0
  16. package/dist/src/marketplace/exchange.js +437 -79
  17. package/dist/src/marketplace/provenance-types.js +1 -0
  18. package/dist/src/marketplace/provenance.js +7 -1
  19. package/dist/src/providers/transformation-receipt-integration.js +448 -0
  20. package/dist/src/providers/transformation-receipt.js +215 -0
  21. package/dist/src/resources/web-release.d.ts +11 -0
  22. package/dist/src/resources/web-release.js +61 -6
  23. package/dist/src/security/artifact-attestation.js +117 -0
  24. package/dist/src/security/artifact-trust.js +557 -0
  25. package/dist/src/security/artifact-verifier.js +478 -0
  26. package/dist/src/tracker/capability-protocol.js +7 -2
  27. package/package.json +5 -1
  28. package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
  29. package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
  30. package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
  31. package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
  32. package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
  33. package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
  34. package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
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
 
@@ -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
@@ -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)
@@ -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
  }
@@ -52,10 +52,54 @@ import { generate as generateContextFiles, discoverDeployedArtifacts, } from '..
52
52
  import { verifyModelWrapperDeployment } from '../../models/wrapper-deployment.js';
53
53
  import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
54
54
  import { aggregateUseDeploymentResult, buildDryRunUseResult, renderUseDeploymentResult, verifyProviderDeployment, } from '../services/deployment-verification.js';
55
+ import { finalizeProviderTransformationReceipt, sourceVerificationsFromSignedWebRelease, } from '../../providers/transformation-receipt-integration.js';
56
+ import { loadResourceTrustRootFile, resolveWebRelease, } from '../../resources/web-release.js';
57
+ import { createResourceCredentialProvider } from '../../auth/resource-credentials.js';
55
58
  /**
56
59
  * Valid framework identifiers
57
60
  */
58
61
  const VALID_FRAMEWORKS = ['sdlc', 'marketing', 'media-curator', 'research', 'forensics', 'dfir', 'security-engineering', 'ops', 'validation', 'knowledge-base', 'writing', 'general', 'all'];
62
+ function providerReceiptWebReleaseOptions() {
63
+ const baseUrl = process.env.AIWG_RESOURCE_BASE_URL;
64
+ const cacheRoot = process.env.AIWG_RESOURCE_CACHE_ROOT;
65
+ const trustRootFile = process.env.AIWG_RESOURCE_TRUST_ROOT_FILE;
66
+ const publicKeyPem = trustRootFile === undefined
67
+ ? undefined
68
+ : loadResourceTrustRootFile(path.resolve(trustRootFile));
69
+ return {
70
+ ...(baseUrl === undefined ? {} : { baseUrl }),
71
+ ...(cacheRoot === undefined ? {} : { cacheRoot }),
72
+ ...(publicKeyPem === undefined ? {} : { publicKeyPem }),
73
+ ...(process.env.AIWG_RESOURCE_ALLOW_INSECURE_LOOPBACK_HTTP === '1'
74
+ ? { allowInsecureLoopbackHttp: true }
75
+ : {}),
76
+ };
77
+ }
78
+ async function signedProviderSourceVerifications(options) {
79
+ const versionInfo = await getVersionInfo();
80
+ if (versionInfo.devMode)
81
+ return undefined;
82
+ const releaseOptions = providerReceiptWebReleaseOptions();
83
+ let release;
84
+ try {
85
+ release = await resolveWebRelease({ ...releaseOptions, selector: versionInfo.version, offline: true });
86
+ }
87
+ catch {
88
+ const credentialProvider = createResourceCredentialProvider(process.env);
89
+ const token = await credentialProvider();
90
+ // Protected production resources require the authenticated release
91
+ // credential. A configured alternate endpoint may intentionally be public.
92
+ if (!token && releaseOptions.baseUrl === undefined)
93
+ return undefined;
94
+ release = await resolveWebRelease({
95
+ ...releaseOptions,
96
+ selector: versionInfo.version,
97
+ credentialProvider: async () => token,
98
+ });
99
+ }
100
+ const verifications = await sourceVerificationsFromSignedWebRelease(options, release);
101
+ return Object.keys(verifications).length > 0 ? verifications : undefined;
102
+ }
59
103
  /**
60
104
  * Framework name to deploy mode mapping.
61
105
  * Mode is passed as `--mode <value>` to deploy-agents.mjs, which resolves
@@ -1964,6 +2008,22 @@ export class UseHandler {
1964
2008
  const effectiveScope = provider === 'openclaw' || provider === 'openhuman'
1965
2009
  ? 'user'
1966
2010
  : requestedScope;
2011
+ if (coreResult.exitCode === 0 && !dryRun) {
2012
+ try {
2013
+ const receiptOptions = {
2014
+ projectRoot: projectDir,
2015
+ frameworkRoot,
2016
+ provider,
2017
+ scope: effectiveScope,
2018
+ requestedBundles: [requestedBundle],
2019
+ };
2020
+ const sourceVerifications = await signedProviderSourceVerifications(receiptOptions);
2021
+ await finalizeProviderTransformationReceipt({ ...receiptOptions, sourceVerifications });
2022
+ }
2023
+ catch (error) {
2024
+ originalConsole.warn(`Provider receipt finalization failed for ${provider}: ${error instanceof Error ? error.message : String(error)}`);
2025
+ }
2026
+ }
1967
2027
  providerResults.push(await verifyProviderDeployment({
1968
2028
  projectRoot: projectDir,
1969
2029
  frameworkRoot,
@@ -1974,6 +2034,10 @@ export class UseHandler {
1974
2034
  invocationStartedAt: dryRun ? undefined : startedAt,
1975
2035
  deploymentExitCode: coreResult.exitCode,
1976
2036
  deploymentMessage: coreResult.message,
2037
+ // A normal first deployment has no authenticated verifier handoff
2038
+ // yet, so receipt absence belongs in doctor/status rather than
2039
+ // degrading an otherwise successful `aiwg use` result.
2040
+ reportMissingReceipt: false,
1977
2041
  }));
1978
2042
  }
1979
2043
  result = aggregateUseDeploymentResult({
@@ -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,