@aiwg/cli 2026.8.11 → 2026.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/aiwg.mjs +2 -0
  2. package/dist/src/api/index.d.ts +6 -0
  3. package/dist/src/api/index.js +6 -0
  4. package/dist/src/cli/handlers/artifact-verify.js +171 -0
  5. package/dist/src/cli/handlers/index.js +3 -1
  6. package/dist/src/cli/handlers/setup-manifest.js +52 -3
  7. package/dist/src/cli/handlers/setup.js +15 -2
  8. package/dist/src/cli/handlers/use.js +71 -6
  9. package/dist/src/cli/scope-resolver.js +6 -1
  10. package/dist/src/cli/services/deployment-verification.js +65 -7
  11. package/dist/src/config/aiwg-config.js +4 -3
  12. package/dist/src/config/cli.js +3 -1
  13. package/dist/src/config/gitignore.js +67 -21
  14. package/dist/src/config/workspace.js +8 -1
  15. package/dist/src/extensions/commands/definitions.js +19 -0
  16. package/dist/src/extensions/project-quickref.js +9 -0
  17. package/dist/src/marketplace/artifact-attestation.js +195 -0
  18. package/dist/src/marketplace/exchange.js +437 -79
  19. package/dist/src/marketplace/provenance-types.js +1 -0
  20. package/dist/src/marketplace/provenance.js +7 -1
  21. package/dist/src/providers/hermes-home.js +20 -0
  22. package/dist/src/providers/provider-definitions.js +5 -4
  23. package/dist/src/providers/transformation-receipt-integration.js +448 -0
  24. package/dist/src/providers/transformation-receipt.js +215 -0
  25. package/dist/src/resources/web-release.d.ts +11 -0
  26. package/dist/src/resources/web-release.js +61 -6
  27. package/dist/src/security/artifact-attestation.js +117 -0
  28. package/dist/src/security/artifact-trust.js +557 -0
  29. package/dist/src/security/artifact-verifier.js +478 -0
  30. package/dist/src/skills/deployer.js +5 -1
  31. package/dist/src/tracker/capability-protocol.js +7 -2
  32. package/package.json +5 -1
  33. package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
  34. package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
  35. package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
  36. package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
  37. package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
  38. package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
  39. package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
  40. package/tools/agents/deploy-agents.mjs +8 -2
  41. package/tools/agents/providers/base.mjs +29 -2
  42. package/tools/agents/providers/hermes.mjs +163 -19
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
@@ -630,7 +674,7 @@ const SESSION_RELOAD_NOTICE = {
630
674
  rationale: 'OpenCode loads agent files on session start and does not hot-reload.',
631
675
  },
632
676
  hermes: {
633
- action: 'In an active Hermes session, run /reload-skills to pick up new skills in ~/.hermes/skills/ and /reload-mcp to pick up MCP server changes (~/.hermes/config.yaml) — both are in-session slash commands, no chat restart needed. Restart the chat only as a fallback if the slash commands are unavailable.',
677
+ action: 'In an active Hermes session, run /reload-skills to pick up new skills in $HERMES_HOME/skills/ and /reload-mcp to pick up MCP server changes ($HERMES_HOME/config.yaml) — both are in-session slash commands, no chat restart needed. Restart the chat only as a fallback if the slash commands are unavailable.',
634
678
  rationale: 'Hermes loads skills and MCP config at session start (verified in hermes_cli/commands.py:178 and hermes_cli/config.py:1228). The /reload-skills and /reload-mcp slash commands re-scan in place; /reload-mcp prompts for confirmation by default.',
635
679
  symptom: 'Until reloaded, newly deployed kernel skills are missing from `hermes skills list` and unreachable via natural-language invocation; new MCP servers (incl. AIWG) are missing from the tool surface.',
636
680
  },
@@ -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({
@@ -3185,11 +3249,12 @@ export class UseHandler {
3185
3249
  // Collect deployment counts for registry persistence and the final
3186
3250
  // orchestrated report. Presentation happens once, after verification, so
3187
3251
  // users do not see a second competing summary.
3188
- let counts = { agents: 0, commands: 0, skills: 0, rules: 0, behaviors: 0 };
3189
- if (quiet) {
3190
- const paths = getProviderPaths(provider);
3191
- counts = await countDeployedArtifacts(target, paths, provider);
3192
- }
3252
+ //
3253
+ // Counts are always populated from the on-disk artifacts so that the
3254
+ // registry record written below (#621) reflects the real deploy even on
3255
+ // a verbose run — the prior `if (quiet)` guard left the record
3256
+ // `{agents: 0, commands: 0, skills: 0, rules: 0}` on `-v` runs.
3257
+ const counts = await countDeployedArtifacts(target, paths, provider);
3193
3258
  // Deploy CI workflow files when --ci-hooks-enabled is set (#661)
3194
3259
  if (ciHooksEnabled) {
3195
3260
  await deployCiHooks({ frameworkRoot, framework, target, dryRun });
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import { homedir } from 'node:os';
13
13
  import * as path from 'node:path';
14
+ import { resolveHermesHome, resolveHermesHomePath } from '../providers/hermes-home.js';
15
+ export const hermesHome = resolveHermesHome;
14
16
  /**
15
17
  * User-scope deploy paths per provider per ADR-4 §2. Each path is absolute
16
18
  * (rooted in os.homedir()) so the orchestrator's existing path-join logic
@@ -159,7 +161,10 @@ export const USER_SCOPE_PATHS = {
159
161
  },
160
162
  hermes: {
161
163
  agents: '',
162
- skills: path.join(homedir(), '.hermes', 'skills'),
164
+ // #2119: honor HERMES_HOME so `--scope user` deploys land under the same
165
+ // root the running Hermes session scans, matching the hermes provider's
166
+ // paths.skills resolution.
167
+ skills: resolveHermesHomePath('skills'),
163
168
  commands: '',
164
169
  rules: '',
165
170
  behaviors: '',