@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.
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +6 -0
- package/dist/src/api/index.js +6 -0
- package/dist/src/cli/handlers/artifact-verify.js +171 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/setup-manifest.js +52 -3
- package/dist/src/cli/handlers/setup.js +15 -2
- package/dist/src/cli/handlers/use.js +64 -0
- package/dist/src/cli/services/deployment-verification.js +65 -7
- package/dist/src/config/aiwg-config.js +4 -3
- package/dist/src/config/cli.js +3 -1
- package/dist/src/config/gitignore.js +67 -21
- package/dist/src/config/workspace.js +8 -1
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/artifact-attestation.js +195 -0
- package/dist/src/marketplace/exchange.js +437 -79
- package/dist/src/marketplace/provenance-types.js +1 -0
- package/dist/src/marketplace/provenance.js +7 -1
- package/dist/src/providers/transformation-receipt-integration.js +448 -0
- package/dist/src/providers/transformation-receipt.js +215 -0
- package/dist/src/resources/web-release.d.ts +11 -0
- package/dist/src/resources/web-release.js +61 -6
- package/dist/src/security/artifact-attestation.js +117 -0
- package/dist/src/security/artifact-trust.js +557 -0
- package/dist/src/security/artifact-verifier.js +478 -0
- package/dist/src/tracker/capability-protocol.js +7 -2
- package/package.json +5 -1
- package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
- package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
- package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
- package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
- package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
- package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
package/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
|
|
package/dist/src/api/index.d.ts
CHANGED
|
@@ -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
|
package/dist/src/api/index.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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],
|
|
246
|
+
const resolved = resolveProviderPathValue(artifactPaths[type], deploymentRoot);
|
|
195
247
|
counts[type] = await countEntries(resolved);
|
|
196
248
|
}
|
|
197
|
-
const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills,
|
|
249
|
+
const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, deploymentRoot);
|
|
198
250
|
const kernelPath = options.scope === 'user'
|
|
199
251
|
? ''
|
|
200
|
-
: resolveProviderPathValue(definition.paths.kernelSkills,
|
|
252
|
+
: resolveProviderPathValue(definition.paths.kernelSkills, deploymentRoot);
|
|
201
253
|
const kernelCount = await countEntries(kernelPath);
|
|
202
254
|
if (kernelPath && kernelPath !== resolvedSkillsPath)
|
|
203
255
|
counts.skills += kernelCount;
|
|
@@ -207,6 +259,7 @@ export async function verifyProviderDeployment(options) {
|
|
|
207
259
|
}
|
|
208
260
|
}
|
|
209
261
|
findings.push(...await collectRegistryFindings(options, normalized, counts));
|
|
262
|
+
findings.push(...await collectProviderReceiptFindings(options, normalized));
|
|
210
263
|
const projectConfig = await readAiwgConfig(options.projectRoot);
|
|
211
264
|
const scopedRegistry = options.scope === 'user'
|
|
212
265
|
? await readUserRegistry()
|
|
@@ -300,7 +353,7 @@ export async function verifyProviderDeployment(options) {
|
|
|
300
353
|
const indexFailed = findings.some((item) => item.id.startsWith('index-'));
|
|
301
354
|
const contextFailed = findings.some((item) => item.id.startsWith('context-') && item.severity === 'blocking');
|
|
302
355
|
const deployFailed = findings.some((item) => item.severity === 'blocking'
|
|
303
|
-
&& (item.id.startsWith('deployment-') || item.id.
|
|
356
|
+
&& (item.id.startsWith('deployment-') || item.id === 'provider-unknown' || item.id === 'provider-artifacts-missing' || item.id.startsWith('registry-')));
|
|
304
357
|
const phases = [
|
|
305
358
|
phase('resolve', 'passed', true, `Resolved ${options.projectRoot}, ${normalized}, ${options.scope} scope.`),
|
|
306
359
|
phase('deploy', deployFailed ? 'failed' : 'passed', true, deployFailed ? 'Deployment invariants failed.' : 'Provider artifacts and installed state verified.', { counts }),
|
|
@@ -406,13 +459,18 @@ export function aggregateUseDeploymentResult(options) {
|
|
|
406
459
|
}
|
|
407
460
|
export async function verifyConfiguredDeployments(projectRoot, filters = {}, frameworkRoot = process.env.AIWG_ROOT || projectRoot) {
|
|
408
461
|
const config = await readAiwgConfig(projectRoot);
|
|
462
|
+
const userRegistry = filters.scope === 'user' ? await readUserRegistry() : null;
|
|
463
|
+
const installed = userRegistry?.installed ?? config?.installed ?? {};
|
|
464
|
+
const registeredProviders = [...new Set(Object.values(installed).flatMap((entry) => Object.keys(entry.deployedTo ?? {})))];
|
|
409
465
|
const providers = filters.provider
|
|
410
466
|
? [filters.provider]
|
|
411
|
-
:
|
|
412
|
-
|
|
467
|
+
: filters.scope === 'user'
|
|
468
|
+
? registeredProviders
|
|
469
|
+
: config?.providers?.length ? config.providers : registeredProviders;
|
|
470
|
+
const bundles = filters.bundle ? [filters.bundle] : Object.keys(installed);
|
|
413
471
|
const results = [];
|
|
414
472
|
for (const provider of providers) {
|
|
415
|
-
const providerBundles = bundles.filter((bundle) => Boolean(
|
|
473
|
+
const providerBundles = bundles.filter((bundle) => Boolean(installed[bundle]?.deployedTo[provider]));
|
|
416
474
|
if (providerBundles.length === 0)
|
|
417
475
|
continue;
|
|
418
476
|
results.push(await verifyProviderDeployment({
|
|
@@ -448,8 +448,8 @@ export async function readIndexConfig(projectDir) {
|
|
|
448
448
|
* - any host containing 'gitea' (or matching the typical Gitea path shape) → 'gitea'
|
|
449
449
|
*
|
|
450
450
|
* Returns 'unknown' for self-hosted instances we can't classify by host alone —
|
|
451
|
-
* callers should then prompt the operator or
|
|
452
|
-
*
|
|
451
|
+
* callers should then prompt the operator or use `remotes.issue_provider`
|
|
452
|
+
* when the project has declared one.
|
|
453
453
|
*
|
|
454
454
|
* @implements #997
|
|
455
455
|
*/
|
|
@@ -466,7 +466,7 @@ export function resolveRemoteProvider(remoteUrl) {
|
|
|
466
466
|
// gitea — identified by hostname token. Self-hosted Gitea instances often
|
|
467
467
|
// don't include 'gitea' in their hostname (e.g. corporate git servers), so
|
|
468
468
|
// 'unknown' is the honest answer there — callers should consult the
|
|
469
|
-
//
|
|
469
|
+
// explicit remotes.issue_provider hint rather than guess.
|
|
470
470
|
if (lower.includes('gitea'))
|
|
471
471
|
return 'gitea';
|
|
472
472
|
return 'unknown';
|
|
@@ -487,6 +487,7 @@ export function resolveRemotes(remotes) {
|
|
|
487
487
|
return {
|
|
488
488
|
primary,
|
|
489
489
|
issue_tracker: remotes?.issue_tracker ?? primary,
|
|
490
|
+
issue_provider: remotes?.issue_provider,
|
|
490
491
|
ci: remotes?.ci ?? primary,
|
|
491
492
|
tracker_actor: remotes?.tracker_actor,
|
|
492
493
|
transport: remotes?.transport,
|