@linkclaw/clawpool 0.2.0 → 0.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 +28 -0
- package/clawpool.ts +95 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,34 @@ Stored at `~/.clawpool/config.json`:
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
## Host Config Inheritance
|
|
102
|
+
|
|
103
|
+
When creating or restarting an instance, clawpool automatically merges settings from the host machine's OpenClaw installation (if present) into each container:
|
|
104
|
+
|
|
105
|
+
- **`~/.openclaw/openclaw.json`** — model providers, default model, env vars, and other settings are inherited. Gateway-specific settings (`gateway`, `meta`) are excluded and managed by clawpool per-instance.
|
|
106
|
+
- **`~/.openclaw/agents/main/agent/auth-profiles.json`** — OAuth auth profiles (e.g. `openai-codex`) are copied into each container, enabling provider access without per-instance login.
|
|
107
|
+
|
|
108
|
+
If OpenClaw is not installed on the host, instances start with a minimal config (gateway auth only). You can then configure providers via environment variables:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
clawpool create alpha -e "ANTHROPIC_API_KEY=sk-ant-..." -e "OPENAI_API_KEY=sk-..."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Reusing auth profiles from another machine
|
|
115
|
+
|
|
116
|
+
If you want to copy auth profiles from a remote OpenClaw installation:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# On the machine where OpenClaw is logged in:
|
|
120
|
+
cat ~/.openclaw/agents/main/agent/auth-profiles.json
|
|
121
|
+
|
|
122
|
+
# Copy the output to the clawpool host:
|
|
123
|
+
mkdir -p ~/.openclaw/agents/main/agent
|
|
124
|
+
# paste into ~/.openclaw/agents/main/agent/auth-profiles.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
clawpool will pick up the file on the next `create` or `restart`.
|
|
128
|
+
|
|
101
129
|
## Environment
|
|
102
130
|
|
|
103
131
|
| Variable | Purpose |
|
package/clawpool.ts
CHANGED
|
@@ -63,8 +63,8 @@ function getLanIP(): string {
|
|
|
63
63
|
return "localhost";
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function dashboardURL(host: string, port: number
|
|
67
|
-
return `http://${host}:${port}
|
|
66
|
+
function dashboardURL(host: string, port: number): string {
|
|
67
|
+
return `http://${host}:${port}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function humanTime(iso: string): string {
|
|
@@ -154,29 +154,94 @@ async function syncStatus(
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const HOST_OC_DIR = join(homedir(), ".openclaw");
|
|
158
|
+
const HOST_OC_CONFIG = join(HOST_OC_DIR, "openclaw.json");
|
|
159
|
+
const HOST_AUTH_PROFILES = join(HOST_OC_DIR, "agents", "main", "agent", "auth-profiles.json");
|
|
160
|
+
|
|
161
|
+
function loadHostOpenClawConfig(): Record<string, unknown> {
|
|
162
|
+
if (!existsSync(HOST_OC_CONFIG)) return {};
|
|
163
|
+
try {
|
|
164
|
+
const raw = require("fs").readFileSync(HOST_OC_CONFIG, "utf-8");
|
|
165
|
+
return JSON.parse(raw);
|
|
166
|
+
} catch {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
|
172
|
+
const result = { ...target };
|
|
173
|
+
for (const key of Object.keys(source)) {
|
|
174
|
+
if (
|
|
175
|
+
source[key] &&
|
|
176
|
+
typeof source[key] === "object" &&
|
|
177
|
+
!Array.isArray(source[key]) &&
|
|
178
|
+
target[key] &&
|
|
179
|
+
typeof target[key] === "object" &&
|
|
180
|
+
!Array.isArray(target[key])
|
|
181
|
+
) {
|
|
182
|
+
result[key] = deepMerge(
|
|
183
|
+
target[key] as Record<string, unknown>,
|
|
184
|
+
source[key] as Record<string, unknown>
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
result[key] = source[key];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function loadHostAuthProfiles(): string | null {
|
|
194
|
+
if (!existsSync(HOST_AUTH_PROFILES)) return null;
|
|
195
|
+
try {
|
|
196
|
+
return require("fs").readFileSync(HOST_AUTH_PROFILES, "utf-8").trim();
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildOpenClawConfig(token: string): string {
|
|
203
|
+
const hostConfig = loadHostOpenClawConfig();
|
|
204
|
+
// Gateway config is always overridden per-instance
|
|
205
|
+
const instanceOverrides: Record<string, unknown> = {
|
|
159
206
|
gateway: {
|
|
160
207
|
auth: { mode: "token", token },
|
|
161
|
-
controlUi: {
|
|
208
|
+
controlUi: {
|
|
209
|
+
dangerouslyAllowHostHeaderOriginFallback: true,
|
|
210
|
+
allowInsecureAuth: true,
|
|
211
|
+
dangerouslyDisableDeviceAuth: true,
|
|
212
|
+
},
|
|
162
213
|
},
|
|
163
|
-
}
|
|
214
|
+
};
|
|
215
|
+
// Strip host gateway settings (we manage those), keep everything else
|
|
216
|
+
const { gateway: _gw, meta: _meta, ...hostSettings } = hostConfig;
|
|
217
|
+
const merged = deepMerge(hostSettings, instanceOverrides);
|
|
218
|
+
return JSON.stringify(merged);
|
|
164
219
|
}
|
|
165
220
|
|
|
166
221
|
function buildRunArgs(config: Config, inst: Instance): string[] {
|
|
167
|
-
const ocConfig =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"chown -R node:node /home/node/data",
|
|
175
|
-
`echo '${ocConfig}' > /home/node/data/.openclaw/openclaw.json`,
|
|
176
|
-
`exec su -s /bin/sh node -c 'exec node dist/index.js gateway --allow-unconfigured --port ${inst.port} --bind lan'`,
|
|
177
|
-
].join(" && "),
|
|
222
|
+
const ocConfig = buildOpenClawConfig(inst.gateway_token);
|
|
223
|
+
const authProfiles = loadHostAuthProfiles();
|
|
224
|
+
|
|
225
|
+
const initSteps = [
|
|
226
|
+
"mkdir -p /home/node/data/.openclaw/agents/main/agent",
|
|
227
|
+
"chown -R node:node /home/node/data",
|
|
228
|
+
`echo '${ocConfig}' > /home/node/data/.openclaw/openclaw.json`,
|
|
178
229
|
];
|
|
179
230
|
|
|
231
|
+
if (authProfiles) {
|
|
232
|
+
// Escape single quotes in JSON for shell embedding
|
|
233
|
+
const escaped = authProfiles.replace(/'/g, "'\\''");
|
|
234
|
+
initSteps.push(
|
|
235
|
+
`echo '${escaped}' > /home/node/data/.openclaw/agents/main/agent/auth-profiles.json`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
initSteps.push(
|
|
240
|
+
`exec su -s /bin/sh node -c 'exec node dist/index.js gateway --allow-unconfigured --port ${inst.port} --bind lan'`
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const entrypoint = ["sh", "-c", initSteps.join(" && ")];
|
|
244
|
+
|
|
180
245
|
const args = [
|
|
181
246
|
"run",
|
|
182
247
|
"-d",
|
|
@@ -285,7 +350,8 @@ async function cmdCreate(config: Config, args: string[]) {
|
|
|
285
350
|
|
|
286
351
|
const host = getLanIP();
|
|
287
352
|
ok(`Instance '${name}' created (port: ${port}, container: ${cid.slice(0, 12)})`);
|
|
288
|
-
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, port
|
|
353
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, port)}`);
|
|
354
|
+
console.log(` ${c.bold("Token:")} ${gatewayToken}`);
|
|
289
355
|
}
|
|
290
356
|
|
|
291
357
|
async function cmdList(config: Config, args: string[]) {
|
|
@@ -310,10 +376,10 @@ async function cmdList(config: Config, args: string[]) {
|
|
|
310
376
|
const host = getLanIP();
|
|
311
377
|
|
|
312
378
|
console.log(
|
|
313
|
-
`${"NAME".padEnd(16)} ${"STATUS".padEnd(10)} ${"PORT".padEnd(7)}
|
|
379
|
+
`${"NAME".padEnd(16)} ${"STATUS".padEnd(10)} ${"PORT".padEnd(7)} ${"TOKEN".padEnd(34)} URL`
|
|
314
380
|
);
|
|
315
381
|
console.log(
|
|
316
|
-
`${"----".padEnd(16)} ${"------".padEnd(10)} ${"----".padEnd(7)}
|
|
382
|
+
`${"----".padEnd(16)} ${"------".padEnd(10)} ${"----".padEnd(7)} ${"-----".padEnd(34)} ---`
|
|
317
383
|
);
|
|
318
384
|
for (const name of names) {
|
|
319
385
|
const inst = config.instances[name];
|
|
@@ -325,10 +391,14 @@ async function cmdList(config: Config, args: string[]) {
|
|
|
325
391
|
: c.yellow(inst.status);
|
|
326
392
|
const url =
|
|
327
393
|
inst.status === "running"
|
|
328
|
-
? dashboardURL(host, inst.port
|
|
394
|
+
? dashboardURL(host, inst.port)
|
|
395
|
+
: c.dim("—");
|
|
396
|
+
const token =
|
|
397
|
+
inst.status === "running"
|
|
398
|
+
? inst.gateway_token
|
|
329
399
|
: c.dim("—");
|
|
330
400
|
console.log(
|
|
331
|
-
`${name.padEnd(16)} ${statusCol.padEnd(21)} ${String(inst.port).padEnd(7)} ${url}`
|
|
401
|
+
`${name.padEnd(16)} ${statusCol.padEnd(21)} ${String(inst.port).padEnd(7)} ${token.padEnd(34)} ${url}`
|
|
332
402
|
);
|
|
333
403
|
}
|
|
334
404
|
}
|
|
@@ -343,7 +413,7 @@ async function cmdStart(config: Config, args: string[]) {
|
|
|
343
413
|
const host = getLanIP();
|
|
344
414
|
const inst = config.instances[name];
|
|
345
415
|
ok(`Instance '${name}' started`);
|
|
346
|
-
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port
|
|
416
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port)}`);
|
|
347
417
|
}
|
|
348
418
|
|
|
349
419
|
async function cmdStop(config: Config, args: string[]) {
|
|
@@ -366,7 +436,7 @@ async function cmdRestart(config: Config, args: string[]) {
|
|
|
366
436
|
const host = getLanIP();
|
|
367
437
|
const inst = config.instances[name];
|
|
368
438
|
ok(`Instance '${name}' restarted`);
|
|
369
|
-
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port
|
|
439
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port)}`);
|
|
370
440
|
}
|
|
371
441
|
|
|
372
442
|
async function cmdDelete(config: Config, args: string[]) {
|
|
@@ -503,7 +573,7 @@ async function cmdStatus(config: Config, args: string[]) {
|
|
|
503
573
|
console.log(`${c.bold("Volume:")} ${inst.volume}`);
|
|
504
574
|
console.log(`${c.bold("Created:")} ${inst.created_at}`);
|
|
505
575
|
if (inst.status === "running") {
|
|
506
|
-
console.log(`${c.bold("Dashboard:")} ${dashboardURL(host, inst.port
|
|
576
|
+
console.log(`${c.bold("Dashboard:")} ${dashboardURL(host, inst.port)}`);
|
|
507
577
|
}
|
|
508
578
|
if (inst.telegram_bot_token) {
|
|
509
579
|
console.log(`${c.bold("Telegram:")} configured`);
|