agenshield 0.6.2 → 0.7.1
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/package.json +9 -8
- package/src/commands/setup.d.ts.map +1 -1
- package/src/commands/setup.js +23 -5
- package/src/commands/setup.js.map +1 -1
- package/src/setup-server/routes.d.ts.map +1 -1
- package/src/setup-server/routes.js +67 -0
- package/src/setup-server/routes.js.map +1 -1
- package/src/setup-server/server.d.ts +1 -1
- package/src/setup-server/server.d.ts.map +1 -1
- package/src/setup-server/server.js +6 -0
- package/src/setup-server/server.js.map +1 -1
- package/src/setup-server/sse.d.ts +1 -1
- package/src/setup-server/sse.d.ts.map +1 -1
- package/src/setup-server/sse.js.map +1 -1
- package/src/utils/daemon.d.ts.map +1 -1
- package/src/utils/daemon.js +10 -0
- package/src/utils/daemon.js.map +1 -1
- package/src/wizard/engine.d.ts +5 -2
- package/src/wizard/engine.d.ts.map +1 -1
- package/src/wizard/engine.js +580 -120
- package/src/wizard/engine.js.map +1 -1
- package/src/wizard/types.d.ts +55 -3
- package/src/wizard/types.d.ts.map +1 -1
- package/src/wizard/types.js +87 -16
- package/src/wizard/types.js.map +1 -1
package/src/wizard/engine.js
CHANGED
|
@@ -2,18 +2,39 @@
|
|
|
2
2
|
* Wizard engine - orchestrates the setup steps
|
|
3
3
|
*/
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
|
-
import { checkPrerequisites,
|
|
5
|
+
import { checkPrerequisites, createUserConfig, createGroups, createAgentUser, createBrokerUser, createAllDirectories, setupSocketDirectory, verifyUsersAndGroups, verifyDirectories, generateAgentProfile, installSeatbeltProfiles, installAllWrappers, installPresetBinaries, generateBrokerPlist, installLaunchDaemon, fixSocketPermissions, createPathsConfig, deployInterceptor, copyNodeBinary, copyBrokerBinary, copyShieldClient, installGuardedShell,
|
|
6
|
+
// NVM (existing)
|
|
7
|
+
installAgentNvm, patchNvmNode,
|
|
6
8
|
// Preset system
|
|
7
9
|
getPreset, autoDetectPreset, } from '@agenshield/sandbox';
|
|
10
|
+
import {
|
|
11
|
+
// Homebrew
|
|
12
|
+
installAgentHomebrew, isAgentHomebrewInstalled,
|
|
13
|
+
// OpenClaw install + config + lifecycle
|
|
14
|
+
detectHostOpenClawVersion, installAgentOpenClaw, copyOpenClawConfig, stopHostOpenClaw, getOriginalUser, getHostOpenClawConfigPath, onboardAgentOpenClaw,
|
|
15
|
+
// OpenClaw LaunchDaemons
|
|
16
|
+
installOpenClawLaunchDaemons, startOpenClawServices, OPENCLAW_DAEMON_PLIST, OPENCLAW_GATEWAY_PLIST, } from '@agenshield/integrations';
|
|
8
17
|
import { createWizardSteps, getStepsByPhase, getAllStepIds } from './types.js';
|
|
9
18
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
19
|
+
* Module-level log callback — allows the setup server to receive verbose
|
|
20
|
+
* messages and broadcast them (e.g. via SSE) to the browser UI.
|
|
21
|
+
*/
|
|
22
|
+
let _logCallback;
|
|
23
|
+
let _currentStepId;
|
|
24
|
+
export function setEngineLogCallback(cb) {
|
|
25
|
+
_logCallback = cb;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verbose logging helper - logs messages when verbose mode is enabled.
|
|
29
|
+
* Writes to stderr (terminal) only in verbose mode, but ALWAYS calls
|
|
30
|
+
* the log callback so the setup server can forward messages to the browser.
|
|
12
31
|
*/
|
|
13
32
|
function logVerbose(message, context) {
|
|
14
33
|
if (context?.options?.verbose || process.env['AGENSHIELD_VERBOSE'] === 'true') {
|
|
15
34
|
process.stderr.write(`[SETUP] ${message}\n`);
|
|
16
35
|
}
|
|
36
|
+
// Always broadcast to the UI regardless of verbose flag
|
|
37
|
+
_logCallback?.(message, _currentStepId);
|
|
17
38
|
}
|
|
18
39
|
/**
|
|
19
40
|
* Execute a step and update state
|
|
@@ -74,8 +95,9 @@ const stepExecutors = {
|
|
|
74
95
|
const detection = await preset.detect();
|
|
75
96
|
if (!detection?.found) {
|
|
76
97
|
// Don't fail — let install-target step handle it
|
|
98
|
+
// Preserve partial detection (e.g. configPath) for scan-source
|
|
77
99
|
context.preset = preset;
|
|
78
|
-
context.presetDetection = { found: false };
|
|
100
|
+
context.presetDetection = detection ?? { found: false };
|
|
79
101
|
context.targetInstallable = true;
|
|
80
102
|
return { success: true };
|
|
81
103
|
}
|
|
@@ -87,10 +109,12 @@ const stepExecutors = {
|
|
|
87
109
|
const result = await autoDetectPreset();
|
|
88
110
|
if (!result) {
|
|
89
111
|
// No target found — mark as installable so install-target step can offer installation
|
|
112
|
+
// Try to capture partial detection (e.g. configPath with skills on disk)
|
|
90
113
|
const openclawPreset = getPreset('openclaw');
|
|
91
114
|
if (openclawPreset) {
|
|
92
115
|
context.preset = openclawPreset;
|
|
93
|
-
|
|
116
|
+
const partialDetection = await openclawPreset.detect();
|
|
117
|
+
context.presetDetection = partialDetection ?? { found: false };
|
|
94
118
|
context.targetInstallable = true;
|
|
95
119
|
return { success: true };
|
|
96
120
|
}
|
|
@@ -123,11 +147,15 @@ const stepExecutors = {
|
|
|
123
147
|
// Run npm install -g openclaw (no sudo — user handles npm config)
|
|
124
148
|
try {
|
|
125
149
|
const { execSync } = await import('node:child_process');
|
|
126
|
-
|
|
150
|
+
logVerbose('Running: npm install -g openclaw', context);
|
|
151
|
+
const output = execSync('npm install -g openclaw', {
|
|
127
152
|
encoding: 'utf-8',
|
|
128
153
|
timeout: 120_000,
|
|
129
154
|
stdio: 'pipe',
|
|
130
155
|
});
|
|
156
|
+
if (output?.trim()) {
|
|
157
|
+
logVerbose(output.trim(), context);
|
|
158
|
+
}
|
|
131
159
|
}
|
|
132
160
|
catch (err) {
|
|
133
161
|
return {
|
|
@@ -164,39 +192,435 @@ const stepExecutors = {
|
|
|
164
192
|
confirm: async (_context) => {
|
|
165
193
|
return { success: true };
|
|
166
194
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
// ── New setup steps ───────────────────────────────────────────────────
|
|
196
|
+
'cleanup-previous': async (context) => {
|
|
197
|
+
logVerbose('Checking for previous installations', context);
|
|
198
|
+
const { execSync } = await import('node:child_process');
|
|
199
|
+
const fs = await import('node:fs');
|
|
200
|
+
// Quick check: does the default agent user exist?
|
|
201
|
+
let agentUserExists = false;
|
|
202
|
+
try {
|
|
203
|
+
execSync('dscl . -read /Users/ash_default_agent', { encoding: 'utf-8', stdio: 'pipe' });
|
|
204
|
+
agentUserExists = true;
|
|
205
|
+
}
|
|
206
|
+
catch { /* user doesn't exist */ }
|
|
207
|
+
if (!agentUserExists) {
|
|
208
|
+
logVerbose('No previous installation detected', context);
|
|
209
|
+
return { success: true };
|
|
170
210
|
}
|
|
171
|
-
|
|
211
|
+
logVerbose('Found previous installation (ash_default_agent exists), cleaning up', context);
|
|
172
212
|
if (context.options?.dryRun) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return { success: false, error: backupResult.error };
|
|
188
|
-
}
|
|
189
|
-
configBackupPath = backupResult.backupPath;
|
|
190
|
-
}
|
|
191
|
-
// Store backup info for later (will be saved after user creation)
|
|
192
|
-
context.originalInstallation = {
|
|
193
|
-
method: context.presetDetection.method || 'npm',
|
|
194
|
-
packagePath: context.presetDetection.packagePath || '',
|
|
195
|
-
binaryPath: context.presetDetection.binaryPath,
|
|
196
|
-
configPath: context.presetDetection.configPath,
|
|
197
|
-
configBackupPath,
|
|
198
|
-
version: context.presetDetection.version,
|
|
213
|
+
logVerbose('[dry-run] Would remove previous installation', context);
|
|
214
|
+
return { success: true };
|
|
215
|
+
}
|
|
216
|
+
// NOTE: We cannot use forceUninstall() here because it calls stopDaemon()
|
|
217
|
+
// which kills the process on port 5200 — that's our own setup wizard.
|
|
218
|
+
// Instead we do the cleanup steps inline, skipping the daemon kill.
|
|
219
|
+
const sudo = (cmd) => {
|
|
220
|
+
try {
|
|
221
|
+
execSync(`sudo ${cmd}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
199
227
|
};
|
|
228
|
+
// 1. Stop broker launchd service (but NOT the daemon on port 5200 — that's us)
|
|
229
|
+
const brokerPlist = '/Library/LaunchDaemons/com.agenshield.broker.plist';
|
|
230
|
+
if (fs.existsSync(brokerPlist)) {
|
|
231
|
+
logVerbose('[cleanup] Stopping broker LaunchDaemon', context);
|
|
232
|
+
sudo(`launchctl bootout system/com.agenshield.broker 2>/dev/null || true`);
|
|
233
|
+
sudo(`rm -f "${brokerPlist}"`);
|
|
234
|
+
}
|
|
235
|
+
// 2. Remove OpenClaw LaunchDaemon plists if present
|
|
236
|
+
for (const plist of [
|
|
237
|
+
'/Library/LaunchDaemons/com.agenshield.openclaw.daemon.plist',
|
|
238
|
+
'/Library/LaunchDaemons/com.agenshield.openclaw.gateway.plist',
|
|
239
|
+
]) {
|
|
240
|
+
if (fs.existsSync(plist)) {
|
|
241
|
+
const label = plist.replace('/Library/LaunchDaemons/', '').replace('.plist', '');
|
|
242
|
+
logVerbose(`[cleanup] Removing ${label}`, context);
|
|
243
|
+
sudo(`launchctl bootout system/${label} 2>/dev/null || true`);
|
|
244
|
+
sudo(`rm -f "${plist}"`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 3. Discover and kill processes for all ash_* users
|
|
248
|
+
let sandboxUsers = [];
|
|
249
|
+
try {
|
|
250
|
+
const output = execSync('dscl . -list /Users', { encoding: 'utf-8' });
|
|
251
|
+
sandboxUsers = output.split('\n').filter(u => u.startsWith('ash_'));
|
|
252
|
+
}
|
|
253
|
+
catch { /* ignore */ }
|
|
254
|
+
for (const username of sandboxUsers) {
|
|
255
|
+
logVerbose(`[cleanup] Killing processes for ${username}`, context);
|
|
256
|
+
sudo(`pkill -u ${username} 2>/dev/null || true`);
|
|
257
|
+
}
|
|
258
|
+
if (sandboxUsers.length > 0) {
|
|
259
|
+
try {
|
|
260
|
+
execSync('sleep 1', { encoding: 'utf-8' });
|
|
261
|
+
}
|
|
262
|
+
catch { /* ignore */ }
|
|
263
|
+
for (const username of sandboxUsers) {
|
|
264
|
+
sudo(`pkill -9 -u ${username} 2>/dev/null || true`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// 4. Delete sandbox users (with home dirs)
|
|
268
|
+
const { deleteSandboxUser } = await import('@agenshield/sandbox');
|
|
269
|
+
for (const username of sandboxUsers) {
|
|
270
|
+
logVerbose(`[cleanup] Deleting user ${username}`, context);
|
|
271
|
+
deleteSandboxUser(username, { removeHomeDir: true });
|
|
272
|
+
}
|
|
273
|
+
// 5. Delete ash_* groups
|
|
274
|
+
let ashGroups = [];
|
|
275
|
+
try {
|
|
276
|
+
const output = execSync('dscl . -list /Groups', { encoding: 'utf-8' });
|
|
277
|
+
ashGroups = output.split('\n').filter(g => g.startsWith('ash_'));
|
|
278
|
+
}
|
|
279
|
+
catch { /* ignore */ }
|
|
280
|
+
for (const groupName of ashGroups) {
|
|
281
|
+
logVerbose(`[cleanup] Deleting group ${groupName}`, context);
|
|
282
|
+
sudo(`dscl . -delete /Groups/${groupName}`);
|
|
283
|
+
}
|
|
284
|
+
// 6. Remove guarded shell from /etc/shells and disk
|
|
285
|
+
const guardedShellPath = '/usr/local/bin/guarded-shell';
|
|
286
|
+
if (fs.existsSync(guardedShellPath)) {
|
|
287
|
+
logVerbose('[cleanup] Removing guarded shell', context);
|
|
288
|
+
sudo(`sed -i '' '\\|${guardedShellPath}|d' /etc/shells`);
|
|
289
|
+
sudo(`rm -f "${guardedShellPath}"`);
|
|
290
|
+
}
|
|
291
|
+
// 7. Remove sudoers drop-in
|
|
292
|
+
if (fs.existsSync('/etc/sudoers.d/agenshield')) {
|
|
293
|
+
logVerbose('[cleanup] Removing /etc/sudoers.d/agenshield', context);
|
|
294
|
+
sudo('rm -f /etc/sudoers.d/agenshield');
|
|
295
|
+
}
|
|
296
|
+
// 8. Clean up directories
|
|
297
|
+
for (const dir of ['/etc/agenshield', '/var/log/agenshield', '/var/run/agenshield', '/opt/agenshield']) {
|
|
298
|
+
if (fs.existsSync(dir)) {
|
|
299
|
+
logVerbose(`[cleanup] Removing ${dir}`, context);
|
|
300
|
+
sudo(`rm -rf "${dir}"`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
logVerbose('Previous installation cleaned up', context);
|
|
304
|
+
return { success: true };
|
|
305
|
+
},
|
|
306
|
+
'install-homebrew': async (context) => {
|
|
307
|
+
if (!context.userConfig) {
|
|
308
|
+
return { success: false, error: 'User configuration not set' };
|
|
309
|
+
}
|
|
310
|
+
const { agentUser } = context.userConfig;
|
|
311
|
+
const socketGroupName = context.userConfig.groups.socket.name;
|
|
312
|
+
if (context.options?.dryRun) {
|
|
313
|
+
logVerbose(`[dry-run] Would install Homebrew to ${agentUser.home}/homebrew`, context);
|
|
314
|
+
context.homebrewInstalled = { brewPath: `${agentUser.home}/homebrew/bin/brew`, success: true };
|
|
315
|
+
return { success: true };
|
|
316
|
+
}
|
|
317
|
+
// Skip if already installed (e.g., retry after partial failure)
|
|
318
|
+
if (await isAgentHomebrewInstalled(agentUser.home)) {
|
|
319
|
+
logVerbose('Homebrew already installed, skipping', context);
|
|
320
|
+
context.homebrewInstalled = { brewPath: `${agentUser.home}/homebrew/bin/brew`, success: true };
|
|
321
|
+
return { success: true };
|
|
322
|
+
}
|
|
323
|
+
logVerbose(`Installing user-specific Homebrew for ${agentUser.username}`, context);
|
|
324
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
325
|
+
const result = await installAgentHomebrew({
|
|
326
|
+
agentHome: agentUser.home,
|
|
327
|
+
agentUsername: agentUser.username,
|
|
328
|
+
socketGroupName,
|
|
329
|
+
verbose: context.options?.verbose,
|
|
330
|
+
onLog,
|
|
331
|
+
});
|
|
332
|
+
if (!result.success) {
|
|
333
|
+
return { success: false, error: result.message };
|
|
334
|
+
}
|
|
335
|
+
context.homebrewInstalled = { brewPath: result.brewPath, success: true };
|
|
336
|
+
return { success: true };
|
|
337
|
+
},
|
|
338
|
+
'install-nvm': async (context) => {
|
|
339
|
+
if (!context.userConfig) {
|
|
340
|
+
return { success: false, error: 'User configuration not set' };
|
|
341
|
+
}
|
|
342
|
+
const { agentUser } = context.userConfig;
|
|
343
|
+
const socketGroupName = context.userConfig.groups.socket.name;
|
|
344
|
+
const nodeVersion = context.options?.nodeVersion || '24';
|
|
345
|
+
if (context.options?.dryRun) {
|
|
346
|
+
logVerbose(`[dry-run] Would install NVM + Node.js v${nodeVersion} for ${agentUser.username}`, context);
|
|
347
|
+
context.nvmInstalled = {
|
|
348
|
+
nvmDir: `${agentUser.home}/.nvm`,
|
|
349
|
+
nodeVersion: `v${nodeVersion}`,
|
|
350
|
+
nodeBinaryPath: `${agentUser.home}/.nvm/versions/node/v${nodeVersion}.0.0/bin/node`,
|
|
351
|
+
success: true,
|
|
352
|
+
};
|
|
353
|
+
return { success: true };
|
|
354
|
+
}
|
|
355
|
+
// Skip if NVM + requested Node version already installed
|
|
356
|
+
const nvmDir = `${agentUser.home}/.nvm`;
|
|
357
|
+
const nvmSh = `${nvmDir}/nvm.sh`;
|
|
358
|
+
const fs = await import('node:fs');
|
|
359
|
+
if (fs.existsSync(nvmSh)) {
|
|
360
|
+
try {
|
|
361
|
+
const { exec: execCb } = await import('node:child_process');
|
|
362
|
+
const { promisify } = await import('node:util');
|
|
363
|
+
const execAsync = promisify(execCb);
|
|
364
|
+
const { stdout } = await execAsync(`sudo -H -u ${agentUser.username} /bin/bash --norc --noprofile -c 'source "${nvmSh}" && nvm which ${nodeVersion}'`, { cwd: '/' });
|
|
365
|
+
if (stdout.trim()) {
|
|
366
|
+
logVerbose(`NVM + Node.js v${nodeVersion} already installed, skipping`, context);
|
|
367
|
+
// Still copy node binary to ensure it's up to date
|
|
368
|
+
const nodeResult = await copyNodeBinary(context.userConfig, stdout.trim());
|
|
369
|
+
if (!nodeResult.success) {
|
|
370
|
+
return { success: false, error: `Node binary copy failed: ${nodeResult.message}` };
|
|
371
|
+
}
|
|
372
|
+
// NOTE: Do NOT patch NVM node here — patching must happen after all
|
|
373
|
+
// npm installs (openclaw etc.) complete. See start-openclaw step.
|
|
374
|
+
context.nvmInstalled = {
|
|
375
|
+
nvmDir,
|
|
376
|
+
nodeVersion: `v${nodeVersion}`,
|
|
377
|
+
nodeBinaryPath: stdout.trim(),
|
|
378
|
+
success: true,
|
|
379
|
+
};
|
|
380
|
+
return { success: true };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Node version not installed, proceed with full install
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
logVerbose(`Installing NVM + Node.js v${nodeVersion} for ${agentUser.username}`, context);
|
|
388
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
389
|
+
const result = await installAgentNvm({
|
|
390
|
+
agentHome: agentUser.home,
|
|
391
|
+
agentUsername: agentUser.username,
|
|
392
|
+
socketGroupName,
|
|
393
|
+
nodeVersion,
|
|
394
|
+
verbose: context.options?.verbose,
|
|
395
|
+
onLog,
|
|
396
|
+
});
|
|
397
|
+
if (!result.success) {
|
|
398
|
+
return { success: false, error: result.message };
|
|
399
|
+
}
|
|
400
|
+
// Copy NVM node binary to /opt/agenshield/bin/node-bin
|
|
401
|
+
logVerbose(`Copying NVM node binary to /opt/agenshield/bin/node-bin`, context);
|
|
402
|
+
const nodeResult = await copyNodeBinary(context.userConfig, result.nodeBinaryPath);
|
|
403
|
+
if (!nodeResult.success) {
|
|
404
|
+
return { success: false, error: `Node binary copy failed: ${nodeResult.message}` };
|
|
405
|
+
}
|
|
406
|
+
// NOTE: Do NOT patch NVM node here — patching must happen after all
|
|
407
|
+
// npm installs (openclaw etc.) complete. See start-openclaw step.
|
|
408
|
+
context.nvmInstalled = {
|
|
409
|
+
nvmDir: result.nvmDir,
|
|
410
|
+
nodeVersion: result.nodeVersion,
|
|
411
|
+
nodeBinaryPath: result.nodeBinaryPath,
|
|
412
|
+
success: true,
|
|
413
|
+
};
|
|
414
|
+
return { success: true };
|
|
415
|
+
},
|
|
416
|
+
'configure-shell': async (context) => {
|
|
417
|
+
if (!context.userConfig) {
|
|
418
|
+
return { success: false, error: 'User configuration not set' };
|
|
419
|
+
}
|
|
420
|
+
if (context.options?.dryRun) {
|
|
421
|
+
logVerbose(`[dry-run] Would configure guarded shell with Homebrew + NVM paths`, context);
|
|
422
|
+
context.shellConfigured = { success: true };
|
|
423
|
+
return { success: true };
|
|
424
|
+
}
|
|
425
|
+
logVerbose('Installing guarded shell with Homebrew and NVM paths', context);
|
|
426
|
+
const result = await installGuardedShell(context.userConfig, { verbose: context.options?.verbose });
|
|
427
|
+
if (!result.success) {
|
|
428
|
+
return { success: false, error: result.message };
|
|
429
|
+
}
|
|
430
|
+
context.shellConfigured = { success: true };
|
|
431
|
+
return { success: true };
|
|
432
|
+
},
|
|
433
|
+
'install-openclaw': async (context) => {
|
|
434
|
+
if (!context.userConfig) {
|
|
435
|
+
return { success: false, error: 'User configuration not set' };
|
|
436
|
+
}
|
|
437
|
+
const { agentUser } = context.userConfig;
|
|
438
|
+
const socketGroupName = context.userConfig.groups.socket.name;
|
|
439
|
+
// Detect host version
|
|
440
|
+
const hostVersion = context.presetDetection?.version || detectHostOpenClawVersion() || 'latest';
|
|
441
|
+
logVerbose(`Host OpenClaw version: ${hostVersion}`, context);
|
|
442
|
+
if (context.options?.dryRun) {
|
|
443
|
+
logVerbose(`[dry-run] Would install openclaw@${hostVersion} for ${agentUser.username}`, context);
|
|
444
|
+
context.openclawInstalled = {
|
|
445
|
+
version: hostVersion,
|
|
446
|
+
binaryPath: `${agentUser.home}/.nvm/versions/node/v24.0.0/bin/openclaw`,
|
|
447
|
+
success: true,
|
|
448
|
+
};
|
|
449
|
+
return { success: true };
|
|
450
|
+
}
|
|
451
|
+
logVerbose(`Installing openclaw@${hostVersion} for agent user`, context);
|
|
452
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
453
|
+
const result = await installAgentOpenClaw({
|
|
454
|
+
agentHome: agentUser.home,
|
|
455
|
+
agentUsername: agentUser.username,
|
|
456
|
+
socketGroupName,
|
|
457
|
+
targetVersion: hostVersion,
|
|
458
|
+
verbose: context.options?.verbose,
|
|
459
|
+
onLog,
|
|
460
|
+
});
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
return { success: false, error: result.message };
|
|
463
|
+
}
|
|
464
|
+
context.openclawInstalled = {
|
|
465
|
+
version: result.version,
|
|
466
|
+
binaryPath: result.binaryPath,
|
|
467
|
+
success: true,
|
|
468
|
+
};
|
|
469
|
+
return { success: true };
|
|
470
|
+
},
|
|
471
|
+
'copy-openclaw-config': async (context) => {
|
|
472
|
+
if (!context.userConfig) {
|
|
473
|
+
return { success: false, error: 'User configuration not set' };
|
|
474
|
+
}
|
|
475
|
+
const { agentUser } = context.userConfig;
|
|
476
|
+
const socketGroupName = context.userConfig.groups.socket.name;
|
|
477
|
+
// Find host config path
|
|
478
|
+
const sourceConfigPath = context.presetDetection?.configPath
|
|
479
|
+
|| getHostOpenClawConfigPath();
|
|
480
|
+
if (context.options?.dryRun) {
|
|
481
|
+
logVerbose(`[dry-run] Would copy .openclaw from ${sourceConfigPath || '(not found)'} to ${agentUser.home}/.openclaw`, context);
|
|
482
|
+
context.openclawConfigCopied = {
|
|
483
|
+
configDir: `${agentUser.home}/.openclaw`,
|
|
484
|
+
sanitized: false,
|
|
485
|
+
success: true,
|
|
486
|
+
};
|
|
487
|
+
return { success: true };
|
|
488
|
+
}
|
|
489
|
+
if (!sourceConfigPath) {
|
|
490
|
+
logVerbose('No host .openclaw config found, creating empty config', context);
|
|
491
|
+
}
|
|
492
|
+
logVerbose(`Copying OpenClaw config to agent user`, context);
|
|
493
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
494
|
+
const result = copyOpenClawConfig({
|
|
495
|
+
sourceConfigPath: sourceConfigPath || '/nonexistent',
|
|
496
|
+
agentHome: agentUser.home,
|
|
497
|
+
agentUsername: agentUser.username,
|
|
498
|
+
socketGroup: socketGroupName,
|
|
499
|
+
verbose: context.options?.verbose,
|
|
500
|
+
onLog,
|
|
501
|
+
});
|
|
502
|
+
if (!result.success) {
|
|
503
|
+
return { success: false, error: result.message };
|
|
504
|
+
}
|
|
505
|
+
context.openclawConfigCopied = {
|
|
506
|
+
configDir: result.configDir,
|
|
507
|
+
sanitized: result.sanitized,
|
|
508
|
+
success: true,
|
|
509
|
+
};
|
|
510
|
+
return { success: true };
|
|
511
|
+
},
|
|
512
|
+
'stop-host-openclaw': async (context) => {
|
|
513
|
+
const originalUser = getOriginalUser();
|
|
514
|
+
if (context.options?.dryRun) {
|
|
515
|
+
logVerbose(`[dry-run] Would stop OpenClaw daemon + gateway for user: ${originalUser}`, context);
|
|
516
|
+
context.hostOpenclawStopped = { daemonStopped: true, gatewayStopped: true };
|
|
517
|
+
return { success: true };
|
|
518
|
+
}
|
|
519
|
+
logVerbose(`Stopping host OpenClaw processes for user: ${originalUser}`, context);
|
|
520
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
521
|
+
const result = await stopHostOpenClaw({
|
|
522
|
+
originalUser,
|
|
523
|
+
verbose: context.options?.verbose,
|
|
524
|
+
onLog,
|
|
525
|
+
});
|
|
526
|
+
context.hostOpenclawStopped = {
|
|
527
|
+
daemonStopped: result.daemonStopped,
|
|
528
|
+
gatewayStopped: result.gatewayStopped,
|
|
529
|
+
};
|
|
530
|
+
// Non-fatal if stop fails (processes might not be running)
|
|
531
|
+
return { success: true };
|
|
532
|
+
},
|
|
533
|
+
'onboard-openclaw': async (context) => {
|
|
534
|
+
if (!context.userConfig) {
|
|
535
|
+
return { success: false, error: 'User configuration not set' };
|
|
536
|
+
}
|
|
537
|
+
const { agentUser } = context.userConfig;
|
|
538
|
+
if (context.options?.dryRun) {
|
|
539
|
+
logVerbose('[dry-run] Would run openclaw onboard --non-interactive', context);
|
|
540
|
+
context.openclawOnboarded = { success: true };
|
|
541
|
+
return { success: true };
|
|
542
|
+
}
|
|
543
|
+
logVerbose('Running openclaw onboard for agent user', context);
|
|
544
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
545
|
+
const result = await onboardAgentOpenClaw({
|
|
546
|
+
agentHome: agentUser.home,
|
|
547
|
+
agentUsername: agentUser.username,
|
|
548
|
+
verbose: context.options?.verbose,
|
|
549
|
+
onLog,
|
|
550
|
+
});
|
|
551
|
+
context.openclawOnboarded = { success: result.success };
|
|
552
|
+
// Non-fatal — onboard may fail if openclaw doesn't support the flags
|
|
553
|
+
if (!result.success) {
|
|
554
|
+
logVerbose(`Onboard returned non-success (non-fatal): ${result.message}`, context);
|
|
555
|
+
}
|
|
556
|
+
return { success: true };
|
|
557
|
+
},
|
|
558
|
+
'start-openclaw': async (context) => {
|
|
559
|
+
if (!context.userConfig) {
|
|
560
|
+
return { success: false, error: 'User configuration not set' };
|
|
561
|
+
}
|
|
562
|
+
const { agentUser } = context.userConfig;
|
|
563
|
+
const socketGroupName = context.userConfig.groups.socket.name;
|
|
564
|
+
if (context.options?.dryRun) {
|
|
565
|
+
logVerbose(`[dry-run] Would install OpenClaw LaunchDaemons (managed by broker)`, context);
|
|
566
|
+
context.openclawLaunchDaemons = {
|
|
567
|
+
daemonPlistPath: OPENCLAW_DAEMON_PLIST,
|
|
568
|
+
gatewayPlistPath: OPENCLAW_GATEWAY_PLIST,
|
|
569
|
+
loaded: false,
|
|
570
|
+
};
|
|
571
|
+
return { success: true };
|
|
572
|
+
}
|
|
573
|
+
// 1. Patch NVM node in-place BEFORE starting OpenClaw services.
|
|
574
|
+
// All npm installs (openclaw, onboard, etc.) are done by now, so it's safe
|
|
575
|
+
// to replace NVM's node with the interceptor wrapper. From this point on,
|
|
576
|
+
// every `node` invocation via NVM goes through the interceptor.
|
|
577
|
+
const onLog = (msg) => logVerbose(msg, context);
|
|
578
|
+
if (context.nvmInstalled?.success && context.nvmInstalled.nodeBinaryPath) {
|
|
579
|
+
logVerbose('Patching NVM node binary in-place with interceptor wrapper', context);
|
|
580
|
+
const patchResult = await patchNvmNode({
|
|
581
|
+
nodeBinaryPath: context.nvmInstalled.nodeBinaryPath,
|
|
582
|
+
agentUsername: agentUser.username,
|
|
583
|
+
socketGroupName,
|
|
584
|
+
interceptorPath: '/opt/agenshield/lib/interceptor/register.cjs',
|
|
585
|
+
socketPath: '/var/run/agenshield/agenshield.sock',
|
|
586
|
+
httpPort: 5201,
|
|
587
|
+
verbose: context.options?.verbose,
|
|
588
|
+
onLog,
|
|
589
|
+
});
|
|
590
|
+
if (!patchResult.success) {
|
|
591
|
+
return { success: false, error: `Failed to patch NVM node: ${patchResult.message}` };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// 2. Install OpenClaw LaunchDaemons (gateway + daemon) — managed by launchd/broker
|
|
595
|
+
logVerbose('Installing OpenClaw LaunchDaemons (broker-managed)', context);
|
|
596
|
+
const installResult = await installOpenClawLaunchDaemons({
|
|
597
|
+
agentUsername: agentUser.username,
|
|
598
|
+
socketGroupName,
|
|
599
|
+
agentHome: agentUser.home,
|
|
600
|
+
});
|
|
601
|
+
if (!installResult.success) {
|
|
602
|
+
return { success: false, error: installResult.message };
|
|
603
|
+
}
|
|
604
|
+
logVerbose('OpenClaw LaunchDaemons installed and loaded', context);
|
|
605
|
+
// 3. Start OpenClaw services via launchctl kickstart
|
|
606
|
+
logVerbose('Starting OpenClaw services via launchctl', context);
|
|
607
|
+
const startResult = await startOpenClawServices();
|
|
608
|
+
if (!startResult.success) {
|
|
609
|
+
logVerbose(`Failed to start OpenClaw services (non-fatal): ${startResult.message}`, context);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
logVerbose('OpenClaw services started', context);
|
|
613
|
+
}
|
|
614
|
+
context.openclawLaunchDaemons = {
|
|
615
|
+
daemonPlistPath: OPENCLAW_DAEMON_PLIST,
|
|
616
|
+
gatewayPlistPath: OPENCLAW_GATEWAY_PLIST,
|
|
617
|
+
loaded: true,
|
|
618
|
+
};
|
|
619
|
+
return { success: true };
|
|
620
|
+
},
|
|
621
|
+
'open-dashboard': async (_context) => {
|
|
622
|
+
// No-op: the browser is already on the setup wizard page which
|
|
623
|
+
// transitions to CompleteStep and polls for daemon readiness.
|
|
200
624
|
return { success: true };
|
|
201
625
|
},
|
|
202
626
|
'create-groups': async (context) => {
|
|
@@ -304,7 +728,6 @@ const stepExecutors = {
|
|
|
304
728
|
binDir: `${agentUser.home}/bin`,
|
|
305
729
|
wrappersDir: `${agentUser.home}/bin`,
|
|
306
730
|
configDir: `${agentUser.home}/.openclaw`,
|
|
307
|
-
packageDir: `${agentUser.home}/.openclaw-pkg`,
|
|
308
731
|
npmDir: `${agentUser.home}/.npm`,
|
|
309
732
|
socketDir: context.pathsConfig.socketDir,
|
|
310
733
|
logDir: context.pathsConfig.logDir,
|
|
@@ -428,6 +851,13 @@ const stepExecutors = {
|
|
|
428
851
|
socketGroupName: context.userConfig.groups.socket.name,
|
|
429
852
|
nodeVersion: context.options?.nodeVersion,
|
|
430
853
|
verbose: context.options?.verbose,
|
|
854
|
+
nvmResult: context.nvmInstalled ? {
|
|
855
|
+
success: context.nvmInstalled.success,
|
|
856
|
+
nvmDir: context.nvmInstalled.nvmDir,
|
|
857
|
+
nodeVersion: context.nvmInstalled.nodeVersion,
|
|
858
|
+
nodeBinaryPath: context.nvmInstalled.nodeBinaryPath,
|
|
859
|
+
message: '',
|
|
860
|
+
} : undefined,
|
|
431
861
|
});
|
|
432
862
|
context.wrappersInstalled = result.installedWrappers;
|
|
433
863
|
if (!result.success) {
|
|
@@ -532,11 +962,14 @@ const stepExecutors = {
|
|
|
532
962
|
}, null, 2);
|
|
533
963
|
logVerbose(`Writing daemon config to ${configPath}`, context);
|
|
534
964
|
// Write config file via sudo tee
|
|
965
|
+
logVerbose(`Running: sudo tee "${configPath}"`, context);
|
|
535
966
|
execSync(`sudo tee "${configPath}" > /dev/null << 'SHIELD_EOF'
|
|
536
967
|
${shieldConfig}
|
|
537
968
|
SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
538
969
|
// Set ownership and permissions
|
|
970
|
+
logVerbose(`Running: sudo chown ${brokerUsername}:${socketGroupName} "${configPath}"`, context);
|
|
539
971
|
execSync(`sudo chown ${brokerUsername}:${socketGroupName} "${configPath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
972
|
+
logVerbose(`Running: sudo chmod 640 "${configPath}"`, context);
|
|
540
973
|
execSync(`sudo chmod 640 "${configPath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
541
974
|
context.daemonConfig = {
|
|
542
975
|
configPath,
|
|
@@ -551,6 +984,79 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
551
984
|
};
|
|
552
985
|
}
|
|
553
986
|
},
|
|
987
|
+
'install-sudoers': async (context) => {
|
|
988
|
+
if (!context.userConfig) {
|
|
989
|
+
return { success: false, error: 'User configuration not set' };
|
|
990
|
+
}
|
|
991
|
+
const brokerUsername = context.userConfig.brokerUser.username;
|
|
992
|
+
const agentUsername = context.userConfig.agentUser.username;
|
|
993
|
+
const agentHome = context.userConfig.agentUser.home;
|
|
994
|
+
if (context.options?.dryRun) {
|
|
995
|
+
logVerbose(`[dry-run] Would install /etc/sudoers.d/agenshield granting ${brokerUsername} sudo for ${agentUsername} operations`, context);
|
|
996
|
+
return { success: true };
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const { execSync } = await import('node:child_process');
|
|
1000
|
+
// Include the original user (who ran setup) so dev-mode daemon can also use sudo
|
|
1001
|
+
const originalUser = getOriginalUser();
|
|
1002
|
+
const users = [brokerUsername];
|
|
1003
|
+
if (originalUser && originalUser !== brokerUsername) {
|
|
1004
|
+
users.push(originalUser);
|
|
1005
|
+
}
|
|
1006
|
+
const lines = [
|
|
1007
|
+
'# AgenShield: allow broker (and host user) to run openclaw commands as agent user',
|
|
1008
|
+
];
|
|
1009
|
+
for (const user of users) {
|
|
1010
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /opt/agenshield/bin/openclaw-launcher.sh *`);
|
|
1011
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /bin/cat ${agentHome}/.openclaw/*`);
|
|
1012
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /usr/bin/tee ${agentHome}/.openclaw/*`);
|
|
1013
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /bin/mkdir -p ${agentHome}/.openclaw/*`);
|
|
1014
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /usr/bin/tee ${agentHome}/bin/*`);
|
|
1015
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /bin/mkdir -p ${agentHome}/bin`);
|
|
1016
|
+
lines.push(`${user} ALL=(${agentUsername}) NOPASSWD: /bin/bash --norc --noprofile -c *`);
|
|
1017
|
+
}
|
|
1018
|
+
lines.push('');
|
|
1019
|
+
lines.push('# AgenShield: allow broker (and host user) to manage openclaw gateway LaunchDaemon');
|
|
1020
|
+
for (const user of users) {
|
|
1021
|
+
lines.push(`${user} ALL=(root) NOPASSWD: /bin/launchctl kickstart system/com.agenshield.openclaw.gateway`);
|
|
1022
|
+
lines.push(`${user} ALL=(root) NOPASSWD: /bin/launchctl kickstart -k system/com.agenshield.openclaw.gateway`);
|
|
1023
|
+
lines.push(`${user} ALL=(root) NOPASSWD: /bin/launchctl kill SIGTERM system/com.agenshield.openclaw.gateway`);
|
|
1024
|
+
lines.push(`${user} ALL=(root) NOPASSWD: /bin/launchctl list com.agenshield.openclaw.gateway`);
|
|
1025
|
+
}
|
|
1026
|
+
lines.push('');
|
|
1027
|
+
const sudoersContent = lines.join('\n');
|
|
1028
|
+
const tmpPath = '/tmp/agenshield-sudoers';
|
|
1029
|
+
// 1. Write to temp file
|
|
1030
|
+
logVerbose('Writing sudoers rules to temp file', context);
|
|
1031
|
+
execSync(`sudo tee "${tmpPath}" > /dev/null`, {
|
|
1032
|
+
input: sudoersContent,
|
|
1033
|
+
encoding: 'utf-8',
|
|
1034
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1035
|
+
});
|
|
1036
|
+
// 2. Validate with visudo
|
|
1037
|
+
logVerbose('Validating sudoers syntax', context);
|
|
1038
|
+
execSync(`sudo visudo -c -f "${tmpPath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1039
|
+
// 3. Move to /etc/sudoers.d/
|
|
1040
|
+
logVerbose('Installing sudoers drop-in to /etc/sudoers.d/agenshield', context);
|
|
1041
|
+
execSync(`sudo mv "${tmpPath}" /etc/sudoers.d/agenshield`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1042
|
+
// 4. Set permissions (440 is required for sudoers files)
|
|
1043
|
+
execSync('sudo chmod 440 /etc/sudoers.d/agenshield', { encoding: 'utf-8', stdio: 'pipe' });
|
|
1044
|
+
logVerbose(`Sudoers rule installed: ${users.join(', ')} → ${agentUsername} + root (launchctl)`, context);
|
|
1045
|
+
return { success: true };
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
// Clean up temp file on failure
|
|
1049
|
+
try {
|
|
1050
|
+
const { execSync } = await import('node:child_process');
|
|
1051
|
+
execSync('sudo rm -f /tmp/agenshield-sudoers', { stdio: 'pipe' });
|
|
1052
|
+
}
|
|
1053
|
+
catch { /* ignore */ }
|
|
1054
|
+
return {
|
|
1055
|
+
success: false,
|
|
1056
|
+
error: `Failed to install sudoers rule: ${err.message}`,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
554
1060
|
'install-policies': async (context) => {
|
|
555
1061
|
if (!context.pathsConfig) {
|
|
556
1062
|
return { success: false, error: 'Configuration not set' };
|
|
@@ -590,17 +1096,22 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
590
1096
|
const socketGroupName = context.userConfig.groups.socket.name;
|
|
591
1097
|
// Remove stale socket from previous installs (may be root-owned, causing EACCES)
|
|
592
1098
|
logVerbose('Removing stale socket file if present', context);
|
|
1099
|
+
logVerbose('Running: sudo rm -f /var/run/agenshield/agenshield.sock', context);
|
|
593
1100
|
execSync(`sudo rm -f /var/run/agenshield/agenshield.sock`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
594
1101
|
// Ensure log files exist with correct ownership BEFORE loading daemon.
|
|
595
1102
|
// launchd opens stdout/stderr files at process start; if they don't exist
|
|
596
1103
|
// or are root-owned, the broker's output may be lost.
|
|
597
1104
|
logVerbose('Ensuring log files have correct ownership', context);
|
|
1105
|
+
logVerbose('Running: sudo mkdir -p /var/log/agenshield', context);
|
|
598
1106
|
execSync(`sudo mkdir -p /var/log/agenshield`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1107
|
+
logVerbose('Running: sudo touch /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log', context);
|
|
599
1108
|
execSync(`sudo touch /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1109
|
+
logVerbose(`Running: sudo chown ${brokerUsername}:${socketGroupName} /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, context);
|
|
600
1110
|
execSync(`sudo chown ${brokerUsername}:${socketGroupName} /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
601
1111
|
// Bootout any stale broker daemon entry from a previous install.
|
|
602
1112
|
// Without this, `launchctl load` may no-op if the old entry is cached.
|
|
603
1113
|
logVerbose('Removing stale launchd entry if present', context);
|
|
1114
|
+
logVerbose('Running: sudo launchctl bootout system/com.agenshield.broker', context);
|
|
604
1115
|
try {
|
|
605
1116
|
execSync(`sudo launchctl bootout system/com.agenshield.broker 2>/dev/null`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
606
1117
|
}
|
|
@@ -620,6 +1131,7 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
620
1131
|
// start the process if launchd throttles it (e.g. ThrottleInterval from a
|
|
621
1132
|
// prior crashed run). kickstart bypasses throttling.
|
|
622
1133
|
logVerbose('Kickstarting broker daemon', context);
|
|
1134
|
+
logVerbose('Running: sudo launchctl kickstart system/com.agenshield.broker', context);
|
|
623
1135
|
try {
|
|
624
1136
|
execSync(`sudo launchctl kickstart system/com.agenshield.broker`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
625
1137
|
}
|
|
@@ -646,82 +1158,7 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
646
1158
|
};
|
|
647
1159
|
}
|
|
648
1160
|
},
|
|
649
|
-
|
|
650
|
-
if (!context.preset || !context.agentUser || !context.directories || !context.userConfig) {
|
|
651
|
-
return { success: false, error: 'Missing required context for migration' };
|
|
652
|
-
}
|
|
653
|
-
// Get the entry command for this preset to determine binary name
|
|
654
|
-
const entryCommand = context.preset.getEntryCommand({
|
|
655
|
-
agentUser: context.userConfig.agentUser,
|
|
656
|
-
directories: context.directories,
|
|
657
|
-
entryPoint: context.options?.entryPoint,
|
|
658
|
-
detection: context.presetDetection,
|
|
659
|
-
});
|
|
660
|
-
// Skip in dry-run mode
|
|
661
|
-
if (context.options?.dryRun) {
|
|
662
|
-
context.migration = {
|
|
663
|
-
success: true,
|
|
664
|
-
newPaths: {
|
|
665
|
-
packagePath: context.directories.packageDir,
|
|
666
|
-
binaryPath: entryCommand,
|
|
667
|
-
configPath: `${context.directories.configDir}/config.json`,
|
|
668
|
-
},
|
|
669
|
-
};
|
|
670
|
-
return { success: true };
|
|
671
|
-
}
|
|
672
|
-
// Use preset's migrate function
|
|
673
|
-
const migrationContext = {
|
|
674
|
-
agentUser: context.userConfig.agentUser,
|
|
675
|
-
directories: context.directories,
|
|
676
|
-
entryPoint: context.options?.entryPoint,
|
|
677
|
-
detection: context.presetDetection,
|
|
678
|
-
};
|
|
679
|
-
const result = await context.preset.migrate(migrationContext);
|
|
680
|
-
if (!result.success) {
|
|
681
|
-
return { success: false, error: result.error };
|
|
682
|
-
}
|
|
683
|
-
// Re-apply correct ownership for .openclaw (broker-owned, setgid per directories.ts)
|
|
684
|
-
// Migration may have set agent ownership; .openclaw must be broker-writable.
|
|
685
|
-
try {
|
|
686
|
-
const { execSync } = await import('node:child_process');
|
|
687
|
-
const agentConfigDir = `${context.userConfig.agentUser.home}/.openclaw`;
|
|
688
|
-
const brokerUser = context.userConfig.brokerUser.username;
|
|
689
|
-
const socketGroup = context.userConfig.groups.socket.name;
|
|
690
|
-
execSync(`sudo chown -R ${brokerUser}:${socketGroup} "${agentConfigDir}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
691
|
-
execSync(`sudo chmod 2775 "${agentConfigDir}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
logVerbose(`Warning: failed to fix .openclaw ownership: ${err.message}`, context);
|
|
695
|
-
}
|
|
696
|
-
context.migration = {
|
|
697
|
-
success: true,
|
|
698
|
-
newPaths: result.newPaths,
|
|
699
|
-
};
|
|
700
|
-
// Now save the full backup with all the information
|
|
701
|
-
if (context.originalInstallation && context.agentUser && result.newPaths) {
|
|
702
|
-
const sandboxUserInfo = {
|
|
703
|
-
username: context.agentUser.username,
|
|
704
|
-
uid: context.agentUser.uid,
|
|
705
|
-
gid: context.agentUser.gid,
|
|
706
|
-
homeDir: context.agentUser.homeDir,
|
|
707
|
-
};
|
|
708
|
-
const migratedPaths = {
|
|
709
|
-
packagePath: result.newPaths.packagePath,
|
|
710
|
-
configPath: result.newPaths.configPath || `${context.directories?.configDir}/config.json`,
|
|
711
|
-
binaryPath: result.newPaths.binaryPath,
|
|
712
|
-
};
|
|
713
|
-
const backupResult = saveBackup({
|
|
714
|
-
originalInstallation: context.originalInstallation,
|
|
715
|
-
sandboxUser: sandboxUserInfo,
|
|
716
|
-
migratedPaths,
|
|
717
|
-
});
|
|
718
|
-
if (!backupResult.success) {
|
|
719
|
-
// Log warning but don't fail - installation is already complete
|
|
720
|
-
console.warn(`Warning: Could not save backup: ${backupResult.error}`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
return { success: true };
|
|
724
|
-
},
|
|
1161
|
+
// NOTE: migrate step removed — replaced by install-openclaw + copy-openclaw-config
|
|
725
1162
|
verify: async (context) => {
|
|
726
1163
|
if (!context.userConfig) {
|
|
727
1164
|
return { success: false, error: 'Configuration not set' };
|
|
@@ -852,24 +1289,34 @@ async function runSteps(state, context, stepIds, onStateChange) {
|
|
|
852
1289
|
const step = state.steps[stepIndex];
|
|
853
1290
|
// Update step status to running
|
|
854
1291
|
step.status = 'running';
|
|
1292
|
+
_currentStepId = stepId;
|
|
1293
|
+
logVerbose(`▶ Starting step: ${step.name} (${step.id})`, context);
|
|
855
1294
|
onStateChange?.(state);
|
|
1295
|
+
// Yield a macrotask tick so the SSE 'running' event flushes to the browser
|
|
1296
|
+
// before the step executor (which may use execSync) blocks the event loop.
|
|
1297
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
856
1298
|
// Execute the step
|
|
857
1299
|
const executor = stepExecutors[stepId];
|
|
858
1300
|
if (!executor) {
|
|
859
1301
|
step.status = 'error';
|
|
860
1302
|
step.error = `No executor for step: ${step.id}`;
|
|
861
1303
|
state.hasError = true;
|
|
1304
|
+
_currentStepId = undefined;
|
|
1305
|
+
logVerbose(`✗ Step ${step.id}: no executor found`, context);
|
|
862
1306
|
onStateChange?.(state);
|
|
863
1307
|
return { success: false, error: step.error };
|
|
864
1308
|
}
|
|
865
1309
|
const result = await executeStep(step, context, executor);
|
|
1310
|
+
_currentStepId = undefined;
|
|
866
1311
|
if (result.success) {
|
|
867
1312
|
step.status = 'completed';
|
|
1313
|
+
logVerbose(`✓ Completed step: ${step.name}`, context);
|
|
868
1314
|
}
|
|
869
1315
|
else {
|
|
870
1316
|
step.status = 'error';
|
|
871
1317
|
step.error = result.error;
|
|
872
1318
|
state.hasError = true;
|
|
1319
|
+
logVerbose(`✗ Failed step: ${step.name} — ${result.error}`, context);
|
|
873
1320
|
onStateChange?.(state);
|
|
874
1321
|
return { success: false, error: result.error };
|
|
875
1322
|
}
|
|
@@ -904,10 +1351,14 @@ export function createWizardEngine(options) {
|
|
|
904
1351
|
return result;
|
|
905
1352
|
},
|
|
906
1353
|
/**
|
|
907
|
-
* Run setup phase
|
|
908
|
-
* Called after user confirms they want to proceed
|
|
1354
|
+
* Run setup phase — all setup steps from confirm through complete.
|
|
1355
|
+
* Called after user confirms they want to proceed.
|
|
1356
|
+
* Excludes: setup-passcode, open-dashboard, complete (handled by runFinalPhase).
|
|
909
1357
|
*/
|
|
910
1358
|
async runSetupPhase() {
|
|
1359
|
+
const excludeFromSetup = [
|
|
1360
|
+
'setup-passcode', 'open-dashboard', 'complete',
|
|
1361
|
+
];
|
|
911
1362
|
// Prompt for sudo credentials before privileged steps (skip in dry-run)
|
|
912
1363
|
if (!context.options?.dryRun) {
|
|
913
1364
|
const { ensureSudoAccess, startSudoKeepalive } = await import('../utils/privileges.js');
|
|
@@ -915,7 +1366,7 @@ export function createWizardEngine(options) {
|
|
|
915
1366
|
const keepalive = startSudoKeepalive();
|
|
916
1367
|
try {
|
|
917
1368
|
const setupSteps = ['confirm', ...getStepsByPhase('setup')
|
|
918
|
-
.filter((id) => id
|
|
1369
|
+
.filter((id) => !excludeFromSetup.includes(id))];
|
|
919
1370
|
await runSteps(state, context, setupSteps, engine.onStateChange);
|
|
920
1371
|
}
|
|
921
1372
|
finally {
|
|
@@ -923,17 +1374,26 @@ export function createWizardEngine(options) {
|
|
|
923
1374
|
}
|
|
924
1375
|
return;
|
|
925
1376
|
}
|
|
926
|
-
// dry-run path
|
|
1377
|
+
// dry-run path
|
|
927
1378
|
const setupSteps = ['confirm', ...getStepsByPhase('setup')
|
|
928
|
-
.filter((id) => id
|
|
1379
|
+
.filter((id) => !excludeFromSetup.includes(id))];
|
|
929
1380
|
await runSteps(state, context, setupSteps, engine.onStateChange);
|
|
930
1381
|
},
|
|
931
1382
|
/**
|
|
932
|
-
* Run
|
|
1383
|
+
* Run migration phase — no longer needed in new flow.
|
|
1384
|
+
* Kept for backward compatibility; does nothing.
|
|
1385
|
+
* @deprecated Use runSetupPhase() which now includes install-openclaw + copy-openclaw-config.
|
|
1386
|
+
*/
|
|
1387
|
+
async runMigrationPhase() {
|
|
1388
|
+
// Migration is now part of the setup phase (install-openclaw + copy-openclaw-config)
|
|
1389
|
+
return;
|
|
1390
|
+
},
|
|
1391
|
+
/**
|
|
1392
|
+
* Run final phase (setup-passcode, open-dashboard, and complete)
|
|
933
1393
|
* Called after the passcode UI has collected the passcode value
|
|
934
1394
|
*/
|
|
935
1395
|
async runFinalPhase() {
|
|
936
|
-
const finalSteps = ['setup-passcode', 'complete'];
|
|
1396
|
+
const finalSteps = ['setup-passcode', 'open-dashboard', 'complete'];
|
|
937
1397
|
await runSteps(state, context, finalSteps, engine.onStateChange);
|
|
938
1398
|
if (!state.hasError) {
|
|
939
1399
|
state.isComplete = true;
|