@aiwg/cockpit 2026.9.6 → 2026.9.7

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.
@@ -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 r = join(LIBRARY_DIR, String(name).replace(/^[/\\]+/, ''));
278
- return r === LIBRARY_DIR || r.startsWith(LIBRARY_DIR + '/') ? r : null;
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(LIBRARY_DIR, { withFileTypes: true }); } catch { return []; }
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(LIBRARY_DIR, e.name, '.cockpit-origin.json'), 'utf8')), name: e.name, kind: 'dir' }; } catch { /* no manifest */ }
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(LIBRARY_DIR, { recursive: true, mode: 0o755 });
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 (LOCAL_DOCKER_FALLBACK && ['docker', 'container'].includes(runtime) && dockerName) {
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 (!LOCAL_DOCKER_FALLBACK) {
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 (!LOCAL_DOCKER_FALLBACK) {
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 (!localLibvirtFallbackAllowed()) {
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, MCP_TOKEN_FILE);
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 === LIBRARY_DIR || !existsSync(target)) return json(res, 404, { error: 'not_in_library' });
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
- { executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
3558
+ requestContext,
3505
3559
  () => handleRequest(req, res),
3506
3560
  ));
3507
3561
  server.on('upgrade', (req, socket, head) => executorRequestContext.run(
3508
- { executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
3562
+ requestContext,
3509
3563
  async () => {
3510
3564
  try {
3511
3565
  const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);