@parall/openclaw-agent 1.30.0 → 1.32.0

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/dist/index.js +153 -85
  2. package/package.json +1 -1
  3. package/src/index.ts +191 -90
package/dist/index.js CHANGED
@@ -4,13 +4,15 @@
4
4
  // agent's credentials, installs the bundled plugin, then execs
5
5
  // `openclaw gateway run`. The daemon supervisor spawns one of these per
6
6
  // attached OpenClaw agent.
7
- import { execFileSync, spawn } from "node:child_process";
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
7
+ import { execFileSync, spawn } from 'node:child_process';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
10
  // ---------------------------------------------------------------------------
11
11
  // 1. Read + validate ENV
12
12
  // ---------------------------------------------------------------------------
13
- function ts() { return new Date().toISOString(); }
13
+ function ts() {
14
+ return new Date().toISOString();
15
+ }
14
16
  const log = {
15
17
  info: (msg) => console.log(`${ts()} [openclaw-agent] ${msg}`),
16
18
  warn: (msg) => console.warn(`${ts()} [openclaw-agent] ${msg}`),
@@ -24,38 +26,34 @@ function env(name) {
24
26
  }
25
27
  return v;
26
28
  }
27
- const PRLL_API_URL = env("PRLL_API_URL");
28
- const PRLL_API_KEY = env("PRLL_API_KEY");
29
- const PRLL_ORG_ID = env("PRLL_ORG_ID");
30
- const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
31
- const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
32
- const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
33
- const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
34
- const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
35
- || "/opt/parall-plugin/parall-plugin.tgz";
29
+ const PRLL_API_URL = env('PRLL_API_URL');
30
+ const PRLL_API_KEY = env('PRLL_API_KEY');
31
+ const PRLL_ORG_ID = env('PRLL_ORG_ID');
32
+ const stateDir = env('PRLL_STATE_DIR');
33
+ const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || '';
34
+ const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || '';
35
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || '0';
36
+ const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim() || '/opt/parall-plugin/parall-plugin.tgz';
36
37
  // ---------------------------------------------------------------------------
37
38
  // 2. Create per-agent state directory
38
39
  // ---------------------------------------------------------------------------
39
- const openclawStateDir = path.join(stateDir, ".openclaw");
40
- const configPath = path.join(openclawStateDir, "openclaw.json");
41
- fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
42
- fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
40
+ const openclawStateDir = path.join(stateDir, '.openclaw');
41
+ const configPath = path.join(openclawStateDir, 'openclaw.json');
42
+ fs.mkdirSync(path.join(openclawStateDir, 'sessions'), { recursive: true });
43
+ fs.mkdirSync(path.join(openclawStateDir, 'workspace'), { recursive: true });
43
44
  // ---------------------------------------------------------------------------
44
45
  // 3. Install plugin from bundled archive
45
46
  // ---------------------------------------------------------------------------
46
47
  if (fs.existsSync(pluginArchive)) {
47
48
  // Clean legacy extension dir before install (only when we have an archive
48
49
  // to replace it — otherwise the existing install is the only copy).
49
- const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
50
+ const legacyExtDir = path.join(openclawStateDir, 'extensions', 'parall');
50
51
  fs.rmSync(legacyExtDir, { recursive: true, force: true });
51
52
  log.info(`Installing Parall plugin from ${pluginArchive}...`);
52
53
  try {
53
- execFileSync("openclaw", [
54
- "plugins", "install", pluginArchive,
55
- "--force", "--dangerously-force-unsafe-install",
56
- ], {
54
+ execFileSync('openclaw', ['plugins', 'install', pluginArchive, '--force', '--dangerously-force-unsafe-install'], {
57
55
  env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
58
- stdio: "inherit",
56
+ stdio: 'inherit',
59
57
  timeout: 60_000,
60
58
  });
61
59
  }
@@ -74,13 +72,17 @@ writeOpenclawConfig();
74
72
  function writeOpenclawConfig() {
75
73
  let cfg = {};
76
74
  try {
77
- cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
75
+ cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
76
+ }
77
+ catch {
78
+ /* fresh config */
78
79
  }
79
- catch { /* fresh config */ }
80
- const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway : {};
81
- gateway.mode = "local";
80
+ const gateway = cfg.gateway && typeof cfg.gateway === 'object' ? cfg.gateway : {};
81
+ gateway.mode = 'local';
82
82
  cfg.gateway = gateway;
83
- const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels : {};
83
+ const channels = cfg.channels && typeof cfg.channels === 'object'
84
+ ? cfg.channels
85
+ : {};
84
86
  const parallChannel = {
85
87
  parall_url: PRLL_API_URL,
86
88
  api_key: PRLL_API_KEY,
@@ -90,8 +92,10 @@ function writeOpenclawConfig() {
90
92
  parallChannel.ws_url = PRLL_WS_URL;
91
93
  channels.parall = parallChannel;
92
94
  cfg.channels = channels;
93
- const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins : {};
94
- const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries : {};
95
+ const plugins = cfg.plugins && typeof cfg.plugins === 'object' ? cfg.plugins : {};
96
+ const entries = plugins.entries && typeof plugins.entries === 'object'
97
+ ? plugins.entries
98
+ : {};
95
99
  const parallPluginConfig = {
96
100
  parall_url: PRLL_API_URL,
97
101
  api_key: PRLL_API_KEY,
@@ -99,7 +103,9 @@ function writeOpenclawConfig() {
99
103
  };
100
104
  if (PRLL_WS_URL)
101
105
  parallPluginConfig.ws_url = PRLL_WS_URL;
102
- const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall : {};
106
+ const existingParall = entries.parall && typeof entries.parall === 'object'
107
+ ? entries.parall
108
+ : {};
103
109
  entries.parall = {
104
110
  ...existingParall,
105
111
  enabled: true,
@@ -109,11 +115,17 @@ function writeOpenclawConfig() {
109
115
  plugins.entries = entries;
110
116
  cfg.plugins = plugins;
111
117
  // sqlite-vec vector index guard
112
- const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
113
- const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
114
- const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch : {};
115
- const store = (ms.store && typeof ms.store === "object") ? ms.store : {};
116
- const vector = (store.vector && typeof store.vector === "object") ? store.vector : {};
118
+ const agents = cfg.agents && typeof cfg.agents === 'object' ? cfg.agents : {};
119
+ const defaults = agents.defaults && typeof agents.defaults === 'object'
120
+ ? agents.defaults
121
+ : {};
122
+ const ms = defaults.memorySearch && typeof defaults.memorySearch === 'object'
123
+ ? defaults.memorySearch
124
+ : {};
125
+ const store = ms.store && typeof ms.store === 'object' ? ms.store : {};
126
+ const vector = store.vector && typeof store.vector === 'object'
127
+ ? store.vector
128
+ : {};
117
129
  if (vector.enabled === undefined)
118
130
  vector.enabled = true;
119
131
  store.vector = vector;
@@ -122,12 +134,12 @@ function writeOpenclawConfig() {
122
134
  agents.defaults = defaults;
123
135
  cfg.agents = agents;
124
136
  // Seed tools.alsoAllow
125
- const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools : {};
137
+ const tools = cfg.tools && typeof cfg.tools === 'object' ? cfg.tools : {};
126
138
  const alsoAllow = new Set(Array.isArray(tools.alsoAllow) ? tools.alsoAllow : []);
127
- alsoAllow.add("group:plugins");
139
+ alsoAllow.add('group:plugins');
128
140
  tools.alsoAllow = Array.from(alsoAllow);
129
141
  cfg.tools = tools;
130
- const tmp = configPath + ".tmp";
142
+ const tmp = configPath + '.tmp';
131
143
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
132
144
  fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
133
145
  fs.renameSync(tmp, configPath);
@@ -144,7 +156,7 @@ async function preseedPlatformConfig() {
144
156
  try {
145
157
  const headers = { Authorization: `Bearer ${PRLL_API_KEY}` };
146
158
  if (PRLL_SWIMLANE_NAME)
147
- headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
159
+ headers['X-Prll-Swimlane'] = PRLL_SWIMLANE_NAME;
148
160
  const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
149
161
  headers,
150
162
  signal: AbortSignal.timeout(15_000),
@@ -155,54 +167,106 @@ async function preseedPlatformConfig() {
155
167
  const pc = (data.config ?? data);
156
168
  let cfg = {};
157
169
  try {
158
- cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
170
+ cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
171
+ }
172
+ catch {
173
+ /* fresh */
159
174
  }
160
- catch { /* fresh */ }
161
175
  // Keep in sync with deploy/openclaw-docker/entrypoint.sh and
162
176
  // ts/openclaw-channel/src/config-manager.ts.
163
- const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
177
+ const ALLOWED_DEFAULTS = new Set(['model', 'compaction', 'memorySearch']);
164
178
  const platformDefaults = pc.agents?.defaults;
165
- if (platformDefaults && typeof platformDefaults === "object") {
166
- const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
167
- const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
179
+ const agents = cfg.agents && typeof cfg.agents === 'object' ? cfg.agents : {};
180
+ const existing = agents.defaults && typeof agents.defaults === 'object'
181
+ ? agents.defaults
182
+ : {};
183
+ // `model` is platform-sourced (delivered only on the catalog-gated Parall
184
+ // route). Drop any carried-over value so a stale `parall/...` model doesn't
185
+ // survive a switch to runtime_auth — the server then omits agents.defaults
186
+ // entirely, and without this drop the runtime keeps using the Parall
187
+ // provider. compaction/memorySearch are operator-tunable, so preserved.
188
+ const hadModel = 'model' in existing;
189
+ delete existing.model;
190
+ if (platformDefaults && typeof platformDefaults === 'object') {
168
191
  for (const [k, v] of Object.entries(platformDefaults)) {
169
192
  if (ALLOWED_DEFAULTS.has(k))
170
193
  existing[k] = v;
171
194
  }
195
+ }
196
+ if (hadModel || (platformDefaults && typeof platformDefaults === 'object')) {
172
197
  agents.defaults = existing;
173
198
  cfg.agents = agents;
174
199
  }
175
200
  // Keep in sync with deploy/openclaw-docker/entrypoint.sh and
176
201
  // ts/openclaw-channel/src/config-manager.ts.
177
- const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
202
+ const ALLOWED_MODEL_KEYS = new Set(['id', 'name', 'contextWindow', 'maxTokens']);
178
203
  const platformModels = pc.models?.providers;
179
- const platformParall = platformModels?.parall;
180
- if (platformParall && typeof platformParall === "object") {
181
- const models = (cfg.models && typeof cfg.models === "object") ? cfg.models : {};
182
- const providers = (models.providers && typeof models.providers === "object") ? models.providers : {};
183
- const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall : {};
184
- const merged = { ...existingParall, ...platformParall };
185
- if (Array.isArray(merged.models)) {
186
- merged.models = merged.models
187
- .filter((m) => m && typeof m === "object")
188
- .map((m) => {
189
- const clean = {};
190
- for (const [k, v] of Object.entries(m)) {
191
- if (ALLOWED_MODEL_KEYS.has(k))
192
- clean[k] = v;
193
- }
194
- return clean;
195
- });
204
+ if (platformModels && typeof platformModels === 'object') {
205
+ const models = cfg.models && typeof cfg.models === 'object' ? cfg.models : {};
206
+ const providers = models.providers && typeof models.providers === 'object'
207
+ ? models.providers
208
+ : {};
209
+ // Parall-managed providers: overlay when present. parall-anthropic is
210
+ // deleted when absent (rollback safety); parall is never deleted.
211
+ // Keep in sync with config-manager.ts.
212
+ for (const providerName of ['parall', 'parall-anthropic']) {
213
+ const platformProvider = platformModels[providerName];
214
+ if (!platformProvider || typeof platformProvider !== 'object') {
215
+ if (providerName !== 'parall')
216
+ delete providers[providerName];
217
+ continue;
218
+ }
219
+ const existing = providers[providerName] && typeof providers[providerName] === 'object'
220
+ ? providers[providerName]
221
+ : {};
222
+ const merged = { ...existing, ...platformProvider };
223
+ if (Array.isArray(merged.models)) {
224
+ merged.models = merged.models
225
+ .filter((m) => m && typeof m === 'object')
226
+ .map((m) => {
227
+ const clean = {};
228
+ for (const [k, v] of Object.entries(m)) {
229
+ if (ALLOWED_MODEL_KEYS.has(k))
230
+ clean[k] = v;
231
+ }
232
+ return clean;
233
+ });
234
+ }
235
+ merged.apiKey = PRLL_API_KEY;
236
+ providers[providerName] = merged;
196
237
  }
197
- merged.apiKey = PRLL_API_KEY;
198
- providers.parall = merged;
199
238
  models.providers = providers;
200
239
  cfg.models = models;
201
240
  }
202
- const tmp = configPath + ".tmp";
241
+ // Bidirectional model rewrite for parall-anthropic — gate on fresh platform
242
+ // payload for rollback safety. Keep in sync with config-manager.ts.
243
+ const agentsCfg = cfg.agents && typeof cfg.agents === 'object' ? cfg.agents : {};
244
+ const defaultsCfg = agentsCfg.defaults && typeof agentsCfg.defaults === 'object'
245
+ ? agentsCfg.defaults
246
+ : {};
247
+ if (typeof defaultsCfg.model === 'string') {
248
+ const modelStr = defaultsCfg.model;
249
+ if (platformModels?.['parall-anthropic']) {
250
+ const fwd = modelStr.match(/^parall\/(anthropic\/.+)$/);
251
+ if (fwd) {
252
+ defaultsCfg.model = `parall-anthropic/${fwd[1]}`;
253
+ agentsCfg.defaults = defaultsCfg;
254
+ cfg.agents = agentsCfg;
255
+ }
256
+ }
257
+ else {
258
+ const rev = modelStr.match(/^parall-anthropic\/(anthropic\/.+)$/);
259
+ if (rev) {
260
+ defaultsCfg.model = `parall/${rev[1]}`;
261
+ agentsCfg.defaults = defaultsCfg;
262
+ cfg.agents = agentsCfg;
263
+ }
264
+ }
265
+ }
266
+ const tmp = configPath + '.tmp';
203
267
  fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
204
268
  fs.renameSync(tmp, configPath);
205
- const model = cfg.agents?.defaults?.model ?? "none";
269
+ const model = cfg.agents?.defaults?.model ?? 'none';
206
270
  log.info(`Platform config pre-seeded (model: ${String(model)}).`);
207
271
  }
208
272
  catch (err) {
@@ -213,18 +277,20 @@ async function preseedPlatformConfig() {
213
277
  // 6. openclaw doctor --fix (non-fatal)
214
278
  // ---------------------------------------------------------------------------
215
279
  try {
216
- execFileSync("openclaw", ["doctor", "--fix"], {
280
+ execFileSync('openclaw', ['doctor', '--fix'], {
217
281
  env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
218
- stdio: "inherit",
282
+ stdio: 'inherit',
219
283
  timeout: 30_000,
220
284
  });
221
285
  }
222
- catch { /* non-fatal */ }
286
+ catch {
287
+ /* non-fatal */
288
+ }
223
289
  // ---------------------------------------------------------------------------
224
290
  // 7. Spawn openclaw gateway run with signal forwarding
225
291
  // ---------------------------------------------------------------------------
226
- log.info("Starting OpenClaw gateway...");
227
- const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
292
+ log.info('Starting OpenClaw gateway...');
293
+ const workspaceDir = process.env.PRLL_WORKSPACE_DIR?.trim() || '';
228
294
  const gatewayEnv = {
229
295
  ...process.env,
230
296
  OPENCLAW_STATE_DIR: openclawStateDir,
@@ -235,31 +301,33 @@ if (workspaceDir) {
235
301
  if (PRLL_SWIMLANE_NAME) {
236
302
  gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
237
303
  }
238
- const gatewayArgs = ["gateway", "run"];
239
- if (gatewayPort !== "0") {
240
- gatewayArgs.push("--port", gatewayPort);
304
+ const gatewayArgs = ['gateway', 'run'];
305
+ if (gatewayPort !== '0') {
306
+ gatewayArgs.push('--port', gatewayPort);
241
307
  gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
242
308
  }
243
- const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
309
+ const cwd = workspaceDir || path.join(openclawStateDir, 'workspace');
244
310
  fs.mkdirSync(cwd, { recursive: true });
245
- const child = spawn("openclaw", gatewayArgs, {
311
+ const child = spawn('openclaw', gatewayArgs, {
246
312
  env: gatewayEnv,
247
313
  cwd,
248
- stdio: "inherit",
314
+ stdio: 'inherit',
249
315
  detached: false,
250
316
  });
251
317
  function forwardSignal(sig) {
252
318
  try {
253
319
  child.kill(sig);
254
320
  }
255
- catch { /* already gone */ }
321
+ catch {
322
+ /* already gone */
323
+ }
256
324
  }
257
- process.on("SIGTERM", () => forwardSignal("SIGTERM"));
258
- process.on("SIGINT", () => forwardSignal("SIGINT"));
259
- child.on("close", (code, signal) => {
260
- if (signal === "SIGTERM")
325
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
326
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
327
+ child.on('close', (code, signal) => {
328
+ if (signal === 'SIGTERM')
261
329
  process.exit(143);
262
- if (signal === "SIGINT")
330
+ if (signal === 'SIGINT')
263
331
  process.exit(130);
264
332
  process.exit(code ?? 1);
265
333
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/openclaw-agent",
3
- "version": "1.30.0",
3
+ "version": "1.32.0",
4
4
  "description": "OpenClaw bootstrap wrapper for daemon-mode Parall agents — sets up per-agent OpenClaw state and execs openclaw gateway run",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -6,15 +6,17 @@
6
6
  // `openclaw gateway run`. The daemon supervisor spawns one of these per
7
7
  // attached OpenClaw agent.
8
8
 
9
- import { execFileSync, spawn } from "node:child_process";
10
- import * as fs from "node:fs";
11
- import * as path from "node:path";
9
+ import { execFileSync, spawn } from 'node:child_process';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // 1. Read + validate ENV
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- function ts(): string { return new Date().toISOString(); }
17
+ function ts(): string {
18
+ return new Date().toISOString();
19
+ }
18
20
  const log = {
19
21
  info: (msg: string) => console.log(`${ts()} [openclaw-agent] ${msg}`),
20
22
  warn: (msg: string) => console.warn(`${ts()} [openclaw-agent] ${msg}`),
@@ -30,26 +32,26 @@ function env(name: string): string {
30
32
  return v;
31
33
  }
32
34
 
33
- const PRLL_API_URL = env("PRLL_API_URL");
34
- const PRLL_API_KEY = env("PRLL_API_KEY");
35
- const PRLL_ORG_ID = env("PRLL_ORG_ID");
36
- const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
35
+ const PRLL_API_URL = env('PRLL_API_URL');
36
+ const PRLL_API_KEY = env('PRLL_API_KEY');
37
+ const PRLL_ORG_ID = env('PRLL_ORG_ID');
38
+ const stateDir = env('PRLL_STATE_DIR');
37
39
 
38
- const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
39
- const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
40
- const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
41
- const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
42
- || "/opt/parall-plugin/parall-plugin.tgz";
40
+ const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || '';
41
+ const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || '';
42
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || '0';
43
+ const pluginArchive =
44
+ process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim() || '/opt/parall-plugin/parall-plugin.tgz';
43
45
 
44
46
  // ---------------------------------------------------------------------------
45
47
  // 2. Create per-agent state directory
46
48
  // ---------------------------------------------------------------------------
47
49
 
48
- const openclawStateDir = path.join(stateDir, ".openclaw");
49
- const configPath = path.join(openclawStateDir, "openclaw.json");
50
+ const openclawStateDir = path.join(stateDir, '.openclaw');
51
+ const configPath = path.join(openclawStateDir, 'openclaw.json');
50
52
 
51
- fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
52
- fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
53
+ fs.mkdirSync(path.join(openclawStateDir, 'sessions'), { recursive: true });
54
+ fs.mkdirSync(path.join(openclawStateDir, 'workspace'), { recursive: true });
53
55
 
54
56
  // ---------------------------------------------------------------------------
55
57
  // 3. Install plugin from bundled archive
@@ -58,27 +60,26 @@ fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
58
60
  if (fs.existsSync(pluginArchive)) {
59
61
  // Clean legacy extension dir before install (only when we have an archive
60
62
  // to replace it — otherwise the existing install is the only copy).
61
- const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
63
+ const legacyExtDir = path.join(openclawStateDir, 'extensions', 'parall');
62
64
  fs.rmSync(legacyExtDir, { recursive: true, force: true });
63
65
 
64
66
  log.info(`Installing Parall plugin from ${pluginArchive}...`);
65
67
  try {
66
- execFileSync("openclaw", [
67
- "plugins", "install", pluginArchive,
68
- "--force", "--dangerously-force-unsafe-install",
69
- ], {
70
- env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
71
- stdio: "inherit",
72
- timeout: 60_000,
73
- });
68
+ execFileSync(
69
+ 'openclaw',
70
+ ['plugins', 'install', pluginArchive, '--force', '--dangerously-force-unsafe-install'],
71
+ {
72
+ env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
73
+ stdio: 'inherit',
74
+ timeout: 60_000,
75
+ },
76
+ );
74
77
  } catch (err) {
75
78
  log.error(`Failed to install Parall plugin: ${String(err)}`);
76
79
  process.exit(1);
77
80
  }
78
81
  } else {
79
- log.warn(
80
- `Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`,
81
- );
82
+ log.warn(`Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`);
82
83
  }
83
84
 
84
85
  // ---------------------------------------------------------------------------
@@ -90,14 +91,20 @@ writeOpenclawConfig();
90
91
  function writeOpenclawConfig(): void {
91
92
  let cfg: Record<string, unknown> = {};
92
93
  try {
93
- cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
94
- } catch { /* fresh config */ }
94
+ cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
95
+ } catch {
96
+ /* fresh config */
97
+ }
95
98
 
96
- const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway as Record<string, unknown> : {};
97
- gateway.mode = "local";
99
+ const gateway =
100
+ cfg.gateway && typeof cfg.gateway === 'object' ? (cfg.gateway as Record<string, unknown>) : {};
101
+ gateway.mode = 'local';
98
102
  cfg.gateway = gateway;
99
103
 
100
- const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels as Record<string, unknown> : {};
104
+ const channels =
105
+ cfg.channels && typeof cfg.channels === 'object'
106
+ ? (cfg.channels as Record<string, unknown>)
107
+ : {};
101
108
  const parallChannel: Record<string, unknown> = {
102
109
  parall_url: PRLL_API_URL,
103
110
  api_key: PRLL_API_KEY,
@@ -107,15 +114,22 @@ function writeOpenclawConfig(): void {
107
114
  channels.parall = parallChannel;
108
115
  cfg.channels = channels;
109
116
 
110
- const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins as Record<string, unknown> : {};
111
- const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries as Record<string, unknown> : {};
117
+ const plugins =
118
+ cfg.plugins && typeof cfg.plugins === 'object' ? (cfg.plugins as Record<string, unknown>) : {};
119
+ const entries =
120
+ plugins.entries && typeof plugins.entries === 'object'
121
+ ? (plugins.entries as Record<string, unknown>)
122
+ : {};
112
123
  const parallPluginConfig: Record<string, unknown> = {
113
124
  parall_url: PRLL_API_URL,
114
125
  api_key: PRLL_API_KEY,
115
126
  org_id: PRLL_ORG_ID,
116
127
  };
117
128
  if (PRLL_WS_URL) parallPluginConfig.ws_url = PRLL_WS_URL;
118
- const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall as Record<string, unknown> : {};
129
+ const existingParall =
130
+ entries.parall && typeof entries.parall === 'object'
131
+ ? (entries.parall as Record<string, unknown>)
132
+ : {};
119
133
  entries.parall = {
120
134
  ...existingParall,
121
135
  enabled: true,
@@ -126,11 +140,22 @@ function writeOpenclawConfig(): void {
126
140
  cfg.plugins = plugins;
127
141
 
128
142
  // sqlite-vec vector index guard
129
- const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
130
- const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
131
- const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch as Record<string, unknown> : {};
132
- const store = (ms.store && typeof ms.store === "object") ? ms.store as Record<string, unknown> : {};
133
- const vector = (store.vector && typeof store.vector === "object") ? store.vector as Record<string, unknown> : {};
143
+ const agents =
144
+ cfg.agents && typeof cfg.agents === 'object' ? (cfg.agents as Record<string, unknown>) : {};
145
+ const defaults =
146
+ agents.defaults && typeof agents.defaults === 'object'
147
+ ? (agents.defaults as Record<string, unknown>)
148
+ : {};
149
+ const ms =
150
+ defaults.memorySearch && typeof defaults.memorySearch === 'object'
151
+ ? (defaults.memorySearch as Record<string, unknown>)
152
+ : {};
153
+ const store =
154
+ ms.store && typeof ms.store === 'object' ? (ms.store as Record<string, unknown>) : {};
155
+ const vector =
156
+ store.vector && typeof store.vector === 'object'
157
+ ? (store.vector as Record<string, unknown>)
158
+ : {};
134
159
  if (vector.enabled === undefined) vector.enabled = true;
135
160
  store.vector = vector;
136
161
  ms.store = store;
@@ -139,15 +164,16 @@ function writeOpenclawConfig(): void {
139
164
  cfg.agents = agents;
140
165
 
141
166
  // Seed tools.alsoAllow
142
- const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools as Record<string, unknown> : {};
167
+ const tools =
168
+ cfg.tools && typeof cfg.tools === 'object' ? (cfg.tools as Record<string, unknown>) : {};
143
169
  const alsoAllow = new Set<string>(
144
170
  Array.isArray(tools.alsoAllow) ? (tools.alsoAllow as string[]) : [],
145
171
  );
146
- alsoAllow.add("group:plugins");
172
+ alsoAllow.add('group:plugins');
147
173
  tools.alsoAllow = Array.from(alsoAllow);
148
174
  cfg.tools = tools;
149
175
 
150
- const tmp = configPath + ".tmp";
176
+ const tmp = configPath + '.tmp';
151
177
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
152
178
  fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
153
179
  fs.renameSync(tmp, configPath);
@@ -166,7 +192,7 @@ await preseedPlatformConfig();
166
192
  async function preseedPlatformConfig(): Promise<void> {
167
193
  try {
168
194
  const headers: Record<string, string> = { Authorization: `Bearer ${PRLL_API_KEY}` };
169
- if (PRLL_SWIMLANE_NAME) headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
195
+ if (PRLL_SWIMLANE_NAME) headers['X-Prll-Swimlane'] = PRLL_SWIMLANE_NAME;
170
196
 
171
197
  const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
172
198
  headers,
@@ -177,53 +203,122 @@ async function preseedPlatformConfig(): Promise<void> {
177
203
  const pc = (data.config ?? data) as Record<string, unknown>;
178
204
 
179
205
  let cfg: Record<string, unknown> = {};
180
- try { cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>; } catch { /* fresh */ }
206
+ try {
207
+ cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
208
+ } catch {
209
+ /* fresh */
210
+ }
181
211
 
182
212
  // Keep in sync with deploy/openclaw-docker/entrypoint.sh and
183
213
  // ts/openclaw-channel/src/config-manager.ts.
184
- const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
214
+ const ALLOWED_DEFAULTS = new Set(['model', 'compaction', 'memorySearch']);
185
215
  const platformDefaults = (pc.agents as Record<string, unknown> | undefined)?.defaults;
186
- if (platformDefaults && typeof platformDefaults === "object") {
187
- const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
188
- const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
216
+ const agents =
217
+ cfg.agents && typeof cfg.agents === 'object' ? (cfg.agents as Record<string, unknown>) : {};
218
+ const existing =
219
+ agents.defaults && typeof agents.defaults === 'object'
220
+ ? (agents.defaults as Record<string, unknown>)
221
+ : {};
222
+ // `model` is platform-sourced (delivered only on the catalog-gated Parall
223
+ // route). Drop any carried-over value so a stale `parall/...` model doesn't
224
+ // survive a switch to runtime_auth — the server then omits agents.defaults
225
+ // entirely, and without this drop the runtime keeps using the Parall
226
+ // provider. compaction/memorySearch are operator-tunable, so preserved.
227
+ const hadModel = 'model' in existing;
228
+ delete existing.model;
229
+ if (platformDefaults && typeof platformDefaults === 'object') {
189
230
  for (const [k, v] of Object.entries(platformDefaults as Record<string, unknown>)) {
190
231
  if (ALLOWED_DEFAULTS.has(k)) existing[k] = v;
191
232
  }
233
+ }
234
+ if (hadModel || (platformDefaults && typeof platformDefaults === 'object')) {
192
235
  agents.defaults = existing;
193
236
  cfg.agents = agents;
194
237
  }
195
238
 
196
239
  // Keep in sync with deploy/openclaw-docker/entrypoint.sh and
197
240
  // ts/openclaw-channel/src/config-manager.ts.
198
- const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
199
- const platformModels = (pc.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined;
200
- const platformParall = platformModels?.parall;
201
- if (platformParall && typeof platformParall === "object") {
202
- const models = (cfg.models && typeof cfg.models === "object") ? cfg.models as Record<string, unknown> : {};
203
- const providers = (models.providers && typeof models.providers === "object") ? models.providers as Record<string, unknown> : {};
204
- const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall as Record<string, unknown> : {};
205
- const merged = { ...existingParall, ...(platformParall as Record<string, unknown>) };
206
- if (Array.isArray(merged.models)) {
207
- merged.models = (merged.models as Record<string, unknown>[])
208
- .filter((m) => m && typeof m === "object")
209
- .map((m) => {
210
- const clean: Record<string, unknown> = {};
211
- for (const [k, v] of Object.entries(m)) {
212
- if (ALLOWED_MODEL_KEYS.has(k)) clean[k] = v;
213
- }
214
- return clean;
215
- });
241
+ const ALLOWED_MODEL_KEYS = new Set(['id', 'name', 'contextWindow', 'maxTokens']);
242
+ const platformModels = (pc.models as Record<string, unknown> | undefined)?.providers as
243
+ | Record<string, unknown>
244
+ | undefined;
245
+ if (platformModels && typeof platformModels === 'object') {
246
+ const models =
247
+ cfg.models && typeof cfg.models === 'object' ? (cfg.models as Record<string, unknown>) : {};
248
+ const providers =
249
+ models.providers && typeof models.providers === 'object'
250
+ ? (models.providers as Record<string, unknown>)
251
+ : {};
252
+
253
+ // Parall-managed providers: overlay when present. parall-anthropic is
254
+ // deleted when absent (rollback safety); parall is never deleted.
255
+ // Keep in sync with config-manager.ts.
256
+ for (const providerName of ['parall', 'parall-anthropic']) {
257
+ const platformProvider = platformModels[providerName];
258
+ if (!platformProvider || typeof platformProvider !== 'object') {
259
+ if (providerName !== 'parall') delete providers[providerName];
260
+ continue;
261
+ }
262
+ const existing =
263
+ providers[providerName] && typeof providers[providerName] === 'object'
264
+ ? (providers[providerName] as Record<string, unknown>)
265
+ : {};
266
+ const merged = { ...existing, ...(platformProvider as Record<string, unknown>) };
267
+ if (Array.isArray(merged.models)) {
268
+ merged.models = (merged.models as Record<string, unknown>[])
269
+ .filter((m) => m && typeof m === 'object')
270
+ .map((m) => {
271
+ const clean: Record<string, unknown> = {};
272
+ for (const [k, v] of Object.entries(m)) {
273
+ if (ALLOWED_MODEL_KEYS.has(k)) clean[k] = v;
274
+ }
275
+ return clean;
276
+ });
277
+ }
278
+ merged.apiKey = PRLL_API_KEY;
279
+ providers[providerName] = merged;
216
280
  }
217
- merged.apiKey = PRLL_API_KEY;
218
- providers.parall = merged;
281
+
219
282
  models.providers = providers;
220
283
  cfg.models = models;
221
284
  }
222
285
 
223
- const tmp = configPath + ".tmp";
286
+ // Bidirectional model rewrite for parall-anthropic — gate on fresh platform
287
+ // payload for rollback safety. Keep in sync with config-manager.ts.
288
+ const agentsCfg =
289
+ cfg.agents && typeof cfg.agents === 'object' ? (cfg.agents as Record<string, unknown>) : {};
290
+ const defaultsCfg =
291
+ agentsCfg.defaults && typeof agentsCfg.defaults === 'object'
292
+ ? (agentsCfg.defaults as Record<string, unknown>)
293
+ : {};
294
+ if (typeof defaultsCfg.model === 'string') {
295
+ const modelStr = defaultsCfg.model as string;
296
+ if (platformModels?.['parall-anthropic']) {
297
+ const fwd = modelStr.match(/^parall\/(anthropic\/.+)$/);
298
+ if (fwd) {
299
+ defaultsCfg.model = `parall-anthropic/${fwd[1]}`;
300
+ agentsCfg.defaults = defaultsCfg;
301
+ cfg.agents = agentsCfg;
302
+ }
303
+ } else {
304
+ const rev = modelStr.match(/^parall-anthropic\/(anthropic\/.+)$/);
305
+ if (rev) {
306
+ defaultsCfg.model = `parall/${rev[1]}`;
307
+ agentsCfg.defaults = defaultsCfg;
308
+ cfg.agents = agentsCfg;
309
+ }
310
+ }
311
+ }
312
+
313
+ const tmp = configPath + '.tmp';
224
314
  fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
225
315
  fs.renameSync(tmp, configPath);
226
- const model = ((cfg.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined)?.model ?? "none";
316
+ const model =
317
+ (
318
+ (cfg.agents as Record<string, unknown> | undefined)?.defaults as
319
+ | Record<string, unknown>
320
+ | undefined
321
+ )?.model ?? 'none';
227
322
  log.info(`Platform config pre-seeded (model: ${String(model)}).`);
228
323
  } catch (err) {
229
324
  log.warn(`Platform config pre-seed skipped: ${String(err)}`);
@@ -235,21 +330,23 @@ async function preseedPlatformConfig(): Promise<void> {
235
330
  // ---------------------------------------------------------------------------
236
331
 
237
332
  try {
238
- execFileSync("openclaw", ["doctor", "--fix"], {
333
+ execFileSync('openclaw', ['doctor', '--fix'], {
239
334
  env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
240
- stdio: "inherit",
335
+ stdio: 'inherit',
241
336
  timeout: 30_000,
242
337
  });
243
- } catch { /* non-fatal */ }
338
+ } catch {
339
+ /* non-fatal */
340
+ }
244
341
 
245
342
  // ---------------------------------------------------------------------------
246
343
  // 7. Spawn openclaw gateway run with signal forwarding
247
344
 
248
345
  // ---------------------------------------------------------------------------
249
346
 
250
- log.info("Starting OpenClaw gateway...");
347
+ log.info('Starting OpenClaw gateway...');
251
348
 
252
- const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
349
+ const workspaceDir = process.env.PRLL_WORKSPACE_DIR?.trim() || '';
253
350
 
254
351
  const gatewayEnv: NodeJS.ProcessEnv = {
255
352
  ...process.env,
@@ -262,30 +359,34 @@ if (PRLL_SWIMLANE_NAME) {
262
359
  gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
263
360
  }
264
361
 
265
- const gatewayArgs = ["gateway", "run"];
266
- if (gatewayPort !== "0") {
267
- gatewayArgs.push("--port", gatewayPort);
362
+ const gatewayArgs = ['gateway', 'run'];
363
+ if (gatewayPort !== '0') {
364
+ gatewayArgs.push('--port', gatewayPort);
268
365
  gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
269
366
  }
270
367
 
271
- const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
368
+ const cwd = workspaceDir || path.join(openclawStateDir, 'workspace');
272
369
  fs.mkdirSync(cwd, { recursive: true });
273
370
 
274
- const child = spawn("openclaw", gatewayArgs, {
371
+ const child = spawn('openclaw', gatewayArgs, {
275
372
  env: gatewayEnv,
276
373
  cwd,
277
- stdio: "inherit",
374
+ stdio: 'inherit',
278
375
  detached: false,
279
376
  });
280
377
 
281
378
  function forwardSignal(sig: NodeJS.Signals): void {
282
- try { child.kill(sig); } catch { /* already gone */ }
379
+ try {
380
+ child.kill(sig);
381
+ } catch {
382
+ /* already gone */
383
+ }
283
384
  }
284
- process.on("SIGTERM", () => forwardSignal("SIGTERM"));
285
- process.on("SIGINT", () => forwardSignal("SIGINT"));
385
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
386
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
286
387
 
287
- child.on("close", (code, signal) => {
288
- if (signal === "SIGTERM") process.exit(143);
289
- if (signal === "SIGINT") process.exit(130);
388
+ child.on('close', (code, signal) => {
389
+ if (signal === 'SIGTERM') process.exit(143);
390
+ if (signal === 'SIGINT') process.exit(130);
290
391
  process.exit(code ?? 1);
291
392
  });