@aiwg/cockpit 2026.9.6 → 2026.9.9
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/bridge/src/server.mjs +71 -17
- package/bridge/src/smoke.mjs +319 -232
- package/package.json +1 -1
- package/web/dist/assets/{index-DWYeO6Yy.js → index-CL5LzhiH.js} +117 -117
- package/web/dist/index.html +1 -1
package/bridge/src/server.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
14
14
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { dirname, join, basename, extname, resolve, sep } from 'node:path';
|
|
17
|
+
import { dirname, join, basename, extname, resolve, sep, isAbsolute, parse } from 'node:path';
|
|
18
18
|
import { storeCockpitToken } from '../../shell-core/keychain.mjs';
|
|
19
19
|
import { assertActivityEvent } from './activity-contract.mjs';
|
|
20
20
|
|
|
@@ -39,7 +39,7 @@ export function localLibvirtFallbackAllowed(platform = process.platform, envValu
|
|
|
39
39
|
return platform === 'linux' || envValue === '1';
|
|
40
40
|
}
|
|
41
41
|
const RUNTIME_DIR = join(homedir(), '.aiwg', 'cockpit', 'runtime');
|
|
42
|
-
const auditDir = () => process.env.AIWG_COCKPIT_AUDIT_DIR || join(homedir(), '.aiwg', 'cockpit', 'audit');
|
|
42
|
+
const auditDir = () => executorRequestContext.getStore()?.auditDir ?? (process.env.AIWG_COCKPIT_AUDIT_DIR || join(homedir(), '.aiwg', 'cockpit', 'audit'));
|
|
43
43
|
const auditLog = () => join(auditDir(), 'events.jsonl');
|
|
44
44
|
// The built React app (apps/cockpit/web/dist). Served when present; falls back to the
|
|
45
45
|
// legacy vanilla page so the Bridge works even before a web build.
|
|
@@ -54,6 +54,8 @@ const CAPABILITY_TYPES = new Set([
|
|
|
54
54
|
]);
|
|
55
55
|
const mcSessionsDir = () => join(process.cwd(), '.aiwg', 'ralph-external', 'mc', 'sessions');
|
|
56
56
|
const executorRequestContext = new AsyncLocalStorage();
|
|
57
|
+
const localDockerFallbackEnabled = () => executorRequestContext.getStore()?.localDockerFallback ?? LOCAL_DOCKER_FALLBACK;
|
|
58
|
+
const localLibvirtFallbackEnabled = () => executorRequestContext.getStore()?.localLibvirtFallback ?? localLibvirtFallbackAllowed();
|
|
57
59
|
|
|
58
60
|
function executorAuthError(code, message, cause) {
|
|
59
61
|
const err = new Error(message, cause ? { cause } : undefined);
|
|
@@ -196,6 +198,8 @@ function spawnCollect(cmd, args) {
|
|
|
196
198
|
});
|
|
197
199
|
}
|
|
198
200
|
async function runAiwg(args) {
|
|
201
|
+
const command = executorRequestContext.getStore()?.aiwgCommand;
|
|
202
|
+
if (command) return command(Object.freeze([...args])); // errors never fall through to PATH
|
|
199
203
|
try { return await spawnCollect('aiwg', args); }
|
|
200
204
|
catch (e) { if (e && e.code === 'ENOENT') return spawnCollect(process.execPath, [REPO_BIN, ...args]); throw e; }
|
|
201
205
|
}
|
|
@@ -272,20 +276,23 @@ async function dispatchMission(body, upstreamUrl) {
|
|
|
272
276
|
// assets, on disk under ~/.aiwg/cockpit/library. AIWG install files are NEVER written
|
|
273
277
|
// (clone reads the catalog read-only, writes only into the library). ---
|
|
274
278
|
const LIBRARY_DIR = join(homedir(), '.aiwg', 'cockpit', 'library');
|
|
279
|
+
const currentLibraryDir = () => executorRequestContext.getStore()?.libraryDir ?? LIBRARY_DIR;
|
|
275
280
|
/** Resolve a name to a path INSIDE the library, or null if it would escape. */
|
|
276
281
|
function inLibrary(name) {
|
|
277
|
-
const
|
|
278
|
-
|
|
282
|
+
const libraryDir = currentLibraryDir();
|
|
283
|
+
const r = join(libraryDir, String(name).replace(/^[/\\]+/, ''));
|
|
284
|
+
return r === libraryDir || r.startsWith(libraryDir + sep) ? r : null;
|
|
279
285
|
}
|
|
280
286
|
async function listLibrary() {
|
|
287
|
+
const libraryDir = currentLibraryDir();
|
|
281
288
|
let entries;
|
|
282
|
-
try { entries = await readdir(
|
|
289
|
+
try { entries = await readdir(libraryDir, { withFileTypes: true }); } catch { return []; }
|
|
283
290
|
const out = [];
|
|
284
291
|
for (const e of entries) {
|
|
285
292
|
if (e.name.startsWith('.')) continue;
|
|
286
293
|
let meta = { name: e.name, kind: e.isDirectory() ? 'dir' : 'file', type: 'unknown', origin: 'imported' };
|
|
287
294
|
if (e.isDirectory()) {
|
|
288
|
-
try { meta = { ...meta, ...JSON.parse(await readFile(join(
|
|
295
|
+
try { meta = { ...meta, ...JSON.parse(await readFile(join(libraryDir, e.name, '.cockpit-origin.json'), 'utf8')), name: e.name, kind: 'dir' }; } catch { /* no manifest */ }
|
|
289
296
|
}
|
|
290
297
|
out.push(meta);
|
|
291
298
|
}
|
|
@@ -293,9 +300,10 @@ async function listLibrary() {
|
|
|
293
300
|
}
|
|
294
301
|
/** Clone a catalog asset (skill dir or single file) into the library — never the reverse. */
|
|
295
302
|
async function cloneToLibrary({ type, name, path }) {
|
|
303
|
+
const libraryDir = currentLibraryDir();
|
|
296
304
|
if (!type || !name || !path) throw new Error('type, name, path required');
|
|
297
305
|
if (!existsSync(path)) throw new Error('source not found');
|
|
298
|
-
await mkdir(
|
|
306
|
+
await mkdir(libraryDir, { recursive: true, mode: 0o755 });
|
|
299
307
|
const destName = String(name).replace(/[^a-z0-9._-]/gi, '-');
|
|
300
308
|
const isDir = /SKILL\.(md|markdown)$/i.test(basename(path)) || (await stat(path)).isDirectory();
|
|
301
309
|
const src = /SKILL\.(md|markdown)$/i.test(basename(path)) ? dirname(path) : path;
|
|
@@ -333,7 +341,7 @@ function resolveCorpusPath(p) {
|
|
|
333
341
|
let abs;
|
|
334
342
|
try { abs = resolve(String(p)); } catch { return null; }
|
|
335
343
|
if (!SHOW_EXT_RE.test(abs)) return null;
|
|
336
|
-
if (!CORPUS_ROOTS.some((root) => abs === root || abs.startsWith(root + sep))) return null;
|
|
344
|
+
if (!(executorRequestContext.getStore()?.corpusRoots ?? CORPUS_ROOTS).some((root) => abs === root || abs.startsWith(root + sep))) return null;
|
|
337
345
|
return abs;
|
|
338
346
|
}
|
|
339
347
|
|
|
@@ -386,7 +394,7 @@ async function loadContributions() {
|
|
|
386
394
|
const sources = [], actions = [], screens = [], hooks = [], workflows = [];
|
|
387
395
|
const manifestIds = new Set();
|
|
388
396
|
const itemIds = new Set();
|
|
389
|
-
for (const [dirIndex, dir] of CONTRIB_DIRS.entries()) {
|
|
397
|
+
for (const [dirIndex, dir] of (executorRequestContext.getStore()?.contributionDirs ?? CONTRIB_DIRS).entries()) {
|
|
390
398
|
const trustTier = dirIndex === 0 ? 'first-party' : 'sandboxed-third-party';
|
|
391
399
|
let entries = [];
|
|
392
400
|
try { entries = (await readdir(dir)).filter((f) => f.endsWith('.json') && f !== 'contribution.schema.json'); } catch { continue; }
|
|
@@ -998,7 +1006,7 @@ async function destroyInstance(upstreamUrl, instanceId) {
|
|
|
998
1006
|
try {
|
|
999
1007
|
const result = await fetchJsonFirst(candidates);
|
|
1000
1008
|
if (result.status < 400) {
|
|
1001
|
-
if (
|
|
1009
|
+
if (localDockerFallbackEnabled() && ['docker', 'container'].includes(runtime) && dockerName) {
|
|
1002
1010
|
try {
|
|
1003
1011
|
await spawnCollect('docker', ['rm', '-f', dockerName]);
|
|
1004
1012
|
return {
|
|
@@ -1050,7 +1058,7 @@ async function destroyInstance(upstreamUrl, instanceId) {
|
|
|
1050
1058
|
body: { error: 'instance_not_destroyable', message: `No destroyable runtime record for ${instanceId}` },
|
|
1051
1059
|
};
|
|
1052
1060
|
}
|
|
1053
|
-
if (!
|
|
1061
|
+
if (!localDockerFallbackEnabled()) {
|
|
1054
1062
|
return {
|
|
1055
1063
|
target: `${upstreamUrl}/api/v2/admin/instances/${encodeURIComponent(instanceId)}/destroy`,
|
|
1056
1064
|
status: 409,
|
|
@@ -1144,7 +1152,7 @@ async function reconnectInstance(upstreamUrl, instanceId) {
|
|
|
1144
1152
|
}
|
|
1145
1153
|
|
|
1146
1154
|
if (['docker', 'container'].includes(runtime) && dockerName) {
|
|
1147
|
-
if (!
|
|
1155
|
+
if (!localDockerFallbackEnabled()) {
|
|
1148
1156
|
return {
|
|
1149
1157
|
target: `${upstreamUrl}/api/v2/admin/instances/${encodeURIComponent(instanceId)}/reconnect`,
|
|
1150
1158
|
status: 409,
|
|
@@ -1185,7 +1193,7 @@ async function reconnectInstance(upstreamUrl, instanceId) {
|
|
|
1185
1193
|
}
|
|
1186
1194
|
|
|
1187
1195
|
if (VM_RUNTIME_KINDS.includes(runtime)) {
|
|
1188
|
-
if (!
|
|
1196
|
+
if (!localLibvirtFallbackEnabled()) {
|
|
1189
1197
|
return {
|
|
1190
1198
|
target: `${upstreamUrl}/api/v2/admin/instances/${encodeURIComponent(instanceId)}/reconnect`,
|
|
1191
1199
|
status: 409,
|
|
@@ -2874,6 +2882,21 @@ async function proxyExecutorWebsocket({ req, socket, head, target, executorToken
|
|
|
2874
2882
|
upstreamRequest.end();
|
|
2875
2883
|
}
|
|
2876
2884
|
|
|
2885
|
+
function embeddingDirectory(value, label) {
|
|
2886
|
+
if (typeof value !== 'string' || !value.trim() || !isAbsolute(value)) {
|
|
2887
|
+
throw new TypeError(`${label} must be a non-empty absolute directory path`);
|
|
2888
|
+
}
|
|
2889
|
+
const normalized = resolve(value);
|
|
2890
|
+
if (normalized === parse(normalized).root) throw new TypeError(`${label} must not be a filesystem root`);
|
|
2891
|
+
return normalized;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
function embeddingDirectories(value, defaults, label) {
|
|
2895
|
+
if (value === undefined) return Object.freeze([...defaults]);
|
|
2896
|
+
if (!Array.isArray(value) || value.length === 0) throw new TypeError(`${label} must be a non-empty array`);
|
|
2897
|
+
return Object.freeze(value.map((entry) => embeddingDirectory(entry, label)));
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2877
2900
|
export function createBridge({
|
|
2878
2901
|
executorUrl = EXECUTOR_URL,
|
|
2879
2902
|
allowMockExecutor = ALLOW_MOCK_EXECUTOR,
|
|
@@ -2884,7 +2907,31 @@ export function createBridge({
|
|
|
2884
2907
|
sessionTtlMs = 12 * 60 * 60 * 1000,
|
|
2885
2908
|
a2aProtocolPolicy = COCKPIT_A2A_PROTOCOL_POLICY,
|
|
2886
2909
|
allowA2AProtocolFallback = COCKPIT_A2A_PROTOCOL_FALLBACK,
|
|
2910
|
+
// Explicit embedding seams are per-instance; omitted options retain operator defaults.
|
|
2911
|
+
libraryDir,
|
|
2912
|
+
auditDir: requestedAuditDir,
|
|
2913
|
+
mcpTokenFile = MCP_TOKEN_FILE,
|
|
2914
|
+
localDockerFallback = LOCAL_DOCKER_FALLBACK,
|
|
2915
|
+
localLibvirtFallback,
|
|
2916
|
+
aiwgCommand,
|
|
2917
|
+
corpusRoots,
|
|
2918
|
+
contributionDirs,
|
|
2887
2919
|
} = {}) {
|
|
2920
|
+
if (typeof mcpTokenFile !== 'string') throw new TypeError('mcpTokenFile must be a string');
|
|
2921
|
+
if (typeof localDockerFallback !== 'boolean' || (localLibvirtFallback !== undefined && typeof localLibvirtFallback !== 'boolean')) {
|
|
2922
|
+
throw new TypeError('local fallback options must be booleans');
|
|
2923
|
+
}
|
|
2924
|
+
if (aiwgCommand !== undefined && typeof aiwgCommand !== 'function') {
|
|
2925
|
+
throw new TypeError('aiwgCommand must be a function');
|
|
2926
|
+
}
|
|
2927
|
+
const instanceLibraryDir = libraryDir === undefined ? LIBRARY_DIR : embeddingDirectory(libraryDir, 'libraryDir');
|
|
2928
|
+
// Undefined preserves the historical per-request environment default.
|
|
2929
|
+
const instanceAuditDir = requestedAuditDir === undefined
|
|
2930
|
+
? undefined
|
|
2931
|
+
: embeddingDirectory(requestedAuditDir, 'auditDir');
|
|
2932
|
+
const instanceCorpusRoots = embeddingDirectories(corpusRoots, CORPUS_ROOTS, 'corpusRoots');
|
|
2933
|
+
const instanceContributionDirs = embeddingDirectories(contributionDirs, CONTRIB_DIRS, 'contributionDirs');
|
|
2934
|
+
|
|
2888
2935
|
if (!['0.3', '1.0', 'auto'].includes(a2aProtocolPolicy)) {
|
|
2889
2936
|
throw new Error(`AIWG_COCKPIT_A2A_PROTOCOL_POLICY must be 0.3, 1.0, or auto (received '${a2aProtocolPolicy}')`);
|
|
2890
2937
|
}
|
|
@@ -2926,6 +2973,12 @@ export function createBridge({
|
|
|
2926
2973
|
? { kind: 'bearer', csrf: TOKEN }
|
|
2927
2974
|
: sessionAuth(req);
|
|
2928
2975
|
const executorOrigin = new URL(upstreamUrl).origin;
|
|
2976
|
+
const requestContext = Object.freeze({
|
|
2977
|
+
executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback,
|
|
2978
|
+
libraryDir: instanceLibraryDir, auditDir: instanceAuditDir,
|
|
2979
|
+
mcpTokenFile, localDockerFallback, localLibvirtFallback, aiwgCommand,
|
|
2980
|
+
corpusRoots: instanceCorpusRoots, contributionDirs: instanceContributionDirs,
|
|
2981
|
+
});
|
|
2929
2982
|
const executorAddress = new URL(upstreamUrl);
|
|
2930
2983
|
const attachTargets = new Map();
|
|
2931
2984
|
const issueAttachUrl = (req, value) => {
|
|
@@ -3035,7 +3088,7 @@ export function createBridge({
|
|
|
3035
3088
|
return json(res, 200, await getBootstrapTrustPosture(upstreamUrl, { requireSandboxMtls }));
|
|
3036
3089
|
}
|
|
3037
3090
|
if (url.pathname === '/api/mcp/discovery' && req.method === 'GET') return json(res, 200, await getMcpDiscovery(upstreamUrl));
|
|
3038
|
-
if (url.pathname === '/api/mcp' && req.method === 'POST') return proxyMcpRequest(req, res, upstreamUrl,
|
|
3091
|
+
if (url.pathname === '/api/mcp' && req.method === 'POST') return proxyMcpRequest(req, res, upstreamUrl, mcpTokenFile);
|
|
3039
3092
|
if (url.pathname === '/api/running') return json(res, 200, await getRunning(upstreamUrl));
|
|
3040
3093
|
if (url.pathname === '/api/missions' && req.method === 'GET') return json(res, 200, await getMissions(upstreamUrl));
|
|
3041
3094
|
if (url.pathname === '/api/missions' && req.method === 'POST') {
|
|
@@ -3250,8 +3303,9 @@ export function createBridge({
|
|
|
3250
3303
|
{
|
|
3251
3304
|
const lm = url.pathname.match(/^\/api\/library\/(.+)$/);
|
|
3252
3305
|
if (lm && req.method === 'DELETE') {
|
|
3306
|
+
const libraryDir = currentLibraryDir();
|
|
3253
3307
|
const target = inLibrary(decodeURIComponent(lm[1]));
|
|
3254
|
-
if (!target || target ===
|
|
3308
|
+
if (!target || target === libraryDir || !existsSync(target)) return json(res, 404, { error: 'not_in_library' });
|
|
3255
3309
|
await rm(target, { recursive: true, force: true });
|
|
3256
3310
|
return json(res, 200, { removed: decodeURIComponent(lm[1]) });
|
|
3257
3311
|
}
|
|
@@ -3501,11 +3555,11 @@ export function createBridge({
|
|
|
3501
3555
|
}
|
|
3502
3556
|
};
|
|
3503
3557
|
const server = http.createServer((req, res) => executorRequestContext.run(
|
|
3504
|
-
|
|
3558
|
+
requestContext,
|
|
3505
3559
|
() => handleRequest(req, res),
|
|
3506
3560
|
));
|
|
3507
3561
|
server.on('upgrade', (req, socket, head) => executorRequestContext.run(
|
|
3508
|
-
|
|
3562
|
+
requestContext,
|
|
3509
3563
|
async () => {
|
|
3510
3564
|
try {
|
|
3511
3565
|
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
|