agenshield 0.6.2 → 0.7.0
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 +561 -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,41 +192,437 @@ 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 };
|
|
210
|
+
}
|
|
211
|
+
logVerbose('Found previous installation (ash_default_agent exists), cleaning up', context);
|
|
212
|
+
if (context.options?.dryRun) {
|
|
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
|
+
}
|
|
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' };
|
|
170
536
|
}
|
|
171
|
-
|
|
537
|
+
const { agentUser } = context.userConfig;
|
|
172
538
|
if (context.options?.dryRun) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
199
618
|
};
|
|
200
619
|
return { success: true };
|
|
201
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.
|
|
624
|
+
return { success: true };
|
|
625
|
+
},
|
|
202
626
|
'create-groups': async (context) => {
|
|
203
627
|
if (!context.userConfig) {
|
|
204
628
|
return { success: false, error: 'User configuration not set' };
|
|
@@ -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,63 @@ 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
|
+
const sudoersContent = [
|
|
1001
|
+
'# AgenShield: allow broker to run openclaw commands as agent user',
|
|
1002
|
+
`${brokerUsername} ALL=(${agentUsername}) NOPASSWD: /opt/agenshield/bin/openclaw-launcher.sh *`,
|
|
1003
|
+
`${brokerUsername} ALL=(${agentUsername}) NOPASSWD: /usr/bin/tee ${agentHome}/.openclaw/*`,
|
|
1004
|
+
'',
|
|
1005
|
+
'# AgenShield: allow broker to manage openclaw gateway LaunchDaemon',
|
|
1006
|
+
`${brokerUsername} ALL=(root) NOPASSWD: /bin/launchctl kickstart system/com.agenshield.openclaw.gateway`,
|
|
1007
|
+
`${brokerUsername} ALL=(root) NOPASSWD: /bin/launchctl kickstart -k system/com.agenshield.openclaw.gateway`,
|
|
1008
|
+
`${brokerUsername} ALL=(root) NOPASSWD: /bin/launchctl kill SIGTERM system/com.agenshield.openclaw.gateway`,
|
|
1009
|
+
`${brokerUsername} ALL=(root) NOPASSWD: /bin/launchctl list com.agenshield.openclaw.gateway`,
|
|
1010
|
+
'',
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
const tmpPath = '/tmp/agenshield-sudoers';
|
|
1013
|
+
// 1. Write to temp file
|
|
1014
|
+
logVerbose('Writing sudoers rules to temp file', context);
|
|
1015
|
+
execSync(`sudo tee "${tmpPath}" > /dev/null`, {
|
|
1016
|
+
input: sudoersContent,
|
|
1017
|
+
encoding: 'utf-8',
|
|
1018
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1019
|
+
});
|
|
1020
|
+
// 2. Validate with visudo
|
|
1021
|
+
logVerbose('Validating sudoers syntax', context);
|
|
1022
|
+
execSync(`sudo visudo -c -f "${tmpPath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1023
|
+
// 3. Move to /etc/sudoers.d/
|
|
1024
|
+
logVerbose('Installing sudoers drop-in to /etc/sudoers.d/agenshield', context);
|
|
1025
|
+
execSync(`sudo mv "${tmpPath}" /etc/sudoers.d/agenshield`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1026
|
+
// 4. Set permissions (440 is required for sudoers files)
|
|
1027
|
+
execSync('sudo chmod 440 /etc/sudoers.d/agenshield', { encoding: 'utf-8', stdio: 'pipe' });
|
|
1028
|
+
logVerbose(`Sudoers rule installed: ${brokerUsername} → ${agentUsername} + root (launchctl)`, context);
|
|
1029
|
+
return { success: true };
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
// Clean up temp file on failure
|
|
1033
|
+
try {
|
|
1034
|
+
const { execSync } = await import('node:child_process');
|
|
1035
|
+
execSync('sudo rm -f /tmp/agenshield-sudoers', { stdio: 'pipe' });
|
|
1036
|
+
}
|
|
1037
|
+
catch { /* ignore */ }
|
|
1038
|
+
return {
|
|
1039
|
+
success: false,
|
|
1040
|
+
error: `Failed to install sudoers rule: ${err.message}`,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
554
1044
|
'install-policies': async (context) => {
|
|
555
1045
|
if (!context.pathsConfig) {
|
|
556
1046
|
return { success: false, error: 'Configuration not set' };
|
|
@@ -590,17 +1080,22 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
590
1080
|
const socketGroupName = context.userConfig.groups.socket.name;
|
|
591
1081
|
// Remove stale socket from previous installs (may be root-owned, causing EACCES)
|
|
592
1082
|
logVerbose('Removing stale socket file if present', context);
|
|
1083
|
+
logVerbose('Running: sudo rm -f /var/run/agenshield/agenshield.sock', context);
|
|
593
1084
|
execSync(`sudo rm -f /var/run/agenshield/agenshield.sock`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
594
1085
|
// Ensure log files exist with correct ownership BEFORE loading daemon.
|
|
595
1086
|
// launchd opens stdout/stderr files at process start; if they don't exist
|
|
596
1087
|
// or are root-owned, the broker's output may be lost.
|
|
597
1088
|
logVerbose('Ensuring log files have correct ownership', context);
|
|
1089
|
+
logVerbose('Running: sudo mkdir -p /var/log/agenshield', context);
|
|
598
1090
|
execSync(`sudo mkdir -p /var/log/agenshield`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1091
|
+
logVerbose('Running: sudo touch /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log', context);
|
|
599
1092
|
execSync(`sudo touch /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1093
|
+
logVerbose(`Running: sudo chown ${brokerUsername}:${socketGroupName} /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, context);
|
|
600
1094
|
execSync(`sudo chown ${brokerUsername}:${socketGroupName} /var/log/agenshield/broker.log /var/log/agenshield/broker.error.log`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
601
1095
|
// Bootout any stale broker daemon entry from a previous install.
|
|
602
1096
|
// Without this, `launchctl load` may no-op if the old entry is cached.
|
|
603
1097
|
logVerbose('Removing stale launchd entry if present', context);
|
|
1098
|
+
logVerbose('Running: sudo launchctl bootout system/com.agenshield.broker', context);
|
|
604
1099
|
try {
|
|
605
1100
|
execSync(`sudo launchctl bootout system/com.agenshield.broker 2>/dev/null`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
606
1101
|
}
|
|
@@ -620,6 +1115,7 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
620
1115
|
// start the process if launchd throttles it (e.g. ThrottleInterval from a
|
|
621
1116
|
// prior crashed run). kickstart bypasses throttling.
|
|
622
1117
|
logVerbose('Kickstarting broker daemon', context);
|
|
1118
|
+
logVerbose('Running: sudo launchctl kickstart system/com.agenshield.broker', context);
|
|
623
1119
|
try {
|
|
624
1120
|
execSync(`sudo launchctl kickstart system/com.agenshield.broker`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
625
1121
|
}
|
|
@@ -646,82 +1142,7 @@ SHIELD_EOF`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
|
646
1142
|
};
|
|
647
1143
|
}
|
|
648
1144
|
},
|
|
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
|
-
},
|
|
1145
|
+
// NOTE: migrate step removed — replaced by install-openclaw + copy-openclaw-config
|
|
725
1146
|
verify: async (context) => {
|
|
726
1147
|
if (!context.userConfig) {
|
|
727
1148
|
return { success: false, error: 'Configuration not set' };
|
|
@@ -852,6 +1273,8 @@ async function runSteps(state, context, stepIds, onStateChange) {
|
|
|
852
1273
|
const step = state.steps[stepIndex];
|
|
853
1274
|
// Update step status to running
|
|
854
1275
|
step.status = 'running';
|
|
1276
|
+
_currentStepId = stepId;
|
|
1277
|
+
logVerbose(`▶ Starting step: ${step.name} (${step.id})`, context);
|
|
855
1278
|
onStateChange?.(state);
|
|
856
1279
|
// Execute the step
|
|
857
1280
|
const executor = stepExecutors[stepId];
|
|
@@ -859,17 +1282,22 @@ async function runSteps(state, context, stepIds, onStateChange) {
|
|
|
859
1282
|
step.status = 'error';
|
|
860
1283
|
step.error = `No executor for step: ${step.id}`;
|
|
861
1284
|
state.hasError = true;
|
|
1285
|
+
_currentStepId = undefined;
|
|
1286
|
+
logVerbose(`✗ Step ${step.id}: no executor found`, context);
|
|
862
1287
|
onStateChange?.(state);
|
|
863
1288
|
return { success: false, error: step.error };
|
|
864
1289
|
}
|
|
865
1290
|
const result = await executeStep(step, context, executor);
|
|
1291
|
+
_currentStepId = undefined;
|
|
866
1292
|
if (result.success) {
|
|
867
1293
|
step.status = 'completed';
|
|
1294
|
+
logVerbose(`✓ Completed step: ${step.name}`, context);
|
|
868
1295
|
}
|
|
869
1296
|
else {
|
|
870
1297
|
step.status = 'error';
|
|
871
1298
|
step.error = result.error;
|
|
872
1299
|
state.hasError = true;
|
|
1300
|
+
logVerbose(`✗ Failed step: ${step.name} — ${result.error}`, context);
|
|
873
1301
|
onStateChange?.(state);
|
|
874
1302
|
return { success: false, error: result.error };
|
|
875
1303
|
}
|
|
@@ -904,10 +1332,14 @@ export function createWizardEngine(options) {
|
|
|
904
1332
|
return result;
|
|
905
1333
|
},
|
|
906
1334
|
/**
|
|
907
|
-
* Run setup phase
|
|
908
|
-
* Called after user confirms they want to proceed
|
|
1335
|
+
* Run setup phase — all setup steps from confirm through complete.
|
|
1336
|
+
* Called after user confirms they want to proceed.
|
|
1337
|
+
* Excludes: setup-passcode, open-dashboard, complete (handled by runFinalPhase).
|
|
909
1338
|
*/
|
|
910
1339
|
async runSetupPhase() {
|
|
1340
|
+
const excludeFromSetup = [
|
|
1341
|
+
'setup-passcode', 'open-dashboard', 'complete',
|
|
1342
|
+
];
|
|
911
1343
|
// Prompt for sudo credentials before privileged steps (skip in dry-run)
|
|
912
1344
|
if (!context.options?.dryRun) {
|
|
913
1345
|
const { ensureSudoAccess, startSudoKeepalive } = await import('../utils/privileges.js');
|
|
@@ -915,7 +1347,7 @@ export function createWizardEngine(options) {
|
|
|
915
1347
|
const keepalive = startSudoKeepalive();
|
|
916
1348
|
try {
|
|
917
1349
|
const setupSteps = ['confirm', ...getStepsByPhase('setup')
|
|
918
|
-
.filter((id) => id
|
|
1350
|
+
.filter((id) => !excludeFromSetup.includes(id))];
|
|
919
1351
|
await runSteps(state, context, setupSteps, engine.onStateChange);
|
|
920
1352
|
}
|
|
921
1353
|
finally {
|
|
@@ -923,17 +1355,26 @@ export function createWizardEngine(options) {
|
|
|
923
1355
|
}
|
|
924
1356
|
return;
|
|
925
1357
|
}
|
|
926
|
-
// dry-run path
|
|
1358
|
+
// dry-run path
|
|
927
1359
|
const setupSteps = ['confirm', ...getStepsByPhase('setup')
|
|
928
|
-
.filter((id) => id
|
|
1360
|
+
.filter((id) => !excludeFromSetup.includes(id))];
|
|
929
1361
|
await runSteps(state, context, setupSteps, engine.onStateChange);
|
|
930
1362
|
},
|
|
931
1363
|
/**
|
|
932
|
-
* Run
|
|
1364
|
+
* Run migration phase — no longer needed in new flow.
|
|
1365
|
+
* Kept for backward compatibility; does nothing.
|
|
1366
|
+
* @deprecated Use runSetupPhase() which now includes install-openclaw + copy-openclaw-config.
|
|
1367
|
+
*/
|
|
1368
|
+
async runMigrationPhase() {
|
|
1369
|
+
// Migration is now part of the setup phase (install-openclaw + copy-openclaw-config)
|
|
1370
|
+
return;
|
|
1371
|
+
},
|
|
1372
|
+
/**
|
|
1373
|
+
* Run final phase (setup-passcode, open-dashboard, and complete)
|
|
933
1374
|
* Called after the passcode UI has collected the passcode value
|
|
934
1375
|
*/
|
|
935
1376
|
async runFinalPhase() {
|
|
936
|
-
const finalSteps = ['setup-passcode', 'complete'];
|
|
1377
|
+
const finalSteps = ['setup-passcode', 'open-dashboard', 'complete'];
|
|
937
1378
|
await runSteps(state, context, finalSteps, engine.onStateChange);
|
|
938
1379
|
if (!state.hasError) {
|
|
939
1380
|
state.isComplete = true;
|