@aiwg/cli 2026.8.16 → 2026.8.17
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/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/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/ralph.js +2 -1
- package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
- package/dist/src/cli/handlers/use.js +117 -12
- 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/providers/transformation-receipt-integration.js +130 -3
- package/dist/src/security/artifact-verifier.js +7 -1
- package/dist/src/serve/pty-bridge.js +6 -11
- package/dist/src/skills/run.js +15 -6
- package/package.json +2 -1
|
@@ -454,6 +454,7 @@ function recordForEntry(cwd, entry, graphName, dependencyGraph, privacy, schemaV
|
|
|
454
454
|
...(entry.searchTerms?.length
|
|
455
455
|
? { aiwg_search_terms: uniqueSorted(entry.searchTerms) }
|
|
456
456
|
: {}),
|
|
457
|
+
...(entry.script ? { aiwg_script: entry.script } : {}),
|
|
457
458
|
},
|
|
458
459
|
},
|
|
459
460
|
}
|
|
@@ -494,7 +494,7 @@ async function handleBuild(args) {
|
|
|
494
494
|
console.log('');
|
|
495
495
|
console.log('Default behavior (no --graph): builds all graphs with defaultBuild: true');
|
|
496
496
|
console.log('Multi-graph builds run by buildOrder/buildTier (refs → citations → bibliography before heavy graphs)');
|
|
497
|
-
console.log(' Built-in defaults: project (always), codebase (
|
|
497
|
+
console.log(' Built-in defaults: project (always), codebase (auto-detects JavaScript/TypeScript and Python layouts)');
|
|
498
498
|
console.log('');
|
|
499
499
|
console.log('Examples:');
|
|
500
500
|
console.log(' aiwg index build');
|
|
@@ -210,6 +210,7 @@ function artifactTypeFromRecord(record) {
|
|
|
210
210
|
function entryFromRecord(record) {
|
|
211
211
|
const indexedFrontmatter = record.search?.frontmatter ?? {};
|
|
212
212
|
const indexedSearchTerms = indexedFrontmatter.aiwg_search_terms;
|
|
213
|
+
const indexedScript = indexedFrontmatter.aiwg_script;
|
|
213
214
|
return {
|
|
214
215
|
path: record.source.path,
|
|
215
216
|
type: artifactTypeFromRecord(record),
|
|
@@ -245,6 +246,11 @@ function entryFromRecord(record) {
|
|
|
245
246
|
kernel: typeof record.search?.frontmatter?.kernel === "boolean"
|
|
246
247
|
? record.search.frontmatter.kernel
|
|
247
248
|
: undefined,
|
|
249
|
+
script: indexedScript && typeof indexedScript === "object"
|
|
250
|
+
&& typeof indexedScript.entrypoint === "string"
|
|
251
|
+
&& typeof indexedScript.runtime === "string"
|
|
252
|
+
? indexedScript
|
|
253
|
+
: undefined,
|
|
248
254
|
operationalState: record.operational_state,
|
|
249
255
|
};
|
|
250
256
|
}
|
|
@@ -195,6 +195,60 @@ export const BUILTIN_GRAPH_CONFIGS = {
|
|
|
195
195
|
* @implements #426
|
|
196
196
|
*/
|
|
197
197
|
export const GRAPH_CONFIGS = { ...BUILTIN_GRAPH_CONFIGS };
|
|
198
|
+
function freshBuiltinGraphConfig(name) {
|
|
199
|
+
const config = BUILTIN_GRAPH_CONFIGS[name];
|
|
200
|
+
return {
|
|
201
|
+
...config,
|
|
202
|
+
scanDirs: [...config.scanDirs],
|
|
203
|
+
extensions: [...config.extensions],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Detect conventional Python layouts without treating every top-level folder
|
|
208
|
+
* as source. A Python project manifest activates `.py`/`.pyi` support; package
|
|
209
|
+
* roots are immediate directories containing `__init__.py`, plus the common
|
|
210
|
+
* `tests/` and `scripts/` roots when present.
|
|
211
|
+
*/
|
|
212
|
+
function detectPythonCodebaseConfig(cwd, base) {
|
|
213
|
+
const hasPythonManifest = ['pyproject.toml', 'setup.py', 'setup.cfg']
|
|
214
|
+
.some((manifest) => fs.existsSync(path.join(cwd, manifest)));
|
|
215
|
+
if (!hasPythonManifest)
|
|
216
|
+
return base;
|
|
217
|
+
const detectedRoots = [];
|
|
218
|
+
for (const root of ['tests', 'scripts']) {
|
|
219
|
+
if (fs.existsSync(path.join(cwd, root)))
|
|
220
|
+
detectedRoots.push(root);
|
|
221
|
+
}
|
|
222
|
+
const excluded = new Set([
|
|
223
|
+
'.aiwg', '.git', '.github', '.venv', 'venv', 'node_modules',
|
|
224
|
+
'src', 'test', 'tests', 'tools', 'scripts', 'docs', 'documentation',
|
|
225
|
+
]);
|
|
226
|
+
try {
|
|
227
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
228
|
+
if (!entry.isDirectory() || excluded.has(entry.name) || entry.name.startsWith('.'))
|
|
229
|
+
continue;
|
|
230
|
+
if (fs.existsSync(path.join(cwd, entry.name, '__init__.py')))
|
|
231
|
+
detectedRoots.push(entry.name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Layout detection is best-effort; the immutable defaults still apply.
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...base,
|
|
239
|
+
scanDirs: [...new Set([...base.scanDirs, ...detectedRoots])],
|
|
240
|
+
extensions: [...new Set([...base.extensions, '.py', '.pyi'])],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function applyBuiltinGraphOverride(base, override) {
|
|
244
|
+
if (!override)
|
|
245
|
+
return base;
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
scanDirs: override.scanDirs ? [...override.scanDirs] : base.scanDirs,
|
|
249
|
+
extensions: override.extensions ? [...override.extensions] : base.extensions,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
198
252
|
/**
|
|
199
253
|
* Normalize metadataSupplements entries.
|
|
200
254
|
*
|
|
@@ -400,6 +454,11 @@ export function loadModuleGraphConfigs(cwd, diagnostics) {
|
|
|
400
454
|
* @implements #426 #726
|
|
401
455
|
*/
|
|
402
456
|
export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
457
|
+
// Built-ins are immutable in index.graphs, but codebase roots/extensions may
|
|
458
|
+
// be adapted through the explicitly bounded graphOverrides contract (#2123).
|
|
459
|
+
// Reset on every project load so a prior cwd cannot leak its override or
|
|
460
|
+
// detected Python package roots into a later build in the same process.
|
|
461
|
+
GRAPH_CONFIGS.codebase = detectPythonCodebaseConfig(cwd, freshBuiltinGraphConfig('codebase'));
|
|
403
462
|
// Load module-declared graphs first (frameworks/addons)
|
|
404
463
|
const moduleLoaded = loadModuleGraphConfigs(cwd, diagnostics);
|
|
405
464
|
const loaded = [...moduleLoaded];
|
|
@@ -408,6 +467,7 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
408
467
|
// config.yaml is a
|
|
409
468
|
// deprecated fallback so un-migrated corpora keep working.
|
|
410
469
|
let graphs;
|
|
470
|
+
let graphOverrides;
|
|
411
471
|
let fromDeprecatedYaml = false;
|
|
412
472
|
// (a) Canonical: .aiwg/aiwg.config (JSON).
|
|
413
473
|
try {
|
|
@@ -418,6 +478,9 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
418
478
|
const g = idx?.graphs;
|
|
419
479
|
if (g && typeof g === 'object')
|
|
420
480
|
graphs = g;
|
|
481
|
+
const overrides = idx?.graphOverrides;
|
|
482
|
+
if (overrides && typeof overrides === 'object')
|
|
483
|
+
graphOverrides = overrides;
|
|
421
484
|
}
|
|
422
485
|
}
|
|
423
486
|
catch {
|
|
@@ -430,7 +493,7 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
430
493
|
});
|
|
431
494
|
}
|
|
432
495
|
// (b) Fallback: legacy .aiwg/config.yaml.
|
|
433
|
-
if (!graphs) {
|
|
496
|
+
if (!graphs && !graphOverrides) {
|
|
434
497
|
try {
|
|
435
498
|
const configPath = projectAiwgPath(cwd, 'config.yaml');
|
|
436
499
|
if (fs.existsSync(configPath)) {
|
|
@@ -441,12 +504,21 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
441
504
|
graphs = g;
|
|
442
505
|
fromDeprecatedYaml = true;
|
|
443
506
|
}
|
|
507
|
+
const overrides = idx?.graphOverrides;
|
|
508
|
+
if (overrides && typeof overrides === 'object') {
|
|
509
|
+
graphOverrides = overrides;
|
|
510
|
+
fromDeprecatedYaml = true;
|
|
511
|
+
}
|
|
444
512
|
}
|
|
445
513
|
}
|
|
446
514
|
catch {
|
|
447
515
|
// best-effort
|
|
448
516
|
}
|
|
449
517
|
}
|
|
518
|
+
const codebaseOverride = graphOverrides?.codebase;
|
|
519
|
+
if (codebaseOverride && typeof codebaseOverride === 'object' && !Array.isArray(codebaseOverride)) {
|
|
520
|
+
GRAPH_CONFIGS.codebase = applyBuiltinGraphOverride(GRAPH_CONFIGS.codebase, codebaseOverride);
|
|
521
|
+
}
|
|
450
522
|
if (!graphs)
|
|
451
523
|
return loaded;
|
|
452
524
|
if (fromDeprecatedYaml && !yamlIndexDeprecationWarned) {
|
|
@@ -27,8 +27,10 @@ export const PROVIDER_CONFIGS = {
|
|
|
27
27
|
},
|
|
28
28
|
codex: {
|
|
29
29
|
binary: 'codex',
|
|
30
|
-
// Codex
|
|
31
|
-
|
|
30
|
+
// Current Codex releases use the explicit bypass flag for unrestricted,
|
|
31
|
+
// non-interactive execution. Keep this centralized so every launcher maps
|
|
32
|
+
// AIWG's --dangerous option consistently.
|
|
33
|
+
dangerousFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
32
34
|
name: 'OpenAI Codex',
|
|
33
35
|
},
|
|
34
36
|
hermes: {
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { formatCockpitDoctor, runCockpitDoctor, } from '../../cockpit/doctor.js';
|
|
6
7
|
export const COCKPIT_PACKAGE_NAME = '@aiwg/cockpit';
|
|
7
8
|
export function cockpitHome() {
|
|
8
9
|
return process.env.AIWG_COCKPIT_HOME || path.join(homedir(), '.aiwg', 'cockpit', 'package');
|
|
@@ -22,6 +23,16 @@ async function coreVersion(frameworkRoot) {
|
|
|
22
23
|
function packageRoot(home = cockpitHome()) {
|
|
23
24
|
return path.join(home, 'node_modules', '@aiwg', 'cockpit');
|
|
24
25
|
}
|
|
26
|
+
function valueAfter(args, flag) {
|
|
27
|
+
const index = args.indexOf(flag);
|
|
28
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
function doctorFormat(args) {
|
|
31
|
+
if (args.includes('--json'))
|
|
32
|
+
return 'json';
|
|
33
|
+
const value = valueAfter(args, '--format');
|
|
34
|
+
return value === 'json' || value === 'markdown' ? value : 'text';
|
|
35
|
+
}
|
|
25
36
|
export async function resolveCockpitInstall(home = cockpitHome()) {
|
|
26
37
|
const root = packageRoot(home);
|
|
27
38
|
const pkg = await readJson(path.join(root, 'package.json'));
|
|
@@ -95,6 +106,36 @@ export const cockpitHandler = {
|
|
|
95
106
|
const install = await resolveCockpitInstall();
|
|
96
107
|
const version = await coreVersion(ctx.frameworkRoot);
|
|
97
108
|
const autoInstall = ctx.args.includes('--install') || ctx.args.includes('--yes') || ctx.args.includes('-y');
|
|
109
|
+
if (ctx.args[0] === 'doctor' || ctx.args.includes('--doctor')) {
|
|
110
|
+
const sourcePackageRoot = path.join(ctx.frameworkRoot, 'apps', 'cockpit');
|
|
111
|
+
const sourcePackage = !install.installed ? await readJson(path.join(sourcePackageRoot, 'package.json')) : null;
|
|
112
|
+
const doctorInstall = install.installed ? install : {
|
|
113
|
+
installed: Boolean(sourcePackage),
|
|
114
|
+
version: typeof sourcePackage?.version === 'string' ? sourcePackage.version : undefined,
|
|
115
|
+
packageRoot: sourcePackageRoot,
|
|
116
|
+
};
|
|
117
|
+
const topologyValue = valueAfter(ctx.args, '--topology');
|
|
118
|
+
const topology = topologyValue === 'ssh-local' || topologyValue === 'ssh-reverse'
|
|
119
|
+
? topologyValue
|
|
120
|
+
: 'same-host';
|
|
121
|
+
const report = await runCockpitDoctor({
|
|
122
|
+
coreVersion: version,
|
|
123
|
+
cockpitInstalled: doctorInstall.installed,
|
|
124
|
+
cockpitVersion: doctorInstall.version,
|
|
125
|
+
cockpitPackageRoot: doctorInstall.packageRoot,
|
|
126
|
+
topology,
|
|
127
|
+
cockpitHost: valueAfter(ctx.args, '--cockpit-host'),
|
|
128
|
+
executorHost: valueAfter(ctx.args, '--executor-host'),
|
|
129
|
+
expectedExecutorVersion: valueAfter(ctx.args, '--executor-version'),
|
|
130
|
+
forwardEndpoint: valueAfter(ctx.args, '--forward-endpoint'),
|
|
131
|
+
runtimeFile: valueAfter(ctx.args, '--runtime-file'),
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
exitCode: report.status === 'blocked' ? 1 : 0,
|
|
135
|
+
message: formatCockpitDoctor(report, doctorFormat(ctx.args)),
|
|
136
|
+
rawOutput: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
98
139
|
if (ctx.args.includes('--status')) {
|
|
99
140
|
return {
|
|
100
141
|
exitCode: 0,
|
|
@@ -91,7 +91,7 @@ function displayHelp() {
|
|
|
91
91
|
]);
|
|
92
92
|
helpGroup('FEATURES', [
|
|
93
93
|
['features', 'Show optional feature install status'],
|
|
94
|
-
['cockpit [--status]', 'Launch
|
|
94
|
+
['cockpit [--status|doctor]', 'Launch Cockpit or diagnose its executor topology'],
|
|
95
95
|
]);
|
|
96
96
|
helpGroup('VALIDATION', [
|
|
97
97
|
['validate-metadata [path]', 'Validate AIWG component metadata (defaults to agentic/code)'],
|
|
@@ -278,7 +278,8 @@ LFD LOOP CONTROLS (hard cumulative ceilings; loop stops with a best-output repor
|
|
|
278
278
|
Spawnable: claude, opencode, codex, hermes
|
|
279
279
|
--dangerous Enable unrestricted mode for the selected provider.
|
|
280
280
|
Passes the provider's native flag (e.g. --dangerously-skip-permissions
|
|
281
|
-
for claude/opencode,
|
|
281
|
+
for claude/opencode,
|
|
282
|
+
--dangerously-bypass-approvals-and-sandbox for codex). No effect if the
|
|
282
283
|
provider doesn't have a dangerous mode flag.
|
|
283
284
|
--params "<args>" Pass arbitrary args verbatim to the agent binary.
|
|
284
285
|
Appended after all other flags. Quoted segments preserved.
|
|
@@ -123,7 +123,8 @@ AGENT OPTIONS:
|
|
|
123
123
|
IDE-integrated (guidance only): copilot, cursor, factory, warp, windsurf
|
|
124
124
|
--dangerous Enable unrestricted mode (skips permission prompts).
|
|
125
125
|
Maps to the provider's native flag — e.g. claude gets
|
|
126
|
-
--dangerously-skip-permissions, codex gets
|
|
126
|
+
--dangerously-skip-permissions, codex gets
|
|
127
|
+
--dangerously-bypass-approvals-and-sandbox.
|
|
127
128
|
Has no effect on providers that don't support it.
|
|
128
129
|
--params "<args>" Pass arbitrary args directly to the agent binary.
|
|
129
130
|
Appended verbatim after all other flags. You are
|
|
@@ -52,7 +52,7 @@ import { generate as generateContextFiles, discoverDeployedArtifacts, } from '..
|
|
|
52
52
|
import { verifyModelWrapperDeployment } from '../../models/wrapper-deployment.js';
|
|
53
53
|
import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
|
|
54
54
|
import { aggregateUseDeploymentResult, buildDryRunUseResult, renderUseDeploymentResult, verifyProviderDeployment, } from '../services/deployment-verification.js';
|
|
55
|
-
import { finalizeProviderTransformationReceipt, sourceVerificationsFromSignedWebRelease, } from '../../providers/transformation-receipt-integration.js';
|
|
55
|
+
import { finalizeProviderTransformationReceipt, providerReceiptHasLocalSources, sourceVerificationsFromSignedWebRelease, } from '../../providers/transformation-receipt-integration.js';
|
|
56
56
|
import { loadResourceTrustRootFile, resolveWebRelease, } from '../../resources/web-release.js';
|
|
57
57
|
import { createResourceCredentialProvider } from '../../auth/resource-credentials.js';
|
|
58
58
|
/**
|
|
@@ -75,10 +75,16 @@ function providerReceiptWebReleaseOptions() {
|
|
|
75
75
|
: {}),
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
function releaseResourceUnavailable(error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
return /fetch failed|request timed out|no fetch implementation/i.test(message);
|
|
81
|
+
}
|
|
82
|
+
export async function resolveProviderReceiptSource(options) {
|
|
83
|
+
if (await providerReceiptHasLocalSources(options))
|
|
84
|
+
return { sourceDisposition: 'local-source' };
|
|
79
85
|
const versionInfo = await getVersionInfo();
|
|
80
86
|
if (versionInfo.devMode)
|
|
81
|
-
return
|
|
87
|
+
return { sourceDisposition: 'local-source' };
|
|
82
88
|
const releaseOptions = providerReceiptWebReleaseOptions();
|
|
83
89
|
let release;
|
|
84
90
|
try {
|
|
@@ -90,15 +96,24 @@ async function signedProviderSourceVerifications(options) {
|
|
|
90
96
|
// Protected production resources require the authenticated release
|
|
91
97
|
// credential. A configured alternate endpoint may intentionally be public.
|
|
92
98
|
if (!token && releaseOptions.baseUrl === undefined)
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
return { sourceDisposition: 'source-unavailable' };
|
|
100
|
+
try {
|
|
101
|
+
release = await resolveWebRelease({
|
|
102
|
+
...releaseOptions,
|
|
103
|
+
selector: versionInfo.version,
|
|
104
|
+
credentialProvider: async () => token,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (releaseResourceUnavailable(error))
|
|
109
|
+
return { sourceDisposition: 'source-unavailable' };
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
99
112
|
}
|
|
100
113
|
const verifications = await sourceVerificationsFromSignedWebRelease(options, release);
|
|
101
|
-
return Object.keys(verifications).length > 0
|
|
114
|
+
return Object.keys(verifications).length > 0
|
|
115
|
+
? { sourceVerifications: verifications }
|
|
116
|
+
: { sourceDisposition: 'verification-failed' };
|
|
102
117
|
}
|
|
103
118
|
/**
|
|
104
119
|
* Framework name to deploy mode mapping.
|
|
@@ -1005,6 +1020,79 @@ async function countBundleDeployedArtifacts(bundlePath, target, provider) {
|
|
|
1005
1020
|
rules: await countDeployedBundleFiles(bundlePath, 'rules', target, paths.rules, ['.md', '.mdc']),
|
|
1006
1021
|
};
|
|
1007
1022
|
}
|
|
1023
|
+
const SKILL_SUPPORT_REFERENCE = /(?:^|[\s`('"\[])((?:templates|references|scripts|assets)\/[A-Za-z0-9._@/+\-]+)(?=$|[\s`)'"\],:;])/gm;
|
|
1024
|
+
/**
|
|
1025
|
+
* Project skill-relative support files may live beside the skill or at the
|
|
1026
|
+
* bundle root (plugin payloads commonly share report templates). Materialize
|
|
1027
|
+
* only paths explicitly named by SKILL.md, and fail closed on missing or
|
|
1028
|
+
* unsafe sources so a deployed instruction can never point at absent assets.
|
|
1029
|
+
*/
|
|
1030
|
+
async function reconcileProjectLocalSkillAssets(bundlePath, target, provider) {
|
|
1031
|
+
const skillsRoot = path.join(bundlePath, 'skills');
|
|
1032
|
+
let skillDirs;
|
|
1033
|
+
try {
|
|
1034
|
+
skillDirs = (await fs.readdir(skillsRoot, { withFileTypes: true }))
|
|
1035
|
+
.filter(entry => entry.isDirectory())
|
|
1036
|
+
.map(entry => entry.name);
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const paths = getProviderPaths(provider);
|
|
1042
|
+
const kernelSkillsPath = getProviderKernelSkillsPath(provider);
|
|
1043
|
+
const deployRoots = [...new Set([
|
|
1044
|
+
paths.skills,
|
|
1045
|
+
kernelSkillsPath,
|
|
1046
|
+
].filter((value) => Boolean(value)).map(value => resolveDeployPath(target, value)))];
|
|
1047
|
+
for (const skillName of skillDirs) {
|
|
1048
|
+
const sourceSkillDir = path.join(skillsRoot, skillName);
|
|
1049
|
+
const sourceSkillMd = path.join(sourceSkillDir, 'SKILL.md');
|
|
1050
|
+
let content;
|
|
1051
|
+
try {
|
|
1052
|
+
content = await fs.readFile(sourceSkillMd, 'utf8');
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
const references = [...new Set([...content.matchAll(SKILL_SUPPORT_REFERENCE)].map(match => match[1]))];
|
|
1058
|
+
for (const relative of references) {
|
|
1059
|
+
const normalized = path.posix.normalize(relative);
|
|
1060
|
+
if (normalized !== relative || normalized.startsWith('../') || path.isAbsolute(normalized)) {
|
|
1061
|
+
throw new Error(`unsafe skill support reference '${relative}' in ${sourceSkillMd}`);
|
|
1062
|
+
}
|
|
1063
|
+
const candidates = [path.join(sourceSkillDir, normalized), path.join(bundlePath, normalized)];
|
|
1064
|
+
let source;
|
|
1065
|
+
for (const candidate of candidates) {
|
|
1066
|
+
try {
|
|
1067
|
+
const stat = await fs.lstat(candidate);
|
|
1068
|
+
if (stat.isFile() && !stat.isSymbolicLink()) {
|
|
1069
|
+
source = candidate;
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch { /* try bundle-root fallback */ }
|
|
1074
|
+
}
|
|
1075
|
+
if (!source)
|
|
1076
|
+
throw new Error(`missing skill support asset '${relative}' referenced by ${sourceSkillMd}`);
|
|
1077
|
+
let deployedSkillRoot;
|
|
1078
|
+
for (const root of deployRoots) {
|
|
1079
|
+
// The deployer may select the bulk or kernel tier; use the tier that
|
|
1080
|
+
// actually contains this skill's transformed SKILL.md.
|
|
1081
|
+
if (await fileExists(path.join(root, skillName, 'SKILL.md'))) {
|
|
1082
|
+
deployedSkillRoot = root;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!deployedSkillRoot)
|
|
1087
|
+
throw new Error(`deployed skill '${skillName}' not found while reconciling support assets`);
|
|
1088
|
+
const destination = path.join(deployedSkillRoot, skillName, ...normalized.split('/'));
|
|
1089
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
1090
|
+
await fs.copyFile(source, destination);
|
|
1091
|
+
const mode = (await fs.stat(source)).mode & 0o777;
|
|
1092
|
+
await fs.chmod(destination, mode);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1008
1096
|
/**
|
|
1009
1097
|
* Deploy a single project-local bundle to one provider via deploy-agents.mjs.
|
|
1010
1098
|
* Runs the same script and flags used for upstream addons, with the bundle
|
|
@@ -1072,6 +1160,15 @@ async function deployOneProjectLocalBundle(opts) {
|
|
|
1072
1160
|
env: { AIWG_ROOT: frameworkRoot },
|
|
1073
1161
|
});
|
|
1074
1162
|
exitCode = result.exitCode;
|
|
1163
|
+
if (exitCode === 0 && !dryRun) {
|
|
1164
|
+
try {
|
|
1165
|
+
await reconcileProjectLocalSkillAssets(bundle.artifactPath, target, provider);
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
ui.warn(`Project-local skill asset deployment failed for '${bundle.id}': ${error.message}`);
|
|
1169
|
+
exitCode = 1;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1075
1172
|
}
|
|
1076
1173
|
if (exitCode === 0 && cliCommandCount > 0) {
|
|
1077
1174
|
try {
|
|
@@ -2017,10 +2114,18 @@ export class UseHandler {
|
|
|
2017
2114
|
scope: effectiveScope,
|
|
2018
2115
|
requestedBundles: [requestedBundle],
|
|
2019
2116
|
};
|
|
2020
|
-
const
|
|
2021
|
-
await finalizeProviderTransformationReceipt({ ...receiptOptions,
|
|
2117
|
+
const sourceResolution = await resolveProviderReceiptSource(receiptOptions);
|
|
2118
|
+
await finalizeProviderTransformationReceipt({ ...receiptOptions, ...sourceResolution });
|
|
2022
2119
|
}
|
|
2023
2120
|
catch (error) {
|
|
2121
|
+
await finalizeProviderTransformationReceipt({
|
|
2122
|
+
projectRoot: projectDir,
|
|
2123
|
+
frameworkRoot,
|
|
2124
|
+
provider,
|
|
2125
|
+
scope: effectiveScope,
|
|
2126
|
+
requestedBundles: [requestedBundle],
|
|
2127
|
+
sourceDisposition: 'verification-failed',
|
|
2128
|
+
}).catch(() => undefined);
|
|
2024
2129
|
originalConsole.warn(`Provider receipt finalization failed for ${provider}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2025
2130
|
}
|
|
2026
2131
|
}
|
|
@@ -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)) {
|
|
@@ -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,
|
|
@@ -695,19 +695,14 @@ export async function handlePtyConnection(sessionId, ws, command = 'aiwg', cmdAr
|
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
697
|
else if (!session.exited) {
|
|
698
|
-
// Reconnect to existing session
|
|
698
|
+
// Reconnect to an existing session by replaying the complete retained
|
|
699
|
+
// buffer. Do not trim at the last full-screen erase: terminal erase
|
|
700
|
+
// sequences repaint the current viewport but xterm still needs the bytes
|
|
701
|
+
// that preceded them to reconstruct scrollback when a user switches away
|
|
702
|
+
// from a session and later returns (#2146).
|
|
699
703
|
registry.addClient(sessionId, clientId, ws);
|
|
700
704
|
if (session.outputBuffer) {
|
|
701
|
-
|
|
702
|
-
// screen-init sequences (cursor moves, status-bar paint) from before the
|
|
703
|
-
// erase don't render as literal garbage in a fresh xterm.js context.
|
|
704
|
-
// Everything before \x1b[2J would be cleared by the erase anyway;
|
|
705
|
-
// everything after is the session content tmux redrew (MOTD, history, etc).
|
|
706
|
-
// If no erase is found, replay the whole buffer unchanged.
|
|
707
|
-
const ERASE = '\x1b[2J';
|
|
708
|
-
const lastErase = session.outputBuffer.lastIndexOf(ERASE);
|
|
709
|
-
const replay = lastErase !== -1 ? session.outputBuffer.slice(lastErase) : session.outputBuffer;
|
|
710
|
-
ws.send(JSON.stringify({ type: 'data', payload: replay }));
|
|
705
|
+
ws.send(JSON.stringify({ type: 'data', payload: session.outputBuffer }));
|
|
711
706
|
}
|
|
712
707
|
}
|
|
713
708
|
else {
|
package/dist/src/skills/run.js
CHANGED
|
@@ -25,6 +25,8 @@ import { spawn } from 'node:child_process';
|
|
|
25
25
|
import { promises as fs } from 'node:fs';
|
|
26
26
|
import * as path from 'node:path';
|
|
27
27
|
import { resolveRuntime, supportedRuntimes } from './runtime.js';
|
|
28
|
+
import { recordTypeForEntry, stableRecordId } from '../artifacts/browser-export.js';
|
|
29
|
+
import { loadFortemiCoreMetadataEntries } from '../artifacts/fortemi-core-query-adapter.js';
|
|
28
30
|
/**
|
|
29
31
|
* Resolve the AIWG installation root. Prefers `$AIWG_ROOT` env, falls
|
|
30
32
|
* back to the channel manager's framework-root resolver.
|
|
@@ -53,22 +55,29 @@ async function findSkillEntry(cwd, name) {
|
|
|
53
55
|
const reader = await import('../artifacts/index-reader.js');
|
|
54
56
|
const entries = [];
|
|
55
57
|
for (const g of ['framework', 'project', 'codebase']) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
entries.push(...
|
|
58
|
+
const canonical = loadFortemiCoreMetadataEntries(cwd, g);
|
|
59
|
+
if (canonical.entries.length > 0) {
|
|
60
|
+
entries.push(...canonical.entries);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
|
|
64
|
+
if (idx)
|
|
65
|
+
entries.push(...Object.values(idx.entries));
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
if (entries.length === 0) {
|
|
61
69
|
const legacy = reader.loadMetadataIndex(cwd);
|
|
62
70
|
if (legacy)
|
|
63
71
|
entries.push(...Object.values(legacy.entries));
|
|
64
72
|
}
|
|
65
|
-
const skills = entries.filter(e => e.type === 'skill');
|
|
73
|
+
const skills = entries.filter(e => e.type === 'skill' || e.type === 'aiwg.skill');
|
|
66
74
|
const needle = name.trim();
|
|
67
75
|
// Basename match — skills are conventionally `<dir>/SKILL.md`
|
|
68
76
|
const matches = skills.filter(e => {
|
|
69
77
|
const dir = path.basename(path.dirname(e.path));
|
|
70
78
|
const stem = path.basename(e.path).replace(/\.[^.]+$/, '');
|
|
71
|
-
|
|
79
|
+
const id = stableRecordId(recordTypeForEntry({ ...e, type: 'skill' }, 'v2'), e.path);
|
|
80
|
+
return id === needle || e.name === needle || dir === needle || stem === needle || e.path === needle;
|
|
72
81
|
});
|
|
73
82
|
if (matches.length === 0)
|
|
74
83
|
return null;
|
|
@@ -248,7 +257,7 @@ export async function main(args, env) {
|
|
|
248
257
|
});
|
|
249
258
|
}
|
|
250
259
|
function printUsage() {
|
|
251
|
-
console.log('Usage: aiwg run skill <name> [--cwd <path>] [-- <args...>]');
|
|
260
|
+
console.log('Usage: aiwg run skill <stable-id-or-name> [--cwd <path>] [-- <args...>]');
|
|
252
261
|
console.log('');
|
|
253
262
|
console.log('Examples:');
|
|
254
263
|
console.log(' aiwg run skill voice-apply -- --voice technical-authority --input draft.md');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cli",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.17",
|
|
4
4
|
"description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@fortemi/core": "2026.7.15",
|
|
69
69
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
70
|
+
"ajv": "^8.20.0",
|
|
70
71
|
"chalk": "^4.1.2",
|
|
71
72
|
"chokidar": "^4.0.3",
|
|
72
73
|
"commander": "^12.1.0",
|