@link-assistant/hive-mind 1.74.2 → 1.74.4
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.74.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9b88700: Fix Telegram Docker isolation to use Hive Mind images with scoped GitHub, Claude, and Codex auth mounts.
|
|
8
|
+
|
|
9
|
+
## 1.74.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 741752e: Bump the Docker-in-Docker base image to `konard/box-dind:2.1.4` so `docker exec` sessions default to the `box` user with `/home/box` while dockerd still starts correctly.
|
|
14
|
+
|
|
3
15
|
## 1.74.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import crypto from 'crypto';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
16
20
|
|
|
17
21
|
if (typeof use === 'undefined') {
|
|
18
22
|
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
@@ -24,6 +28,11 @@ const { $ } = await use('command-stream');
|
|
|
24
28
|
const VALID_ISOLATION_BACKENDS = ['screen', 'tmux', 'docker'];
|
|
25
29
|
const RUNNING_SESSION_STATUSES = new Set(['executing', 'running']);
|
|
26
30
|
const TERMINAL_SESSION_STATUSES = new Set(['executed', 'completed', 'failed', 'cancelled', 'canceled', 'error']);
|
|
31
|
+
const DEFAULT_HIVE_MIND_IMAGE = 'konard/hive-mind:latest';
|
|
32
|
+
const DEFAULT_HIVE_MIND_DIND_IMAGE = 'konard/hive-mind-dind:latest';
|
|
33
|
+
const DOCKER_ISOLATION_TRACKING_BACKEND = 'screen';
|
|
34
|
+
const DOCKER_CONTAINER_HOME = '/home/box';
|
|
35
|
+
const DOCKER_CONTAINER_PREFIX = 'hive-mind-isolation';
|
|
27
36
|
|
|
28
37
|
function normalizeProcessIds(value) {
|
|
29
38
|
if (!value || typeof value !== 'object') return {};
|
|
@@ -35,6 +44,149 @@ function normalizeProcessIds(value) {
|
|
|
35
44
|
return out;
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
function normalizeTool(tool) {
|
|
48
|
+
return String(tool || 'claude')
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shellQuote(value) {
|
|
54
|
+
const stringValue = String(value);
|
|
55
|
+
if (stringValue === '') return "''";
|
|
56
|
+
return `'${stringValue.replaceAll("'", "'\\''")}'`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shellDoubleQuote(value) {
|
|
60
|
+
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$').replaceAll('`', '\\`')}"`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildShellCommand(command, args = []) {
|
|
64
|
+
return [command, ...args].map(shellQuote).join(' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeDockerContainerName(sessionId) {
|
|
68
|
+
const normalizedSession = String(sessionId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.-]/g, '-');
|
|
69
|
+
return `${DOCKER_CONTAINER_PREFIX}-${normalizedSession}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function shouldRunPrivilegedDockerIsolation(image, env = process.env) {
|
|
73
|
+
return String(env.HIVE_MIND_IMAGE_VARIANT || '').toLowerCase() === 'dind' || String(image || '').includes('hive-mind-dind');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function maybeAddMount(mounts, source, target, existsSync) {
|
|
77
|
+
if (!source) return;
|
|
78
|
+
if (!existsSync(source)) return;
|
|
79
|
+
mounts.push({ source, target });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pick the Docker image used for `--isolation docker`.
|
|
84
|
+
*
|
|
85
|
+
* start-command defaults its Docker backend to a base OS image. Hive Mind needs
|
|
86
|
+
* an image with the same CLI/tooling baseline as the parent process instead.
|
|
87
|
+
*/
|
|
88
|
+
export function getDockerIsolationImage({ env = process.env } = {}) {
|
|
89
|
+
if (env.HIVE_MIND_DOCKER_ISOLATION_IMAGE) return env.HIVE_MIND_DOCKER_ISOLATION_IMAGE;
|
|
90
|
+
return String(env.HIVE_MIND_IMAGE_VARIANT || '').toLowerCase() === 'dind' ? DEFAULT_HIVE_MIND_DIND_IMAGE : DEFAULT_HIVE_MIND_IMAGE;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build host auth mounts for a Docker-isolated task.
|
|
95
|
+
*
|
|
96
|
+
* GitHub auth is mounted for every task because solve/hive/task need gh. Tool
|
|
97
|
+
* credentials are deliberately scoped: Codex sessions do not receive Claude
|
|
98
|
+
* files and Claude sessions do not receive Codex files.
|
|
99
|
+
*/
|
|
100
|
+
export function getDockerIsolationAuthMounts({ tool = 'claude', env = process.env, homeDir = os.homedir(), existsSync = fs.existsSync } = {}) {
|
|
101
|
+
const mounts = [];
|
|
102
|
+
const normalizedTool = normalizeTool(tool);
|
|
103
|
+
|
|
104
|
+
maybeAddMount(mounts, env.GH_CONFIG_DIR || path.join(homeDir, '.config', 'gh'), path.join(DOCKER_CONTAINER_HOME, '.config', 'gh'), existsSync);
|
|
105
|
+
|
|
106
|
+
if (normalizedTool === 'codex') {
|
|
107
|
+
maybeAddMount(mounts, path.join(homeDir, '.codex'), path.join(DOCKER_CONTAINER_HOME, '.codex'), existsSync);
|
|
108
|
+
} else if (normalizedTool === 'claude') {
|
|
109
|
+
maybeAddMount(mounts, path.join(homeDir, '.claude'), path.join(DOCKER_CONTAINER_HOME, '.claude'), existsSync);
|
|
110
|
+
maybeAddMount(mounts, path.join(homeDir, '.claude.json'), path.join(DOCKER_CONTAINER_HOME, '.claude.json'), existsSync);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return mounts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the shell command executed inside a start-command wrapper session for
|
|
118
|
+
* Docker isolation. The wrapper remains a start-command session so Telegram can
|
|
119
|
+
* keep using the same status/log lifecycle while Hive Mind controls image and
|
|
120
|
+
* auth mounts directly.
|
|
121
|
+
*/
|
|
122
|
+
export function buildDockerIsolationCommand(command, args = [], options = {}) {
|
|
123
|
+
const { sessionId, tool = 'claude', env = process.env, homeDir = os.homedir(), existsSync = fs.existsSync } = options;
|
|
124
|
+
const image = getDockerIsolationImage({ env });
|
|
125
|
+
const innerCommand = buildShellCommand(command, args);
|
|
126
|
+
const dockerArgs = ['docker', 'run', '--rm', '--name', makeDockerContainerName(sessionId), '--workdir', DOCKER_CONTAINER_HOME, '-e', `HOME=${DOCKER_CONTAINER_HOME}`, '-e', `HIVE_MIND_PARENT_SESSION_ID=${sessionId || ''}`];
|
|
127
|
+
|
|
128
|
+
if (shouldRunPrivilegedDockerIsolation(image, env)) {
|
|
129
|
+
dockerArgs.push('--privileged');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const imageVariant = image.includes('hive-mind-dind') ? 'dind' : env.HIVE_MIND_IMAGE_VARIANT || 'regular';
|
|
133
|
+
dockerArgs.push('-e', `HIVE_MIND_IMAGE_VARIANT=${imageVariant}`);
|
|
134
|
+
|
|
135
|
+
for (const mount of getDockerIsolationAuthMounts({ tool, env, homeDir, existsSync })) {
|
|
136
|
+
dockerArgs.push('--volume', `${mount.source}:${mount.target}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
dockerArgs.push(image, 'bash', '-lc');
|
|
140
|
+
|
|
141
|
+
return [...dockerArgs.map(shellQuote), shellDoubleQuote(innerCommand)].join(' ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildStartCommandArgs(command, args = [], options = {}) {
|
|
145
|
+
const { backend, sessionId } = options;
|
|
146
|
+
if (backend === 'docker') {
|
|
147
|
+
return ['--isolated', DOCKER_ISOLATION_TRACKING_BACKEND, '--detached', '--session', sessionId, '--', buildDockerIsolationCommand(command, args, options)];
|
|
148
|
+
}
|
|
149
|
+
return ['--isolated', backend, '--detached', '--session', sessionId, '--', buildShellCommand(command, args)];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runStartCommand(binPath, startCommandArgs) {
|
|
153
|
+
return await new Promise(resolve => {
|
|
154
|
+
const child = spawn(binPath, startCommandArgs, {
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
env: process.env,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let stdout = '';
|
|
160
|
+
let stderr = '';
|
|
161
|
+
|
|
162
|
+
child.stdout.on('data', data => {
|
|
163
|
+
stdout += data.toString();
|
|
164
|
+
});
|
|
165
|
+
child.stderr.on('data', data => {
|
|
166
|
+
stderr += data.toString();
|
|
167
|
+
});
|
|
168
|
+
child.on('error', error => {
|
|
169
|
+
resolve({
|
|
170
|
+
success: false,
|
|
171
|
+
output: (stdout + stderr).trim(),
|
|
172
|
+
error: error.message,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
child.on('close', code => {
|
|
176
|
+
const output = (stdout + (stderr ? `\n${stderr}` : '')).trim();
|
|
177
|
+
if (code === 0) {
|
|
178
|
+
resolve({ success: true, output, error: null });
|
|
179
|
+
} else {
|
|
180
|
+
resolve({
|
|
181
|
+
success: false,
|
|
182
|
+
output,
|
|
183
|
+
error: stderr.trim() || `start-command exited with code ${code}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
38
190
|
/**
|
|
39
191
|
* Generate a UUID v4 for unique session identification
|
|
40
192
|
* @returns {string} UUID v4 string
|
|
@@ -169,6 +321,7 @@ async function findStartCommandBinary() {
|
|
|
169
321
|
* @param {Object} options - Isolation options
|
|
170
322
|
* @param {string} options.backend - Isolation backend: 'screen', 'tmux', or 'docker'
|
|
171
323
|
* @param {string} [options.sessionId] - UUID for session tracking (auto-generated if not provided)
|
|
324
|
+
* @param {string} [options.tool] - AI tool selected for the task; used to scope Docker auth mounts
|
|
172
325
|
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
173
326
|
* @returns {Promise<{success: boolean, sessionId: string, output: string, error?: string, warning?: string}>}
|
|
174
327
|
*/
|
|
@@ -201,47 +354,40 @@ export async function executeWithIsolation(command, args, options = {}) {
|
|
|
201
354
|
console.log(`[VERBOSE] isolation-runner: Backend: ${backend}, Session ID: ${sessionId}`);
|
|
202
355
|
}
|
|
203
356
|
|
|
204
|
-
|
|
205
|
-
// $ --isolated <backend> --detached --session <sessionId> -- <command> <args...>
|
|
206
|
-
const argsStr = args.join(' ');
|
|
357
|
+
const startCommandArgs = buildStartCommandArgs(command, args, { ...options, sessionId });
|
|
207
358
|
|
|
208
359
|
if (verbose) {
|
|
209
|
-
console.log(`[VERBOSE] isolation-runner: $
|
|
360
|
+
console.log(`[VERBOSE] isolation-runner: ${[binPath, ...startCommandArgs].map(shellQuote).join(' ')}`);
|
|
361
|
+
if (backend === 'docker') {
|
|
362
|
+
const image = getDockerIsolationImage({ env: options.env || process.env });
|
|
363
|
+
const mounts = getDockerIsolationAuthMounts({ tool: options.tool, env: options.env || process.env, homeDir: options.homeDir || os.homedir(), existsSync: options.existsSync || fs.existsSync });
|
|
364
|
+
console.log(`[VERBOSE] isolation-runner: Docker isolation image: ${image}`);
|
|
365
|
+
console.log(`[VERBOSE] isolation-runner: Docker isolation mounts: ${mounts.map(m => m.target).join(', ') || '(none)'}`);
|
|
366
|
+
}
|
|
210
367
|
}
|
|
211
368
|
|
|
212
|
-
|
|
213
|
-
const result = await $({ mirror: false })`${binPath} --isolated ${backend} --detached --session ${sessionId} -- ${command} ${argsStr}`;
|
|
214
|
-
|
|
215
|
-
const stdout = result.stdout?.toString() || '';
|
|
216
|
-
const stderr = result.stderr?.toString() || '';
|
|
217
|
-
const output = stdout + (stderr ? '\n' + stderr : '');
|
|
369
|
+
const result = await runStartCommand(binPath, startCommandArgs);
|
|
218
370
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
371
|
+
if (verbose) {
|
|
372
|
+
const stream = result.success ? console.log : console.error;
|
|
373
|
+
stream(`[VERBOSE] isolation-runner: Output: ${result.output.substring(0, 500)}`);
|
|
374
|
+
if (result.error) stream(`[VERBOSE] isolation-runner: Error: ${result.error}`);
|
|
375
|
+
}
|
|
222
376
|
|
|
377
|
+
if (result.success) {
|
|
223
378
|
return {
|
|
224
379
|
success: true,
|
|
225
380
|
sessionId,
|
|
226
|
-
output: output
|
|
227
|
-
};
|
|
228
|
-
} catch (error) {
|
|
229
|
-
const stdout = error.stdout?.toString() || '';
|
|
230
|
-
const stderr = error.stderr?.toString() || '';
|
|
231
|
-
const output = stdout + stderr;
|
|
232
|
-
|
|
233
|
-
if (verbose) {
|
|
234
|
-
console.error(`[VERBOSE] isolation-runner: Error: ${error.message}`);
|
|
235
|
-
console.error(`[VERBOSE] isolation-runner: Output: ${output.substring(0, 500)}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
success: false,
|
|
240
|
-
sessionId,
|
|
241
|
-
output: output.trim(),
|
|
242
|
-
error: error.message,
|
|
381
|
+
output: result.output,
|
|
243
382
|
};
|
|
244
383
|
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
sessionId,
|
|
388
|
+
output: result.output,
|
|
389
|
+
error: result.error,
|
|
390
|
+
};
|
|
245
391
|
}
|
|
246
392
|
|
|
247
393
|
/**
|
|
@@ -378,12 +524,14 @@ export async function isSessionRunning(sessionId, options = {}) {
|
|
|
378
524
|
}
|
|
379
525
|
}
|
|
380
526
|
|
|
381
|
-
// Fallback: for screen
|
|
527
|
+
// Fallback: for screen-backed sessions, check screen -ls directly.
|
|
528
|
+
// Docker isolation is also tracked through a screen wrapper so Hive Mind can
|
|
529
|
+
// control image selection and credential mounts while preserving logs/status.
|
|
382
530
|
// Only use this when $ --status has no usable record. This works around
|
|
383
531
|
// older start-command bugs where:
|
|
384
532
|
// 1. $ --status can't find session by --session name (only by internal UUID)
|
|
385
533
|
// See: https://github.com/link-assistant/hive-mind/issues/1545
|
|
386
|
-
if (backend === 'screen' && shouldFallbackToScreenStatus(result)) {
|
|
534
|
+
if ((backend === 'screen' || backend === 'docker') && shouldFallbackToScreenStatus(result)) {
|
|
387
535
|
const screenRunning = await checkScreenSessionRunning(sessionId, verbose);
|
|
388
536
|
if (screenRunning && verbose) {
|
|
389
537
|
console.log(`[VERBOSE] isolation-runner: $ --status says not running, but screen -ls confirms session '${sessionId}' is still active`);
|
|
@@ -115,7 +115,7 @@ export function buildExecuteAndUpdateMessage(deps) {
|
|
|
115
115
|
if (iso) {
|
|
116
116
|
session = iso.runner.generateSessionId();
|
|
117
117
|
VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
|
|
118
|
-
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
|
|
118
|
+
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, tool, verbose: VERBOSE });
|
|
119
119
|
if (result.success) {
|
|
120
120
|
sessionInfo = { ...baseSessionInfo, isolationBackend: iso.backend, sessionId: session };
|
|
121
121
|
trackSession(session, sessionInfo, VERBOSE);
|
|
@@ -79,7 +79,8 @@ export function createIsolationAwareQueueCallback(botIsolationBackend, botIsolat
|
|
|
79
79
|
const iso = await resolveIsolation(item.perCommandIsolation, botIsolationBackend, botIsolationRunner, verbose);
|
|
80
80
|
if (iso) {
|
|
81
81
|
const sid = iso.runner.generateSessionId();
|
|
82
|
-
const
|
|
82
|
+
const tool = item.tool || 'claude';
|
|
83
|
+
const r = await iso.runner.executeWithIsolation(item.command || 'solve', item.args, { backend: iso.backend, sessionId: sid, tool, verbose });
|
|
83
84
|
if (r.success)
|
|
84
85
|
trackSession(
|
|
85
86
|
sid,
|
|
@@ -91,7 +92,7 @@ export function createIsolationAwareQueueCallback(botIsolationBackend, botIsolat
|
|
|
91
92
|
command: item.command || 'solve',
|
|
92
93
|
isolationBackend: iso.backend,
|
|
93
94
|
sessionId: sid,
|
|
94
|
-
tool
|
|
95
|
+
tool,
|
|
95
96
|
infoBlock: item.infoBlock,
|
|
96
97
|
// Issue #1688: propagate URL context + requester through the queue so the
|
|
97
98
|
// completion notification can append a 'Pull request:' line and skip
|
|
@@ -109,8 +109,9 @@ export function resolveLogPath({ statusResult, isolationBackend }) {
|
|
|
109
109
|
if (statusResult?.logPath) return statusResult.logPath;
|
|
110
110
|
const uuid = statusResult?.uuid;
|
|
111
111
|
if (!uuid) return null;
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const logBackend = statusResult?.isolation || isolationBackend;
|
|
113
|
+
if (logBackend && ISOLATION_BACKENDS.has(logBackend)) {
|
|
114
|
+
return path.join('/tmp/start-command/logs/isolation', logBackend, `${uuid}.log`);
|
|
114
115
|
}
|
|
115
116
|
return path.join('/tmp/start-command/logs/direct', `${uuid}.log`);
|
|
116
117
|
}
|