@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.
Files changed (55) hide show
  1. package/bin/aiwg.mjs +24 -2
  2. package/dist/src/a2a/agent-card.js +4 -1
  3. package/dist/src/a2a/client.js +148 -68
  4. package/dist/src/a2a/codecs.js +480 -0
  5. package/dist/src/a2a/events.js +226 -0
  6. package/dist/src/a2a/hitl-driver.js +8 -6
  7. package/dist/src/a2a/hitl.js +2 -1
  8. package/dist/src/a2a/http.js +85 -5
  9. package/dist/src/a2a/protocol.js +136 -0
  10. package/dist/src/a2a/types.js +4 -14
  11. package/dist/src/a2a/webhook.js +101 -4
  12. package/dist/src/artifacts/browser-export.js +1 -0
  13. package/dist/src/artifacts/cli.js +1 -1
  14. package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -0
  15. package/dist/src/artifacts/types.js +73 -1
  16. package/dist/src/audit/operator-decision.js +15 -1
  17. package/dist/src/channel/manager.mjs +89 -17
  18. package/dist/src/cli/agent-spawn.js +4 -2
  19. package/dist/src/cli/handlers/cockpit.js +41 -0
  20. package/dist/src/cli/handlers/help.js +1 -1
  21. package/dist/src/cli/handlers/index.js +3 -1
  22. package/dist/src/cli/handlers/installation.js +79 -0
  23. package/dist/src/cli/handlers/ralph.js +2 -1
  24. package/dist/src/cli/handlers/refresh.js +4 -2
  25. package/dist/src/cli/handlers/runtime-info.js +9 -1
  26. package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
  27. package/dist/src/cli/handlers/serve.js +107 -4
  28. package/dist/src/cli/handlers/session.js +12 -26
  29. package/dist/src/cli/handlers/use.js +117 -12
  30. package/dist/src/cli/handlers/utilities.js +4 -0
  31. package/dist/src/cli/handlers/version.js +4 -0
  32. package/dist/src/cli/handlers/workspace.js +24 -2
  33. package/dist/src/cli/services/deployment-verification.js +9 -2
  34. package/dist/src/cockpit/doctor.js +257 -0
  35. package/dist/src/config/aiwg-config.js +36 -3
  36. package/dist/src/config/user-config-dir.mjs +29 -0
  37. package/dist/src/config/user-config.js +4 -22
  38. package/dist/src/extensions/commands/definitions.js +20 -1
  39. package/dist/src/features/catalog.js +2 -1
  40. package/dist/src/flow/graph-metadata.js +56 -0
  41. package/dist/src/installation/manager.mjs +243 -0
  42. package/dist/src/providers/transformation-receipt-integration.js +130 -3
  43. package/dist/src/security/artifact-verifier.js +7 -1
  44. package/dist/src/serve/a2a-terminal-observer.js +28 -5
  45. package/dist/src/serve/dispatch-router.js +32 -4
  46. package/dist/src/serve/executor-registry.js +29 -0
  47. package/dist/src/serve/mission-conductor.js +15 -1
  48. package/dist/src/serve/pty-bridge.js +6 -11
  49. package/dist/src/serve/stack-adapters.js +2 -1
  50. package/dist/src/serve/telemetry.js +5 -1
  51. package/dist/src/skills/run.js +15 -6
  52. package/dist/src/update/checker.mjs +16 -15
  53. package/dist/src/update/notifier.mjs +8 -3
  54. package/dist/src/update/service.mjs +49 -5
  55. 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: await writeProviderTransformationReceipt(options.projectRoot, receipt),
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
- const status = task.status;
103
- return typeof status.summary === 'string' ? status.summary : undefined;
127
+ return task.status.summary;
104
128
  }
105
129
  function exitCodeData(task) {
106
- const status = task.status;
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] = { kind: 'text', text: ... }
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
- if (err instanceof A2AError && err.status === 404) {
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
- const client = new A2AClient(clientOpts);
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: [{ kind: 'text', text: payload.objective }],
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