@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.
Files changed (46) hide show
  1. package/README.md +52 -8
  2. package/cli.mjs +538 -224
  3. package/index.ts +76 -27
  4. package/openclaw.plugin.json +53 -28
  5. package/package.json +5 -2
  6. package/skills/teamclaw/SKILL.md +213 -0
  7. package/skills/teamclaw/references/api-quick-ref.md +117 -0
  8. package/skills/teamclaw-setup/SKILL.md +81 -0
  9. package/skills/teamclaw-setup/references/install-modes.md +136 -0
  10. package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
  11. package/src/config.ts +44 -16
  12. package/src/controller/controller-capacity.ts +2 -2
  13. package/src/controller/controller-service.ts +193 -47
  14. package/src/controller/controller-tools.ts +102 -2
  15. package/src/controller/delivery-report.ts +563 -0
  16. package/src/controller/http-server.ts +1907 -172
  17. package/src/controller/kickoff-orchestrator.ts +292 -0
  18. package/src/controller/managed-gateway-process.ts +330 -0
  19. package/src/controller/orchestration-manifest.ts +69 -1
  20. package/src/controller/preview-manager.ts +676 -0
  21. package/src/controller/prompt-injector.ts +116 -67
  22. package/src/controller/role-inference.ts +41 -0
  23. package/src/controller/websocket.ts +3 -1
  24. package/src/controller/worker-provisioning.ts +429 -74
  25. package/src/discovery.ts +1 -1
  26. package/src/git-collaboration.ts +198 -47
  27. package/src/identity.ts +12 -2
  28. package/src/interaction-contracts.ts +179 -3
  29. package/src/networking.ts +99 -0
  30. package/src/openclaw-workspace.ts +478 -11
  31. package/src/prompt-policy.ts +381 -0
  32. package/src/roles.ts +37 -36
  33. package/src/state.ts +40 -1
  34. package/src/task-executor.ts +282 -78
  35. package/src/types.ts +150 -7
  36. package/src/ui/app.js +1403 -175
  37. package/src/ui/assets/teamclaw-app-icon.png +0 -0
  38. package/src/ui/index.html +122 -40
  39. package/src/ui/style.css +829 -143
  40. package/src/worker/http-handler.ts +40 -4
  41. package/src/worker/prompt-injector.ts +9 -38
  42. package/src/worker/skill-installer.ts +2 -2
  43. package/src/worker/tools.ts +31 -5
  44. package/src/worker/worker-service.ts +49 -8
  45. package/src/workspace-browser.ts +20 -7
  46. 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: "single-local",
47
- label: "Single machine controller + localRoles",
48
- hint: "Recommended for first-time setup.",
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 (manual distributed workers)",
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 sanitizeInstallerPathSegment(value) {
202
- const normalized = String(value || "")
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, teamName) {
210
- return path.join(
211
- resolveOpenClawStateDirForConfigPath(configPath),
212
- "teamclaw-workspaces",
213
- sanitizeInstallerPathSegment(teamName),
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 resolveInstallerWorkspaceDefault(configPath, config, teamName) {
338
- const currentWorkspacePath = getCurrentWorkspacePath(config);
339
- const sharedWorkspacePath = resolveOpenClawWorkspaceDirForConfigPath(configPath);
340
- if (currentWorkspacePath && path.resolve(currentWorkspacePath) !== path.resolve(sharedWorkspacePath)) {
341
- return currentWorkspacePath;
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 resolveDefaultTeamClawWorkspaceDir(configPath, teamName);
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
- if (existingRoles.length === 0) {
366
- return [];
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
- return `OPENCLAW_CONFIG_PATH=${shellEscape(configPath)} openclaw gateway run`;
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
- urls.push({
655
- address: record.address,
656
- url: `http://${record.address}:${port}/ui`,
657
- });
871
+ if (!seen.has(record.address)) {
872
+ seen.add(record.address);
873
+ fallbackAddresses.push(record.address);
874
+ }
658
875
  }
659
876
  }
660
- return urls
661
- .sort((left, right) => rankLanAddress(left.address) - rankLanAddress(right.address) || left.address.localeCompare(right.address))
662
- .map((entry) => entry.url);
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 createPackageTarball(env) {
722
- let tempDir = "";
723
- try {
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
- const candidates = [];
843
- const tarballResult = createPackageTarball(env);
844
- if (tarballResult.ok) {
845
- console.log(
846
- `\nPacked ${PACKAGE_INSTALL_SPEC} into ${path.basename(tarballResult.tarballPath)} for local plugin install.`,
847
- );
848
- candidates.push(
849
- {
850
- label: "openclaw (local tarball)",
851
- command: "openclaw",
852
- args: ["plugins", "install", tarballResult.tarballPath],
853
- targetDescription: tarballResult.tarballPath,
854
- },
855
- {
856
- label: "npm exec fallback (local tarball)",
857
- command: "npm",
858
- args: ["exec", "-y", "openclaw@latest", "--", "plugins", "install", tarballResult.tarballPath],
859
- targetDescription: tarballResult.tarballPath,
860
- },
861
- );
862
- } else {
863
- console.log(
864
- `\nCould not pack ${PACKAGE_INSTALL_SPEC} into a local tarball (${tarballResult.error}). Falling back to registry install...`,
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: ["exec", "-y", "openclaw@latest", "--", "plugins", "install", PACKAGE_INSTALL_SPEC],
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
- try {
883
- const failures = [];
884
- for (let index = 0; index < candidates.length; index += 1) {
885
- const candidate = candidates[index];
886
- console.log(`\nInstalling ${candidate.targetDescription} with ${candidate.label}...`);
887
- const result = installPluginWithCommand(candidate.command, candidate.args, env);
888
- if (result.status === 0 && !result.error) {
889
- return {
890
- ok: true,
891
- method: candidate.label,
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
- return {
910
- ok: false,
911
- error: failures.length > 0 ? failures.join("; ") : "No install command was available.",
912
- };
913
- } finally {
914
- if (tarballResult.ok) {
915
- fsSync.rmSync(tarballResult.tempDir, { recursive: true, force: true });
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() + 30_000;
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
- lastError = `HTTP ${response.statusCode}`;
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 30s",
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
- let modeDefault = "single-local";
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 if (!Array.isArray(existingTeamClaw.localRoles) || existingTeamClaw.localRoles.length === 0) {
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: "OpenClaw workspace directory",
1073
- defaultValue: resolveInstallerWorkspaceDefault(configPath, config, teamName),
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 controllerUrl = await prompter.text({
1095
- message: "Controller URL",
1096
- defaultValue:
1097
- typeof existingTeamClaw.controllerUrl === "string" && existingTeamClaw.controllerUrl.trim()
1098
- ? existingTeamClaw.controllerUrl.trim()
1099
- : "http://127.0.0.1:9527",
1100
- validate: (value) => value.startsWith("http://") || value.startsWith("https://")
1101
- ? ""
1102
- : 'Controller URL must start with "http://" or "https://".',
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 applyInstallerChoices(config, choices) {
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 === "single-local") {
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
- lines.push(`Controller URL: ${params.choices.controllerUrl}`);
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,