@teamclaws/teamclaw 2026.3.25 → 2026.3.26-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/cli.mjs +519 -28
- package/index.ts +31 -16
- package/openclaw.plugin.json +3 -3
- package/package.json +16 -4
- package/src/config.ts +2 -2
- package/src/controller/controller-capacity.ts +23 -0
- package/src/controller/controller-service.ts +27 -1
- package/src/controller/controller-tools.ts +251 -7
- package/src/controller/http-server.ts +976 -38
- package/src/controller/orchestration-manifest.ts +105 -0
- package/src/controller/prompt-injector.ts +42 -7
- package/src/controller/worker-provisioning.ts +171 -13
- package/src/git-collaboration.ts +2 -0
- package/src/interaction-contracts.ts +459 -0
- package/src/task-executor.ts +482 -33
- package/src/types.ts +96 -0
- package/src/ui/app.js +313 -8
- package/src/ui/style.css +152 -0
- package/src/worker/http-handler.ts +15 -7
- package/src/worker/prompt-injector.ts +2 -0
- package/src/worker/skill-installer.ts +13 -0
- package/src/worker/tools.ts +172 -3
- package/src/worker/worker-service.ts +9 -6
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ControllerManifestCreatedTask,
|
|
3
|
+
ControllerManifestDeferredTask,
|
|
4
|
+
ControllerOrchestrationManifest,
|
|
5
|
+
RoleId,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
|
|
8
|
+
const TEAMCLAW_ROLE_IDS = new Set<RoleId>([
|
|
9
|
+
"pm",
|
|
10
|
+
"architect",
|
|
11
|
+
"developer",
|
|
12
|
+
"qa",
|
|
13
|
+
"release-engineer",
|
|
14
|
+
"infra-engineer",
|
|
15
|
+
"devops",
|
|
16
|
+
"security-engineer",
|
|
17
|
+
"designer",
|
|
18
|
+
"marketing",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function normalizeManifestRoleList(raw: unknown): RoleId[] {
|
|
22
|
+
if (!Array.isArray(raw)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const roleIds: RoleId[] = [];
|
|
26
|
+
for (const entry of raw) {
|
|
27
|
+
if (typeof entry !== "string") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const normalized = entry.trim() as RoleId;
|
|
31
|
+
if (!normalized || !TEAMCLAW_ROLE_IDS.has(normalized) || roleIds.includes(normalized)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
roleIds.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
return roleIds;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeManifestStringList(raw: unknown): string[] {
|
|
40
|
+
if (!Array.isArray(raw)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return raw
|
|
44
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeOptionalManifestText(raw: unknown): string | undefined {
|
|
49
|
+
if (typeof raw !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const normalized = raw.trim();
|
|
53
|
+
return normalized || undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeManifestCreatedTasks(raw: unknown): ControllerManifestCreatedTask[] {
|
|
57
|
+
if (!Array.isArray(raw)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return raw
|
|
61
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === "object")
|
|
62
|
+
.map((entry) => ({
|
|
63
|
+
title: typeof entry.title === "string" ? entry.title.trim() : "",
|
|
64
|
+
assignedRole: normalizeManifestRoleList([entry.assignedRole])[0],
|
|
65
|
+
expectedOutcome: typeof entry.expectedOutcome === "string" ? entry.expectedOutcome.trim() : "",
|
|
66
|
+
}))
|
|
67
|
+
.filter((entry) => entry.title && entry.expectedOutcome);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeManifestDeferredTasks(raw: unknown): ControllerManifestDeferredTask[] {
|
|
71
|
+
if (!Array.isArray(raw)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return raw
|
|
75
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === "object")
|
|
76
|
+
.map((entry) => ({
|
|
77
|
+
title: typeof entry.title === "string" ? entry.title.trim() : "",
|
|
78
|
+
assignedRole: normalizeManifestRoleList([entry.assignedRole])[0],
|
|
79
|
+
blockedBy: typeof entry.blockedBy === "string" ? entry.blockedBy.trim() : "",
|
|
80
|
+
whenReady: typeof entry.whenReady === "string" ? entry.whenReady.trim() : "",
|
|
81
|
+
}))
|
|
82
|
+
.filter((entry) => entry.title && entry.blockedBy && entry.whenReady);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeControllerManifest(raw: unknown): ControllerOrchestrationManifest | null {
|
|
86
|
+
if (!raw || typeof raw !== "object") {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const input = raw as Record<string, unknown>;
|
|
90
|
+
const requirementSummary = typeof input.requirementSummary === "string" ? input.requirementSummary.trim() : "";
|
|
91
|
+
if (!requirementSummary) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
version: typeof input.version === "string" && input.version.trim() ? input.version.trim() : "1.0",
|
|
96
|
+
requirementSummary,
|
|
97
|
+
requiredRoles: normalizeManifestRoleList(input.requiredRoles),
|
|
98
|
+
clarificationsNeeded: Boolean(input.clarificationsNeeded),
|
|
99
|
+
clarificationQuestions: normalizeManifestStringList(input.clarificationQuestions),
|
|
100
|
+
createdTasks: normalizeManifestCreatedTasks(input.createdTasks),
|
|
101
|
+
deferredTasks: normalizeManifestDeferredTasks(input.deferredTasks),
|
|
102
|
+
handoffPlan: normalizeOptionalManifestText(input.handoffPlan),
|
|
103
|
+
notes: normalizeOptionalManifestText(input.notes),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginConfig, TeamState } from "../types.js";
|
|
2
2
|
import { ROLES } from "../roles.js";
|
|
3
|
+
import { hasOnDemandWorkerProvisioning, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
|
|
3
4
|
|
|
4
5
|
const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
5
6
|
"pm",
|
|
@@ -22,15 +23,13 @@ export type ControllerPromptDeps = {
|
|
|
22
23
|
export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
23
24
|
return () => {
|
|
24
25
|
const state = deps.getTeamState();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const workers = Object.values(state.workers);
|
|
28
|
-
const tasks = Object.values(state.tasks);
|
|
26
|
+
const workers = Object.values(state?.workers ?? {});
|
|
27
|
+
const tasks = Object.values(state?.tasks ?? {});
|
|
29
28
|
const pendingTasks = tasks.filter((t) => t.status === "pending");
|
|
30
29
|
const activeTasks = tasks.filter((t) => t.status === "in_progress" || t.status === "assigned");
|
|
31
30
|
const blockedTasks = tasks.filter((t) => t.status === "blocked");
|
|
32
31
|
const completedTasks = tasks.filter((t) => t.status === "completed");
|
|
33
|
-
const pendingClarifications = Object.values(state
|
|
32
|
+
const pendingClarifications = Object.values(state?.clarifications ?? {}).filter((c) => c.status === "pending");
|
|
34
33
|
|
|
35
34
|
const parts: string[] = [
|
|
36
35
|
"## TeamClaw Controller Mode",
|
|
@@ -39,6 +38,7 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
39
38
|
"",
|
|
40
39
|
"### Available Tools",
|
|
41
40
|
"- teamclaw_create_task: Create a new task with role assignment",
|
|
41
|
+
"- teamclaw_submit_manifest: Submit the required structured orchestration manifest for this intake run",
|
|
42
42
|
"- teamclaw_list_tasks: List all tasks with status filtering",
|
|
43
43
|
"- teamclaw_assign_task: Assign a task to a specific worker",
|
|
44
44
|
"- teamclaw_send_message: Send messages between team members",
|
|
@@ -46,8 +46,17 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
46
46
|
"### Current Team Status",
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
-
if (
|
|
50
|
-
parts.push("-
|
|
49
|
+
if (!state) {
|
|
50
|
+
parts.push("- Team state is not loaded yet; treat this as a fresh controller intake and establish execution-ready tasks from the human requirement.");
|
|
51
|
+
} else if (workers.length === 0) {
|
|
52
|
+
if (shouldBlockControllerWithoutWorkers(deps.config, state)) {
|
|
53
|
+
parts.push("- No workers are registered and on-demand provisioning is disabled.");
|
|
54
|
+
parts.push("- Blocking rule: you may analyze the requirement and identify the needed roles, but do not create TeamClaw tasks yet.");
|
|
55
|
+
parts.push("- Do not start doing the worker-role work yourself. Tell the human to bring workers online or enable process/docker/kubernetes provisioning first.");
|
|
56
|
+
} else {
|
|
57
|
+
parts.push("- No workers are registered yet, but on-demand provisioning is enabled.");
|
|
58
|
+
parts.push("- You may still create execution-ready TeamClaw tasks for the required roles; the controller will provision workers on demand.");
|
|
59
|
+
}
|
|
51
60
|
} else {
|
|
52
61
|
for (const w of workers) {
|
|
53
62
|
const roleDef = ROLES.find((r) => r.id === w.role);
|
|
@@ -86,6 +95,24 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
86
95
|
parts.push(`- ${role.icon} ${role.label}: ${role.description}.${skillLine}`);
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
parts.push("");
|
|
99
|
+
parts.push("## Controller Workflow");
|
|
100
|
+
parts.push("- First determine which TeamClaw roles are needed for the human requirement.");
|
|
101
|
+
parts.push("- Then translate the requirement into the minimum execution-ready TeamClaw tasks owned by those roles.");
|
|
102
|
+
parts.push("- TeamClaw workers, not the controller, do the specialist work in the shared repo/workspace.");
|
|
103
|
+
parts.push("- After workers report progress, results, or handoffs, create only the next tasks whose prerequisites are now satisfied.");
|
|
104
|
+
parts.push("- A completed upstream task with a structured result contract, concrete deliverables, or an explicit handoff is strong evidence that its dependent downstream work can now be created.");
|
|
105
|
+
|
|
106
|
+
parts.push("");
|
|
107
|
+
parts.push("## Structured Orchestration Contract");
|
|
108
|
+
parts.push("- Freeform prose is not enough for TeamClaw scheduling decisions.");
|
|
109
|
+
parts.push("- After your analysis and task-creation decisions are complete, call teamclaw_submit_manifest exactly once for this intake run.");
|
|
110
|
+
parts.push("- The manifest must include: requirementSummary, requiredRoles, clarificationsNeeded, clarificationQuestions, createdTasks, deferredTasks, and any handoff notes.");
|
|
111
|
+
parts.push("- Use createdTasks for execution-ready tasks that this run activated now, including a deliberately reused existing TeamClaw task when you chose not to duplicate it.");
|
|
112
|
+
parts.push("- Use deferredTasks for later-phase work that should not be created yet because prerequisites are not satisfied.");
|
|
113
|
+
parts.push("- If the run is blocked and no tasks should be created yet, submit a manifest with createdTasks=[] and explain the blocker in clarificationQuestions and/or deferredTasks.");
|
|
114
|
+
parts.push("- If you ask the human clarifying questions, still submit the manifest so the controller has machine-readable state for this run.");
|
|
115
|
+
|
|
89
116
|
parts.push("");
|
|
90
117
|
parts.push("## Requirement Intake Rules");
|
|
91
118
|
parts.push("- Human messages are the initial requirement, not an already-decomposed task tree.");
|
|
@@ -111,6 +138,14 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
111
138
|
parts.push("- Do not let a worker task turn itself into a controller/coordinator workflow.");
|
|
112
139
|
parts.push("- If the correct role is busy, prefer waiting, messaging, or explicit reassignment over routing core work to an unrelated role.");
|
|
113
140
|
parts.push("- If a task is blocked by missing information, keep it in the clarification queue until the human answers; do not guess on the user's behalf.");
|
|
141
|
+
parts.push("- You are never a substitute worker. Do not personally perform architecture, implementation, QA, release, infra, design, marketing, research, or other specialist work.");
|
|
142
|
+
parts.push("- Your own reply must stay at the orchestration layer: clarification, role selection, task decomposition, assignment decisions, and concise status updates.");
|
|
143
|
+
parts.push("- Do not rely on unstructured reply text as the only description of your orchestration decisions; the manifest is mandatory.");
|
|
144
|
+
if (hasOnDemandWorkerProvisioning(deps.config)) {
|
|
145
|
+
parts.push("- If no workers are currently registered but on-demand provisioning is enabled, you may still create execution-ready tasks so the required roles can be provisioned.");
|
|
146
|
+
} else {
|
|
147
|
+
parts.push("- If no workers are registered, you may mention which roles would be needed, but stop there and report the worker-capacity block to the human.");
|
|
148
|
+
}
|
|
114
149
|
parts.push("- Use the controller itself for requirement analysis; use the PM role only for PM-owned deliverables after intake is clear.");
|
|
115
150
|
parts.push(`- Use exact TeamClaw role IDs only: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
|
|
116
151
|
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import readline from "node:readline";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
import JSON5 from "json5";
|
|
10
11
|
import type { PluginLogger } from "../../api.js";
|
|
11
12
|
import { generateId } from "../protocol.js";
|
|
@@ -28,10 +29,11 @@ import type {
|
|
|
28
29
|
|
|
29
30
|
const DEFAULT_CONTAINER_WORKER_PORT = 9527;
|
|
30
31
|
const DEFAULT_CONTAINER_GATEWAY_PORT = 18789;
|
|
32
|
+
const DEFAULT_DOCKER_BUNDLED_TEAMCLAW_PLUGIN_DIR = "/app/extensions/teamclaw";
|
|
31
33
|
const PROVISIONING_RECORD_RETENTION_MS = 6 * 60 * 60 * 1000;
|
|
32
34
|
const PROVISIONING_FAILURE_COOLDOWN_MS = 30_000;
|
|
33
35
|
const PROCESS_TERMINATION_TIMEOUT_MS = 10_000;
|
|
34
|
-
const DOCKER_API_VERSION =
|
|
36
|
+
const DOCKER_API_VERSION = resolveDockerApiVersion();
|
|
35
37
|
|
|
36
38
|
export type WorkerProvisioningManagerDeps = {
|
|
37
39
|
config: PluginConfig;
|
|
@@ -258,10 +260,18 @@ export class WorkerProvisioningManager {
|
|
|
258
260
|
return;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
const roles = this.getProvisionableRoles();
|
|
263
|
+
const roles = this.getProvisionableRoles(state);
|
|
262
264
|
for (const role of roles) {
|
|
263
265
|
const demand = this.computeRoleDemand(state, role);
|
|
264
266
|
if (demand > 0) {
|
|
267
|
+
if (
|
|
268
|
+
this.deps.config.workerProvisioningRoles.length > 0 &&
|
|
269
|
+
!this.deps.config.workerProvisioningRoles.includes(role)
|
|
270
|
+
) {
|
|
271
|
+
this.deps.logger.info(
|
|
272
|
+
`Provisioner: allowing role ${role} because pending task demand exists outside configured workerProvisioningRoles`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
265
275
|
this.deps.logger.info(`Provisioner: role ${role} needs ${demand} additional worker(s) (${reason})`);
|
|
266
276
|
}
|
|
267
277
|
for (let i = 0; i < demand; i += 1) {
|
|
@@ -301,7 +311,7 @@ export class WorkerProvisioningManager {
|
|
|
301
311
|
return;
|
|
302
312
|
}
|
|
303
313
|
|
|
304
|
-
const roles = this.getProvisionableRoles();
|
|
314
|
+
const roles = this.getProvisionableRoles(state);
|
|
305
315
|
for (const role of roles) {
|
|
306
316
|
const activeWorkers = Object.values(state.workers).filter(
|
|
307
317
|
(worker) => worker.role === role && worker.status !== "offline",
|
|
@@ -353,10 +363,14 @@ export class WorkerProvisioningManager {
|
|
|
353
363
|
const workerId = `provisioned-${role}-${generateId()}`;
|
|
354
364
|
const launchToken = `${generateId()}-${generateId()}`;
|
|
355
365
|
const controllerUrl = this.resolveControllerUrl();
|
|
356
|
-
const
|
|
366
|
+
const requiresDedicatedHostPorts = requiresDedicatedHostPortsForProvisioner(
|
|
367
|
+
this.backend.type,
|
|
368
|
+
this.deps.config,
|
|
369
|
+
);
|
|
370
|
+
const workerPort = requiresDedicatedHostPorts
|
|
357
371
|
? await reserveEphemeralPort()
|
|
358
372
|
: DEFAULT_CONTAINER_WORKER_PORT;
|
|
359
|
-
const gatewayPort =
|
|
373
|
+
const gatewayPort = requiresDedicatedHostPorts
|
|
360
374
|
? await reserveEphemeralPort()
|
|
361
375
|
: DEFAULT_CONTAINER_GATEWAY_PORT;
|
|
362
376
|
const now = Date.now();
|
|
@@ -390,7 +404,7 @@ export class WorkerProvisioningManager {
|
|
|
390
404
|
workerPort,
|
|
391
405
|
gatewayPort,
|
|
392
406
|
workspaceDir: getConfiguredWorkerWorkspaceDir(workerConfig),
|
|
393
|
-
env: this.buildForwardedEnv(),
|
|
407
|
+
env: this.buildForwardedEnv(controllerUrl),
|
|
394
408
|
configJson: `${JSON.stringify(workerConfig, null, 2)}\n`,
|
|
395
409
|
});
|
|
396
410
|
|
|
@@ -473,10 +487,35 @@ export class WorkerProvisioningManager {
|
|
|
473
487
|
});
|
|
474
488
|
}
|
|
475
489
|
|
|
476
|
-
private getProvisionableRoles(): RoleId[] {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
490
|
+
private getProvisionableRoles(state: TeamState | null): RoleId[] {
|
|
491
|
+
const roleIds = new Set<RoleId>(
|
|
492
|
+
this.deps.config.workerProvisioningRoles.length > 0
|
|
493
|
+
? this.deps.config.workerProvisioningRoles
|
|
494
|
+
: ROLES.map((role) => role.id),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
if (!state) {
|
|
498
|
+
return [...roleIds];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (const task of Object.values(state.tasks)) {
|
|
502
|
+
if (task.status !== "pending" && task.status !== "assigned") {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const taskRole = this.inferTaskRole(task);
|
|
506
|
+
if (taskRole) {
|
|
507
|
+
roleIds.add(taskRole);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (const worker of Object.values(state.workers)) {
|
|
512
|
+
roleIds.add(worker.role);
|
|
513
|
+
}
|
|
514
|
+
for (const record of Object.values(state.provisioning?.workers ?? {})) {
|
|
515
|
+
roleIds.add(record.role);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return [...roleIds];
|
|
480
519
|
}
|
|
481
520
|
|
|
482
521
|
private countPendingTasksForRole(state: TeamState, role: RoleId): number {
|
|
@@ -493,6 +532,9 @@ export class WorkerProvisioningManager {
|
|
|
493
532
|
return false;
|
|
494
533
|
}
|
|
495
534
|
}
|
|
535
|
+
if (task.assignedRole) {
|
|
536
|
+
return task.assignedRole === role;
|
|
537
|
+
}
|
|
496
538
|
return this.inferTaskRole(task) === role;
|
|
497
539
|
}
|
|
498
540
|
|
|
@@ -557,7 +599,7 @@ export class WorkerProvisioningManager {
|
|
|
557
599
|
);
|
|
558
600
|
}
|
|
559
601
|
|
|
560
|
-
private buildForwardedEnv(): Record<string, string> {
|
|
602
|
+
private buildForwardedEnv(controllerUrl: string): Record<string, string> {
|
|
561
603
|
const env: Record<string, string> = {
|
|
562
604
|
...this.deps.config.workerProvisioningExtraEnv,
|
|
563
605
|
};
|
|
@@ -567,7 +609,7 @@ export class WorkerProvisioningManager {
|
|
|
567
609
|
env[name] = value;
|
|
568
610
|
}
|
|
569
611
|
}
|
|
570
|
-
return env;
|
|
612
|
+
return appendNoProxyEntries(env, controllerUrl);
|
|
571
613
|
}
|
|
572
614
|
|
|
573
615
|
private async loadBaseOpenClawConfig(): Promise<Record<string, unknown>> {
|
|
@@ -636,6 +678,7 @@ class ProcessProvisioner implements WorkerProvisionerBackend {
|
|
|
636
678
|
const stateDir = path.join(runtimeHomeDir, ".openclaw");
|
|
637
679
|
const configPath = path.join(stateDir, "openclaw.json");
|
|
638
680
|
await fs.mkdir(stateDir, { recursive: true });
|
|
681
|
+
await prepareProcessRuntimeExtensions(stateDir);
|
|
639
682
|
await fs.writeFile(configPath, spec.configJson, "utf8");
|
|
640
683
|
|
|
641
684
|
const gatewayEntrypoint = resolveGatewayEntrypoint();
|
|
@@ -943,7 +986,8 @@ class DockerApiClient {
|
|
|
943
986
|
okStatuses: number[],
|
|
944
987
|
): Promise<{ status: number; body: string }> {
|
|
945
988
|
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
946
|
-
|
|
989
|
+
// Prefer the daemon's negotiated default unless the operator pins a version explicitly.
|
|
990
|
+
const finalPath = DOCKER_API_VERSION ? `/${DOCKER_API_VERSION}${requestPath}` : requestPath;
|
|
947
991
|
|
|
948
992
|
return await new Promise<{ status: number; body: string }>((resolve, reject) => {
|
|
949
993
|
const transport = this.endpoint.protocol === "https:" ? https : http;
|
|
@@ -989,6 +1033,14 @@ type DockerEndpoint = {
|
|
|
989
1033
|
port?: number;
|
|
990
1034
|
};
|
|
991
1035
|
|
|
1036
|
+
function resolveDockerApiVersion(): string | null {
|
|
1037
|
+
const configured = process.env.DOCKER_API_VERSION?.trim();
|
|
1038
|
+
if (!configured) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
return configured.startsWith("v") ? configured : `v${configured}`;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
992
1044
|
function createProvisionerBackend(
|
|
993
1045
|
config: PluginConfig,
|
|
994
1046
|
logger: PluginLogger,
|
|
@@ -1018,6 +1070,7 @@ function buildProvisionedWorkerConfig(
|
|
|
1018
1070
|
},
|
|
1019
1071
|
): Record<string, unknown> {
|
|
1020
1072
|
const config = cloneJson(baseConfig);
|
|
1073
|
+
delete config.channels;
|
|
1021
1074
|
const agents = ensureRecord(config.agents);
|
|
1022
1075
|
const agentDefaults = ensureRecord(agents.defaults);
|
|
1023
1076
|
delete agentDefaults.repoRoot;
|
|
@@ -1033,10 +1086,14 @@ function buildProvisionedWorkerConfig(
|
|
|
1033
1086
|
gateway.mode = "local";
|
|
1034
1087
|
gateway.bind = "loopback";
|
|
1035
1088
|
gateway.port = spec.gatewayPort;
|
|
1089
|
+
delete gateway.remote;
|
|
1036
1090
|
config.gateway = gateway;
|
|
1037
1091
|
|
|
1038
1092
|
const plugins = ensureRecord(config.plugins);
|
|
1039
1093
|
plugins.enabled = true;
|
|
1094
|
+
if (controllerConfig.workerProvisioningType === "docker" || controllerConfig.workerProvisioningType === "kubernetes") {
|
|
1095
|
+
delete plugins.load;
|
|
1096
|
+
}
|
|
1040
1097
|
const entries = ensureRecord(plugins.entries);
|
|
1041
1098
|
const teamclawEntry = ensureRecord(entries.teamclaw);
|
|
1042
1099
|
teamclawEntry.enabled = true;
|
|
@@ -1097,10 +1154,104 @@ async function loadOpenClawConfig(configPath: string): Promise<Record<string, un
|
|
|
1097
1154
|
return parseLooseJsonObject(raw, configPath);
|
|
1098
1155
|
}
|
|
1099
1156
|
|
|
1157
|
+
async function prepareProcessRuntimeExtensions(stateDir: string): Promise<void> {
|
|
1158
|
+
const runtimeExtensionsDir = path.join(stateDir, "extensions");
|
|
1159
|
+
const controllerExtensionsDir = path.join(path.dirname(resolveDefaultOpenClawConfigPath()), "extensions");
|
|
1160
|
+
if (await pathExists(controllerExtensionsDir)) {
|
|
1161
|
+
await fs.symlink(controllerExtensionsDir, runtimeExtensionsDir, "dir");
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
await fs.mkdir(runtimeExtensionsDir, { recursive: true });
|
|
1166
|
+
await fs.symlink(resolveCurrentTeamClawPluginRootDir(), path.join(runtimeExtensionsDir, "teamclaw"), "dir");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1100
1169
|
function cloneJson<T>(value: T): T {
|
|
1101
1170
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
1102
1171
|
}
|
|
1103
1172
|
|
|
1173
|
+
function requiresDedicatedHostPortsForProvisioner(
|
|
1174
|
+
provider: WorkerProvisioningType,
|
|
1175
|
+
config: PluginConfig,
|
|
1176
|
+
): boolean {
|
|
1177
|
+
if (provider === "process") {
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
return provider === "docker" && isDockerHostNetwork(config.workerProvisioningDockerNetwork);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function isDockerHostNetwork(networkMode: string): boolean {
|
|
1184
|
+
return networkMode.trim().toLowerCase() === "host";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function extractDockerBindTarget(bind: string): string | null {
|
|
1188
|
+
for (const part of bind.split(":").reverse()) {
|
|
1189
|
+
const trimmed = part.trim();
|
|
1190
|
+
if (trimmed.startsWith("/")) {
|
|
1191
|
+
return trimmed;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function appendNoProxyEntries(env: Record<string, string>, controllerUrl: string): Record<string, string> {
|
|
1198
|
+
const requiredEntries = resolveRequiredNoProxyEntries(controllerUrl);
|
|
1199
|
+
if (requiredEntries.length === 0) {
|
|
1200
|
+
return env;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const upperBase = env.NO_PROXY ?? process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1204
|
+
const lowerBase = env.no_proxy ?? process.env.no_proxy ?? env.NO_PROXY ?? process.env.NO_PROXY ?? "";
|
|
1205
|
+
|
|
1206
|
+
env.NO_PROXY = mergeNoProxyEntries(upperBase, requiredEntries);
|
|
1207
|
+
env.no_proxy = mergeNoProxyEntries(lowerBase, requiredEntries);
|
|
1208
|
+
return env;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function resolveRequiredNoProxyEntries(controllerUrl: string): string[] {
|
|
1212
|
+
const required = new Set<string>(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
1213
|
+
try {
|
|
1214
|
+
const host = new URL(controllerUrl).hostname.trim();
|
|
1215
|
+
if (host) {
|
|
1216
|
+
required.add(host);
|
|
1217
|
+
}
|
|
1218
|
+
} catch {
|
|
1219
|
+
// Ignore malformed controller URLs; worker registration will fail separately.
|
|
1220
|
+
}
|
|
1221
|
+
return Array.from(required);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function mergeNoProxyEntries(existing: string, requiredEntries: string[]): string {
|
|
1225
|
+
const tokens = new Map<string, string>();
|
|
1226
|
+
for (const entry of existing.split(",")) {
|
|
1227
|
+
const trimmed = entry.trim();
|
|
1228
|
+
if (!trimmed) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
tokens.set(trimmed.toLowerCase(), trimmed);
|
|
1232
|
+
}
|
|
1233
|
+
for (const entry of requiredEntries) {
|
|
1234
|
+
const trimmed = entry.trim();
|
|
1235
|
+
if (!trimmed) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
tokens.set(trimmed.toLowerCase(), trimmed);
|
|
1239
|
+
}
|
|
1240
|
+
return Array.from(tokens.values()).join(",");
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async function pathExists(targetPath: string): Promise<boolean> {
|
|
1244
|
+
try {
|
|
1245
|
+
await fs.access(targetPath);
|
|
1246
|
+
return true;
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
throw err;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1104
1255
|
function ensureRecord(value: unknown): Record<string, unknown> {
|
|
1105
1256
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
1106
1257
|
? { ...(value as Record<string, unknown>) }
|
|
@@ -1147,6 +1298,10 @@ function resolveGatewayEntrypoint(): string {
|
|
|
1147
1298
|
return path.resolve(scriptPath);
|
|
1148
1299
|
}
|
|
1149
1300
|
|
|
1301
|
+
function resolveCurrentTeamClawPluginRootDir(): string {
|
|
1302
|
+
return path.resolve(fileURLToPath(new URL("../../", import.meta.url)));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1150
1305
|
function attachChildLogs(child: ChildProcess, logger: PluginLogger, prefix: string): void {
|
|
1151
1306
|
if (child.stdout) {
|
|
1152
1307
|
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
@@ -1266,6 +1421,9 @@ function buildContainerBootstrapScript(): string {
|
|
|
1266
1421
|
|
|
1267
1422
|
function buildDockerBinds(config: PluginConfig): string[] {
|
|
1268
1423
|
const binds = [...config.workerProvisioningDockerMounts];
|
|
1424
|
+
if (!binds.some((bind) => extractDockerBindTarget(bind) === DEFAULT_DOCKER_BUNDLED_TEAMCLAW_PLUGIN_DIR)) {
|
|
1425
|
+
binds.unshift(`${resolveCurrentTeamClawPluginRootDir()}:${DEFAULT_DOCKER_BUNDLED_TEAMCLAW_PLUGIN_DIR}:ro`);
|
|
1426
|
+
}
|
|
1269
1427
|
if (config.workerProvisioningDockerWorkspaceVolume && config.workerProvisioningWorkspaceRoot) {
|
|
1270
1428
|
binds.unshift(`${config.workerProvisioningDockerWorkspaceVolume}:${config.workerProvisioningWorkspaceRoot}`);
|
|
1271
1429
|
}
|
package/src/git-collaboration.ts
CHANGED
|
@@ -9,11 +9,13 @@ import { resolveDefaultOpenClawWorkspaceDir } from "./openclaw-workspace.js";
|
|
|
9
9
|
const TEAMCLAW_IMPORT_REF_PREFIX = "refs/teamclaw/imports";
|
|
10
10
|
const TEAMCLAW_RUNTIME_EXCLUDES = [
|
|
11
11
|
".openclaw/",
|
|
12
|
+
".clawhub/",
|
|
12
13
|
"AGENTS.md",
|
|
13
14
|
"BOOTSTRAP.md",
|
|
14
15
|
"HEARTBEAT.md",
|
|
15
16
|
"IDENTITY.md",
|
|
16
17
|
"SOUL.md",
|
|
18
|
+
"skills/",
|
|
17
19
|
"TOOLS.md",
|
|
18
20
|
"USER.md",
|
|
19
21
|
];
|