@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-2
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/README.md +52 -8
- package/cli.mjs +538 -224
- package/index.ts +76 -27
- package/openclaw.plugin.json +53 -28
- package/package.json +5 -2
- package/skills/teamclaw/SKILL.md +213 -0
- package/skills/teamclaw/references/api-quick-ref.md +117 -0
- package/skills/teamclaw-setup/SKILL.md +81 -0
- package/skills/teamclaw-setup/references/install-modes.md +136 -0
- package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
- package/src/config.ts +44 -16
- package/src/controller/controller-capacity.ts +2 -2
- package/src/controller/controller-service.ts +193 -47
- package/src/controller/controller-tools.ts +102 -2
- package/src/controller/delivery-report.ts +563 -0
- package/src/controller/http-server.ts +1907 -172
- package/src/controller/kickoff-orchestrator.ts +292 -0
- package/src/controller/managed-gateway-process.ts +330 -0
- package/src/controller/orchestration-manifest.ts +69 -1
- package/src/controller/preview-manager.ts +676 -0
- package/src/controller/prompt-injector.ts +116 -67
- package/src/controller/role-inference.ts +41 -0
- package/src/controller/websocket.ts +3 -1
- package/src/controller/worker-provisioning.ts +429 -74
- package/src/discovery.ts +1 -1
- package/src/git-collaboration.ts +198 -47
- package/src/identity.ts +12 -2
- package/src/interaction-contracts.ts +179 -3
- package/src/networking.ts +99 -0
- package/src/openclaw-workspace.ts +478 -11
- package/src/prompt-policy.ts +381 -0
- package/src/roles.ts +37 -36
- package/src/state.ts +40 -1
- package/src/task-executor.ts +282 -78
- package/src/types.ts +150 -7
- package/src/ui/app.js +1403 -175
- package/src/ui/assets/teamclaw-app-icon.png +0 -0
- package/src/ui/index.html +122 -40
- package/src/ui/style.css +829 -143
- package/src/worker/http-handler.ts +40 -4
- package/src/worker/prompt-injector.ts +9 -38
- package/src/worker/skill-installer.ts +2 -2
- package/src/worker/tools.ts +31 -5
- package/src/worker/worker-service.ts +49 -8
- package/src/workspace-browser.ts +20 -7
- package/src/controller/local-worker-manager.ts +0 -533
package/cli.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const PACKAGE_NAME = packageMetadata.name;
|
|
|
18
18
|
const PACKAGE_VERSION = packageMetadata.version;
|
|
19
19
|
const PACKAGE_INSTALL_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
20
20
|
const PLUGIN_ID = "teamclaw";
|
|
21
|
+
const DANGEROUS_INSTALL_FLAG = "--dangerously-force-unsafe-install";
|
|
21
22
|
const DEFAULT_TEAMCLAW_IMAGE = "ghcr.io/topcheer/teamclaw-openclaw:latest";
|
|
22
23
|
const DEFAULT_CONTROLLER_PORT = 9527;
|
|
23
24
|
const DEFAULT_WORKER_PORT = 9528;
|
|
@@ -25,8 +26,21 @@ const DEFAULT_GATEWAY_PORT = 18789;
|
|
|
25
26
|
const DEFAULT_TEAM_NAME = "default";
|
|
26
27
|
const DEFAULT_TASK_TIMEOUT_MS = 1_800_000;
|
|
27
28
|
const DEFAULT_AGENT_TIMEOUT_SECONDS = 2_400;
|
|
28
|
-
const DEFAULT_LOCAL_ROLES = ["architect", "developer", "qa"];
|
|
29
29
|
const LEGACY_DEFAULT_PROVISIONING_ROLES = ["architect", "developer", "qa"];
|
|
30
|
+
const TEAMCLAW_AGENT_ID = "teamclaw";
|
|
31
|
+
const TEAMCLAW_RECOMMENDED_EXEC_SECURITY = "full";
|
|
32
|
+
const TEAMCLAW_RECOMMENDED_EXEC_ASK = "off";
|
|
33
|
+
const TEAMCLAW_RECOMMENDED_COMMAND_MODE = "auto";
|
|
34
|
+
const AGENT_MODE_OPTIONS = [
|
|
35
|
+
{
|
|
36
|
+
value: "independent",
|
|
37
|
+
label: "Dedicated TeamClaw agent/workspace",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: "main",
|
|
41
|
+
label: "Legacy shared main-agent mode",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
30
44
|
|
|
31
45
|
const ROLE_OPTIONS = [
|
|
32
46
|
{ value: "pm", label: "Product Manager" },
|
|
@@ -43,20 +57,15 @@ const ROLE_OPTIONS = [
|
|
|
43
57
|
|
|
44
58
|
const INSTALL_MODE_OPTIONS = [
|
|
45
59
|
{
|
|
46
|
-
value: "
|
|
47
|
-
label: "
|
|
48
|
-
hint: "Recommended
|
|
60
|
+
value: "controller-process",
|
|
61
|
+
label: "Controller + on-demand process workers",
|
|
62
|
+
hint: "Recommended first setup on one host.",
|
|
49
63
|
},
|
|
50
64
|
{
|
|
51
65
|
value: "controller-manual",
|
|
52
|
-
label: "Controller only
|
|
66
|
+
label: "Controller only + external workers",
|
|
53
67
|
hint: "Use separate OpenClaw installs for workers.",
|
|
54
68
|
},
|
|
55
|
-
{
|
|
56
|
-
value: "controller-process",
|
|
57
|
-
label: "Controller + on-demand process workers",
|
|
58
|
-
hint: "Launch workers as child processes on the same host.",
|
|
59
|
-
},
|
|
60
69
|
{
|
|
61
70
|
value: "controller-docker",
|
|
62
71
|
label: "Controller + on-demand Docker workers",
|
|
@@ -89,6 +98,11 @@ Commands:
|
|
|
89
98
|
Options:
|
|
90
99
|
--config <path> Override the OpenClaw config path
|
|
91
100
|
--yes Accept the recommended defaults without prompting
|
|
101
|
+
--install-mode <mode> Install mode: controller-process, controller-manual, controller-docker, controller-kubernetes, worker
|
|
102
|
+
--controller-url <url> Worker/manual controller URL override
|
|
103
|
+
--team-name <name> Team name override
|
|
104
|
+
--worker-role <role> Worker role override for --install-mode worker
|
|
105
|
+
--agent-mode <mode> Advanced: "independent" (default) or "main"
|
|
92
106
|
--skip-plugin-install Only update openclaw.json; skip "openclaw plugins install"
|
|
93
107
|
--dry-run Show what would happen without writing files
|
|
94
108
|
`);
|
|
@@ -98,6 +112,11 @@ function parseArgs(argv) {
|
|
|
98
112
|
const options = {
|
|
99
113
|
configPath: "",
|
|
100
114
|
yes: false,
|
|
115
|
+
installMode: "",
|
|
116
|
+
controllerUrl: "",
|
|
117
|
+
teamName: "",
|
|
118
|
+
workerRole: "",
|
|
119
|
+
agentMode: "",
|
|
101
120
|
skipPluginInstall: false,
|
|
102
121
|
dryRun: false,
|
|
103
122
|
};
|
|
@@ -122,6 +141,53 @@ function parseArgs(argv) {
|
|
|
122
141
|
options.yes = true;
|
|
123
142
|
continue;
|
|
124
143
|
}
|
|
144
|
+
if (arg === "--install-mode") {
|
|
145
|
+
const value = argv[index + 1];
|
|
146
|
+
const validModes = new Set(INSTALL_MODE_OPTIONS.map((option) => option.value));
|
|
147
|
+
if (!value || !validModes.has(value)) {
|
|
148
|
+
throw new Error(`--install-mode requires one of: ${INSTALL_MODE_OPTIONS.map((option) => option.value).join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
options.installMode = value;
|
|
151
|
+
index += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg === "--controller-url") {
|
|
155
|
+
const value = argv[index + 1];
|
|
156
|
+
if (!value || (!value.startsWith("http://") && !value.startsWith("https://"))) {
|
|
157
|
+
throw new Error('--controller-url requires a value starting with "http://" or "https://"');
|
|
158
|
+
}
|
|
159
|
+
options.controllerUrl = value;
|
|
160
|
+
index += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (arg === "--team-name") {
|
|
164
|
+
const value = argv[index + 1];
|
|
165
|
+
if (!value || !value.trim()) {
|
|
166
|
+
throw new Error("--team-name requires a non-empty value");
|
|
167
|
+
}
|
|
168
|
+
options.teamName = value.trim();
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (arg === "--worker-role") {
|
|
173
|
+
const value = argv[index + 1];
|
|
174
|
+
const validRoles = new Set(ROLE_OPTIONS.map((option) => option.value));
|
|
175
|
+
if (!value || !validRoles.has(value)) {
|
|
176
|
+
throw new Error(`--worker-role requires one of: ${ROLE_OPTIONS.map((option) => option.value).join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
options.workerRole = value;
|
|
179
|
+
index += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (arg === "--agent-mode") {
|
|
183
|
+
const value = argv[index + 1];
|
|
184
|
+
if (!value || (value !== "independent" && value !== "main")) {
|
|
185
|
+
throw new Error('--agent-mode requires "independent" or "main"');
|
|
186
|
+
}
|
|
187
|
+
options.agentMode = value;
|
|
188
|
+
index += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
125
191
|
if (arg === "--skip-plugin-install") {
|
|
126
192
|
options.skipPluginInstall = true;
|
|
127
193
|
continue;
|
|
@@ -198,20 +264,36 @@ function resolveOpenClawWorkspaceDirForConfigPath(configPath) {
|
|
|
198
264
|
return path.join(resolveOpenClawStateDirForConfigPath(configPath), "workspace");
|
|
199
265
|
}
|
|
200
266
|
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
.toLowerCase()
|
|
204
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
205
|
-
.replace(/^-+|-+$/g, "");
|
|
206
|
-
return normalized || "default";
|
|
267
|
+
function resolveDefaultTeamClawAgentDirForConfigPath(configPath) {
|
|
268
|
+
return path.join(resolveOpenClawStateDirForConfigPath(configPath), "agents", TEAMCLAW_AGENT_ID, "agent");
|
|
207
269
|
}
|
|
208
270
|
|
|
209
|
-
function resolveDefaultTeamClawWorkspaceDir(configPath
|
|
210
|
-
return path.join(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
271
|
+
function resolveDefaultTeamClawWorkspaceDir(configPath) {
|
|
272
|
+
return path.join(resolveOpenClawStateDirForConfigPath(configPath), `workspace-${TEAMCLAW_AGENT_ID}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resolveMainAgentDirForConfigPath(configPath) {
|
|
276
|
+
return path.join(resolveOpenClawStateDirForConfigPath(configPath), "agents", "main", "agent");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function detectMdnsCapability() {
|
|
280
|
+
try {
|
|
281
|
+
const Bonjour = (await import("bonjour-service")).default;
|
|
282
|
+
const bonjour = new Bonjour();
|
|
283
|
+
try {
|
|
284
|
+
const browser = bonjour.find({ type: "teamclaw" }, () => {});
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
286
|
+
browser?.stop?.();
|
|
287
|
+
} finally {
|
|
288
|
+
bonjour.destroy();
|
|
289
|
+
}
|
|
290
|
+
return { available: true, reason: "" };
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return {
|
|
293
|
+
available: false,
|
|
294
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
215
297
|
}
|
|
216
298
|
|
|
217
299
|
async function pathExists(targetPath) {
|
|
@@ -301,6 +383,67 @@ function resolveModelPrimaryValue(model) {
|
|
|
301
383
|
return model.primary.trim();
|
|
302
384
|
}
|
|
303
385
|
|
|
386
|
+
function cloneJsonValue(value) {
|
|
387
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resolveConfiguredAgentEntryRecord(config, agentId) {
|
|
391
|
+
const agents = isRecord(config.agents) ? config.agents : {};
|
|
392
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
393
|
+
for (const entry of list) {
|
|
394
|
+
if (!isRecord(entry) || entry.id !== agentId) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
return entry;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveEffectiveTeamClawModel(config) {
|
|
403
|
+
const teamclawEntry = resolveConfiguredAgentEntryRecord(config, TEAMCLAW_AGENT_ID);
|
|
404
|
+
if (teamclawEntry && teamclawEntry.model != null) {
|
|
405
|
+
return cloneJsonValue(teamclawEntry.model);
|
|
406
|
+
}
|
|
407
|
+
const agents = isRecord(config.agents) ? config.agents : {};
|
|
408
|
+
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
|
|
409
|
+
return defaults.model != null ? cloneJsonValue(defaults.model) : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function findExistingAuthProfilesPath(configPath) {
|
|
413
|
+
const candidates = [
|
|
414
|
+
path.join(resolveDefaultTeamClawAgentDirForConfigPath(configPath), "auth-profiles.json"),
|
|
415
|
+
path.join(resolveMainAgentDirForConfigPath(configPath), "auth-profiles.json"),
|
|
416
|
+
];
|
|
417
|
+
for (const candidatePath of candidates) {
|
|
418
|
+
if (await pathExists(candidatePath)) {
|
|
419
|
+
return candidatePath;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return "";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function bootstrapTeamClawAgentAuth(configPath, config) {
|
|
426
|
+
const teamclawEntry = resolveConfiguredAgentEntryRecord(config, TEAMCLAW_AGENT_ID);
|
|
427
|
+
if (!teamclawEntry || typeof teamclawEntry.agentDir !== "string" || !teamclawEntry.agentDir.trim()) {
|
|
428
|
+
return { copied: false, sourcePath: "", targetPath: "", warning: "" };
|
|
429
|
+
}
|
|
430
|
+
const targetPath = path.join(teamclawEntry.agentDir.trim(), "auth-profiles.json");
|
|
431
|
+
const sourcePath = await findExistingAuthProfilesPath(configPath);
|
|
432
|
+
if (!sourcePath) {
|
|
433
|
+
return {
|
|
434
|
+
copied: false,
|
|
435
|
+
sourcePath: "",
|
|
436
|
+
targetPath,
|
|
437
|
+
warning: "No existing OpenClaw auth-profiles.json was found, so TeamClaw can start but cannot work until host auth is configured.",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
441
|
+
if (path.resolve(sourcePath) !== path.resolve(targetPath)) {
|
|
442
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
443
|
+
}
|
|
444
|
+
return { copied: true, sourcePath, targetPath, warning: "" };
|
|
445
|
+
}
|
|
446
|
+
|
|
304
447
|
function applySelectedModel(existingModel, selectedModel) {
|
|
305
448
|
const nextPrimary = typeof selectedModel === "string" ? selectedModel.trim() : "";
|
|
306
449
|
if (!nextPrimary) {
|
|
@@ -334,13 +477,30 @@ function getCurrentWorkspacePath(config) {
|
|
|
334
477
|
return typeof defaults.workspace === "string" ? expandUserPath(defaults.workspace) : "";
|
|
335
478
|
}
|
|
336
479
|
|
|
337
|
-
function
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
480
|
+
function getCurrentTeamClawAgentWorkspacePath(config) {
|
|
481
|
+
const agents = isRecord(config.agents) ? config.agents : {};
|
|
482
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
483
|
+
for (const entry of list) {
|
|
484
|
+
if (!isRecord(entry) || entry.id !== TEAMCLAW_AGENT_ID) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
return typeof entry.workspace === "string" ? expandUserPath(entry.workspace) : "";
|
|
342
488
|
}
|
|
343
|
-
return
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveCurrentAgentIsolationMode(config) {
|
|
493
|
+
const existingTeamClaw = getExistingTeamClawConfig(config);
|
|
494
|
+
return existingTeamClaw.agentIsolationMode === "main" ? "main" : "independent";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function resolveInstallerWorkspaceDefault(configPath, config, agentIsolationMode) {
|
|
498
|
+
if (agentIsolationMode === "main") {
|
|
499
|
+
const currentWorkspacePath = getCurrentWorkspacePath(config);
|
|
500
|
+
return currentWorkspacePath || resolveOpenClawWorkspaceDirForConfigPath(configPath);
|
|
501
|
+
}
|
|
502
|
+
const currentWorkspacePath = getCurrentTeamClawAgentWorkspacePath(config);
|
|
503
|
+
return currentWorkspacePath || resolveDefaultTeamClawWorkspaceDir(configPath);
|
|
344
504
|
}
|
|
345
505
|
|
|
346
506
|
function dedupeStrings(values) {
|
|
@@ -362,10 +522,9 @@ function normalizeConfiguredRoleList(raw) {
|
|
|
362
522
|
|
|
363
523
|
function resolveDefaultProvisioningRoles(existingTeamClaw) {
|
|
364
524
|
const existingRoles = normalizeConfiguredRoleList(existingTeamClaw.workerProvisioningRoles);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return hasSameStringSet(existingRoles, LEGACY_DEFAULT_PROVISIONING_ROLES) ? [] : existingRoles;
|
|
525
|
+
return existingRoles.length > 0 && !hasSameStringSet(existingRoles, LEGACY_DEFAULT_PROVISIONING_ROLES)
|
|
526
|
+
? existingRoles
|
|
527
|
+
: [];
|
|
369
528
|
}
|
|
370
529
|
|
|
371
530
|
function extractModelOptions(config) {
|
|
@@ -601,7 +760,8 @@ function buildStartCommand(configPath) {
|
|
|
601
760
|
if (path.resolve(configPath) === path.resolve(defaultPath)) {
|
|
602
761
|
return "openclaw gateway run";
|
|
603
762
|
}
|
|
604
|
-
|
|
763
|
+
const stateDir = resolveOpenClawStateDirForConfigPath(configPath);
|
|
764
|
+
return `OPENCLAW_STATE_DIR=${shellEscape(stateDir)} OPENCLAW_CONFIG_PATH=${shellEscape(configPath)} openclaw gateway run`;
|
|
605
765
|
}
|
|
606
766
|
|
|
607
767
|
function shellEscape(value) {
|
|
@@ -643,23 +803,83 @@ function rankLanAddress(address) {
|
|
|
643
803
|
return 3;
|
|
644
804
|
}
|
|
645
805
|
|
|
806
|
+
function parseDefaultRouteInterface(text) {
|
|
807
|
+
const directMatch = String(text || "").match(/(?:^|\n)\s*interface:\s*(\S+)/i);
|
|
808
|
+
if (directMatch && directMatch[1]) {
|
|
809
|
+
return directMatch[1];
|
|
810
|
+
}
|
|
811
|
+
const devMatch = String(text || "").match(/(?:^|\n)default(?:\s+via\s+\S+)?\s+dev\s+(\S+)/i);
|
|
812
|
+
if (devMatch && devMatch[1]) {
|
|
813
|
+
return devMatch[1];
|
|
814
|
+
}
|
|
815
|
+
return "";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function resolveDefaultRouteInterface() {
|
|
819
|
+
const candidates = process.platform === "darwin"
|
|
820
|
+
? [
|
|
821
|
+
{ command: "route", args: ["-n", "get", "default"] },
|
|
822
|
+
{ command: "ip", args: ["route", "show", "default"] },
|
|
823
|
+
]
|
|
824
|
+
: [
|
|
825
|
+
{ command: "ip", args: ["route", "show", "default"] },
|
|
826
|
+
{ command: "route", args: ["-n", "get", "default"] },
|
|
827
|
+
];
|
|
828
|
+
for (const candidate of candidates) {
|
|
829
|
+
const result = spawnSync(candidate.command, candidate.args, { encoding: "utf8" });
|
|
830
|
+
if (result.status !== 0 || result.error) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const interfaceName = parseDefaultRouteInterface(result.stdout || "");
|
|
834
|
+
if (interfaceName) {
|
|
835
|
+
return interfaceName;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return "";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function isPrivateLanIpv4(address) {
|
|
842
|
+
if (String(address).startsWith("192.168.") || String(address).startsWith("10.")) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
const parts = String(address).split(".").map((value) => Number.parseInt(value, 10));
|
|
846
|
+
return parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
|
|
847
|
+
}
|
|
848
|
+
|
|
646
849
|
function listLanUiUrls(port) {
|
|
647
|
-
const urls = [];
|
|
648
850
|
const interfaces = os.networkInterfaces();
|
|
851
|
+
const seen = new Set();
|
|
852
|
+
const orderedAddresses = [];
|
|
853
|
+
const defaultRouteInterface = resolveDefaultRouteInterface();
|
|
854
|
+
if (defaultRouteInterface && Array.isArray(interfaces[defaultRouteInterface])) {
|
|
855
|
+
for (const record of interfaces[defaultRouteInterface] || []) {
|
|
856
|
+
if (!record || record.internal || record.family !== "IPv4") {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (!seen.has(record.address)) {
|
|
860
|
+
seen.add(record.address);
|
|
861
|
+
orderedAddresses.push(record.address);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const fallbackAddresses = [];
|
|
649
866
|
for (const records of Object.values(interfaces)) {
|
|
650
867
|
for (const record of records ?? []) {
|
|
651
868
|
if (!record || record.internal || record.family !== "IPv4") {
|
|
652
869
|
continue;
|
|
653
870
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
871
|
+
if (!seen.has(record.address)) {
|
|
872
|
+
seen.add(record.address);
|
|
873
|
+
fallbackAddresses.push(record.address);
|
|
874
|
+
}
|
|
658
875
|
}
|
|
659
876
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
877
|
+
fallbackAddresses.sort((left, right) => {
|
|
878
|
+
const leftScore = isPrivateLanIpv4(left) ? rankLanAddress(left) : 99;
|
|
879
|
+
const rightScore = isPrivateLanIpv4(right) ? rankLanAddress(right) : 99;
|
|
880
|
+
return leftScore - rightScore || left.localeCompare(right);
|
|
881
|
+
});
|
|
882
|
+
return orderedAddresses.concat(fallbackAddresses).map((address) => `http://${address}:${port}/ui`);
|
|
663
883
|
}
|
|
664
884
|
|
|
665
885
|
function installPluginWithCommand(command, args, env) {
|
|
@@ -718,56 +938,17 @@ function inspectInstalledPlugin(configPath) {
|
|
|
718
938
|
};
|
|
719
939
|
}
|
|
720
940
|
|
|
721
|
-
function
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "teamclaw-installer-pack-"));
|
|
725
|
-
const result = spawnSync(
|
|
726
|
-
"npm",
|
|
727
|
-
["pack", PACKAGE_ROOT, "--pack-destination", tempDir, "--json", "--ignore-scripts"],
|
|
728
|
-
{
|
|
729
|
-
env,
|
|
730
|
-
encoding: "utf8",
|
|
731
|
-
},
|
|
732
|
-
);
|
|
733
|
-
if (result.status !== 0 || result.error) {
|
|
734
|
-
const detail = result.error
|
|
735
|
-
? result.error.message
|
|
736
|
-
: (result.stderr || result.stdout || `exited with code ${result.status}`).trim();
|
|
737
|
-
throw new Error(detail || "npm pack failed");
|
|
738
|
-
}
|
|
739
|
-
const payload = JSON.parse(result.stdout);
|
|
740
|
-
const filename = Array.isArray(payload) && payload[0] && typeof payload[0].filename === "string"
|
|
741
|
-
? payload[0].filename.trim()
|
|
742
|
-
: "";
|
|
743
|
-
if (!filename) {
|
|
744
|
-
throw new Error("npm pack did not report a tarball filename");
|
|
745
|
-
}
|
|
746
|
-
const tarballPath = path.join(tempDir, filename);
|
|
747
|
-
if (!fsSync.existsSync(tarballPath)) {
|
|
748
|
-
throw new Error(`tarball was not created at ${tarballPath}`);
|
|
749
|
-
}
|
|
750
|
-
return {
|
|
751
|
-
ok: true,
|
|
752
|
-
tempDir,
|
|
753
|
-
tarballPath,
|
|
754
|
-
};
|
|
755
|
-
} catch (error) {
|
|
756
|
-
if (tempDir) {
|
|
757
|
-
fsSync.rmSync(tempDir, { recursive: true, force: true });
|
|
758
|
-
}
|
|
759
|
-
return {
|
|
760
|
-
ok: false,
|
|
761
|
-
error: error instanceof Error ? error.message : String(error),
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function attemptPluginUninstall({ configPath }) {
|
|
767
|
-
const env = {
|
|
941
|
+
function buildOpenClawCommandEnv(configPath) {
|
|
942
|
+
const stateDir = resolveOpenClawStateDirForConfigPath(configPath);
|
|
943
|
+
return {
|
|
768
944
|
...process.env,
|
|
945
|
+
OPENCLAW_STATE_DIR: stateDir,
|
|
769
946
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
770
947
|
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function attemptPluginUninstall({ configPath }) {
|
|
951
|
+
const env = buildOpenClawCommandEnv(configPath);
|
|
771
952
|
const candidates = [
|
|
772
953
|
{
|
|
773
954
|
label: "openclaw",
|
|
@@ -811,10 +992,7 @@ function attemptPluginUninstall({ configPath }) {
|
|
|
811
992
|
}
|
|
812
993
|
|
|
813
994
|
function attemptPluginInstall({ configPath }) {
|
|
814
|
-
const env =
|
|
815
|
-
...process.env,
|
|
816
|
-
OPENCLAW_CONFIG_PATH: configPath,
|
|
817
|
-
};
|
|
995
|
+
const env = buildOpenClawCommandEnv(configPath);
|
|
818
996
|
const installedPlugin = inspectInstalledPlugin(configPath);
|
|
819
997
|
if (installedPlugin?.version === PACKAGE_VERSION) {
|
|
820
998
|
console.log(
|
|
@@ -839,89 +1017,88 @@ function attemptPluginInstall({ configPath }) {
|
|
|
839
1017
|
};
|
|
840
1018
|
}
|
|
841
1019
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
candidates.push(
|
|
1020
|
+
console.log(
|
|
1021
|
+
`\nTeamClaw uses host-level orchestration capabilities, so OpenClaw requires ${DANGEROUS_INSTALL_FLAG} during plugin installation.`,
|
|
1022
|
+
);
|
|
1023
|
+
const candidates = [
|
|
1024
|
+
{
|
|
1025
|
+
label: "openclaw (local package directory)",
|
|
1026
|
+
command: "openclaw",
|
|
1027
|
+
args: ["plugins", "install", DANGEROUS_INSTALL_FLAG, PACKAGE_ROOT],
|
|
1028
|
+
targetDescription: PACKAGE_ROOT,
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
label: "npm exec fallback (local package directory)",
|
|
1032
|
+
command: "npm",
|
|
1033
|
+
args: [
|
|
1034
|
+
"exec",
|
|
1035
|
+
"-y",
|
|
1036
|
+
"openclaw@latest",
|
|
1037
|
+
"--",
|
|
1038
|
+
"plugins",
|
|
1039
|
+
"install",
|
|
1040
|
+
DANGEROUS_INSTALL_FLAG,
|
|
1041
|
+
PACKAGE_ROOT,
|
|
1042
|
+
],
|
|
1043
|
+
targetDescription: PACKAGE_ROOT,
|
|
1044
|
+
},
|
|
868
1045
|
{
|
|
869
1046
|
label: "openclaw (exact version fallback)",
|
|
870
1047
|
command: "openclaw",
|
|
871
|
-
args: ["plugins", "install", PACKAGE_INSTALL_SPEC],
|
|
1048
|
+
args: ["plugins", "install", DANGEROUS_INSTALL_FLAG, PACKAGE_INSTALL_SPEC],
|
|
872
1049
|
targetDescription: PACKAGE_INSTALL_SPEC,
|
|
873
1050
|
},
|
|
874
1051
|
{
|
|
875
1052
|
label: "npm exec fallback (exact version fallback)",
|
|
876
1053
|
command: "npm",
|
|
877
|
-
args: [
|
|
1054
|
+
args: [
|
|
1055
|
+
"exec",
|
|
1056
|
+
"-y",
|
|
1057
|
+
"openclaw@latest",
|
|
1058
|
+
"--",
|
|
1059
|
+
"plugins",
|
|
1060
|
+
"install",
|
|
1061
|
+
DANGEROUS_INSTALL_FLAG,
|
|
1062
|
+
PACKAGE_INSTALL_SPEC,
|
|
1063
|
+
],
|
|
878
1064
|
targetDescription: PACKAGE_INSTALL_SPEC,
|
|
879
1065
|
},
|
|
880
|
-
|
|
1066
|
+
];
|
|
881
1067
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
const errorCode = result.error && typeof result.error === "object" ? result.error.code : "";
|
|
895
|
-
const detail = result.error
|
|
896
|
-
? result.error.message
|
|
897
|
-
: result.signal
|
|
898
|
-
? `terminated by signal ${result.signal}`
|
|
899
|
-
: `exited with code ${result.status}`;
|
|
900
|
-
failures.push(`${candidate.label} failed: ${detail}`);
|
|
901
|
-
if (errorCode === "ENOENT" && index < candidates.length - 1) {
|
|
902
|
-
console.log(`${candidate.command} was not found. Trying the next install fallback...`);
|
|
903
|
-
continue;
|
|
904
|
-
}
|
|
905
|
-
if (index < candidates.length - 1) {
|
|
906
|
-
console.log(`${candidate.label} failed (${detail}). Trying the next install fallback...`);
|
|
907
|
-
}
|
|
1068
|
+
const failures = [];
|
|
1069
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
1070
|
+
const candidate = candidates[index];
|
|
1071
|
+
console.log(`\nInstalling ${candidate.targetDescription} with ${candidate.label}...`);
|
|
1072
|
+
const result = installPluginWithCommand(candidate.command, candidate.args, env);
|
|
1073
|
+
if (result.status === 0 && !result.error) {
|
|
1074
|
+
return {
|
|
1075
|
+
ok: true,
|
|
1076
|
+
method: candidate.label,
|
|
1077
|
+
};
|
|
908
1078
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1079
|
+
const errorCode = result.error && typeof result.error === "object" ? result.error.code : "";
|
|
1080
|
+
const detail = result.error
|
|
1081
|
+
? result.error.message
|
|
1082
|
+
: result.signal
|
|
1083
|
+
? `terminated by signal ${result.signal}`
|
|
1084
|
+
: `exited with code ${result.status}`;
|
|
1085
|
+
failures.push(`${candidate.label} failed: ${detail}`);
|
|
1086
|
+
if (errorCode === "ENOENT" && index < candidates.length - 1) {
|
|
1087
|
+
console.log(`${candidate.command} was not found. Trying the next install fallback...`);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (index < candidates.length - 1) {
|
|
1091
|
+
console.log(`${candidate.label} failed (${detail}). Trying the next install fallback...`);
|
|
916
1092
|
}
|
|
917
1093
|
}
|
|
1094
|
+
return {
|
|
1095
|
+
ok: false,
|
|
1096
|
+
error: failures.length > 0 ? failures.join("; ") : "No install command was available.",
|
|
1097
|
+
};
|
|
918
1098
|
}
|
|
919
1099
|
|
|
920
1100
|
function attemptGatewayRestart({ configPath }) {
|
|
921
|
-
const env =
|
|
922
|
-
...process.env,
|
|
923
|
-
OPENCLAW_CONFIG_PATH: configPath,
|
|
924
|
-
};
|
|
1101
|
+
const env = buildOpenClawCommandEnv(configPath);
|
|
925
1102
|
const candidates = [
|
|
926
1103
|
{
|
|
927
1104
|
label: "openclaw",
|
|
@@ -963,7 +1140,7 @@ function attemptGatewayRestart({ configPath }) {
|
|
|
963
1140
|
|
|
964
1141
|
async function waitForControllerHealth(port) {
|
|
965
1142
|
const url = `http://127.0.0.1:${port}/api/v1/health`;
|
|
966
|
-
const deadline = Date.now() +
|
|
1143
|
+
const deadline = Date.now() + 120_000;
|
|
967
1144
|
let lastError = "";
|
|
968
1145
|
while (Date.now() < deadline) {
|
|
969
1146
|
try {
|
|
@@ -1005,7 +1182,14 @@ async function waitForControllerHealth(port) {
|
|
|
1005
1182
|
}
|
|
1006
1183
|
lastError = "unexpected health payload";
|
|
1007
1184
|
} else {
|
|
1008
|
-
|
|
1185
|
+
try {
|
|
1186
|
+
const payload = JSON.parse(response.body);
|
|
1187
|
+
lastError = payload?.status
|
|
1188
|
+
? `HTTP ${response.statusCode} (${payload.status})`
|
|
1189
|
+
: `HTTP ${response.statusCode}`;
|
|
1190
|
+
} catch {
|
|
1191
|
+
lastError = `HTTP ${response.statusCode}`;
|
|
1192
|
+
}
|
|
1009
1193
|
}
|
|
1010
1194
|
} catch (error) {
|
|
1011
1195
|
lastError = error instanceof Error ? error.message : String(error);
|
|
@@ -1015,16 +1199,17 @@ async function waitForControllerHealth(port) {
|
|
|
1015
1199
|
return {
|
|
1016
1200
|
ok: false,
|
|
1017
1201
|
url,
|
|
1018
|
-
error: lastError || "timed out after
|
|
1202
|
+
error: lastError || "timed out after 120s",
|
|
1019
1203
|
};
|
|
1020
1204
|
}
|
|
1021
1205
|
|
|
1022
|
-
async function collectInstallChoices(configPath, config, prompter) {
|
|
1206
|
+
async function collectInstallChoices(configPath, config, prompter, options) {
|
|
1023
1207
|
const existingTeamClaw = getExistingTeamClawConfig(config);
|
|
1024
1208
|
const existingMode = typeof existingTeamClaw.mode === "string" ? existingTeamClaw.mode.trim() : "";
|
|
1025
1209
|
const existingProvisioningType =
|
|
1026
1210
|
typeof existingTeamClaw.workerProvisioningType === "string" ? existingTeamClaw.workerProvisioningType.trim() : "";
|
|
1027
|
-
|
|
1211
|
+
const agentIsolationMode = options.agentMode || resolveCurrentAgentIsolationMode(config);
|
|
1212
|
+
let modeDefault = "controller-process";
|
|
1028
1213
|
if (existingMode === "worker") {
|
|
1029
1214
|
modeDefault = "worker";
|
|
1030
1215
|
} else if (existingMode === "controller") {
|
|
@@ -1034,12 +1219,12 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1034
1219
|
modeDefault = "controller-kubernetes";
|
|
1035
1220
|
} else if (existingProvisioningType === "process") {
|
|
1036
1221
|
modeDefault = "controller-process";
|
|
1037
|
-
} else
|
|
1222
|
+
} else {
|
|
1038
1223
|
modeDefault = "controller-manual";
|
|
1039
1224
|
}
|
|
1040
1225
|
}
|
|
1041
1226
|
|
|
1042
|
-
const installMode = await prompter.select({
|
|
1227
|
+
const installMode = options.installMode || await prompter.select({
|
|
1043
1228
|
message: "Choose an installation mode",
|
|
1044
1229
|
options: INSTALL_MODE_OPTIONS,
|
|
1045
1230
|
defaultValue: modeDefault,
|
|
@@ -1063,24 +1248,28 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1063
1248
|
|
|
1064
1249
|
const teamName = await prompter.text({
|
|
1065
1250
|
message: "Team name",
|
|
1066
|
-
defaultValue:
|
|
1251
|
+
defaultValue: options.teamName || (
|
|
1067
1252
|
typeof existingTeamClaw.teamName === "string" && existingTeamClaw.teamName.trim()
|
|
1068
1253
|
? existingTeamClaw.teamName.trim()
|
|
1069
|
-
: DEFAULT_TEAM_NAME
|
|
1254
|
+
: DEFAULT_TEAM_NAME
|
|
1255
|
+
),
|
|
1070
1256
|
});
|
|
1071
1257
|
const workspacePath = expandUserPath(await prompter.text({
|
|
1072
|
-
message:
|
|
1073
|
-
|
|
1258
|
+
message: agentIsolationMode === "main"
|
|
1259
|
+
? "Main OpenClaw workspace directory"
|
|
1260
|
+
: "TeamClaw dedicated workspace directory",
|
|
1261
|
+
defaultValue: resolveInstallerWorkspaceDefault(configPath, config, agentIsolationMode),
|
|
1074
1262
|
}));
|
|
1075
1263
|
|
|
1076
1264
|
if (installMode === "worker") {
|
|
1077
1265
|
const workerRole = await prompter.select({
|
|
1078
1266
|
message: "Choose the worker role for this node",
|
|
1079
1267
|
options: ROLE_OPTIONS,
|
|
1080
|
-
defaultValue:
|
|
1268
|
+
defaultValue: options.workerRole || (
|
|
1081
1269
|
typeof existingTeamClaw.role === "string" && existingTeamClaw.role.trim()
|
|
1082
1270
|
? existingTeamClaw.role.trim()
|
|
1083
|
-
: "developer"
|
|
1271
|
+
: "developer"
|
|
1272
|
+
),
|
|
1084
1273
|
});
|
|
1085
1274
|
const workerPort = await prompter.number({
|
|
1086
1275
|
message: "Worker API port",
|
|
@@ -1091,24 +1280,60 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1091
1280
|
min: 1,
|
|
1092
1281
|
max: 65535,
|
|
1093
1282
|
});
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1283
|
+
const existingControllerUrl =
|
|
1284
|
+
typeof existingTeamClaw.controllerUrl === "string" && existingTeamClaw.controllerUrl.trim()
|
|
1285
|
+
? existingTeamClaw.controllerUrl.trim()
|
|
1286
|
+
: "";
|
|
1287
|
+
let workerControllerMode = existingControllerUrl ? "manual" : "mdns";
|
|
1288
|
+
let mdnsCapability = { available: true, reason: "" };
|
|
1289
|
+
if (options.controllerUrl) {
|
|
1290
|
+
workerControllerMode = "manual";
|
|
1291
|
+
} else if (!prompter.yes) {
|
|
1292
|
+
mdnsCapability = await detectMdnsCapability();
|
|
1293
|
+
if (mdnsCapability.available) {
|
|
1294
|
+
prompter.note("mDNS discovery looks available on this machine.");
|
|
1295
|
+
prompter.note("Use LAN auto-registration only when the controller is reachable on the same local network. Otherwise enter the controller URL manually.");
|
|
1296
|
+
workerControllerMode = await prompter.select({
|
|
1297
|
+
message: "How should this worker find its controller?",
|
|
1298
|
+
options: [
|
|
1299
|
+
{
|
|
1300
|
+
value: "mdns",
|
|
1301
|
+
label: "Use LAN auto-registration via mDNS",
|
|
1302
|
+
hint: "Best when worker and controller are on the same LAN.",
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
value: "manual",
|
|
1306
|
+
label: "Enter controller URL manually",
|
|
1307
|
+
hint: "Required when controller is outside the LAN or mDNS is blocked.",
|
|
1308
|
+
},
|
|
1309
|
+
],
|
|
1310
|
+
defaultValue: existingControllerUrl ? "manual" : "mdns",
|
|
1311
|
+
});
|
|
1312
|
+
} else {
|
|
1313
|
+
prompter.note(`mDNS auto-registration is not available on this machine (${mdnsCapability.reason || "probe failed"}).`);
|
|
1314
|
+
workerControllerMode = "manual";
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const controllerUrl = workerControllerMode === "manual"
|
|
1318
|
+
? await prompter.text({
|
|
1319
|
+
message: "Controller URL",
|
|
1320
|
+
defaultValue: options.controllerUrl || existingControllerUrl || "http://127.0.0.1:9527",
|
|
1321
|
+
validate: (value) => value.startsWith("http://") || value.startsWith("https://")
|
|
1322
|
+
? ""
|
|
1323
|
+
: 'Controller URL must start with "http://" or "https://".',
|
|
1324
|
+
})
|
|
1325
|
+
: "";
|
|
1104
1326
|
return {
|
|
1105
1327
|
installMode,
|
|
1328
|
+
agentIsolationMode,
|
|
1106
1329
|
selectedModel,
|
|
1107
1330
|
teamName,
|
|
1108
1331
|
workspacePath,
|
|
1109
1332
|
workerRole,
|
|
1110
1333
|
workerPort,
|
|
1111
1334
|
controllerUrl,
|
|
1335
|
+
workerControllerMode,
|
|
1336
|
+
mdnsAvailable: mdnsCapability.available,
|
|
1112
1337
|
};
|
|
1113
1338
|
}
|
|
1114
1339
|
|
|
@@ -1122,27 +1347,10 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1122
1347
|
max: 65535,
|
|
1123
1348
|
});
|
|
1124
1349
|
|
|
1125
|
-
if (installMode === "single-local") {
|
|
1126
|
-
const localRoles = await promptRoleList(
|
|
1127
|
-
prompter,
|
|
1128
|
-
"Local roles to run in this OpenClaw instance (comma-separated)",
|
|
1129
|
-
Array.isArray(existingTeamClaw.localRoles) && existingTeamClaw.localRoles.length > 0
|
|
1130
|
-
? existingTeamClaw.localRoles
|
|
1131
|
-
: DEFAULT_LOCAL_ROLES,
|
|
1132
|
-
);
|
|
1133
|
-
return {
|
|
1134
|
-
installMode,
|
|
1135
|
-
selectedModel,
|
|
1136
|
-
teamName,
|
|
1137
|
-
workspacePath,
|
|
1138
|
-
controllerPort,
|
|
1139
|
-
localRoles,
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1350
|
if (installMode === "controller-manual") {
|
|
1144
1351
|
return {
|
|
1145
1352
|
installMode,
|
|
1353
|
+
agentIsolationMode,
|
|
1146
1354
|
selectedModel,
|
|
1147
1355
|
teamName,
|
|
1148
1356
|
workspacePath,
|
|
@@ -1168,6 +1376,7 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1168
1376
|
if (installMode === "controller-process") {
|
|
1169
1377
|
return {
|
|
1170
1378
|
installMode,
|
|
1379
|
+
agentIsolationMode,
|
|
1171
1380
|
selectedModel,
|
|
1172
1381
|
teamName,
|
|
1173
1382
|
workspacePath,
|
|
@@ -1205,6 +1414,7 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1205
1414
|
});
|
|
1206
1415
|
return {
|
|
1207
1416
|
installMode,
|
|
1417
|
+
agentIsolationMode,
|
|
1208
1418
|
selectedModel,
|
|
1209
1419
|
teamName,
|
|
1210
1420
|
workspacePath,
|
|
@@ -1260,6 +1470,7 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1260
1470
|
});
|
|
1261
1471
|
return {
|
|
1262
1472
|
installMode,
|
|
1473
|
+
agentIsolationMode,
|
|
1263
1474
|
selectedModel,
|
|
1264
1475
|
teamName,
|
|
1265
1476
|
workspacePath,
|
|
@@ -1274,7 +1485,82 @@ async function collectInstallChoices(configPath, config, prompter) {
|
|
|
1274
1485
|
};
|
|
1275
1486
|
}
|
|
1276
1487
|
|
|
1277
|
-
function
|
|
1488
|
+
function upsertAgentListEntry(agents, agentId, update) {
|
|
1489
|
+
const list = Array.isArray(agents.list) ? agents.list.filter(isRecord) : [];
|
|
1490
|
+
const existingIndex = list.findIndex((entry) => entry.id === agentId);
|
|
1491
|
+
const nextEntry = {
|
|
1492
|
+
...(existingIndex >= 0 ? list[existingIndex] : {}),
|
|
1493
|
+
id: agentId,
|
|
1494
|
+
...update,
|
|
1495
|
+
};
|
|
1496
|
+
if (existingIndex >= 0) {
|
|
1497
|
+
list[existingIndex] = nextEntry;
|
|
1498
|
+
} else {
|
|
1499
|
+
list.push(nextEntry);
|
|
1500
|
+
}
|
|
1501
|
+
agents.list = list;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function removeAgentListEntry(agents, agentId) {
|
|
1505
|
+
if (!Array.isArray(agents.list)) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
agents.list = agents.list.filter((entry) => !isRecord(entry) || entry.id !== agentId);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function applyTeamClawHostRuntimeDefaults(next) {
|
|
1512
|
+
const commands = ensureRecord(next, "commands");
|
|
1513
|
+
if (typeof commands.native !== "string" || !commands.native.trim()) {
|
|
1514
|
+
commands.native = TEAMCLAW_RECOMMENDED_COMMAND_MODE;
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof commands.nativeSkills !== "string" || !commands.nativeSkills.trim()) {
|
|
1517
|
+
commands.nativeSkills = TEAMCLAW_RECOMMENDED_COMMAND_MODE;
|
|
1518
|
+
}
|
|
1519
|
+
if (typeof commands.restart !== "boolean") {
|
|
1520
|
+
commands.restart = true;
|
|
1521
|
+
}
|
|
1522
|
+
if (typeof commands.ownerDisplay !== "string" || !commands.ownerDisplay.trim()) {
|
|
1523
|
+
commands.ownerDisplay = "raw";
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const tools = ensureRecord(next, "tools");
|
|
1527
|
+
const exec = ensureRecord(tools, "exec");
|
|
1528
|
+
if (typeof exec.security !== "string" || !exec.security.trim()) {
|
|
1529
|
+
exec.security = TEAMCLAW_RECOMMENDED_EXEC_SECURITY;
|
|
1530
|
+
}
|
|
1531
|
+
if (typeof exec.ask !== "string" || !exec.ask.trim()) {
|
|
1532
|
+
exec.ask = TEAMCLAW_RECOMMENDED_EXEC_ASK;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function collectTeamClawHostRuntimeWarnings(config) {
|
|
1537
|
+
const warnings = [];
|
|
1538
|
+
const commands = isRecord(config.commands) ? config.commands : null;
|
|
1539
|
+
const tools = isRecord(config.tools) ? config.tools : null;
|
|
1540
|
+
const exec = tools && isRecord(tools.exec) ? tools.exec : null;
|
|
1541
|
+
|
|
1542
|
+
const execSecurity = typeof exec?.security === "string" ? exec.security.trim() : "";
|
|
1543
|
+
if (execSecurity && execSecurity !== TEAMCLAW_RECOMMENDED_EXEC_SECURITY) {
|
|
1544
|
+
warnings.push(
|
|
1545
|
+
`tools.exec.security is set to "${execSecurity}" (TeamClaw works best with "${TEAMCLAW_RECOMMENDED_EXEC_SECURITY}"; stricter settings can block task execution).`,
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const execAsk = typeof exec?.ask === "string" ? exec.ask.trim() : "";
|
|
1550
|
+
if (execAsk && execAsk !== TEAMCLAW_RECOMMENDED_EXEC_ASK) {
|
|
1551
|
+
warnings.push(
|
|
1552
|
+
`tools.exec.ask is set to "${execAsk}" (TeamClaw works best with "${TEAMCLAW_RECOMMENDED_EXEC_ASK}"; stricter settings can trigger repeated approvals).`,
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (commands?.restart === false) {
|
|
1557
|
+
warnings.push('commands.restart is disabled, so the installer cannot auto-restart OpenClaw after config changes.');
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
return warnings;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function applyInstallerChoices(config, choices, configPath) {
|
|
1278
1564
|
const next = isRecord(config) ? structuredClone(config) : {};
|
|
1279
1565
|
const gateway = ensureRecord(next, "gateway");
|
|
1280
1566
|
if (typeof gateway.port !== "number" || gateway.port < 1) {
|
|
@@ -1292,9 +1578,18 @@ function applyInstallerChoices(config, choices) {
|
|
|
1292
1578
|
if (choices.selectedModel) {
|
|
1293
1579
|
agentDefaults.model = applySelectedModel(agentDefaults.model, choices.selectedModel);
|
|
1294
1580
|
}
|
|
1295
|
-
if (choices.workspacePath) {
|
|
1581
|
+
if (choices.agentIsolationMode === "main" && choices.workspacePath) {
|
|
1296
1582
|
agentDefaults.workspace = choices.workspacePath;
|
|
1297
1583
|
}
|
|
1584
|
+
if (choices.agentIsolationMode === "independent") {
|
|
1585
|
+
upsertAgentListEntry(agents, TEAMCLAW_AGENT_ID, {
|
|
1586
|
+
workspace: choices.workspacePath,
|
|
1587
|
+
agentDir: resolveDefaultTeamClawAgentDirForConfigPath(configPath),
|
|
1588
|
+
...(agentDefaults.model != null ? { model: cloneJsonValue(agentDefaults.model) } : {}),
|
|
1589
|
+
});
|
|
1590
|
+
} else {
|
|
1591
|
+
removeAgentListEntry(agents, TEAMCLAW_AGENT_ID);
|
|
1592
|
+
}
|
|
1298
1593
|
const existingTimeout = typeof agentDefaults.timeoutSeconds === "number"
|
|
1299
1594
|
? agentDefaults.timeoutSeconds
|
|
1300
1595
|
: 0;
|
|
@@ -1320,6 +1615,7 @@ function applyInstallerChoices(config, choices) {
|
|
|
1320
1615
|
typeof teamclawConfig.taskTimeoutMs === "number" ? teamclawConfig.taskTimeoutMs : 0,
|
|
1321
1616
|
DEFAULT_TASK_TIMEOUT_MS,
|
|
1322
1617
|
);
|
|
1618
|
+
teamclawConfig.processModel = "multi";
|
|
1323
1619
|
teamclawConfig.gitEnabled = typeof teamclawConfig.gitEnabled === "boolean" ? teamclawConfig.gitEnabled : true;
|
|
1324
1620
|
teamclawConfig.gitDefaultBranch = typeof teamclawConfig.gitDefaultBranch === "string" && teamclawConfig.gitDefaultBranch.trim()
|
|
1325
1621
|
? teamclawConfig.gitDefaultBranch.trim()
|
|
@@ -1330,6 +1626,7 @@ function applyInstallerChoices(config, choices) {
|
|
|
1330
1626
|
teamclawConfig.gitAuthorEmail = typeof teamclawConfig.gitAuthorEmail === "string" && teamclawConfig.gitAuthorEmail.trim()
|
|
1331
1627
|
? teamclawConfig.gitAuthorEmail.trim()
|
|
1332
1628
|
: "teamclaw@local";
|
|
1629
|
+
teamclawConfig.agentIsolationMode = choices.agentIsolationMode;
|
|
1333
1630
|
|
|
1334
1631
|
teamclawConfig.workerProvisioningMinPerRole = 0;
|
|
1335
1632
|
teamclawConfig.workerProvisioningIdleTtlMs = typeof teamclawConfig.workerProvisioningIdleTtlMs === "number" &&
|
|
@@ -1373,8 +1670,8 @@ function applyInstallerChoices(config, choices) {
|
|
|
1373
1670
|
teamclawConfig.port = choices.workerPort;
|
|
1374
1671
|
teamclawConfig.role = choices.workerRole;
|
|
1375
1672
|
teamclawConfig.controllerUrl = choices.controllerUrl;
|
|
1376
|
-
teamclawConfig.localRoles = [];
|
|
1377
1673
|
teamclawConfig.workerProvisioningType = "none";
|
|
1674
|
+
teamclawConfig.workerProvisioningDisabled = true;
|
|
1378
1675
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1379
1676
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1380
1677
|
teamclawConfig.workerProvisioningMaxPerRole = 1;
|
|
@@ -1392,23 +1689,9 @@ function applyInstallerChoices(config, choices) {
|
|
|
1392
1689
|
teamclawConfig.controllerUrl = "";
|
|
1393
1690
|
delete teamclawConfig.role;
|
|
1394
1691
|
|
|
1395
|
-
if (choices.installMode === "
|
|
1396
|
-
teamclawConfig.localRoles = choices.localRoles;
|
|
1397
|
-
teamclawConfig.workerProvisioningType = "none";
|
|
1398
|
-
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1399
|
-
teamclawConfig.workerProvisioningRoles = [];
|
|
1400
|
-
teamclawConfig.workerProvisioningMaxPerRole = 1;
|
|
1401
|
-
teamclawConfig.workerProvisioningImage = "";
|
|
1402
|
-
teamclawConfig.workerProvisioningPassEnv = [];
|
|
1403
|
-
teamclawConfig.workerProvisioningExtraEnv = {};
|
|
1404
|
-
teamclawConfig.workerProvisioningWorkspaceRoot = "";
|
|
1405
|
-
teamclawConfig.workerProvisioningDockerWorkspaceVolume = "";
|
|
1406
|
-
teamclawConfig.workerProvisioningKubernetesNamespace = "default";
|
|
1407
|
-
teamclawConfig.workerProvisioningKubernetesServiceAccount = "";
|
|
1408
|
-
teamclawConfig.workerProvisioningKubernetesWorkspacePersistentVolumeClaim = "";
|
|
1409
|
-
} else if (choices.installMode === "controller-manual") {
|
|
1410
|
-
teamclawConfig.localRoles = [];
|
|
1692
|
+
if (choices.installMode === "controller-manual") {
|
|
1411
1693
|
teamclawConfig.workerProvisioningType = "none";
|
|
1694
|
+
teamclawConfig.workerProvisioningDisabled = true;
|
|
1412
1695
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1413
1696
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1414
1697
|
teamclawConfig.workerProvisioningMaxPerRole = 1;
|
|
@@ -1421,8 +1704,8 @@ function applyInstallerChoices(config, choices) {
|
|
|
1421
1704
|
teamclawConfig.workerProvisioningKubernetesServiceAccount = "";
|
|
1422
1705
|
teamclawConfig.workerProvisioningKubernetesWorkspacePersistentVolumeClaim = "";
|
|
1423
1706
|
} else if (choices.installMode === "controller-process") {
|
|
1424
|
-
teamclawConfig.localRoles = [];
|
|
1425
1707
|
teamclawConfig.workerProvisioningType = "process";
|
|
1708
|
+
teamclawConfig.workerProvisioningDisabled = false;
|
|
1426
1709
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1427
1710
|
teamclawConfig.workerProvisioningRoles = choices.provisioningRoles;
|
|
1428
1711
|
teamclawConfig.workerProvisioningMaxPerRole = choices.maxPerRole;
|
|
@@ -1435,8 +1718,8 @@ function applyInstallerChoices(config, choices) {
|
|
|
1435
1718
|
teamclawConfig.workerProvisioningKubernetesServiceAccount = "";
|
|
1436
1719
|
teamclawConfig.workerProvisioningKubernetesWorkspacePersistentVolumeClaim = "";
|
|
1437
1720
|
} else if (choices.installMode === "controller-docker") {
|
|
1438
|
-
teamclawConfig.localRoles = [];
|
|
1439
1721
|
teamclawConfig.workerProvisioningType = "docker";
|
|
1722
|
+
teamclawConfig.workerProvisioningDisabled = false;
|
|
1440
1723
|
teamclawConfig.workerProvisioningControllerUrl = choices.controllerUrl;
|
|
1441
1724
|
teamclawConfig.workerProvisioningRoles = choices.provisioningRoles;
|
|
1442
1725
|
teamclawConfig.workerProvisioningMaxPerRole = choices.maxPerRole;
|
|
@@ -1449,8 +1732,8 @@ function applyInstallerChoices(config, choices) {
|
|
|
1449
1732
|
teamclawConfig.workerProvisioningKubernetesServiceAccount = "";
|
|
1450
1733
|
teamclawConfig.workerProvisioningKubernetesWorkspacePersistentVolumeClaim = "";
|
|
1451
1734
|
} else if (choices.installMode === "controller-kubernetes") {
|
|
1452
|
-
teamclawConfig.localRoles = [];
|
|
1453
1735
|
teamclawConfig.workerProvisioningType = "kubernetes";
|
|
1736
|
+
teamclawConfig.workerProvisioningDisabled = false;
|
|
1454
1737
|
teamclawConfig.workerProvisioningControllerUrl = choices.controllerUrl;
|
|
1455
1738
|
teamclawConfig.workerProvisioningRoles = choices.provisioningRoles;
|
|
1456
1739
|
teamclawConfig.workerProvisioningMaxPerRole = choices.maxPerRole;
|
|
@@ -1473,6 +1756,7 @@ function applyInstallerChoices(config, choices) {
|
|
|
1473
1756
|
plugins.entries = entries;
|
|
1474
1757
|
next.plugins = plugins;
|
|
1475
1758
|
next.agents = agents;
|
|
1759
|
+
applyTeamClawHostRuntimeDefaults(next);
|
|
1476
1760
|
next.gateway = gateway;
|
|
1477
1761
|
return next;
|
|
1478
1762
|
}
|
|
@@ -1481,11 +1765,18 @@ function buildSummaryLines(params) {
|
|
|
1481
1765
|
const lines = [
|
|
1482
1766
|
`Config path: ${params.configPath}`,
|
|
1483
1767
|
`Install mode: ${params.choices.installMode}`,
|
|
1768
|
+
`Agent isolation: ${params.choices.agentIsolationMode}`,
|
|
1484
1769
|
`Workspace: ${params.choices.workspacePath}`,
|
|
1485
1770
|
];
|
|
1486
1771
|
if (params.choices.selectedModel) {
|
|
1487
1772
|
lines.push(`Default model: ${params.choices.selectedModel}`);
|
|
1488
1773
|
}
|
|
1774
|
+
const effectiveTeamClawModel = resolveModelPrimaryValue(resolveEffectiveTeamClawModel(params.nextConfig));
|
|
1775
|
+
if (effectiveTeamClawModel) {
|
|
1776
|
+
lines.push(`TeamClaw agent model: ${effectiveTeamClawModel}`);
|
|
1777
|
+
} else {
|
|
1778
|
+
lines.push("Warning: TeamClaw has no effective model configured yet, so it can start but cannot work until a host model is configured.");
|
|
1779
|
+
}
|
|
1489
1780
|
if (params.backupPath) {
|
|
1490
1781
|
lines.push(`Backup: ${params.backupPath}`);
|
|
1491
1782
|
}
|
|
@@ -1508,6 +1799,14 @@ function buildSummaryLines(params) {
|
|
|
1508
1799
|
} else if (params.controllerHealthStatus === "failed") {
|
|
1509
1800
|
lines.push(`Controller health: ${params.controllerHealthError} (${params.controllerHealthUrl})`);
|
|
1510
1801
|
}
|
|
1802
|
+
if (params.teamclawAuthBootstrap?.copied) {
|
|
1803
|
+
lines.push(`TeamClaw auth bootstrap: copied from ${params.teamclawAuthBootstrap.sourcePath}`);
|
|
1804
|
+
} else if (params.teamclawAuthBootstrap?.warning) {
|
|
1805
|
+
lines.push(`Warning: ${params.teamclawAuthBootstrap.warning}`);
|
|
1806
|
+
}
|
|
1807
|
+
lines.push(
|
|
1808
|
+
`Host exec defaults: security=${TEAMCLAW_RECOMMENDED_EXEC_SECURITY}, ask=${TEAMCLAW_RECOMMENDED_EXEC_ASK} (applied when missing)`,
|
|
1809
|
+
);
|
|
1511
1810
|
lines.push(`Start command: ${buildStartCommand(params.configPath)}`);
|
|
1512
1811
|
|
|
1513
1812
|
if (isControllerInstallMode(params.choices.installMode)) {
|
|
@@ -1534,7 +1833,15 @@ function buildSummaryLines(params) {
|
|
|
1534
1833
|
}
|
|
1535
1834
|
if (params.choices.installMode === "worker") {
|
|
1536
1835
|
lines.push(`Worker role: ${params.choices.workerRole}`);
|
|
1537
|
-
|
|
1836
|
+
if (params.choices.controllerUrl) {
|
|
1837
|
+
lines.push(`Controller URL: ${params.choices.controllerUrl}`);
|
|
1838
|
+
} else {
|
|
1839
|
+
lines.push("Controller discovery: mDNS auto-registration");
|
|
1840
|
+
lines.push("Note: mDNS auto-registration only works when the controller is reachable on the same LAN.");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
for (const warning of params.hostRuntimeWarnings ?? []) {
|
|
1844
|
+
lines.push(`Warning: ${warning}`);
|
|
1538
1845
|
}
|
|
1539
1846
|
return lines;
|
|
1540
1847
|
}
|
|
@@ -1591,14 +1898,18 @@ async function runInstall(options) {
|
|
|
1591
1898
|
}
|
|
1592
1899
|
|
|
1593
1900
|
const config = await readOpenClawConfig(configPath);
|
|
1594
|
-
const choices = await collectInstallChoices(configPath, config, prompter);
|
|
1595
|
-
const nextConfig = applyInstallerChoices(config, choices);
|
|
1901
|
+
const choices = await collectInstallChoices(configPath, config, prompter, options);
|
|
1902
|
+
const nextConfig = applyInstallerChoices(config, choices, configPath);
|
|
1596
1903
|
|
|
1597
1904
|
if (options.dryRun) {
|
|
1598
1905
|
prompter.note("\nDry run only; no files were written.");
|
|
1599
1906
|
} else {
|
|
1600
1907
|
await writeConfig(configPath, nextConfig);
|
|
1601
1908
|
}
|
|
1909
|
+
const teamclawAuthBootstrap = options.dryRun
|
|
1910
|
+
? { copied: false, sourcePath: "", targetPath: "", warning: "" }
|
|
1911
|
+
: await bootstrapTeamClawAgentAuth(configPath, nextConfig);
|
|
1912
|
+
const hostRuntimeWarnings = collectTeamClawHostRuntimeWarnings(nextConfig);
|
|
1602
1913
|
|
|
1603
1914
|
let gatewayRestartStatus = "skipped";
|
|
1604
1915
|
let gatewayRestartMethod = "";
|
|
@@ -1627,6 +1938,9 @@ async function runInstall(options) {
|
|
|
1627
1938
|
configPath,
|
|
1628
1939
|
choices,
|
|
1629
1940
|
backupPath,
|
|
1941
|
+
nextConfig,
|
|
1942
|
+
teamclawAuthBootstrap,
|
|
1943
|
+
hostRuntimeWarnings,
|
|
1630
1944
|
pluginInstallStatus,
|
|
1631
1945
|
pluginInstallMethod,
|
|
1632
1946
|
pluginInstallError,
|