@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.
Files changed (3) hide show
  1. package/README.md +28 -0
  2. package/clawpool.ts +95 -25
  3. 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, token: string): string {
67
- return `http://${host}:${port}?token=${token}`;
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
- function openclawConfig(token: string): string {
158
- return JSON.stringify({
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: { dangerouslyAllowHostHeaderOriginFallback: true },
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 = openclawConfig(inst.gateway_token);
168
- // Init as root to fix volume permissions, write config, then drop to node user
169
- const entrypoint = [
170
- "sh",
171
- "-c",
172
- [
173
- "mkdir -p /home/node/data/.openclaw",
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, gatewayToken)}`);
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)} DASHBOARD`
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, inst.gateway_token)
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, inst.gateway_token)}`);
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, inst.gateway_token)}`);
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, inst.gateway_token)}`);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkclaw/clawpool",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clawpool": "./clawpool.ts"