@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
|
@@ -53,6 +53,7 @@ function collectFingerprint(versionInfo) {
|
|
|
53
53
|
logFile: loggerInfo.logFile,
|
|
54
54
|
},
|
|
55
55
|
invocation_id: loggerInfo.provenance.invocation_id,
|
|
56
|
+
installation: versionInfo.installation,
|
|
56
57
|
};
|
|
57
58
|
if (versionInfo.gitHash) {
|
|
58
59
|
fp.git = {
|
|
@@ -115,6 +116,9 @@ async function displayVersion(opts) {
|
|
|
115
116
|
ui.dim(` path: ${fp.packageRoot}`);
|
|
116
117
|
}
|
|
117
118
|
ui.dim(` channel: ${fp.channel}`);
|
|
119
|
+
ui.dim(` install: ${fp.installation.identity?.method ?? 'unrecorded'} (${fp.installation.state})`);
|
|
120
|
+
ui.dim(` canonical: ${fp.installation.identity?.root ?? '(unrecorded)'}`);
|
|
121
|
+
ui.dim(` actual: ${fp.installation.actualRoot}`);
|
|
118
122
|
ui.dim(` node: ${fp.node}`);
|
|
119
123
|
ui.dim(` platform: ${fp.platform.os} ${fp.platform.arch} (${fp.platform.release})`);
|
|
120
124
|
ui.dim(` tty: stdin=${fp.tty.stdin} stdout=${fp.tty.stdout} stderr=${fp.tty.stderr}`);
|
|
@@ -12,6 +12,23 @@ import { createScriptRunner } from './script-runner.js';
|
|
|
12
12
|
import { getFrameworkRoot } from '../../channel/manager.mjs';
|
|
13
13
|
import { maybePrintCommunityFooter } from '../../community/footer.js';
|
|
14
14
|
import { buildDeploymentStatusProbe } from '../services/deployment-verification.js';
|
|
15
|
+
import { detectScope } from '../scope-resolver.js';
|
|
16
|
+
function statusProjectRoot(args, fallback) {
|
|
17
|
+
const valueFlags = new Set(['--scope', '--provider', '--bundle']);
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
if (valueFlags.has(args[index])) {
|
|
20
|
+
index += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!args[index].startsWith('-'))
|
|
24
|
+
return args[index];
|
|
25
|
+
}
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
function statusFlagValue(args, flag) {
|
|
29
|
+
const index = args.indexOf(flag);
|
|
30
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
31
|
+
}
|
|
15
32
|
/**
|
|
16
33
|
* Handler for workspace status command
|
|
17
34
|
*
|
|
@@ -31,8 +48,13 @@ export const statusHandler = {
|
|
|
31
48
|
aliases: ['-status', '--status'],
|
|
32
49
|
async execute(ctx) {
|
|
33
50
|
if (ctx.args.includes('--probe')) {
|
|
34
|
-
const projectRoot = ctx.args
|
|
35
|
-
const
|
|
51
|
+
const projectRoot = statusProjectRoot(ctx.args, ctx.cwd ?? process.cwd());
|
|
52
|
+
const scope = detectScope(ctx.args);
|
|
53
|
+
const probe = await buildDeploymentStatusProbe(projectRoot, ctx.frameworkRoot, {
|
|
54
|
+
scope,
|
|
55
|
+
provider: statusFlagValue(ctx.args, '--provider'),
|
|
56
|
+
bundle: statusFlagValue(ctx.args, '--bundle'),
|
|
57
|
+
});
|
|
36
58
|
return {
|
|
37
59
|
exitCode: probe.status === 'needs-repair' ? 1 : 0,
|
|
38
60
|
message: JSON.stringify(probe, null, 2),
|
|
@@ -142,6 +142,13 @@ const RECEIPT_DRIFT_POLICY = {
|
|
|
142
142
|
severity: 'advisory',
|
|
143
143
|
remediation: 'Re-run the same aiwg use command to establish provider transformation evidence.',
|
|
144
144
|
},
|
|
145
|
+
'policy-exempt': {
|
|
146
|
+
severity: 'info',
|
|
147
|
+
},
|
|
148
|
+
'source-evidence-unavailable': {
|
|
149
|
+
severity: 'advisory',
|
|
150
|
+
remediation: 'Run aiwg auth login, then aiwg versions resolve <installed-version> once online to warm the verified cache; re-run the same aiwg use command afterward.',
|
|
151
|
+
},
|
|
145
152
|
};
|
|
146
153
|
async function collectProviderReceiptFindings(options, provider) {
|
|
147
154
|
try {
|
|
@@ -497,8 +504,8 @@ export async function verifyConfiguredDeployments(projectRoot, filters = {}, fra
|
|
|
497
504
|
}
|
|
498
505
|
return aggregateUseDeploymentResult({ projectRoot, frameworkRoot, scope: filters.scope ?? 'project', requestedBundles: bundles, providers: results });
|
|
499
506
|
}
|
|
500
|
-
export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = process.env.AIWG_ROOT || projectRoot) {
|
|
501
|
-
const result = await verifyConfiguredDeployments(projectRoot,
|
|
507
|
+
export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = process.env.AIWG_ROOT || projectRoot, filters = {}) {
|
|
508
|
+
const result = await verifyConfiguredDeployments(projectRoot, filters, frameworkRoot);
|
|
502
509
|
const notConfigured = result.requestedBundles.length === 0
|
|
503
510
|
&& result.findings.length > 0
|
|
504
511
|
&& result.findings.every((item) => item.id === 'deployment-not-configured');
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { homedir, hostname, platform } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
export const COCKPIT_DOCTOR_SCHEMA = 'aiwg.cockpit-doctor/v1';
|
|
10
|
+
function safeText(value) {
|
|
11
|
+
return String(value ?? '')
|
|
12
|
+
.replace(/((?:bearer|token|nonce|secret|password|authorization)\s*)[:=]\s*[^\s,;]+/gi, '$1=[redacted]')
|
|
13
|
+
.replace(/([?#](?:token|nonce|secret|password|authorization)=)[^&#\s]+/gi, '$1[redacted]')
|
|
14
|
+
.slice(0, 240);
|
|
15
|
+
}
|
|
16
|
+
function evidence(values) {
|
|
17
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
18
|
+
key,
|
|
19
|
+
typeof value === 'string' ? safeText(value) : value,
|
|
20
|
+
]));
|
|
21
|
+
}
|
|
22
|
+
function row(id, status, code, summary, values, recovery) {
|
|
23
|
+
return { id, status, code, summary, evidence: evidence(values), recovery: status === 'pass' ? null : recovery };
|
|
24
|
+
}
|
|
25
|
+
function overall(rows) {
|
|
26
|
+
if (rows.some(item => item.status === 'blocked'))
|
|
27
|
+
return 'blocked';
|
|
28
|
+
if (rows.some(item => item.status === 'warn'))
|
|
29
|
+
return 'warn';
|
|
30
|
+
return 'pass';
|
|
31
|
+
}
|
|
32
|
+
function isLoopback(host) {
|
|
33
|
+
return ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(host.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
function looksMock(body) {
|
|
36
|
+
const text = JSON.stringify({
|
|
37
|
+
service: body?.service,
|
|
38
|
+
name: body?.name,
|
|
39
|
+
executor: body?.executor,
|
|
40
|
+
implementation: body?.implementation,
|
|
41
|
+
mock: body?.mock,
|
|
42
|
+
}).toLowerCase();
|
|
43
|
+
return body?.mock === true || /mock[-_ ]?executor|cockpit[-_ ]?mock/.test(text);
|
|
44
|
+
}
|
|
45
|
+
function listenerRows(stdout) {
|
|
46
|
+
const lines = stdout.split(/\r?\n/).filter(line => /:(8120|8121|8122|8140)\b/.test(line));
|
|
47
|
+
const publicLines = lines.filter(line => /(?:0\.0\.0\.0|\[::\]|\*):(8120|8121|8122|8140)\b/.test(line));
|
|
48
|
+
if (publicLines.length > 0)
|
|
49
|
+
return row('listeners', 'blocked', 'public_bind', 'A Cockpit or executor listener is publicly bound.', { checked_ports: '8120-8122,8140', public_listener_count: publicLines.length }, 'Bind the named service to 127.0.0.1 and restart only that service.');
|
|
50
|
+
return row('listeners', lines.length > 0 ? 'pass' : 'warn', lines.length > 0 ? 'loopback_only' : 'listeners_not_observed', lines.length > 0 ? 'Observed application listeners are loopback-only.' : 'No application listeners were observed locally.', { checked_ports: '8120-8122,8140', observed_listener_count: lines.length }, 'Start the expected user services, then rerun the doctor.');
|
|
51
|
+
}
|
|
52
|
+
export function defaultCockpitDoctorProbes(cockpitPackageRoot) {
|
|
53
|
+
return {
|
|
54
|
+
async readRuntime(file) {
|
|
55
|
+
const [raw, info] = await Promise.all([readFile(file, 'utf8'), stat(file)]);
|
|
56
|
+
const record = JSON.parse(raw);
|
|
57
|
+
if (!record.token && record.token_ref && cockpitPackageRoot) {
|
|
58
|
+
try {
|
|
59
|
+
const keychain = await import(pathToFileURL(path.join(cockpitPackageRoot, 'shell-core', 'keychain.mjs')).href);
|
|
60
|
+
record.token = await keychain.readCockpitToken(record.token_ref);
|
|
61
|
+
}
|
|
62
|
+
catch { /* authentication row reports a focused failure */ }
|
|
63
|
+
}
|
|
64
|
+
return { record, mode: info.mode & 0o777, owned: info.uid === process.getuid?.() };
|
|
65
|
+
},
|
|
66
|
+
async fetchJson(url, headers = {}) {
|
|
67
|
+
const response = await fetch(url, { headers, signal: AbortSignal.timeout(2500) });
|
|
68
|
+
let body = null;
|
|
69
|
+
try {
|
|
70
|
+
body = await response.json();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
body = null;
|
|
74
|
+
}
|
|
75
|
+
return { status: response.status, body };
|
|
76
|
+
},
|
|
77
|
+
async command(command, args) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await execFile(command, args, { timeout: 3000, maxBuffer: 1024 * 1024 });
|
|
80
|
+
return { ok: true, stdout: result.stdout };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return { ok: false, stdout: typeof error?.stdout === 'string' ? error.stdout : '' };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
pathExists: existsSync,
|
|
87
|
+
hostName: hostname,
|
|
88
|
+
platform,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function runCockpitDoctor(options, probes = defaultCockpitDoctorProbes(options.cockpitPackageRoot)) {
|
|
92
|
+
const topology = options.topology ?? 'same-host';
|
|
93
|
+
const cockpitHost = options.cockpitHost ?? probes.hostName();
|
|
94
|
+
const executorHost = options.executorHost ?? (topology === 'same-host' ? cockpitHost : 'unspecified');
|
|
95
|
+
const rows = [];
|
|
96
|
+
rows.push(options.cockpitInstalled
|
|
97
|
+
? row('package', options.cockpitVersion === options.coreVersion ? 'pass' : 'blocked', options.cockpitVersion === options.coreVersion ? 'version_lockstep' : 'version_skew', options.cockpitVersion === options.coreVersion ? 'Cockpit and AIWG versions match.' : 'Cockpit and AIWG versions differ.', {
|
|
98
|
+
core_version: options.coreVersion,
|
|
99
|
+
cockpit_version: options.cockpitVersion ?? 'unknown',
|
|
100
|
+
source: options.cockpitPackageRoot?.includes('node_modules') ? 'managed-package' : 'source-workspace',
|
|
101
|
+
location: options.cockpitPackageRoot?.includes('node_modules')
|
|
102
|
+
? '$AIWG_COCKPIT_HOME/node_modules/@aiwg/cockpit'
|
|
103
|
+
: 'apps/cockpit',
|
|
104
|
+
}, 'Run `aiwg use cockpit` to install the core-matched Cockpit package.')
|
|
105
|
+
: row('package', 'blocked', 'cockpit_not_installed', 'Cockpit is not installed.', { core_version: options.coreVersion, cockpit_version: null, source: 'absent' }, 'Run `aiwg use cockpit`.'));
|
|
106
|
+
const runtimeFile = options.runtimeFile ?? path.join(homedir(), '.aiwg', 'cockpit', 'runtime', 'bridge.json');
|
|
107
|
+
let runtime = null;
|
|
108
|
+
try {
|
|
109
|
+
const observed = await probes.readRuntime(runtimeFile);
|
|
110
|
+
runtime = observed.record;
|
|
111
|
+
const secure = observed.mode === 0o600 && observed.owned && Boolean(runtime.port) && Boolean(runtime.token || runtime.token_ref);
|
|
112
|
+
rows.push(row('bridge-runtime', secure ? 'pass' : 'blocked', secure ? 'runtime_secure' : 'runtime_insecure', secure ? 'Bridge runtime metadata has the required ownership and mode.' : 'Bridge runtime metadata is missing or insecure.', { mode: observed.mode.toString(8), owned_by_current_user: observed.owned, credential_present: Boolean(runtime.token || runtime.token_ref), port: runtime.port ?? null }, 'Stop Cockpit, restrict the runtime directory to 0700 and bridge.json to 0600, then restart Cockpit.'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
rows.push(row('bridge-runtime', 'blocked', 'runtime_missing', 'Bridge runtime metadata is unavailable.', { runtime_file: 'default-cockpit-runtime', credential_present: false }, 'Start Cockpit as the intended user, then rerun the doctor.'));
|
|
116
|
+
}
|
|
117
|
+
let bridgeHealth = null;
|
|
118
|
+
if (runtime?.port) {
|
|
119
|
+
const base = `http://127.0.0.1:${runtime.port}`;
|
|
120
|
+
try {
|
|
121
|
+
const live = await probes.fetchJson(`${base}/healthz`);
|
|
122
|
+
if (live.status < 200 || live.status >= 300)
|
|
123
|
+
throw new Error('not live');
|
|
124
|
+
const token = typeof runtime.token === 'string' ? runtime.token : '';
|
|
125
|
+
const authed = await probes.fetchJson(`${base}/api/health`, token ? { authorization: `Bearer ${token}` } : {});
|
|
126
|
+
if ([401, 403].includes(authed.status)) {
|
|
127
|
+
rows.push(row('bridge', 'blocked', 'bridge_unauthenticated', 'Bridge is reachable but authentication failed.', { reachable: true, authenticated: false }, 'Restart the Bridge to mint fresh runtime credentials, then rerun the doctor.'));
|
|
128
|
+
}
|
|
129
|
+
else if (authed.status >= 200 && authed.status < 300) {
|
|
130
|
+
bridgeHealth = authed.body;
|
|
131
|
+
rows.push(row('bridge', 'pass', 'bridge_authenticated', 'Bridge is reachable and authenticated.', { reachable: true, authenticated: true, port: runtime.port }, null));
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
throw new Error('unexpected response');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
rows.push(row('bridge', 'blocked', 'bridge_unreachable', 'Bridge is not reachable through its runtime endpoint.', { reachable: false, authenticated: false, port: runtime.port }, 'Restart the Cockpit user service and rerun the doctor.'));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
rows.push(row('bridge', 'blocked', 'bridge_unreachable', 'Bridge endpoint is unknown.', { reachable: false, authenticated: false }, 'Start Cockpit as the intended user, then rerun the doctor.'));
|
|
142
|
+
}
|
|
143
|
+
const executorUrlText = typeof bridgeHealth?.executor_url === 'string' ? bridgeHealth.executor_url : '';
|
|
144
|
+
if (executorUrlText) {
|
|
145
|
+
let executorUrl = null;
|
|
146
|
+
try {
|
|
147
|
+
executorUrl = new URL(executorUrlText);
|
|
148
|
+
}
|
|
149
|
+
catch { /* reported as unreachable */ }
|
|
150
|
+
const hostMatches = executorUrl && (topology === 'same-host'
|
|
151
|
+
? isLoopback(executorUrl.hostname)
|
|
152
|
+
: executorHost !== 'unspecified' && executorUrl.hostname === executorHost);
|
|
153
|
+
if (!hostMatches) {
|
|
154
|
+
rows.push(row('executor', 'blocked', 'wrong_host', 'Bridge targets a host inconsistent with the declared topology.', { topology, expected_host: executorHost, configured_host: executorUrl?.hostname ?? 'invalid' }, 'Correct the declared executor host or the Bridge executor URL; do not create a tunnel until they agree.'));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
try {
|
|
158
|
+
const bridgeExecutor = bridgeHealth?.executor;
|
|
159
|
+
const deep = bridgeExecutor && typeof bridgeExecutor === 'object'
|
|
160
|
+
? { status: bridgeExecutor.status === 'ok' ? 200 : 503, body: bridgeExecutor }
|
|
161
|
+
: await probes.fetchJson(`${executorUrlText.replace(/\/$/, '')}/healthz/deep`);
|
|
162
|
+
if ([401, 403].includes(deep.status)) {
|
|
163
|
+
rows.push(row('executor', 'blocked', 'executor_unauthenticated', 'Executor is reachable but authentication failed.', { reachable: true, authenticated: false, host_matches: true }, 'Configure the Bridge executor credential file with mode 0600, then restart the Bridge.'));
|
|
164
|
+
}
|
|
165
|
+
else if (deep.status < 200 || deep.status >= 300)
|
|
166
|
+
throw new Error('deep health failed');
|
|
167
|
+
else if (deep.body?.real_executor === false || looksMock(deep.body)) {
|
|
168
|
+
rows.push(row('executor', 'blocked', 'mock_executor', 'Configured executor identifies as a mock.', { reachable: true, authenticated: true, real_executor: false }, 'Point the Bridge at the real Agentic Sandbox executor and restart it without mock allowance.'));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const observedVersion = safeText(deep.body?.version ?? deep.body?.commit ?? 'unknown');
|
|
172
|
+
const skew = Boolean(options.expectedExecutorVersion && observedVersion !== options.expectedExecutorVersion);
|
|
173
|
+
rows.push(row('executor', skew ? 'blocked' : 'pass', skew ? 'version_skew' : 'executor_ready', skew ? 'Executor identity does not match the expected version.' : 'Real executor deep health is ready.', { reachable: true, authenticated: true, real_executor: true, version_or_commit: observedVersion, auth_configured: Boolean(bridgeHealth.executor_auth_configured) }, 'Install or select the expected Agentic Sandbox release, then restart the executor and Bridge.'));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
rows.push(row('executor', 'blocked', 'executor_unreachable', 'Executor deep health is unreachable.', { reachable: false, host_matches: true }, 'Start the executor on the declared host and verify only its required transport before retrying.'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
rows.push(row('executor', 'blocked', 'executor_unreachable', 'Bridge did not report an executor endpoint.', { reachable: false, host_matches: false }, 'Configure the Bridge executor URL and restart the Bridge.'));
|
|
183
|
+
}
|
|
184
|
+
rows.push(row('host-runtime', 'pass', 'host_ready', 'Host runtime is available.', { platform: probes.platform(), node: process.version }, null));
|
|
185
|
+
const docker = await probes.command('docker', ['info', '--format', '{{json .ServerVersion}}']);
|
|
186
|
+
rows.push(row('docker-runtime', docker.ok ? 'pass' : 'warn', docker.ok ? 'docker_ready' : 'docker_unavailable', docker.ok ? 'Docker runtime is reachable.' : 'Docker runtime is not reachable.', { ready: docker.ok }, 'Start Docker or choose the host runtime tier; host readiness is independent.'));
|
|
187
|
+
const kvm = probes.pathExists('/dev/kvm');
|
|
188
|
+
rows.push(row('vm-runtime', kvm ? 'pass' : 'warn', kvm ? 'kvm_ready' : 'kvm_unavailable', kvm ? 'KVM device is available.' : 'VM readiness is not claimed because KVM is unavailable.', { kvm_available: kvm }, 'Enable KVM access only if the VM runtime tier is required.'));
|
|
189
|
+
const listeners = await probes.command('ss', ['-ltn']);
|
|
190
|
+
rows.push(listeners.ok ? listenerRows(listeners.stdout) : row('listeners', 'warn', 'listener_inspection_unavailable', 'Listener inspection is unavailable.', { checked_ports: '8120-8122,8140' }, 'Install `ss` support or inspect these listeners locally, then rerun the doctor.'));
|
|
191
|
+
const cockpitEnabled = await probes.command('systemctl', ['--user', 'is-enabled', 'aiwg-cockpit.service']);
|
|
192
|
+
const executorEnabled = await probes.command('systemctl', ['--user', 'is-enabled', 'agentic-sandbox.service']);
|
|
193
|
+
const cockpitUnit = await probes.command('systemctl', ['--user', 'show', 'aiwg-cockpit.service', '--property=ActiveState,After,Requires,Restart']);
|
|
194
|
+
const executorUnit = await probes.command('systemctl', ['--user', 'show', 'agentic-sandbox.service', '--property=ActiveState,Restart']);
|
|
195
|
+
const linger = await probes.command('loginctl', ['show-user', process.env.USER ?? '', '--property=Linger', '--value']);
|
|
196
|
+
const dependencyOrdered = topology !== 'same-host' || /(?:After|Requires)=.*agentic-sandbox\.service/.test(cockpitUnit.stdout);
|
|
197
|
+
const restartReady = /Restart=(?:on-failure|always)/.test(cockpitUnit.stdout)
|
|
198
|
+
&& /Restart=(?:on-failure|always)/.test(executorUnit.stdout);
|
|
199
|
+
const active = /ActiveState=active/.test(cockpitUnit.stdout) && /ActiveState=active/.test(executorUnit.stdout);
|
|
200
|
+
const persistent = cockpitEnabled.ok && executorEnabled.ok && linger.ok && linger.stdout.trim() === 'yes'
|
|
201
|
+
&& cockpitUnit.ok && executorUnit.ok && dependencyOrdered && restartReady && active;
|
|
202
|
+
rows.push(row('persistence', persistent ? 'pass' : 'warn', persistent ? 'user_systemd_ready' : 'user_systemd_incomplete', persistent ? 'User services and linger support persistence.' : 'User-service persistence is incomplete or unavailable.', {
|
|
203
|
+
cockpit_enabled: cockpitEnabled.ok,
|
|
204
|
+
executor_enabled: executorEnabled.ok,
|
|
205
|
+
units_active: active,
|
|
206
|
+
linger_enabled: linger.stdout.trim() === 'yes',
|
|
207
|
+
dependency_order_ready: dependencyOrdered,
|
|
208
|
+
restart_recovery_ready: restartReady,
|
|
209
|
+
}, 'Enable only the Cockpit and executor user units, then enable linger for the service account.'));
|
|
210
|
+
const topologyValid = topology === 'same-host' ? cockpitHost === executorHost : executorHost !== 'unspecified';
|
|
211
|
+
if (topology !== 'same-host' && options.forwardEndpoint) {
|
|
212
|
+
try {
|
|
213
|
+
const endpoint = new URL(options.forwardEndpoint);
|
|
214
|
+
const forward = await probes.fetchJson(`${options.forwardEndpoint.replace(/\/$/, '')}/healthz`);
|
|
215
|
+
const safeEndpoint = `${endpoint.protocol}//${endpoint.hostname}:${endpoint.port || 'default'}`;
|
|
216
|
+
rows.push(row('ssh-forward', forward.status >= 200 && forward.status < 300 ? 'pass' : 'blocked', forward.status >= 200 && forward.status < 300 ? 'forward_ready' : 'forward_unreachable', forward.status >= 200 && forward.status < 300 ? 'Declared SSH forward reaches the expected service.' : 'Declared SSH forward is unreachable.', { kind: topology, endpoint: safeEndpoint, reachable: forward.status >= 200 && forward.status < 300 }, 'Correct only the declared SSH forward endpoint, then rerun the doctor.'));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
rows.push(row('ssh-forward', 'blocked', 'forward_unreachable', 'Declared SSH forward is invalid or unreachable.', { kind: topology, reachable: false }, 'Correct only the declared SSH forward endpoint, then rerun the doctor.'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
rows.push(row('topology', topologyValid ? 'pass' : 'blocked', topologyValid ? 'topology_declared' : 'topology_ambiguous', topologyValid ? 'Cockpit, executor, and operator access topology is explicit.' : 'Executor host is not explicitly declared.', { kind: topology, cockpit_host: cockpitHost, executor_host: executorHost, operator_access: topology === 'same-host' ? 'local' : topology === 'ssh-local' ? 'ssh-local-forward' : 'ssh-reverse-forward' }, 'Declare the executor host before generating or validating any forward.'));
|
|
223
|
+
return {
|
|
224
|
+
schema: COCKPIT_DOCTOR_SCHEMA,
|
|
225
|
+
generated_at: (options.now ?? (() => new Date()))().toISOString(),
|
|
226
|
+
topology: {
|
|
227
|
+
kind: topology,
|
|
228
|
+
cockpit_host: safeText(cockpitHost),
|
|
229
|
+
executor_host: safeText(executorHost),
|
|
230
|
+
operator_access: topology === 'same-host' ? 'local' : topology === 'ssh-local' ? 'ssh-local-forward' : 'ssh-reverse-forward',
|
|
231
|
+
},
|
|
232
|
+
status: overall(rows),
|
|
233
|
+
rows,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export function formatCockpitDoctor(report, format) {
|
|
237
|
+
if (format === 'json')
|
|
238
|
+
return JSON.stringify(report, null, 2);
|
|
239
|
+
if (format === 'markdown') {
|
|
240
|
+
return [
|
|
241
|
+
`# Cockpit Connection Doctor`,
|
|
242
|
+
'',
|
|
243
|
+
`Status: **${report.status}** `,
|
|
244
|
+
`Topology: \`${report.topology.kind}\``,
|
|
245
|
+
'',
|
|
246
|
+
'| Check | Status | Code | Summary | Recovery |',
|
|
247
|
+
'|---|---|---|---|---|',
|
|
248
|
+
...report.rows.map(item => `| ${item.id} | ${item.status} | ${item.code} | ${item.summary} | ${item.recovery ?? '—'} |`),
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
251
|
+
return [
|
|
252
|
+
`Cockpit connection doctor: ${report.status}`,
|
|
253
|
+
`Topology: ${report.topology.kind} (${report.topology.cockpit_host} -> ${report.topology.executor_host})`,
|
|
254
|
+
...report.rows.map(item => `${item.status.toUpperCase().padEnd(7)} ${item.id.padEnd(16)} ${item.code}: ${item.summary}${item.recovery ? ` Recovery: ${item.recovery}` : ''}`),
|
|
255
|
+
].join('\n');
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -158,13 +158,46 @@ export function validateIndexConfig(index) {
|
|
|
158
158
|
if (typeof index !== 'object' || Array.isArray(index)) {
|
|
159
159
|
return ['index: must be an object'];
|
|
160
160
|
}
|
|
161
|
-
const
|
|
161
|
+
const indexObject = index;
|
|
162
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
163
|
+
const graphOverrides = indexObject.graphOverrides;
|
|
164
|
+
if (graphOverrides !== undefined) {
|
|
165
|
+
if (typeof graphOverrides !== 'object' || graphOverrides === null || Array.isArray(graphOverrides)) {
|
|
166
|
+
errors.push('index.graphOverrides: must be an object mapping supported built-in graph names to overrides');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
for (const [name, rawOverride] of Object.entries(graphOverrides)) {
|
|
170
|
+
const where = `index.graphOverrides.${name}`;
|
|
171
|
+
if (name !== 'codebase') {
|
|
172
|
+
errors.push(`${where}: unsupported built-in graph override (supported: codebase)`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (typeof rawOverride !== 'object' || rawOverride === null || Array.isArray(rawOverride)) {
|
|
176
|
+
errors.push(`${where}: must be an object`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const override = rawOverride;
|
|
180
|
+
for (const field of Object.keys(override)) {
|
|
181
|
+
if (field !== 'scanDirs' && field !== 'extensions') {
|
|
182
|
+
errors.push(`${where}.${field}: unknown field (supported: scanDirs, extensions)`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (override.scanDirs !== undefined && (!isStringArray(override.scanDirs) || override.scanDirs.length === 0)) {
|
|
186
|
+
errors.push(`${where}.scanDirs: must be a non-empty array of strings`);
|
|
187
|
+
}
|
|
188
|
+
if (override.extensions !== undefined && (!isStringArray(override.extensions) || override.extensions.length === 0)) {
|
|
189
|
+
errors.push(`${where}.extensions: must be a non-empty array of strings`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const graphs = indexObject.graphs;
|
|
162
195
|
if (graphs === undefined)
|
|
163
196
|
return errors; // index with no graphs is permissible
|
|
164
197
|
if (typeof graphs !== 'object' || graphs === null || Array.isArray(graphs)) {
|
|
165
|
-
|
|
198
|
+
errors.push('index.graphs: must be an object mapping graph names to definitions');
|
|
199
|
+
return errors;
|
|
166
200
|
}
|
|
167
|
-
const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
168
201
|
for (const [name, rawDef] of Object.entries(graphs)) {
|
|
169
202
|
const where = `index.graphs.${name}`;
|
|
170
203
|
if (typeof rawDef !== 'object' || rawDef === null || Array.isArray(rawDef)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve AIWG's global, provider-neutral user configuration directory.
|
|
7
|
+
*
|
|
8
|
+
* Contract: explicit override > AIWG_CONFIG > existing ~/.aiwg > existing
|
|
9
|
+
* ~/.config/aiwg > ~/.aiwg. Keeping this in a dependency-free ESM module lets
|
|
10
|
+
* the launcher, channel manager, updater, and TypeScript config API share the
|
|
11
|
+
* exact same resolution rules.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveUserConfigDir(options = {}) {
|
|
14
|
+
if (options.configDir) return path.resolve(options.configDir);
|
|
15
|
+
const env = options.env ?? process.env;
|
|
16
|
+
if (env.AIWG_CONFIG) return path.resolve(env.AIWG_CONFIG);
|
|
17
|
+
|
|
18
|
+
const home = options.homeDir ?? os.homedir();
|
|
19
|
+
const legacy = path.join(home, '.aiwg');
|
|
20
|
+
const xdg = path.join(home, '.config', 'aiwg');
|
|
21
|
+
const pathExists = options.exists ?? existsSync;
|
|
22
|
+
if (pathExists(legacy)) return legacy;
|
|
23
|
+
if (pathExists(xdg)) return xdg;
|
|
24
|
+
return legacy;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function userConfigFile(name, options = {}) {
|
|
28
|
+
return path.join(resolveUserConfigDir(options), name);
|
|
29
|
+
}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* @implements #545
|
|
17
17
|
*/
|
|
18
18
|
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
19
|
-
import { resolve } from 'path';
|
|
20
|
-
import { homedir } from 'os';
|
|
21
19
|
import { existsSync } from 'fs';
|
|
20
|
+
import { resolve } from 'path';
|
|
21
|
+
import { resolveUserConfigDir } from './user-config-dir.mjs';
|
|
22
22
|
/**
|
|
23
23
|
* Known config files in the user config directory
|
|
24
24
|
*/
|
|
@@ -28,6 +28,7 @@ export const KNOWN_CONFIG_FILES = [
|
|
|
28
28
|
{ filename: 'ops.json', description: 'Ops workspace registry' },
|
|
29
29
|
{ filename: 'mcp-servers.json', description: 'MCP server registry (single source of truth)' },
|
|
30
30
|
{ filename: 'packages.yaml', description: 'Installed remote packages (aiwg install)' },
|
|
31
|
+
{ filename: 'installation.json', description: 'Canonical global installation identity' },
|
|
31
32
|
];
|
|
32
33
|
/**
|
|
33
34
|
* Default user config values
|
|
@@ -60,26 +61,7 @@ export const DEFAULT_USER_CONFIG = {
|
|
|
60
61
|
* 4. ~/.aiwg (default if neither exists)
|
|
61
62
|
*/
|
|
62
63
|
export function resolveConfigDir(overridePath) {
|
|
63
|
-
|
|
64
|
-
const envOverride = process.env.AIWG_CONFIG;
|
|
65
|
-
if (overridePath) {
|
|
66
|
-
return resolve(overridePath);
|
|
67
|
-
}
|
|
68
|
-
if (envOverride) {
|
|
69
|
-
return resolve(envOverride);
|
|
70
|
-
}
|
|
71
|
-
// 2. Check primary path: ~/.aiwg
|
|
72
|
-
const primaryPath = resolve(homedir(), '.aiwg');
|
|
73
|
-
if (existsSync(primaryPath)) {
|
|
74
|
-
return primaryPath;
|
|
75
|
-
}
|
|
76
|
-
// 3. Check fallback path: ~/.config/aiwg
|
|
77
|
-
const fallbackPath = resolve(homedir(), '.config/aiwg');
|
|
78
|
-
if (existsSync(fallbackPath)) {
|
|
79
|
-
return fallbackPath;
|
|
80
|
-
}
|
|
81
|
-
// 4. Default to primary if neither exists
|
|
82
|
-
return primaryPath;
|
|
64
|
+
return resolveUserConfigDir({ configDir: overridePath });
|
|
83
65
|
}
|
|
84
66
|
/**
|
|
85
67
|
* User-level configuration manager
|
|
@@ -164,6 +164,24 @@ export const updateCommand = {
|
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
166
|
};
|
|
167
|
+
export const installationCommand = {
|
|
168
|
+
id: 'installation',
|
|
169
|
+
type: 'command',
|
|
170
|
+
name: 'Installation Identity',
|
|
171
|
+
description: 'Inspect, adopt, or deliberately switch the canonical global AIWG installation',
|
|
172
|
+
version: '1.0.0',
|
|
173
|
+
capabilities: ['cli', 'installation', 'update', 'diagnostics', 'recovery'],
|
|
174
|
+
keywords: ['installation', 'canonical', 'adopt', 'switch', 'package-manager', 'drift'],
|
|
175
|
+
category: 'maintenance',
|
|
176
|
+
platforms: { claude: 'full', generic: 'full' },
|
|
177
|
+
deployment: { pathTemplate: '.{platform}/commands/{id}.md', core: true },
|
|
178
|
+
metadata: {
|
|
179
|
+
type: 'command',
|
|
180
|
+
template: 'utility',
|
|
181
|
+
argumentHint: '<show|adopt|switch> [--root <path>] [--method <npm|web|source>] [--manager <absolute-path>] [--json]',
|
|
182
|
+
allowedTools: ['Read', 'Write'],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
167
185
|
// Renamed from `refreshCommand` as part of #694 (avoid collision with git sync
|
|
168
186
|
// semantics) and re-linked to `refreshHandler` in #919. Users who type
|
|
169
187
|
// `aiwg sync` still reach this handler via its 'sync' alias and see a
|
|
@@ -912,7 +930,7 @@ export const serveCommand = {
|
|
|
912
930
|
triggerPhrases: ['serve dashboard', 'start server', 'open dashboard', 'aiwg serve'],
|
|
913
931
|
commandHint: {
|
|
914
932
|
template: 'utility',
|
|
915
|
-
argumentHint: '[--port <n>] [--bind <host>] [--no-open] [--read-only]',
|
|
933
|
+
argumentHint: '[--port <n>] [--bind <host>] [--no-open] [--read-only] [--a2a-protocol <0.3|1.0|auto>] [--a2a-protocol-fallback] [--no-a2a-legacy-executor-fallback]',
|
|
916
934
|
allowedTools: ['Bash'],
|
|
917
935
|
},
|
|
918
936
|
},
|
|
@@ -3569,6 +3587,7 @@ export const commandDefinitions = [
|
|
|
3569
3587
|
doctorCommand,
|
|
3570
3588
|
contextFirewallCommand,
|
|
3571
3589
|
updateCommand,
|
|
3590
|
+
installationCommand,
|
|
3572
3591
|
refreshCommand,
|
|
3573
3592
|
regenerateCommand,
|
|
3574
3593
|
workspaceContextCommand,
|
|
@@ -72,7 +72,7 @@ export const FEATURE_CATALOG = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: 'graph',
|
|
75
|
-
description: 'Graphology backend for
|
|
75
|
+
description: 'Graphology backend for artifact-index traversal only; not Flow graph execution',
|
|
76
76
|
packages: ['graphology', 'graphology-operators', 'graphology-traversal'],
|
|
77
77
|
packageSpecs: {
|
|
78
78
|
graphology: '0.26.0',
|
|
@@ -82,6 +82,7 @@ export const FEATURE_CATALOG = [
|
|
|
82
82
|
enables: [
|
|
83
83
|
'index.graphBackend: graphology',
|
|
84
84
|
'in-memory attributed graph traversal and operator workflows',
|
|
85
|
+
'artifact graph data operations (use graph-pattern addon for Flow execution graphs)',
|
|
85
86
|
],
|
|
86
87
|
cost: '~2 MB — pure JS, no native deps',
|
|
87
88
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** A2A/telemetry metadata key reserved for the optional Flow graph profile. */
|
|
2
|
+
export const AIWG_GRAPH_METADATA_KEY = 'aiwg.flow.graph';
|
|
3
|
+
function nonEmpty(value) {
|
|
4
|
+
return typeof value === 'string' && value.length > 0;
|
|
5
|
+
}
|
|
6
|
+
export function isGraphExecutionMetadata(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
return false;
|
|
9
|
+
const item = value;
|
|
10
|
+
return item.schemaVersion === 'graph.flow.aiwg.io/v1'
|
|
11
|
+
&& nonEmpty(item.graphId)
|
|
12
|
+
&& nonEmpty(item.graphVersion)
|
|
13
|
+
&& nonEmpty(item.runId)
|
|
14
|
+
&& nonEmpty(item.nodeId)
|
|
15
|
+
&& nonEmpty(item.nodeRunId);
|
|
16
|
+
}
|
|
17
|
+
export function graphMetadataRecord(value) {
|
|
18
|
+
return { [AIWG_GRAPH_METADATA_KEY]: value };
|
|
19
|
+
}
|
|
20
|
+
export function extractGraphMetadata(metadata) {
|
|
21
|
+
const value = metadata?.[AIWG_GRAPH_METADATA_KEY];
|
|
22
|
+
return isGraphExecutionMetadata(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Project graph metadata to an explicitly named audience. Public projection
|
|
26
|
+
* removes execution identifiers and decision evidence. Cockpit receives only
|
|
27
|
+
* the declared route-decision summary; arbitrary user/task context is never a
|
|
28
|
+
* member of this contract and is therefore dropped by construction.
|
|
29
|
+
*/
|
|
30
|
+
export function projectGraphMetadata(value, audience) {
|
|
31
|
+
if (!isGraphExecutionMetadata(value))
|
|
32
|
+
throw new Error('Invalid graph execution metadata.');
|
|
33
|
+
if (audience === 'internal')
|
|
34
|
+
return structuredClone(value);
|
|
35
|
+
const common = {
|
|
36
|
+
schemaVersion: value.schemaVersion,
|
|
37
|
+
graphVersion: value.graphVersion,
|
|
38
|
+
nodeId: value.nodeId,
|
|
39
|
+
...(value.edgeId ? { edgeId: value.edgeId } : {}),
|
|
40
|
+
...(value.routeName ? { routeName: value.routeName } : {}),
|
|
41
|
+
...(value.runtimeBinding ? { runtimeBinding: value.runtimeBinding } : {}),
|
|
42
|
+
...(value.nodeState ? { nodeState: value.nodeState } : {}),
|
|
43
|
+
};
|
|
44
|
+
if (audience === 'public')
|
|
45
|
+
return common;
|
|
46
|
+
return {
|
|
47
|
+
...common,
|
|
48
|
+
graphId: value.graphId,
|
|
49
|
+
runId: value.runId,
|
|
50
|
+
nodeRunId: value.nodeRunId,
|
|
51
|
+
...(value.checkpointId ? { checkpointId: value.checkpointId } : {}),
|
|
52
|
+
...(value.routeReason ? { routeReason: value.routeReason } : {}),
|
|
53
|
+
...(value.routeEvidence === undefined ? {} : { routeEvidence: structuredClone(value.routeEvidence) }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=graph-metadata.js.map
|