@parall/openclaw-agent 1.28.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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +255 -0
- package/package.json +36 -0
- package/src/index.ts +280 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bootstrap wrapper for daemon-mode OpenClaw agents.
|
|
3
|
+
// Sets up a per-agent OpenClaw state dir, writes openclaw.json with the
|
|
4
|
+
// agent's credentials, installs the bundled plugin, then execs
|
|
5
|
+
// `openclaw gateway run`. The daemon supervisor spawns one of these per
|
|
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";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// 1. Read + validate ENV
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const PRLL_API_URL = env("PRLL_API_URL");
|
|
14
|
+
const PRLL_API_KEY = env("PRLL_API_KEY");
|
|
15
|
+
const PRLL_ORG_ID = env("PRLL_ORG_ID");
|
|
16
|
+
const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
|
|
17
|
+
const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
|
|
18
|
+
const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
|
|
19
|
+
const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
|
|
20
|
+
const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
|
|
21
|
+
|| "/opt/parall-plugin/parall-plugin.tgz";
|
|
22
|
+
function env(name) {
|
|
23
|
+
const v = process.env[name]?.trim();
|
|
24
|
+
if (!v) {
|
|
25
|
+
console.error(`ERROR: Missing required environment variable: ${name}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// 2. Create per-agent state directory
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const openclawStateDir = path.join(stateDir, ".openclaw");
|
|
34
|
+
const configPath = path.join(openclawStateDir, "openclaw.json");
|
|
35
|
+
fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
|
|
36
|
+
fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// 3. Install plugin from bundled archive
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
if (fs.existsSync(pluginArchive)) {
|
|
41
|
+
// Clean legacy extension dir before install (only when we have an archive
|
|
42
|
+
// to replace it — otherwise the existing install is the only copy).
|
|
43
|
+
const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
|
|
44
|
+
fs.rmSync(legacyExtDir, { recursive: true, force: true });
|
|
45
|
+
console.log(`Installing Parall plugin from ${pluginArchive}...`);
|
|
46
|
+
try {
|
|
47
|
+
execFileSync("openclaw", [
|
|
48
|
+
"plugins", "install", pluginArchive,
|
|
49
|
+
"--force", "--dangerously-force-unsafe-install",
|
|
50
|
+
], {
|
|
51
|
+
env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
timeout: 60_000,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error(`ERROR: Failed to install Parall plugin: ${String(err)}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.warn(`Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`);
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 4. Write openclaw.json
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
writeOpenclawConfig();
|
|
68
|
+
function writeOpenclawConfig() {
|
|
69
|
+
let cfg = {};
|
|
70
|
+
try {
|
|
71
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch { /* fresh config */ }
|
|
74
|
+
const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway : {};
|
|
75
|
+
gateway.mode = "local";
|
|
76
|
+
cfg.gateway = gateway;
|
|
77
|
+
const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels : {};
|
|
78
|
+
const parallChannel = {
|
|
79
|
+
parall_url: PRLL_API_URL,
|
|
80
|
+
api_key: PRLL_API_KEY,
|
|
81
|
+
org_id: PRLL_ORG_ID,
|
|
82
|
+
};
|
|
83
|
+
if (PRLL_WS_URL)
|
|
84
|
+
parallChannel.ws_url = PRLL_WS_URL;
|
|
85
|
+
channels.parall = parallChannel;
|
|
86
|
+
cfg.channels = channels;
|
|
87
|
+
const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins : {};
|
|
88
|
+
const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries : {};
|
|
89
|
+
const parallPluginConfig = {
|
|
90
|
+
parall_url: PRLL_API_URL,
|
|
91
|
+
api_key: PRLL_API_KEY,
|
|
92
|
+
org_id: PRLL_ORG_ID,
|
|
93
|
+
};
|
|
94
|
+
if (PRLL_WS_URL)
|
|
95
|
+
parallPluginConfig.ws_url = PRLL_WS_URL;
|
|
96
|
+
const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall : {};
|
|
97
|
+
entries.parall = {
|
|
98
|
+
...existingParall,
|
|
99
|
+
enabled: true,
|
|
100
|
+
hooks: { allowPromptInjection: true, allowConversationAccess: true },
|
|
101
|
+
config: parallPluginConfig,
|
|
102
|
+
};
|
|
103
|
+
plugins.entries = entries;
|
|
104
|
+
cfg.plugins = plugins;
|
|
105
|
+
// sqlite-vec vector index guard
|
|
106
|
+
const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
|
|
107
|
+
const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
|
|
108
|
+
const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch : {};
|
|
109
|
+
const store = (ms.store && typeof ms.store === "object") ? ms.store : {};
|
|
110
|
+
const vector = (store.vector && typeof store.vector === "object") ? store.vector : {};
|
|
111
|
+
if (vector.enabled === undefined)
|
|
112
|
+
vector.enabled = true;
|
|
113
|
+
store.vector = vector;
|
|
114
|
+
ms.store = store;
|
|
115
|
+
defaults.memorySearch = ms;
|
|
116
|
+
agents.defaults = defaults;
|
|
117
|
+
cfg.agents = agents;
|
|
118
|
+
// Seed tools.alsoAllow
|
|
119
|
+
const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools : {};
|
|
120
|
+
const alsoAllow = new Set(Array.isArray(tools.alsoAllow) ? tools.alsoAllow : []);
|
|
121
|
+
alsoAllow.add("group:plugins");
|
|
122
|
+
tools.alsoAllow = Array.from(alsoAllow);
|
|
123
|
+
cfg.tools = tools;
|
|
124
|
+
const tmp = configPath + ".tmp";
|
|
125
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
126
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
127
|
+
fs.renameSync(tmp, configPath);
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// 5. Pre-seed platform config (model + provider) before gateway reads it
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// On first boot openclaw.json has no agents.defaults.model. Without this,
|
|
133
|
+
// OpenClaw uses its built-in default which may lack credentials through the
|
|
134
|
+
// Parall proxy. The plugin refreshes this during startAccount but OpenClaw's
|
|
135
|
+
// hot reload only reloads the model catalog, not the active model.
|
|
136
|
+
await preseedPlatformConfig();
|
|
137
|
+
async function preseedPlatformConfig() {
|
|
138
|
+
try {
|
|
139
|
+
const headers = { Authorization: `Bearer ${PRLL_API_KEY}` };
|
|
140
|
+
if (PRLL_SWIMLANE_NAME)
|
|
141
|
+
headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
|
|
142
|
+
const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
|
|
143
|
+
headers,
|
|
144
|
+
signal: AbortSignal.timeout(15_000),
|
|
145
|
+
});
|
|
146
|
+
if (!resp.ok)
|
|
147
|
+
throw new Error(`platform-config ${resp.status}`);
|
|
148
|
+
const data = (await resp.json());
|
|
149
|
+
const pc = (data.config ?? data);
|
|
150
|
+
let cfg = {};
|
|
151
|
+
try {
|
|
152
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
153
|
+
}
|
|
154
|
+
catch { /* fresh */ }
|
|
155
|
+
const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
|
|
156
|
+
const platformDefaults = pc.agents?.defaults;
|
|
157
|
+
if (platformDefaults && typeof platformDefaults === "object") {
|
|
158
|
+
const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
|
|
159
|
+
const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
|
|
160
|
+
for (const [k, v] of Object.entries(platformDefaults)) {
|
|
161
|
+
if (ALLOWED_DEFAULTS.has(k))
|
|
162
|
+
existing[k] = v;
|
|
163
|
+
}
|
|
164
|
+
agents.defaults = existing;
|
|
165
|
+
cfg.agents = agents;
|
|
166
|
+
}
|
|
167
|
+
const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
|
|
168
|
+
const platformModels = pc.models?.providers;
|
|
169
|
+
const platformParall = platformModels?.parall;
|
|
170
|
+
if (platformParall && typeof platformParall === "object") {
|
|
171
|
+
const models = (cfg.models && typeof cfg.models === "object") ? cfg.models : {};
|
|
172
|
+
const providers = (models.providers && typeof models.providers === "object") ? models.providers : {};
|
|
173
|
+
const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall : {};
|
|
174
|
+
const merged = { ...existingParall, ...platformParall };
|
|
175
|
+
if (Array.isArray(merged.models)) {
|
|
176
|
+
merged.models = merged.models
|
|
177
|
+
.filter((m) => m && typeof m === "object")
|
|
178
|
+
.map((m) => {
|
|
179
|
+
const clean = {};
|
|
180
|
+
for (const [k, v] of Object.entries(m)) {
|
|
181
|
+
if (ALLOWED_MODEL_KEYS.has(k))
|
|
182
|
+
clean[k] = v;
|
|
183
|
+
}
|
|
184
|
+
return clean;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
merged.apiKey = PRLL_API_KEY;
|
|
188
|
+
providers.parall = merged;
|
|
189
|
+
models.providers = providers;
|
|
190
|
+
cfg.models = models;
|
|
191
|
+
}
|
|
192
|
+
const tmp = configPath + ".tmp";
|
|
193
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
194
|
+
fs.renameSync(tmp, configPath);
|
|
195
|
+
const model = cfg.agents?.defaults?.model ?? "none";
|
|
196
|
+
console.log(`Platform config pre-seeded (model: ${String(model)}).`);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.warn(`Platform config pre-seed skipped: ${String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// 6. openclaw doctor --fix (non-fatal)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
try {
|
|
206
|
+
execFileSync("openclaw", ["doctor", "--fix"], {
|
|
207
|
+
env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
|
|
208
|
+
stdio: "inherit",
|
|
209
|
+
timeout: 30_000,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch { /* non-fatal */ }
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// 7. Spawn openclaw gateway run with signal forwarding
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
console.log("Starting OpenClaw gateway...");
|
|
217
|
+
const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
|
|
218
|
+
const gatewayEnv = {
|
|
219
|
+
...process.env,
|
|
220
|
+
OPENCLAW_STATE_DIR: openclawStateDir,
|
|
221
|
+
};
|
|
222
|
+
if (workspaceDir) {
|
|
223
|
+
gatewayEnv.PRLL_WIKI_MOUNT_ROOT = workspaceDir;
|
|
224
|
+
}
|
|
225
|
+
if (PRLL_SWIMLANE_NAME) {
|
|
226
|
+
gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
|
|
227
|
+
}
|
|
228
|
+
const gatewayArgs = ["gateway", "run"];
|
|
229
|
+
if (gatewayPort !== "0") {
|
|
230
|
+
gatewayArgs.push("--port", gatewayPort);
|
|
231
|
+
gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
|
|
232
|
+
}
|
|
233
|
+
const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
|
|
234
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
235
|
+
const child = spawn("openclaw", gatewayArgs, {
|
|
236
|
+
env: gatewayEnv,
|
|
237
|
+
cwd,
|
|
238
|
+
stdio: "inherit",
|
|
239
|
+
detached: false,
|
|
240
|
+
});
|
|
241
|
+
function forwardSignal(sig) {
|
|
242
|
+
try {
|
|
243
|
+
child.kill(sig);
|
|
244
|
+
}
|
|
245
|
+
catch { /* already gone */ }
|
|
246
|
+
}
|
|
247
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
248
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
249
|
+
child.on("close", (code, signal) => {
|
|
250
|
+
if (signal === "SIGTERM")
|
|
251
|
+
process.exit(143);
|
|
252
|
+
if (signal === "SIGINT")
|
|
253
|
+
process.exit(130);
|
|
254
|
+
process.exit(code ?? 1);
|
|
255
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parall/openclaw-agent",
|
|
3
|
+
"version": "1.28.0",
|
|
4
|
+
"description": "OpenClaw bootstrap wrapper for daemon-mode Parall agents — sets up per-agent OpenClaw state and execs openclaw gateway run",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/parall-hq/parall-mono",
|
|
9
|
+
"directory": "ts/openclaw-agent"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"parall-openclaw-agent": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.7.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"start": "node dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Bootstrap wrapper for daemon-mode OpenClaw agents.
|
|
4
|
+
// Sets up a per-agent OpenClaw state dir, writes openclaw.json with the
|
|
5
|
+
// agent's credentials, installs the bundled plugin, then execs
|
|
6
|
+
// `openclaw gateway run`. The daemon supervisor spawns one of these per
|
|
7
|
+
// attached OpenClaw agent.
|
|
8
|
+
|
|
9
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// 1. Read + validate ENV
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const PRLL_API_URL = env("PRLL_API_URL");
|
|
18
|
+
const PRLL_API_KEY = env("PRLL_API_KEY");
|
|
19
|
+
const PRLL_ORG_ID = env("PRLL_ORG_ID");
|
|
20
|
+
const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
|
|
21
|
+
|
|
22
|
+
const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
|
|
23
|
+
const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
|
|
24
|
+
const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
|
|
25
|
+
const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
|
|
26
|
+
|| "/opt/parall-plugin/parall-plugin.tgz";
|
|
27
|
+
|
|
28
|
+
function env(name: string): string {
|
|
29
|
+
const v = process.env[name]?.trim();
|
|
30
|
+
if (!v) {
|
|
31
|
+
console.error(`ERROR: Missing required environment variable: ${name}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// 2. Create per-agent state directory
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const openclawStateDir = path.join(stateDir, ".openclaw");
|
|
42
|
+
const configPath = path.join(openclawStateDir, "openclaw.json");
|
|
43
|
+
|
|
44
|
+
fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
|
|
45
|
+
fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// 3. Install plugin from bundled archive
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(pluginArchive)) {
|
|
52
|
+
// Clean legacy extension dir before install (only when we have an archive
|
|
53
|
+
// to replace it — otherwise the existing install is the only copy).
|
|
54
|
+
const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
|
|
55
|
+
fs.rmSync(legacyExtDir, { recursive: true, force: true });
|
|
56
|
+
|
|
57
|
+
console.log(`Installing Parall plugin from ${pluginArchive}...`);
|
|
58
|
+
try {
|
|
59
|
+
execFileSync("openclaw", [
|
|
60
|
+
"plugins", "install", pluginArchive,
|
|
61
|
+
"--force", "--dangerously-force-unsafe-install",
|
|
62
|
+
], {
|
|
63
|
+
env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
|
|
64
|
+
stdio: "inherit",
|
|
65
|
+
timeout: 60_000,
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`ERROR: Failed to install Parall plugin: ${String(err)}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.warn(
|
|
73
|
+
`Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// 4. Write openclaw.json
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
writeOpenclawConfig();
|
|
82
|
+
|
|
83
|
+
function writeOpenclawConfig(): void {
|
|
84
|
+
let cfg: Record<string, unknown> = {};
|
|
85
|
+
try {
|
|
86
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
87
|
+
} catch { /* fresh config */ }
|
|
88
|
+
|
|
89
|
+
const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway as Record<string, unknown> : {};
|
|
90
|
+
gateway.mode = "local";
|
|
91
|
+
cfg.gateway = gateway;
|
|
92
|
+
|
|
93
|
+
const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels as Record<string, unknown> : {};
|
|
94
|
+
const parallChannel: Record<string, unknown> = {
|
|
95
|
+
parall_url: PRLL_API_URL,
|
|
96
|
+
api_key: PRLL_API_KEY,
|
|
97
|
+
org_id: PRLL_ORG_ID,
|
|
98
|
+
};
|
|
99
|
+
if (PRLL_WS_URL) parallChannel.ws_url = PRLL_WS_URL;
|
|
100
|
+
channels.parall = parallChannel;
|
|
101
|
+
cfg.channels = channels;
|
|
102
|
+
|
|
103
|
+
const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins as Record<string, unknown> : {};
|
|
104
|
+
const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries as Record<string, unknown> : {};
|
|
105
|
+
const parallPluginConfig: Record<string, unknown> = {
|
|
106
|
+
parall_url: PRLL_API_URL,
|
|
107
|
+
api_key: PRLL_API_KEY,
|
|
108
|
+
org_id: PRLL_ORG_ID,
|
|
109
|
+
};
|
|
110
|
+
if (PRLL_WS_URL) parallPluginConfig.ws_url = PRLL_WS_URL;
|
|
111
|
+
const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall as Record<string, unknown> : {};
|
|
112
|
+
entries.parall = {
|
|
113
|
+
...existingParall,
|
|
114
|
+
enabled: true,
|
|
115
|
+
hooks: { allowPromptInjection: true, allowConversationAccess: true },
|
|
116
|
+
config: parallPluginConfig,
|
|
117
|
+
};
|
|
118
|
+
plugins.entries = entries;
|
|
119
|
+
cfg.plugins = plugins;
|
|
120
|
+
|
|
121
|
+
// sqlite-vec vector index guard
|
|
122
|
+
const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
|
|
123
|
+
const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
|
|
124
|
+
const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch as Record<string, unknown> : {};
|
|
125
|
+
const store = (ms.store && typeof ms.store === "object") ? ms.store as Record<string, unknown> : {};
|
|
126
|
+
const vector = (store.vector && typeof store.vector === "object") ? store.vector as Record<string, unknown> : {};
|
|
127
|
+
if (vector.enabled === undefined) vector.enabled = true;
|
|
128
|
+
store.vector = vector;
|
|
129
|
+
ms.store = store;
|
|
130
|
+
defaults.memorySearch = ms;
|
|
131
|
+
agents.defaults = defaults;
|
|
132
|
+
cfg.agents = agents;
|
|
133
|
+
|
|
134
|
+
// Seed tools.alsoAllow
|
|
135
|
+
const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools as Record<string, unknown> : {};
|
|
136
|
+
const alsoAllow = new Set<string>(
|
|
137
|
+
Array.isArray(tools.alsoAllow) ? (tools.alsoAllow as string[]) : [],
|
|
138
|
+
);
|
|
139
|
+
alsoAllow.add("group:plugins");
|
|
140
|
+
tools.alsoAllow = Array.from(alsoAllow);
|
|
141
|
+
cfg.tools = tools;
|
|
142
|
+
|
|
143
|
+
const tmp = configPath + ".tmp";
|
|
144
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
145
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
146
|
+
fs.renameSync(tmp, configPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// 5. Pre-seed platform config (model + provider) before gateway reads it
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// On first boot openclaw.json has no agents.defaults.model. Without this,
|
|
153
|
+
// OpenClaw uses its built-in default which may lack credentials through the
|
|
154
|
+
// Parall proxy. The plugin refreshes this during startAccount but OpenClaw's
|
|
155
|
+
// hot reload only reloads the model catalog, not the active model.
|
|
156
|
+
|
|
157
|
+
await preseedPlatformConfig();
|
|
158
|
+
|
|
159
|
+
async function preseedPlatformConfig(): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${PRLL_API_KEY}` };
|
|
162
|
+
if (PRLL_SWIMLANE_NAME) headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
|
|
163
|
+
|
|
164
|
+
const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
|
|
165
|
+
headers,
|
|
166
|
+
signal: AbortSignal.timeout(15_000),
|
|
167
|
+
});
|
|
168
|
+
if (!resp.ok) throw new Error(`platform-config ${resp.status}`);
|
|
169
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
170
|
+
const pc = (data.config ?? data) as Record<string, unknown>;
|
|
171
|
+
|
|
172
|
+
let cfg: Record<string, unknown> = {};
|
|
173
|
+
try { cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>; } catch { /* fresh */ }
|
|
174
|
+
|
|
175
|
+
const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
|
|
176
|
+
const platformDefaults = (pc.agents as Record<string, unknown> | undefined)?.defaults;
|
|
177
|
+
if (platformDefaults && typeof platformDefaults === "object") {
|
|
178
|
+
const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
|
|
179
|
+
const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
|
|
180
|
+
for (const [k, v] of Object.entries(platformDefaults as Record<string, unknown>)) {
|
|
181
|
+
if (ALLOWED_DEFAULTS.has(k)) existing[k] = v;
|
|
182
|
+
}
|
|
183
|
+
agents.defaults = existing;
|
|
184
|
+
cfg.agents = agents;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
|
|
188
|
+
const platformModels = (pc.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined;
|
|
189
|
+
const platformParall = platformModels?.parall;
|
|
190
|
+
if (platformParall && typeof platformParall === "object") {
|
|
191
|
+
const models = (cfg.models && typeof cfg.models === "object") ? cfg.models as Record<string, unknown> : {};
|
|
192
|
+
const providers = (models.providers && typeof models.providers === "object") ? models.providers as Record<string, unknown> : {};
|
|
193
|
+
const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall as Record<string, unknown> : {};
|
|
194
|
+
const merged = { ...existingParall, ...(platformParall as Record<string, unknown>) };
|
|
195
|
+
if (Array.isArray(merged.models)) {
|
|
196
|
+
merged.models = (merged.models as Record<string, unknown>[])
|
|
197
|
+
.filter((m) => m && typeof m === "object")
|
|
198
|
+
.map((m) => {
|
|
199
|
+
const clean: Record<string, unknown> = {};
|
|
200
|
+
for (const [k, v] of Object.entries(m)) {
|
|
201
|
+
if (ALLOWED_MODEL_KEYS.has(k)) clean[k] = v;
|
|
202
|
+
}
|
|
203
|
+
return clean;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
merged.apiKey = PRLL_API_KEY;
|
|
207
|
+
providers.parall = merged;
|
|
208
|
+
models.providers = providers;
|
|
209
|
+
cfg.models = models;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tmp = configPath + ".tmp";
|
|
213
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
214
|
+
fs.renameSync(tmp, configPath);
|
|
215
|
+
const model = ((cfg.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined)?.model ?? "none";
|
|
216
|
+
console.log(`Platform config pre-seeded (model: ${String(model)}).`);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn(`Platform config pre-seed skipped: ${String(err)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// 6. openclaw doctor --fix (non-fatal)
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
execFileSync("openclaw", ["doctor", "--fix"], {
|
|
228
|
+
env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
|
|
229
|
+
stdio: "inherit",
|
|
230
|
+
timeout: 30_000,
|
|
231
|
+
});
|
|
232
|
+
} catch { /* non-fatal */ }
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// 7. Spawn openclaw gateway run with signal forwarding
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
console.log("Starting OpenClaw gateway...");
|
|
240
|
+
|
|
241
|
+
const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
|
|
242
|
+
|
|
243
|
+
const gatewayEnv: NodeJS.ProcessEnv = {
|
|
244
|
+
...process.env,
|
|
245
|
+
OPENCLAW_STATE_DIR: openclawStateDir,
|
|
246
|
+
};
|
|
247
|
+
if (workspaceDir) {
|
|
248
|
+
gatewayEnv.PRLL_WIKI_MOUNT_ROOT = workspaceDir;
|
|
249
|
+
}
|
|
250
|
+
if (PRLL_SWIMLANE_NAME) {
|
|
251
|
+
gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const gatewayArgs = ["gateway", "run"];
|
|
255
|
+
if (gatewayPort !== "0") {
|
|
256
|
+
gatewayArgs.push("--port", gatewayPort);
|
|
257
|
+
gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
|
|
261
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
262
|
+
|
|
263
|
+
const child = spawn("openclaw", gatewayArgs, {
|
|
264
|
+
env: gatewayEnv,
|
|
265
|
+
cwd,
|
|
266
|
+
stdio: "inherit",
|
|
267
|
+
detached: false,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
function forwardSignal(sig: NodeJS.Signals): void {
|
|
271
|
+
try { child.kill(sig); } catch { /* already gone */ }
|
|
272
|
+
}
|
|
273
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
274
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
275
|
+
|
|
276
|
+
child.on("close", (code, signal) => {
|
|
277
|
+
if (signal === "SIGTERM") process.exit(143);
|
|
278
|
+
if (signal === "SIGINT") process.exit(130);
|
|
279
|
+
process.exit(code ?? 1);
|
|
280
|
+
});
|