@aiwg/cli 2026.8.16 → 2026.8.18
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 +24 -2
- package/dist/src/a2a/agent-card.js +4 -1
- package/dist/src/a2a/client.js +148 -68
- package/dist/src/a2a/codecs.js +480 -0
- package/dist/src/a2a/events.js +226 -0
- package/dist/src/a2a/hitl-driver.js +8 -6
- package/dist/src/a2a/hitl.js +2 -1
- package/dist/src/a2a/http.js +85 -5
- package/dist/src/a2a/protocol.js +136 -0
- package/dist/src/a2a/types.js +4 -14
- package/dist/src/a2a/webhook.js +101 -4
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -0
- package/dist/src/artifacts/types.js +73 -1
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/agent-spawn.js +4 -2
- package/dist/src/cli/handlers/cockpit.js +41 -0
- package/dist/src/cli/handlers/help.js +1 -1
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/ralph.js +2 -1
- package/dist/src/cli/handlers/refresh.js +4 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/use.js +117 -12
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/cli/handlers/workspace.js +24 -2
- package/dist/src/cli/services/deployment-verification.js +9 -2
- package/dist/src/cockpit/doctor.js +257 -0
- package/dist/src/config/aiwg-config.js +36 -3
- package/dist/src/config/user-config-dir.mjs +29 -0
- package/dist/src/config/user-config.js +4 -22
- package/dist/src/extensions/commands/definitions.js +20 -1
- package/dist/src/features/catalog.js +2 -1
- package/dist/src/flow/graph-metadata.js +56 -0
- package/dist/src/installation/manager.mjs +243 -0
- package/dist/src/providers/transformation-receipt-integration.js +130 -3
- package/dist/src/security/artifact-verifier.js +7 -1
- package/dist/src/serve/a2a-terminal-observer.js +28 -5
- package/dist/src/serve/dispatch-router.js +32 -4
- package/dist/src/serve/executor-registry.js +29 -0
- package/dist/src/serve/mission-conductor.js +15 -1
- package/dist/src/serve/pty-bridge.js +6 -11
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/skills/run.js +15 -6
- package/dist/src/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +49 -5
- package/package.json +2 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
accessSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
15
|
+
|
|
16
|
+
export const INSTALLATION_IDENTITY_VERSION = 1;
|
|
17
|
+
export const INSTALLATION_FILE = 'installation.json';
|
|
18
|
+
const METHODS = new Set(['npm', 'web', 'source']);
|
|
19
|
+
const RUN_MODES = new Set(['normal', 'development']);
|
|
20
|
+
const CHANNELS = new Set(['stable', 'next', 'nightly', 'edge']);
|
|
21
|
+
const STRATEGIES = new Set(['npm-global', 'signed-web', 'source-git']);
|
|
22
|
+
|
|
23
|
+
function canonicalPath(value) {
|
|
24
|
+
const resolved = path.resolve(value);
|
|
25
|
+
try { return realpathSync.native(resolved); } catch { return resolved; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function packageName(root) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function inferInstallationMethod(root) {
|
|
37
|
+
if (packageName(root) === '@aiwg/cli') return 'web';
|
|
38
|
+
if (existsSync(path.join(root, '.git'))) return 'source';
|
|
39
|
+
return 'npm';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveExecutable(name, options = {}) {
|
|
43
|
+
const env = options.env ?? process.env;
|
|
44
|
+
const explicit = options.managerExecutable ?? env.AIWG_PACKAGE_MANAGER_EXECUTABLE;
|
|
45
|
+
if (explicit) return canonicalPath(explicit);
|
|
46
|
+
if (name === 'npm' && env.npm_execpath) return canonicalPath(env.npm_execpath);
|
|
47
|
+
|
|
48
|
+
const besideNode = path.join(path.dirname(process.execPath), process.platform === 'win32' ? `${name}.cmd` : name);
|
|
49
|
+
if (existsSync(besideNode)) return canonicalPath(besideNode);
|
|
50
|
+
try {
|
|
51
|
+
const finder = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
52
|
+
const found = execFileSync(finder, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
53
|
+
.split(/\r?\n/, 1)[0]?.trim();
|
|
54
|
+
return found ? canonicalPath(found) : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function strategyFor(method) {
|
|
61
|
+
if (method === 'web') return 'signed-web';
|
|
62
|
+
if (method === 'source') return 'source-git';
|
|
63
|
+
return 'npm-global';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function executableIsUsable(file) {
|
|
67
|
+
try {
|
|
68
|
+
if (!statSync(file).isFile()) return false;
|
|
69
|
+
if (process.platform !== 'win32') accessSync(file, constants.X_OK);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateIdentity(value, file) {
|
|
77
|
+
const invalid = !value || typeof value !== 'object'
|
|
78
|
+
|| value.schemaVersion !== INSTALLATION_IDENTITY_VERSION
|
|
79
|
+
|| !METHODS.has(value.method)
|
|
80
|
+
|| !RUN_MODES.has(value.runMode)
|
|
81
|
+
|| !CHANNELS.has(value.channel)
|
|
82
|
+
|| typeof value.root !== 'string'
|
|
83
|
+
|| !path.isAbsolute(value.root)
|
|
84
|
+
|| !STRATEGIES.has(value.updateStrategy)
|
|
85
|
+
|| value.updateStrategy !== strategyFor(value.method)
|
|
86
|
+
|| (value.managerExecutable !== null
|
|
87
|
+
&& (typeof value.managerExecutable !== 'string' || !path.isAbsolute(value.managerExecutable)));
|
|
88
|
+
if (invalid) {
|
|
89
|
+
const error = new Error(`Invalid AIWG installation identity at ${file}. Run \`aiwg installation adopt\` to replace it.`);
|
|
90
|
+
error.code = 'AIWG_INSTALLATION_INVALID';
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function installationFile(options = {}) {
|
|
97
|
+
return path.join(resolveUserConfigDir(options), INSTALLATION_FILE);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createInstallationIdentity(options) {
|
|
101
|
+
if (!options?.actualRoot) throw new Error('actualRoot is required to create an installation identity');
|
|
102
|
+
const root = canonicalPath(options.root ?? options.actualRoot);
|
|
103
|
+
const method = options.method ?? inferInstallationMethod(root);
|
|
104
|
+
const runMode = options.runMode ?? (method === 'source' ? 'development' : 'normal');
|
|
105
|
+
const requestedChannel = options.channel ?? (runMode === 'development' ? 'edge' : 'stable');
|
|
106
|
+
const channel = requestedChannel === 'latest'
|
|
107
|
+
? 'stable'
|
|
108
|
+
: ['alpha', 'beta', 'rc'].includes(requestedChannel) ? 'next' : requestedChannel;
|
|
109
|
+
const executableName = method === 'npm' ? 'npm' : method === 'source' ? 'git' : null;
|
|
110
|
+
return {
|
|
111
|
+
schemaVersion: INSTALLATION_IDENTITY_VERSION,
|
|
112
|
+
runMode,
|
|
113
|
+
method,
|
|
114
|
+
root,
|
|
115
|
+
updateStrategy: options.updateStrategy ?? strategyFor(method),
|
|
116
|
+
managerExecutable: executableName ? resolveExecutable(executableName, options) : null,
|
|
117
|
+
channel,
|
|
118
|
+
edgePath: options.edgePath ? canonicalPath(options.edgePath) : (runMode === 'development' ? root : null),
|
|
119
|
+
checkOnStartup: options.checkOnStartup ?? true,
|
|
120
|
+
lastUpdateCheck: options.lastUpdateCheck ?? null,
|
|
121
|
+
updateCheckInterval: options.updateCheckInterval ?? 86_400_000,
|
|
122
|
+
recordedAt: options.recordedAt ?? new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function saveInstallationIdentity(identity, options = {}) {
|
|
127
|
+
const file = installationFile(options);
|
|
128
|
+
const validated = validateIdentity(identity, file);
|
|
129
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
130
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
131
|
+
writeFileSync(temporary, `${JSON.stringify(validated, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
132
|
+
renameSync(temporary, file);
|
|
133
|
+
return validated;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readLegacy(options = {}) {
|
|
137
|
+
const file = path.join(resolveUserConfigDir(options), 'channel.json');
|
|
138
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Load the canonical record, migrating legacy channel.json on first access. */
|
|
142
|
+
export function loadInstallationIdentity(options = {}) {
|
|
143
|
+
const file = installationFile(options);
|
|
144
|
+
if (existsSync(file)) {
|
|
145
|
+
try { return validateIdentity(JSON.parse(readFileSync(file, 'utf8')), file); }
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error?.code === 'AIWG_INSTALLATION_INVALID') throw error;
|
|
148
|
+
const wrapped = new Error(`Cannot read AIWG installation identity at ${file}: ${error.message}`);
|
|
149
|
+
wrapped.code = 'AIWG_INSTALLATION_INVALID';
|
|
150
|
+
throw wrapped;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (options.createIfMissing === false) return null;
|
|
154
|
+
if (!options.actualRoot) return null;
|
|
155
|
+
|
|
156
|
+
const legacy = options.legacyConfig ?? readLegacy(options) ?? {};
|
|
157
|
+
const development = legacy.devMode === true;
|
|
158
|
+
const root = development && legacy.edgePath ? legacy.edgePath : options.actualRoot;
|
|
159
|
+
const method = options.method ?? (development ? 'source' : inferInstallationMethod(root));
|
|
160
|
+
const identity = createInstallationIdentity({
|
|
161
|
+
...options,
|
|
162
|
+
root,
|
|
163
|
+
method,
|
|
164
|
+
runMode: development ? 'development' : undefined,
|
|
165
|
+
channel: legacy.channel ?? options.channel,
|
|
166
|
+
edgePath: legacy.edgePath,
|
|
167
|
+
lastUpdateCheck: legacy.lastUpdateCheck,
|
|
168
|
+
updateCheckInterval: legacy.updateCheckInterval,
|
|
169
|
+
checkOnStartup: legacy.checkOnStartup,
|
|
170
|
+
});
|
|
171
|
+
return saveInstallationIdentity(identity, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function inspectInstallation(options = {}) {
|
|
175
|
+
const actualRoot = canonicalPath(options.actualRoot);
|
|
176
|
+
const actualMethod = options.actualMethod ?? inferInstallationMethod(actualRoot);
|
|
177
|
+
const identity = options.identity ?? loadInstallationIdentity({ ...options, actualRoot });
|
|
178
|
+
if (!identity) return { state: 'unrecorded', identity: null, actualRoot, actualMethod, drift: ['installation identity is not recorded'] };
|
|
179
|
+
|
|
180
|
+
const drift = [];
|
|
181
|
+
const canonicalRoot = canonicalPath(identity.root);
|
|
182
|
+
if (!existsSync(canonicalRoot)) drift.push(`canonical root does not exist: ${canonicalRoot}`);
|
|
183
|
+
if (canonicalRoot !== actualRoot) drift.push(`actual root ${actualRoot} differs from canonical root ${canonicalRoot}`);
|
|
184
|
+
if (identity.method !== actualMethod) drift.push(`actual method ${actualMethod} differs from canonical method ${identity.method}`);
|
|
185
|
+
if (identity.method !== 'web' && !identity.managerExecutable) {
|
|
186
|
+
drift.push(`canonical ${identity.method} installation has no recorded manager executable`);
|
|
187
|
+
}
|
|
188
|
+
if (identity.managerExecutable && !executableIsUsable(identity.managerExecutable)) {
|
|
189
|
+
drift.push(`recorded manager executable is missing or not executable: ${identity.managerExecutable}`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
state: drift.length === 0 ? 'aligned' : (existsSync(canonicalRoot) ? 'mismatch' : 'stale'),
|
|
193
|
+
identity,
|
|
194
|
+
canonicalRoot,
|
|
195
|
+
actualRoot,
|
|
196
|
+
actualMethod,
|
|
197
|
+
drift,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatInstallationDiagnostic(status) {
|
|
202
|
+
if (status.state === 'aligned') return 'Canonical installation is aligned.';
|
|
203
|
+
return [
|
|
204
|
+
'AIWG installation identity drift detected; update and refresh are blocked.',
|
|
205
|
+
...status.drift.map((item) => `- ${item}`),
|
|
206
|
+
'Inspect: aiwg installation show',
|
|
207
|
+
'Adopt this installation: aiwg installation adopt',
|
|
208
|
+
'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function assertCanonicalInstallation(options = {}) {
|
|
213
|
+
const status = inspectInstallation(options);
|
|
214
|
+
if (status.state !== 'aligned') {
|
|
215
|
+
const error = new Error(formatInstallationDiagnostic(status));
|
|
216
|
+
error.code = 'AIWG_INSTALLATION_DRIFT';
|
|
217
|
+
error.status = status;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return status;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function adoptInstallation(options) {
|
|
224
|
+
const inferred = inferInstallationMethod(options.actualRoot);
|
|
225
|
+
if (options.method && options.method !== inferred) {
|
|
226
|
+
throw new Error(`Cannot adopt ${options.actualRoot} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
227
|
+
}
|
|
228
|
+
const identity = createInstallationIdentity({ ...options, root: options.actualRoot });
|
|
229
|
+
saveInstallationIdentity(identity, options);
|
|
230
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function switchInstallation(options) {
|
|
234
|
+
if (!options?.root || !options?.method) throw new Error('switch requires root and method');
|
|
235
|
+
if (!existsSync(path.resolve(options.root))) throw new Error(`Installation root does not exist: ${path.resolve(options.root)}`);
|
|
236
|
+
const inferred = inferInstallationMethod(options.root);
|
|
237
|
+
if (options.method !== inferred) {
|
|
238
|
+
throw new Error(`Cannot switch ${options.root} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
239
|
+
}
|
|
240
|
+
const identity = createInstallationIdentity({ ...options, actualRoot: options.root });
|
|
241
|
+
saveInstallationIdentity(identity, options);
|
|
242
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
243
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { access, lstat, readFile, readdir } from 'node:fs/promises';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { access, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { readAiwgConfig } from '../config/aiwg-config.js';
|
|
@@ -25,6 +25,65 @@ const FRAMEWORK_BUNDLE_DIRS = {
|
|
|
25
25
|
validation: 'validation-complete',
|
|
26
26
|
'knowledge-base': 'knowledge-base',
|
|
27
27
|
};
|
|
28
|
+
const PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA = 'aiwg.provider-transformation-evidence-state.v1';
|
|
29
|
+
export function providerTransformationEvidenceStatePath(projectRoot, provider, scope) {
|
|
30
|
+
const receiptPath = providerTransformationReceiptPath(projectRoot, provider, scope);
|
|
31
|
+
return receiptPath.replace(/\.json$/, '.evidence.json');
|
|
32
|
+
}
|
|
33
|
+
function validateEvidenceState(value) {
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
35
|
+
throw new Error('evidence state must be an object');
|
|
36
|
+
const state = value;
|
|
37
|
+
if (state.schemaVersion !== PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA)
|
|
38
|
+
throw new Error('unsupported evidence state schema');
|
|
39
|
+
if (!Number.isFinite(Date.parse(state.recordedAt)))
|
|
40
|
+
throw new Error('recordedAt must be an RFC 3339 date-time');
|
|
41
|
+
if (state.scope !== 'project' && state.scope !== 'user')
|
|
42
|
+
throw new Error('scope must be project or user');
|
|
43
|
+
if (!['local-source', 'source-unavailable', 'verification-failed'].includes(state.disposition)) {
|
|
44
|
+
throw new Error('unsupported source evidence disposition');
|
|
45
|
+
}
|
|
46
|
+
if (!state.provider || state.provider.includes('/') || state.provider.includes('\\'))
|
|
47
|
+
throw new Error('provider is invalid');
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
async function writeEvidenceState(options, disposition) {
|
|
51
|
+
const provider = normalizeProviderDefinitionId(options.provider) ?? options.provider;
|
|
52
|
+
const target = providerTransformationEvidenceStatePath(options.projectRoot, provider, options.scope);
|
|
53
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
54
|
+
const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${randomUUID()}.tmp`);
|
|
55
|
+
const state = {
|
|
56
|
+
schemaVersion: PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA,
|
|
57
|
+
recordedAt: options.generatedAt ?? new Date().toISOString(),
|
|
58
|
+
provider,
|
|
59
|
+
scope: options.scope,
|
|
60
|
+
disposition,
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
64
|
+
await rename(temporary, target);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
await rm(providerTransformationReceiptPath(options.projectRoot, provider, options.scope), { force: true });
|
|
71
|
+
return target;
|
|
72
|
+
}
|
|
73
|
+
async function readEvidenceState(options) {
|
|
74
|
+
const provider = normalizeProviderDefinitionId(options.provider) ?? options.provider;
|
|
75
|
+
try {
|
|
76
|
+
const state = validateEvidenceState(JSON.parse(await readFile(providerTransformationEvidenceStatePath(options.projectRoot, provider, options.scope), 'utf8')));
|
|
77
|
+
if (state.provider !== provider || state.scope !== options.scope)
|
|
78
|
+
throw new Error('evidence state identity does not match deployment');
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT')
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
28
87
|
function sha256(value) {
|
|
29
88
|
return createHash('sha256').update(value).digest('hex');
|
|
30
89
|
}
|
|
@@ -225,6 +284,17 @@ function receiptBundles(installed, options) {
|
|
|
225
284
|
.sort();
|
|
226
285
|
return deployed.length > 0 ? deployed : [...new Set(options.requestedBundles)].sort();
|
|
227
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Return whether the deployed provider surface includes project-local source
|
|
289
|
+
* material that cannot be authenticated by an AIWG signed web release.
|
|
290
|
+
*/
|
|
291
|
+
export async function providerReceiptHasLocalSources(rawOptions) {
|
|
292
|
+
const provider = normalizeProviderDefinitionId(rawOptions.provider) ?? rawOptions.provider;
|
|
293
|
+
const options = { ...rawOptions, provider };
|
|
294
|
+
const installed = await installedEntries(options);
|
|
295
|
+
return receiptBundles(installed, options)
|
|
296
|
+
.some(bundle => installed[bundle]?.source === 'project-local');
|
|
297
|
+
}
|
|
228
298
|
/**
|
|
229
299
|
* Convert an already signature-verified web release into the stable verifier
|
|
230
300
|
* result contract for the complete canonical bundle consumed by deployment.
|
|
@@ -375,6 +445,27 @@ export async function resolveProviderReceiptRuntimeEvidence(rawOptions) {
|
|
|
375
445
|
};
|
|
376
446
|
}
|
|
377
447
|
export async function finalizeProviderTransformationReceipt(options) {
|
|
448
|
+
if (options.sourceDisposition) {
|
|
449
|
+
const evidenceStatePath = await writeEvidenceState(options, options.sourceDisposition);
|
|
450
|
+
if (options.sourceDisposition === 'local-source') {
|
|
451
|
+
return {
|
|
452
|
+
status: 'policy-exempt',
|
|
453
|
+
receiptPath: null,
|
|
454
|
+
evidenceStatePath,
|
|
455
|
+
outputCount: 0,
|
|
456
|
+
reason: 'local-source development deployments are exempt from signed-release receipt issuance',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
status: options.sourceDisposition === 'source-unavailable' ? 'source-unavailable' : 'skipped',
|
|
461
|
+
receiptPath: null,
|
|
462
|
+
evidenceStatePath,
|
|
463
|
+
outputCount: 0,
|
|
464
|
+
reason: options.sourceDisposition === 'source-unavailable'
|
|
465
|
+
? 'verified signed-release source evidence is not available from cache or configured resource access'
|
|
466
|
+
: 'canonical source verification failed',
|
|
467
|
+
};
|
|
468
|
+
}
|
|
378
469
|
if (!options.sourceVerifications) {
|
|
379
470
|
return {
|
|
380
471
|
status: 'skipped',
|
|
@@ -412,9 +503,11 @@ export async function finalizeProviderTransformationReceipt(options) {
|
|
|
412
503
|
transformer: evidence.transformer,
|
|
413
504
|
outputPaths: evidence.outputPaths,
|
|
414
505
|
});
|
|
506
|
+
const receiptPath = await writeProviderTransformationReceipt(options.projectRoot, receipt);
|
|
507
|
+
await rm(providerTransformationEvidenceStatePath(options.projectRoot, evidence.provider, options.scope), { force: true });
|
|
415
508
|
return {
|
|
416
509
|
status: 'written',
|
|
417
|
-
receiptPath
|
|
510
|
+
receiptPath,
|
|
418
511
|
outputCount: receipt.outputs.length,
|
|
419
512
|
};
|
|
420
513
|
}
|
|
@@ -425,6 +518,40 @@ export async function diagnoseIntegratedProviderTransformationReceipt(options) {
|
|
|
425
518
|
await access(receiptPath);
|
|
426
519
|
}
|
|
427
520
|
catch {
|
|
521
|
+
const state = await readEvidenceState(options);
|
|
522
|
+
if (state?.disposition === 'local-source') {
|
|
523
|
+
return {
|
|
524
|
+
status: 'policy-exempt',
|
|
525
|
+
receiptPath,
|
|
526
|
+
checkedOutputs: 0,
|
|
527
|
+
findings: [{
|
|
528
|
+
kind: 'policy-exempt',
|
|
529
|
+
message: 'This local-source development deployment is explicitly exempt from signed-release transformation receipts.',
|
|
530
|
+
}],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (state?.disposition === 'source-unavailable') {
|
|
534
|
+
return {
|
|
535
|
+
status: 'source-evidence-unavailable',
|
|
536
|
+
receiptPath,
|
|
537
|
+
checkedOutputs: 0,
|
|
538
|
+
findings: [{
|
|
539
|
+
kind: 'source-evidence-unavailable',
|
|
540
|
+
message: 'The deployment succeeded, but verified signed-release source evidence was unavailable from cache or configured resource access.',
|
|
541
|
+
}],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (state?.disposition === 'verification-failed') {
|
|
545
|
+
return {
|
|
546
|
+
status: 'drifted',
|
|
547
|
+
receiptPath,
|
|
548
|
+
checkedOutputs: 0,
|
|
549
|
+
findings: [{
|
|
550
|
+
kind: 'source-verification-failure',
|
|
551
|
+
message: 'Canonical source verification failed during receipt finalization.',
|
|
552
|
+
}],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
428
555
|
return {
|
|
429
556
|
status: 'missing-receipt',
|
|
430
557
|
receiptPath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARTIFACT_TRUST_STATE_SCHEMA_VERSION, channelStateKey, decodeBase64, dssePae, isIdentityRevoked, parseTrustRoot, publicKeyFingerprint, scopeMatches, selectDelegations, sha256, validateTrustState, verifyBytes, } from './artifact-trust.js';
|
|
1
|
+
import { ARTIFACT_TRUST_STATE_SCHEMA_VERSION, canonicalJson, channelStateKey, decodeBase64, dssePae, isIdentityRevoked, parseTrustRoot, publicKeyFingerprint, scopeMatches, selectDelegations, sha256, validateTrustState, verifyBytes, } from './artifact-trust.js';
|
|
2
2
|
export const ARTIFACT_VERIFICATION_RESULT_SCHEMA_VERSION = 'aiwg.verify.result.v1';
|
|
3
3
|
export const ARTIFACT_VERIFICATION_EXIT_CODES = {
|
|
4
4
|
verified: 0,
|
|
@@ -351,6 +351,12 @@ export async function verifyArtifact(input) {
|
|
|
351
351
|
identities: allAuthenticated.map(identity => identity.id).sort(),
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
|
+
if (!Buffer.from(canonicalJson(statement), 'utf8').equals(payload)) {
|
|
355
|
+
return result('mismatched', input, [{ code: 'NONCANONICAL_SIGNED_PAYLOAD', message: 'Signed provenance payload is not canonical JSON' }], {
|
|
356
|
+
...common,
|
|
357
|
+
identities: allAuthenticated.map(identity => identity.id).sort(),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
354
360
|
const scopeInput = {
|
|
355
361
|
assetType: statement.predicate.assetType,
|
|
356
362
|
namespace: statement.predicate.publisher.namespace,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { A2AClient } from '../a2a/client.js';
|
|
12
12
|
import { isTerminalTaskState, } from '../a2a/types.js';
|
|
13
|
+
import { extractGraphMetadata } from '../flow/graph-metadata.js';
|
|
13
14
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
14
15
|
const DEFAULT_MAX_POLLS = 300;
|
|
15
16
|
export async function observeA2ATerminalState(registry, executor, missionId, a2aInstanceId, initialTask, opts = {}) {
|
|
@@ -18,7 +19,11 @@ export async function observeA2ATerminalState(registry, executor, missionId, a2a
|
|
|
18
19
|
baseUrl: executor.transportEndpoints.rest,
|
|
19
20
|
bearer: executor.token,
|
|
20
21
|
instanceId: a2aInstanceId,
|
|
22
|
+
protocolVersion: opts.protocolVersion ?? '0.3',
|
|
23
|
+
protocolPolicy: opts.protocolVersion ?? '0.3',
|
|
21
24
|
};
|
|
25
|
+
if (opts.selectedInterface)
|
|
26
|
+
clientOpts.selectedInterface = opts.selectedInterface;
|
|
22
27
|
if (opts.fetch)
|
|
23
28
|
clientOpts.fetch = opts.fetch;
|
|
24
29
|
const client = new A2AClient(clientOpts);
|
|
@@ -90,21 +95,39 @@ function emitTerminalTask(registry, executorId, missionId, task) {
|
|
|
90
95
|
}));
|
|
91
96
|
}
|
|
92
97
|
function makeEnvelope(event, executorId, missionId, task, data) {
|
|
98
|
+
const graph = extractGraphMetadata(task.metadata);
|
|
93
99
|
return {
|
|
94
100
|
event,
|
|
95
101
|
executor_id: executorId,
|
|
96
102
|
mission_id: missionId,
|
|
97
103
|
ts: task.status.timestamp ?? new Date().toISOString(),
|
|
98
|
-
data
|
|
104
|
+
data: {
|
|
105
|
+
...data,
|
|
106
|
+
...(graph ? {
|
|
107
|
+
graph_metadata: { ...graph, nodeState: taskStateToGraphState(task.status.state) },
|
|
108
|
+
graph_node_state: taskStateToGraphState(task.status.state),
|
|
109
|
+
} : {}),
|
|
110
|
+
},
|
|
99
111
|
};
|
|
100
112
|
}
|
|
113
|
+
function taskStateToGraphState(state) {
|
|
114
|
+
switch (state) {
|
|
115
|
+
case 'submitted': return 'pending';
|
|
116
|
+
case 'working': return 'running';
|
|
117
|
+
case 'input-required':
|
|
118
|
+
case 'auth-required': return 'blocked-hitl';
|
|
119
|
+
case 'completed': return 'succeeded';
|
|
120
|
+
case 'failed':
|
|
121
|
+
case 'rejected': return 'failed';
|
|
122
|
+
case 'canceled': return 'canceled';
|
|
123
|
+
default: return 'unknown';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
101
126
|
function taskStatusSummary(task) {
|
|
102
|
-
|
|
103
|
-
return typeof status.summary === 'string' ? status.summary : undefined;
|
|
127
|
+
return task.status.summary;
|
|
104
128
|
}
|
|
105
129
|
function exitCodeData(task) {
|
|
106
|
-
|
|
107
|
-
return typeof status.exit_code === 'number' ? { exit_code: status.exit_code } : {};
|
|
130
|
+
return task.status.exitCode !== undefined ? { exit_code: task.status.exitCode } : {};
|
|
108
131
|
}
|
|
109
132
|
function sleep(ms) {
|
|
110
133
|
return new Promise((resolve) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* v1 payload → A2A Message
|
|
13
13
|
* -------------------------------- --------------------------------------
|
|
14
14
|
* mission_id message.messageId (idempotency key)
|
|
15
|
-
* objective parts[0] =
|
|
15
|
+
* objective parts[0] = normalized text content
|
|
16
16
|
* completion metadata.completion
|
|
17
17
|
* executor_filter metadata.executor_filter
|
|
18
18
|
* long_running metadata.long_running
|
|
@@ -39,7 +39,10 @@ export async function routeDispatch(executor, payload, opts = {}) {
|
|
|
39
39
|
}
|
|
40
40
|
catch (err) {
|
|
41
41
|
// Only fall back on a 404 from the v2 path. Everything else propagates.
|
|
42
|
-
|
|
42
|
+
const policy = opts.a2aProtocolPolicy ?? '0.3';
|
|
43
|
+
const allowLegacyFallback = policy !== '1.0'
|
|
44
|
+
&& (opts.allowLegacyExecutorFallback ?? true);
|
|
45
|
+
if (allowLegacyFallback && err instanceof A2AError && err.status === 404) {
|
|
43
46
|
// Capture sunset for the telemetry event if any was attached.
|
|
44
47
|
const sunset = err.problem.code === 'aiwg.deprecation_strict' ? undefined : undefined;
|
|
45
48
|
if (opts.onV1Fallback) {
|
|
@@ -62,6 +65,8 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
62
65
|
bearer: executor.token,
|
|
63
66
|
instanceId: a2aInstanceId,
|
|
64
67
|
requiredExtensions: opts.requiredExtensions ?? [A2A_RUNTIME_V1, A2A_IDEMPOTENCY_V1],
|
|
68
|
+
protocolPolicy: opts.a2aProtocolPolicy ?? '0.3',
|
|
69
|
+
allowProtocolFallback: opts.allowA2AProtocolFallback ?? false,
|
|
65
70
|
};
|
|
66
71
|
if (opts.fetch)
|
|
67
72
|
clientOpts.fetch = opts.fetch;
|
|
@@ -69,7 +74,27 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
69
74
|
clientOpts.optionalExtensions = opts.optionalExtensions;
|
|
70
75
|
if (opts.onDeprecation)
|
|
71
76
|
clientOpts.onDeprecation = opts.onDeprecation;
|
|
72
|
-
|
|
77
|
+
if (opts.onA2AProtocolFallback)
|
|
78
|
+
clientOpts.onProtocolFallback = opts.onA2AProtocolFallback;
|
|
79
|
+
if (opts.onA2AProtocolSelection) {
|
|
80
|
+
clientOpts.onProtocolSelection = info => opts.onA2AProtocolSelection?.(info);
|
|
81
|
+
}
|
|
82
|
+
// The deployed 0.3 compatibility route predates AgentCard negotiation. Model
|
|
83
|
+
// it as an explicit interface so headerless legacy selection is observable
|
|
84
|
+
// in registry, telemetry, audit, and dispatch results without adding a new
|
|
85
|
+
// discovery dependency to the compatibility path.
|
|
86
|
+
if (clientOpts.protocolPolicy === '0.3') {
|
|
87
|
+
clientOpts.selectedInterface = {
|
|
88
|
+
url: `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/agents/${encodeURIComponent(a2aInstanceId)}`,
|
|
89
|
+
protocolBinding: 'REST',
|
|
90
|
+
protocolVersion: '0.3',
|
|
91
|
+
preference: 0,
|
|
92
|
+
legacy: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const client = clientOpts.protocolPolicy === '0.3'
|
|
96
|
+
? new A2AClient(clientOpts)
|
|
97
|
+
: await A2AClient.negotiate(clientOpts);
|
|
73
98
|
const message = payloadToMessage(payload);
|
|
74
99
|
const result = await client.sendMessage(message);
|
|
75
100
|
return {
|
|
@@ -79,6 +104,9 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
79
104
|
dispatchPath: 'v2',
|
|
80
105
|
task: result.task,
|
|
81
106
|
idempotentReplayed: result.idempotentReplayed,
|
|
107
|
+
a2aProtocolVersion: result.protocolVersion,
|
|
108
|
+
...(result.selectedInterface ? { a2aInterface: result.selectedInterface } : {}),
|
|
109
|
+
...(result.fallbackReason ? { a2aFallbackReason: result.fallbackReason } : {}),
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
function resolveA2AInstanceId(executor, payload, opts) {
|
|
@@ -145,7 +173,7 @@ function payloadToMessage(payload) {
|
|
|
145
173
|
return {
|
|
146
174
|
messageId: payload.mission_id,
|
|
147
175
|
role: 'user',
|
|
148
|
-
parts: [{
|
|
176
|
+
parts: [{ type: 'text', text: payload.objective }],
|
|
149
177
|
metadata,
|
|
150
178
|
};
|
|
151
179
|
}
|
|
@@ -232,6 +232,7 @@ export class ExecutorRegistry extends EventEmitter {
|
|
|
232
232
|
// Upsert — preserve token and registeredAt
|
|
233
233
|
existing.name = req.name;
|
|
234
234
|
existing.a2aInstanceId = req.a2a_instance_id;
|
|
235
|
+
delete existing.a2aProtocol;
|
|
235
236
|
existing.version = req.version;
|
|
236
237
|
existing.specVersion = req.spec_version;
|
|
237
238
|
existing.transportEndpoints = req.transport_endpoints;
|
|
@@ -614,6 +615,24 @@ export class ExecutorRegistry extends EventEmitter {
|
|
|
614
615
|
getRegistration(executorId) {
|
|
615
616
|
return this.executors.get(executorId);
|
|
616
617
|
}
|
|
618
|
+
/** Publish the negotiated interface used for the most recent A2A dispatch. */
|
|
619
|
+
recordA2AProtocolSelection(executorId, selection) {
|
|
620
|
+
const executor = this.executors.get(executorId);
|
|
621
|
+
if (!executor)
|
|
622
|
+
return false;
|
|
623
|
+
executor.a2aProtocol = {
|
|
624
|
+
...selection,
|
|
625
|
+
selectedAt: selection.selectedAt ?? new Date().toISOString(),
|
|
626
|
+
};
|
|
627
|
+
this.emit('executor:a2a_protocol_selected', {
|
|
628
|
+
executorId,
|
|
629
|
+
selectedVersion: selection.selectedVersion,
|
|
630
|
+
protocolBinding: selection.interface.protocolBinding,
|
|
631
|
+
interfaceUrl: selection.interface.url,
|
|
632
|
+
...(selection.fallbackReason ? { fallbackReason: selection.fallbackReason } : {}),
|
|
633
|
+
});
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
617
636
|
/**
|
|
618
637
|
* Pick the best executor matching the given filter.
|
|
619
638
|
*
|
|
@@ -708,6 +727,16 @@ function toSummary(e) {
|
|
|
708
727
|
};
|
|
709
728
|
if (e.a2aInstanceId)
|
|
710
729
|
summary.a2a_instance_id = e.a2aInstanceId;
|
|
730
|
+
if (e.a2aProtocol) {
|
|
731
|
+
summary.a2a_protocol = {
|
|
732
|
+
policy: e.a2aProtocol.policy,
|
|
733
|
+
selected_version: e.a2aProtocol.selectedVersion,
|
|
734
|
+
protocol_binding: e.a2aProtocol.interface.protocolBinding,
|
|
735
|
+
interface_url: e.a2aProtocol.interface.url,
|
|
736
|
+
selected_at: e.a2aProtocol.selectedAt,
|
|
737
|
+
...(e.a2aProtocol.fallbackReason ? { fallback_reason: e.a2aProtocol.fallbackReason } : {}),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
711
740
|
return summary;
|
|
712
741
|
}
|
|
713
742
|
// Singleton instance
|