@parall/daemon 1.28.0 → 1.28.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.
@@ -4,6 +4,11 @@ import * as path from "node:path";
4
4
  import { ParallWs } from "@parall/sdk";
5
5
  import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from "./config.js";
6
6
  import { assertAgentKey, getRuntimeAdapter } from "./runtimes.js";
7
+ const RUNTIME_PACKAGES = {
8
+ 'claude-code': '@parall/claude-agent',
9
+ 'codex': '@parall/codex-agent',
10
+ 'openclaw': '@parall/openclaw-agent',
11
+ };
7
12
  /**
8
13
  * Sleep that wakes early on abort. Returns true if the full delay elapsed,
9
14
  * false if aborted. Used by bootstrap retry and the outer keepalive in
@@ -71,6 +76,7 @@ export class DaemonSupervisor {
71
76
  this.running = false;
72
77
  throw err;
73
78
  }
79
+ this.migrateFlatLayout();
74
80
  await this.fullReconcile();
75
81
  this.ws = new ParallWs({
76
82
  getTicket: () => this.client.getMachineWsTicket(),
@@ -209,6 +215,54 @@ export class DaemonSupervisor {
209
215
  }
210
216
  }
211
217
  }
218
+ // ---- Flat layout migration (self-hosted → daemon) ----
219
+ /**
220
+ * Detects a legacy flat state layout (no agents/ subdir) and migrates it
221
+ * into the per-agent directory for the owning agent. Ownership is determined
222
+ * by parsing session state files which embed the agent ID in the runtimeKey.
223
+ */
224
+ migrateFlatLayout() {
225
+ const root = this.config.rootStateDir;
226
+ const agentsDir = path.join(root, "agents");
227
+ const flatWorkspace = path.join(root, "workspace");
228
+ if (!fs.existsSync(flatWorkspace) || fs.existsSync(agentsDir))
229
+ return;
230
+ let ownerAgentId;
231
+ const sessionsDir = path.join(root, "sessions");
232
+ if (fs.existsSync(sessionsDir)) {
233
+ try {
234
+ for (const file of fs.readdirSync(sessionsDir)) {
235
+ if (!file.endsWith(".json"))
236
+ continue;
237
+ const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
238
+ // runtimeKey format: "agent:main:{runtime}:{agentId}:orchestrator"
239
+ const parts = decoded.split(":");
240
+ if (parts.length >= 4 && parts[3].startsWith("usr_")) {
241
+ ownerAgentId = parts[3];
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ catch {
247
+ // best-effort scan
248
+ }
249
+ }
250
+ const targetId = ownerAgentId ?? "_orphan";
251
+ const targetDir = path.join(agentsDir, targetId);
252
+ try {
253
+ fs.mkdirSync(targetDir, { recursive: true });
254
+ for (const sub of ["workspace", "sessions", "dispatch-context"]) {
255
+ const src = path.join(root, sub);
256
+ if (fs.existsSync(src)) {
257
+ fs.renameSync(src, path.join(targetDir, sub));
258
+ }
259
+ }
260
+ this.log.info(`migrated legacy flat state → agents/${targetId}/`);
261
+ }
262
+ catch (err) {
263
+ this.log.warn(`flat layout migration failed: ${String(err)}`);
264
+ }
265
+ }
212
266
  // ---- WS event handlers (incremental) ----
213
267
  async handleAgentAttached(agentId) {
214
268
  if (this.children.has(agentId))
@@ -275,13 +329,23 @@ export class DaemonSupervisor {
275
329
  return;
276
330
  }
277
331
  const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
278
- const workspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
279
- const claudeHome = agentClaudeHomeFor(this.config.rootClaudeHome, agentId);
332
+ const workspaceDir = attached.daemon_config?.workspace_path
333
+ || agentWorkspaceDirFor(this.config.rootStateDir, agentId);
334
+ const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
335
+ const claudeHome = isK8s
336
+ ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId)
337
+ : this.config.rootClaudeHome;
280
338
  try {
281
339
  fs.mkdirSync(stateDir, { recursive: true });
282
- fs.mkdirSync(workspaceDir, { recursive: true });
283
- fs.mkdirSync(claudeHome, { recursive: true });
284
- this.ensureSharedCredentialLink(claudeHome, agentId);
340
+ // Only create workspace dir when using the default isolated path —
341
+ // user-specified workspace_path should already exist.
342
+ if (!attached.daemon_config?.workspace_path) {
343
+ fs.mkdirSync(workspaceDir, { recursive: true });
344
+ }
345
+ if (isK8s) {
346
+ fs.mkdirSync(claudeHome, { recursive: true });
347
+ this.ensureSharedCredentialLink(claudeHome, agentId);
348
+ }
285
349
  }
286
350
  catch (err) {
287
351
  this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
@@ -291,6 +355,8 @@ export class DaemonSupervisor {
291
355
  agentId,
292
356
  orgId,
293
357
  runtimeType,
358
+ workspacePath: workspaceDir,
359
+ claudeHome,
294
360
  child: null,
295
361
  credential,
296
362
  restartAttempts: 0,
@@ -311,8 +377,8 @@ export class DaemonSupervisor {
311
377
  const adapter = getRuntimeAdapter(state.runtimeType);
312
378
  const dirs = {
313
379
  stateDir: agentStateDirFor(this.config.rootStateDir, state.agentId),
314
- workspaceDir: agentWorkspaceDirFor(this.config.rootStateDir, state.agentId),
315
- claudeHome: agentClaudeHomeFor(this.config.rootClaudeHome, state.agentId),
380
+ workspaceDir: state.workspacePath,
381
+ claudeHome: state.claudeHome,
316
382
  };
317
383
  const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs);
318
384
  this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
@@ -347,7 +413,13 @@ export class DaemonSupervisor {
347
413
  this.startChild(state);
348
414
  }, delay);
349
415
  };
350
- child.once("error", (err) => settleChild("error", null, null, err));
416
+ child.once("error", (err) => {
417
+ if (err.code === 'ENOENT') {
418
+ const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
419
+ this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
420
+ }
421
+ settleChild("error", null, null, err);
422
+ });
351
423
  child.once("close", (code, signal) => settleChild("close", code, signal));
352
424
  setTimeout(() => {
353
425
  if (state.child === child) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@parall/daemon",
3
- "version": "1.28.0",
4
- "description": "Per-host supervisor for daemon-mode Parall Machines fans one mck_ bearer out into N agent-runtime subprocesses, one per attached agent",
3
+ "version": "1.28.1",
4
+ "description": "Parall local agent runtimedaemon supervisor + bridge runtimes, bundled as standalone JS files",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,10 +9,13 @@
9
9
  "directory": "ts/daemon"
10
10
  },
11
11
  "type": "module",
12
- "main": "./dist/index.js",
12
+ "main": "./bundle/parall-daemon.js",
13
13
  "types": "./dist/index.d.ts",
14
14
  "bin": {
15
- "parall-daemon": "./dist/index.js"
15
+ "parall-daemon": "./bundle/parall-daemon.js",
16
+ "parall-claude-agent": "./bundle/parall-claude-agent.js",
17
+ "parall-codex-agent": "./bundle/parall-codex-agent.js",
18
+ "parall-openclaw-agent": "./bundle/parall-openclaw-agent.js"
16
19
  },
17
20
  "exports": {
18
21
  ".": {
@@ -21,18 +24,23 @@
21
24
  }
22
25
  },
23
26
  "files": [
24
- "dist",
25
- "src"
27
+ "bundle",
28
+ "dist"
26
29
  ],
27
30
  "dependencies": {
28
- "@parall/sdk": "1.28.0"
31
+ "@parall/sdk": "1.28.1",
32
+ "@parall/claude-agent": "1.28.1",
33
+ "@parall/codex-agent": "1.28.1",
34
+ "@parall/openclaw-agent": "1.28.1"
29
35
  },
30
36
  "devDependencies": {
31
37
  "@types/node": "^22.0.0",
38
+ "esbuild": "^0.25.0",
32
39
  "typescript": "^5.7.0"
33
40
  },
34
41
  "scripts": {
35
42
  "build": "tsc -b",
36
- "start": "node dist/index.js"
43
+ "bundle": "node ../../scripts/bundle-daemon.mjs",
44
+ "start": "node bundle/parall-daemon.js"
37
45
  }
38
46
  }
package/src/config.ts DELETED
@@ -1,146 +0,0 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
3
-
4
- /**
5
- * Daemon-mode env. The daemon authenticates to the Parall API with an
6
- * `mck_*` bearer token (PRLL_API_KEY) and supervises N per-agent
7
- * `parall-claude-agent` subprocesses on this host.
8
- *
9
- * Compared to claude-agent's config:
10
- * - `apiKey` is an mck_ bearer (Machine-scoped), not an agk_
11
- * - there is no PRLL_ORG_ID — the Machine row's org_id is implicit;
12
- * each spawned per-agent subprocess is given its own PRLL_ORG_ID
13
- * resolved from the AttachedAgent's profile / org context
14
- * - per-agent state lives at `<rootStateDir>/agents/<agent_id>/`
15
- */
16
- export type ClaudeDaemonConfig = {
17
- apiUrl: string;
18
- /** mck_*-prefixed Machine bearer. */
19
- apiKey: string;
20
- /** @deprecated Superseded by RuntimeAdapter pattern (runtimes.ts). Kept for backward compat / fallback. */
21
- agentBin: string;
22
- /** Root for per-agent state dirs. Each agent gets `<rootStateDir>/agents/<agent_id>`. */
23
- rootStateDir: string;
24
- /** Shared host home root. Per-agent HOME dirs live under `<rootClaudeHome>/agents/<agent_id>`. */
25
- rootClaudeHome: string;
26
- /** Optional WS URL override for the machine control-plane WebSocket. */
27
- wsUrl?: string;
28
- /** @deprecated Superseded by WS event-driven model; kept for backward compat. */
29
- pollIntervalMs: number;
30
- /** @deprecated Superseded by WS event-driven model; kept for backward compat. */
31
- heartbeatIntervalMs: number;
32
- /** Backoff base after a subprocess crash, in ms. */
33
- restartBackoffMs: number;
34
- /** Hard cap on parallel restart attempts per agent before giving up for a cycle. */
35
- restartBackoffMaxMs: number;
36
- /**
37
- * Bootstrap retry: when `getMachineSelf` fails on startup (network blip,
38
- * mck_ key not yet propagated, server warming up), wait this long and
39
- * retry instead of crashing the daemon. Exponential backoff up to
40
- * `bootstrapBackoffMaxMs`. Set 0 to disable (fail-fast on first error).
41
- */
42
- bootstrapBackoffMs: number;
43
- bootstrapBackoffMaxMs: number;
44
- /**
45
- * Outer supervisor keepalive: if `supervisor.run()` rejects with an
46
- * unexpected error, wait this long and reinstantiate. Same exp-backoff
47
- * shape as bootstrap. Set 0 to disable (let main() exit, rely on K8s/PID-1).
48
- */
49
- supervisorRestartBackoffMs: number;
50
- supervisorRestartBackoffMaxMs: number;
51
- };
52
-
53
- function requireEnv(env: NodeJS.ProcessEnv, name: string): string {
54
- const value = env[name]?.trim();
55
- if (!value) {
56
- throw new Error(`Missing required env var: ${name}`);
57
- }
58
- return value;
59
- }
60
-
61
- function resolvePath(value: string): string {
62
- return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
63
- }
64
-
65
- function parseMs(value: string | undefined, fallback: number): number {
66
- if (!value) return fallback;
67
- const n = Number(value);
68
- return Number.isFinite(n) && n > 0 ? n : fallback;
69
- }
70
-
71
- /** Like parseMs but allows 0 (operator opt-out). */
72
- function parseMsAllowZero(value: string | undefined, fallback: number): number {
73
- if (value === undefined) return fallback;
74
- const n = Number(value);
75
- return Number.isFinite(n) && n >= 0 ? n : fallback;
76
- }
77
-
78
- export function resolveClaudeDaemonConfig(env: NodeJS.ProcessEnv = process.env): ClaudeDaemonConfig {
79
- const apiUrl = requireEnv(env, "PRLL_API_URL");
80
- const apiKey = requireEnv(env, "PRLL_API_KEY");
81
- if (!apiKey.startsWith("mck_")) {
82
- // Fatal startup validation: the daemon must never run with an agent or
83
- // human key because child launch credentials are minted from this bearer.
84
- throw new Error(
85
- `PRLL_API_KEY does not look like a Machine bearer (expected prefix "mck_"). ` +
86
- `Daemon mode requires a machine-scoped key issued via POST /machines/{id}/keys.`,
87
- );
88
- }
89
- const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
90
- const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
91
-
92
- return {
93
- apiUrl,
94
- apiKey,
95
- agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
96
- rootStateDir,
97
- rootClaudeHome,
98
- wsUrl: env.PRLL_WS_URL?.trim() || undefined,
99
- pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 30_000),
100
- heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 30_000),
101
- restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5_000),
102
- restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 60_000),
103
- bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2_000),
104
- bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 60_000),
105
- supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5_000),
106
- supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 60_000),
107
- };
108
- }
109
-
110
- function assertSafeAgentId(agentId: string): string {
111
- if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
112
- throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
113
- }
114
- return agentId;
115
- }
116
-
117
- /** Per-agent state dir under the shared host volume. */
118
- export function agentStateDirFor(rootStateDir: string, agentId: string): string {
119
- return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
120
- }
121
-
122
- /** Per-agent HOME dir. Claude Code stores project/session state under
123
- * `${HOME}/.claude`, so each agent gets its own HOME root while the daemon
124
- * links shared OAuth credentials into that `.claude` directory. */
125
- export function agentClaudeHomeFor(rootClaudeHome: string, agentId: string): string {
126
- return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
127
- }
128
-
129
- /** Shared Claude Code OAuth credential written by server-side runtime auth. */
130
- export function sharedClaudeCredentialsFileFor(rootClaudeHome: string): string {
131
- return path.join(rootClaudeHome, ".claude", ".credentials.json");
132
- }
133
-
134
- /** Per-agent credential location inside that agent's isolated HOME. */
135
- export function agentClaudeCredentialsFileFor(agentClaudeHome: string): string {
136
- return path.join(agentClaudeHome, ".claude", ".credentials.json");
137
- }
138
-
139
- /** Per-agent workspace dir where the agent runs git commands. */
140
- export function agentWorkspaceDirFor(rootStateDir: string, agentId: string): string {
141
- return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
142
- }
143
-
144
- export function resolveWsUrl(apiUrl: string, explicitWsUrl?: string): string {
145
- return explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
146
- }
package/src/index.ts DELETED
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { ParallClient } from "@parall/sdk";
4
- import { resolveClaudeDaemonConfig, resolveWsUrl, type ClaudeDaemonConfig } from "./config.js";
5
- import { DaemonSupervisor, sleepCancellable, type DaemonLogger } from "./supervisor.js";
6
-
7
- function createLogger(prefix: string): DaemonLogger {
8
- return {
9
- info: (msg: string) => console.log(`[${prefix}] ${msg}`),
10
- warn: (msg: string) => console.warn(`[${prefix}] ${msg}`),
11
- error: (msg: string) => console.error(`[${prefix}] ${msg}`),
12
- };
13
- }
14
-
15
- function formatError(reason: unknown): string {
16
- if (reason instanceof Error) {
17
- return reason.stack ?? reason.message;
18
- }
19
- return String(reason);
20
- }
21
-
22
- /**
23
- * Outer keepalive: runs `supervisor.run()` in a loop, restarting it with
24
- * exponential backoff if it rejects. The daemon process only exits when
25
- * the abort signal fires (SIGINT/SIGTERM) — anything else is treated as
26
- * a transient failure that we recover from in-process.
27
- *
28
- * Why this exists in addition to entrypoint.sh's `while :; do parall-daemon; done`:
29
- * - In-process restart preserves the `client` (= same TCP/TLS pool) and
30
- * skips the ~hundreds-of-ms cost of Node startup + module loading per
31
- * cycle, which matters when the API is flapping.
32
- * - The shell wrapper is the "process really crashed" backstop —
33
- * segfault, OOM, uncaughtException, etc. The two layers are
34
- * complementary: in-process for soft failures, shell for hard failures.
35
- *
36
- * Operators can disable the in-process loop by setting
37
- * PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0 — main() will then exit on
38
- * any supervisor.run() rejection and rely entirely on the shell wrapper +
39
- * K8s for restart.
40
- */
41
- async function runForever(
42
- config: ClaudeDaemonConfig,
43
- client: ParallClient,
44
- log: DaemonLogger,
45
- signal: AbortSignal,
46
- ): Promise<void> {
47
- let attempt = 0;
48
- while (!signal.aborted) {
49
- const supervisor = new DaemonSupervisor(config, client, log);
50
- try {
51
- await supervisor.run(signal);
52
- // Clean exit (signal aborted) — done.
53
- await supervisor.stop();
54
- return;
55
- } catch (err) {
56
- // supervisor.run() rejected. The supervisor's per-tick handlers
57
- // already swallow individual failures, so the only paths that reach
58
- // here are (a) bootstrap fail-fast mode and (b) genuine programmer
59
- // bugs. Restart anyway — operators rely on this daemon as the only
60
- // thing keeping per-agent children alive on this host.
61
- log.error(`supervisor crashed: ${String(err)}`);
62
- try {
63
- await supervisor.stop();
64
- } catch (stopErr) {
65
- log.warn(`supervisor.stop() after crash threw: ${String(stopErr)}`);
66
- }
67
- if (config.supervisorRestartBackoffMs === 0) {
68
- log.error(
69
- "supervisor keepalive disabled (PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS=0) — exiting",
70
- );
71
- throw err;
72
- }
73
- const delay = Math.min(
74
- config.supervisorRestartBackoffMs * Math.pow(2, attempt),
75
- config.supervisorRestartBackoffMaxMs,
76
- );
77
- attempt += 1;
78
- log.warn(`restarting supervisor in ${delay}ms (attempt ${attempt})`);
79
- const slept = await sleepCancellable(delay, signal);
80
- if (!slept) return;
81
- }
82
- }
83
- }
84
-
85
- async function main(): Promise<void> {
86
- const config = resolveClaudeDaemonConfig(process.env);
87
- const log = createLogger("daemon");
88
-
89
- log.info(
90
- `boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`,
91
- );
92
- log.info(
93
- `keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`,
94
- );
95
-
96
- // The daemon talks to the API as a Machine — the bearer is mck_*.
97
- // No orgId is configured here; per-agent subprocesses get their
98
- // own org_id via the spawn env.
99
- const client = new ParallClient({
100
- baseUrl: config.apiUrl,
101
- token: config.apiKey,
102
- });
103
-
104
- const abortController = new AbortController();
105
- const onSignal = (sig: NodeJS.Signals) => {
106
- log.info(`received ${sig} — initiating shutdown`);
107
- abortController.abort();
108
- };
109
- process.on("SIGINT", () => onSignal("SIGINT"));
110
- process.on("SIGTERM", () => onSignal("SIGTERM"));
111
-
112
- // Defensive: unexpected async failures are logged explicitly. Rejections
113
- // stay in-process so the supervisor loop can recover; uncaught exceptions
114
- // exit so entrypoint.sh's shell-level keepalive restarts from a clean VM.
115
- process.on("unhandledRejection", (reason) => {
116
- log.error(`unhandledRejection: ${formatError(reason)}`);
117
- });
118
- process.on("uncaughtException", (err) => {
119
- log.error(`uncaughtException: ${formatError(err)}`);
120
- process.exitCode = 1;
121
- process.exit(1);
122
- });
123
-
124
- config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
125
-
126
- await runForever(config, client, log, abortController.signal);
127
- }
128
-
129
- main().catch((err) => {
130
- console.error(`[daemon] fatal: ${formatError(err)}`);
131
- process.exitCode = 1;
132
- });
package/src/runtimes.ts DELETED
@@ -1,91 +0,0 @@
1
- export interface RuntimeAdapter {
2
- bin: string;
3
- buildEnv(baseEnv: NodeJS.ProcessEnv, agentId: string, orgId: string, apiKey: string, dirs: AgentDirs): NodeJS.ProcessEnv;
4
- }
5
-
6
- export interface AgentDirs {
7
- stateDir: string;
8
- workspaceDir: string;
9
- claudeHome: string;
10
- }
11
-
12
- const claudeCodeAdapter: RuntimeAdapter = {
13
- bin: "parall-claude-agent",
14
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
15
- const env = { ...baseEnv };
16
- env.PRLL_API_KEY = apiKey;
17
- env.PRLL_ORG_ID = orgId;
18
- env.AGENT_ID = agentId;
19
- env.PRLL_AGENT_ID = agentId;
20
- env.PRLL_CLAUDE_HOME = dirs.claudeHome;
21
- env.PRLL_CLAUDE_STATE_DIR = dirs.stateDir;
22
- env.PRLL_CLAUDE_WORKSPACE_DIR = dirs.workspaceDir;
23
- if ("ANTHROPIC_AUTH_TOKEN" in env) {
24
- env.ANTHROPIC_AUTH_TOKEN = apiKey;
25
- }
26
- delete env.PRLL_DAEMON_MODE;
27
- return env;
28
- },
29
- };
30
-
31
- const codexAdapter: RuntimeAdapter = {
32
- bin: "parall-codex-agent",
33
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
34
- const env = { ...baseEnv };
35
- env.PRLL_API_KEY = apiKey;
36
- env.PRLL_ORG_ID = orgId;
37
- env.AGENT_ID = agentId;
38
- env.PRLL_AGENT_ID = agentId;
39
- env.PRLL_CODEX_STATE_DIR = dirs.stateDir;
40
- env.PRLL_CODEX_WORKSPACE_DIR = dirs.workspaceDir;
41
- delete env.PRLL_DAEMON_MODE;
42
- return env;
43
- },
44
- };
45
-
46
- const defaultAdapter: RuntimeAdapter = {
47
- bin: "parall-agent",
48
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
49
- const env = { ...baseEnv };
50
- env.PRLL_API_KEY = apiKey;
51
- env.PRLL_ORG_ID = orgId;
52
- env.AGENT_ID = agentId;
53
- env.PRLL_AGENT_ID = agentId;
54
- env.PRLL_STATE_DIR = dirs.stateDir;
55
- env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
56
- delete env.PRLL_DAEMON_MODE;
57
- return env;
58
- },
59
- };
60
-
61
- const openclawAdapter: RuntimeAdapter = {
62
- bin: "parall-openclaw-agent",
63
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
64
- const env = { ...baseEnv };
65
- env.PRLL_API_KEY = apiKey;
66
- env.PRLL_ORG_ID = orgId;
67
- env.AGENT_ID = agentId;
68
- env.PRLL_AGENT_ID = agentId;
69
- env.PRLL_OPENCLAW_STATE_DIR = dirs.stateDir;
70
- env.PRLL_OPENCLAW_WORKSPACE_DIR = dirs.workspaceDir;
71
- env.OPENCLAW_GATEWAY_PORT = env.OPENCLAW_GATEWAY_PORT || "0";
72
- delete env.PRLL_DAEMON_MODE;
73
- return env;
74
- },
75
- };
76
-
77
- const RUNTIME_ADAPTERS: Record<string, RuntimeAdapter> = {
78
- "claude-code": claudeCodeAdapter,
79
- "codex": codexAdapter,
80
- "openclaw": openclawAdapter,
81
- };
82
-
83
- export function getRuntimeAdapter(runtimeType: string): RuntimeAdapter {
84
- return RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
85
- }
86
-
87
- export function assertAgentKey(apiKey: string): void {
88
- if (apiKey.startsWith("mck_")) {
89
- throw new Error("BUG: machine key leaked to child process — expected agk_, got mck_");
90
- }
91
- }