@parall/daemon 1.42.1 → 1.44.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/bundle/manifest.json +15 -11
- package/bundle/parall-browser-pod.js +30 -17
- package/bundle/parall-channel-exec.js +224 -0
- package/bundle/parall-claude-agent.js +1847 -1470
- package/bundle/parall-codex-agent.js +1887 -1505
- package/bundle/parall-daemon.js +388 -216
- package/dist/clip-runtime/hub-client.d.ts +1 -1
- package/dist/clip-runtime/hub-client.js +1 -1
- package/dist/clip-runtime/process-manager.d.ts +2 -2
- package/dist/clip-runtime/process-manager.js +2 -2
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/home-isolation.d.ts +68 -0
- package/dist/home-isolation.d.ts.map +1 -0
- package/dist/home-isolation.js +130 -0
- package/dist/runtimes.d.ts +7 -0
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +20 -0
- package/dist/supervisor.d.ts +8 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +97 -45
- package/package.json +6 -6
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* it reads its binding via GetBindings, then passes clip_token straight to
|
|
8
8
|
* Invoke(clip_name="browser", ...). The hub verifies the signed token, resolves
|
|
9
9
|
* the bound BrowserProfile's host machine (B), and forwards the InvokeCommand
|
|
10
|
-
* there. See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
10
|
+
* there. See docs/archive/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
11
11
|
*
|
|
12
12
|
* Protocol (same Connect envelope framing as clip-provider.ts):
|
|
13
13
|
* POST /clip.v1.ClipHubService/GetBindings (unary)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* it reads its binding via GetBindings, then passes clip_token straight to
|
|
8
8
|
* Invoke(clip_name="browser", ...). The hub verifies the signed token, resolves
|
|
9
9
|
* the bound BrowserProfile's host machine (B), and forwards the InvokeCommand
|
|
10
|
-
* there. See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
10
|
+
* there. See docs/archive/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
11
11
|
*
|
|
12
12
|
* Protocol (same Connect envelope framing as clip-provider.ts):
|
|
13
13
|
* POST /clip.v1.ClipHubService/GetBindings (unary)
|
|
@@ -95,7 +95,7 @@ export declare class ClipProcessManager {
|
|
|
95
95
|
* (GetBindings), then forward the invoke through the hub (Invoke) to the bound
|
|
96
96
|
* BrowserProfile's host. No local shortcut, no fallback — unbound is rejected.
|
|
97
97
|
* The execution daemon never verifies clip_token; the hub does.
|
|
98
|
-
* See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
98
|
+
* See docs/archive/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
99
99
|
*/
|
|
100
100
|
private invokeDependency;
|
|
101
101
|
/**
|
|
@@ -103,7 +103,7 @@ export declare class ClipProcessManager {
|
|
|
103
103
|
* synthetic "browser" clip). The hub forwards clip_token as the PLAINTEXT
|
|
104
104
|
* browser_profile_id, which the host daemon uses directly as the bb-browser
|
|
105
105
|
* account. Only daemons that host BrowserProfiles register this capability.
|
|
106
|
-
* See docs/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
106
|
+
* See docs/archive/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
107
107
|
*/
|
|
108
108
|
invokeBrowserCapability(clipToken: string, command: string, input: unknown): Promise<unknown>;
|
|
109
109
|
private setStatus;
|
|
@@ -417,7 +417,7 @@ export class ClipProcessManager {
|
|
|
417
417
|
* (GetBindings), then forward the invoke through the hub (Invoke) to the bound
|
|
418
418
|
* BrowserProfile's host. No local shortcut, no fallback — unbound is rejected.
|
|
419
419
|
* The execution daemon never verifies clip_token; the hub does.
|
|
420
|
-
* See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
420
|
+
* See docs/archive/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
421
421
|
*/
|
|
422
422
|
async invokeDependency(clipName, command, input, context) {
|
|
423
423
|
if (!isBrowserDependencyName(clipName)) {
|
|
@@ -459,7 +459,7 @@ export class ClipProcessManager {
|
|
|
459
459
|
* synthetic "browser" clip). The hub forwards clip_token as the PLAINTEXT
|
|
460
460
|
* browser_profile_id, which the host daemon uses directly as the bb-browser
|
|
461
461
|
* account. Only daemons that host BrowserProfiles register this capability.
|
|
462
|
-
* See docs/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
462
|
+
* See docs/archive/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
463
463
|
*/
|
|
464
464
|
async invokeBrowserCapability(clipToken, command, input) {
|
|
465
465
|
if (!this.browserProfileManager) {
|
package/dist/config.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export type ClaudeDaemonConfig = {
|
|
|
63
63
|
* 60s; 0 = confirm immediately on the first machine.hello.
|
|
64
64
|
*/
|
|
65
65
|
updateConfirmDelayMs: number;
|
|
66
|
+
/**
|
|
67
|
+
* Machine-level kill switch for per-agent HOME isolation
|
|
68
|
+
* (PRLL_DAEMON_HOME_ISOLATION=0). When true, every agent spawns with the
|
|
69
|
+
* legacy shared-HOME behavior regardless of its daemon_config.home_isolation
|
|
70
|
+
* — an instant local rollback that needs no server round-trip.
|
|
71
|
+
*/
|
|
72
|
+
homeIsolationDisabled: boolean;
|
|
66
73
|
};
|
|
67
74
|
export declare function daemonConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
68
75
|
export declare function resolveClaudeDaemonConfig(env?: NodeJS.ProcessEnv): ClaudeDaemonConfig;
|
|
@@ -78,6 +85,8 @@ export declare function sharedClaudeCredentialsFileFor(rootClaudeHome: string):
|
|
|
78
85
|
export declare function agentClaudeCredentialsFileFor(agentClaudeHome: string): string;
|
|
79
86
|
/** Per-agent workspace dir where the agent runs git commands. */
|
|
80
87
|
export declare function agentWorkspaceDirFor(rootStateDir: string, agentId: string): string;
|
|
88
|
+
/** Per-agent fake HOME for local credential isolation (home-isolation.ts). */
|
|
89
|
+
export declare function agentHomeDirFor(rootStateDir: string, agentId: string): string;
|
|
81
90
|
/**
|
|
82
91
|
* Whether manual CLI update verifies the Ed25519 bundle signature. Supervised
|
|
83
92
|
* daemon self-update is always fail-closed; this resolver is only a LOCAL-DEV
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC,6BAA6B,EAAE,MAAM,CAAC;IACtC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,oBAAoB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC,6BAA6B,EAAE,MAAM,CAAC;IACtC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAuBF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAyBD,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CA+DpB;AASD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;mEAEmE;AACnE,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,+EAA+E;AAC/E,wBAAgB,8BAA8B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,uEAAuE;AACvE,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEzF;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAMR"}
|
package/dist/config.js
CHANGED
|
@@ -94,6 +94,7 @@ export function resolveClaudeDaemonConfig(env = process.env) {
|
|
|
94
94
|
updateIntervalMs: parseMsAllowZero(env.PRLL_DAEMON_UPDATE_INTERVAL_MS, 6 * 60 * 60_000),
|
|
95
95
|
updateDisabled: env.PRLL_DAEMON_UPDATE_DISABLED === 'true' || !!env.KUBERNETES_SERVICE_HOST,
|
|
96
96
|
updateConfirmDelayMs: parseMsAllowZero(env.PRLL_DAEMON_UPDATE_CONFIRM_DELAY_MS?.trim() || undefined, 60_000),
|
|
97
|
+
homeIsolationDisabled: env.PRLL_DAEMON_HOME_ISOLATION?.trim() === '0',
|
|
97
98
|
};
|
|
98
99
|
}
|
|
99
100
|
function assertSafeAgentId(agentId) {
|
|
@@ -124,6 +125,10 @@ export function agentClaudeCredentialsFileFor(agentClaudeHome) {
|
|
|
124
125
|
export function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
125
126
|
return path.join(rootStateDir, 'agents', assertSafeAgentId(agentId), 'workspace');
|
|
126
127
|
}
|
|
128
|
+
/** Per-agent fake HOME for local credential isolation (home-isolation.ts). */
|
|
129
|
+
export function agentHomeDirFor(rootStateDir, agentId) {
|
|
130
|
+
return path.join(rootStateDir, 'agents', assertSafeAgentId(agentId), 'home');
|
|
131
|
+
}
|
|
127
132
|
/**
|
|
128
133
|
* Whether manual CLI update verifies the Ed25519 bundle signature. Supervised
|
|
129
134
|
* daemon self-update is always fail-closed; this resolver is only a LOCAL-DEV
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent HOME isolation for local (BYOC) daemon machines.
|
|
3
|
+
*
|
|
4
|
+
* A spawned agent gets a fake HOME at `<stateDir>/home` so third-party CLI
|
|
5
|
+
* credentials it writes while working (gh / aws / npm / docker / kubectl …)
|
|
6
|
+
* land in a per-agent directory instead of the operator's real $HOME. Runtime
|
|
7
|
+
* login state stays SHARED with the operator through an explicit passthrough
|
|
8
|
+
* list:
|
|
9
|
+
*
|
|
10
|
+
* .claude → <claudeStateRoot>/.claude (settings + Linux OAuth file)
|
|
11
|
+
* .claude.json → <claudeStateRoot>/.claude.json (Claude Code global state)
|
|
12
|
+
* Library/Keychains → <systemHome>/Library/Keychains (macOS only — the Security
|
|
13
|
+
* framework locates the login keychain via $HOME, so without
|
|
14
|
+
* this link the whole Keychain, and Claude's OAuth token with
|
|
15
|
+
* it, disappears from the child's view)
|
|
16
|
+
*
|
|
17
|
+
* Codex needs no link: the supervisor points runtime_auth children at the
|
|
18
|
+
* operator's real ~/.codex via CODEX_HOME (runtimes.ts). ssh needs none
|
|
19
|
+
* either — OpenSSH resolves ~ via getpwuid(), not $HOME, so it always sees
|
|
20
|
+
* the real ~/.ssh regardless of the fake HOME.
|
|
21
|
+
*
|
|
22
|
+
* Directory-level symlinks are deliberate: CLIs that rewrite files via
|
|
23
|
+
* temp-file + atomic rename do so INSIDE the real directory, so the links
|
|
24
|
+
* can't be silently replaced. For the one file-level link (.claude.json,
|
|
25
|
+
* in-place writes as of Claude Code 2.1.x) `ensureLink` self-heals on every
|
|
26
|
+
* child start: if a rename ever swaps the symlink for a regular file, the
|
|
27
|
+
* file is preserved aside and the link rebuilt.
|
|
28
|
+
*
|
|
29
|
+
* This module is NOT a security boundary — it prevents accidental credential
|
|
30
|
+
* cross-writes, not malicious absolute-path access. K8s daemons never come
|
|
31
|
+
* through here (single-tenant pods use the per-agent claude-home branch with
|
|
32
|
+
* ensureSharedCredentialLink below).
|
|
33
|
+
*/
|
|
34
|
+
interface Log {
|
|
35
|
+
info(msg: string): void;
|
|
36
|
+
warn(msg: string): void;
|
|
37
|
+
}
|
|
38
|
+
export interface IsolatedHomeSpec {
|
|
39
|
+
/** The fake HOME to build, `<stateDir>/home`. */
|
|
40
|
+
homeDir: string;
|
|
41
|
+
/** Where the shared Claude state actually lives (rootClaudeHome). */
|
|
42
|
+
claudeStateRoot: string;
|
|
43
|
+
/** The operator's real OS home (Keychain location on macOS). */
|
|
44
|
+
systemHome: string;
|
|
45
|
+
/** Git identity written to a per-agent .gitconfig (created once, never overwritten). */
|
|
46
|
+
gitUserName: string;
|
|
47
|
+
gitUserEmail: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the fake HOME. Throws only when the HOME directory itself cannot be
|
|
51
|
+
* created (nothing to isolate into); individual passthrough links and the
|
|
52
|
+
* gitconfig are SOFT failures collected into the return value — the caller
|
|
53
|
+
* decides whether the failed surface is load-bearing for this agent's
|
|
54
|
+
* runtime + LLM source (e.g. a broken Claude link only matters for
|
|
55
|
+
* claude-code on runtime_auth; a parall-source agent runs fine in a fake
|
|
56
|
+
* HOME with no links, exactly like a hosted K8s pod).
|
|
57
|
+
*/
|
|
58
|
+
export declare function ensureIsolatedHome(spec: IsolatedHomeSpec, agentId: string, log: Log, platform?: NodeJS.Platform): string[];
|
|
59
|
+
/**
|
|
60
|
+
* K8s branch: each agent has an isolated claude home, but all of them share
|
|
61
|
+
* one runtime-auth OAuth credential file. Link the agent's
|
|
62
|
+
* `.claude/.credentials.json` back to the shared copy under rootClaudeHome.
|
|
63
|
+
* (Moved verbatim from DaemonSupervisor; local isolation above does not use
|
|
64
|
+
* this — its directory-level .claude link already carries the credential.)
|
|
65
|
+
*/
|
|
66
|
+
export declare function ensureSharedCredentialLink(rootClaudeHome: string, agentClaudeHome: string, agentId: string, log: Log): void;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=home-isolation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"home-isolation.d.ts","sourceRoot":"","sources":["../src/home-isolation.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,UAAU,GAAG;IACX,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,EACR,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,MAAM,EAAE,CA0DV;AAiDD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,GACP,IAAI,CA6BN"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { agentClaudeCredentialsFileFor, sharedClaudeCredentialsFileFor } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Build the fake HOME. Throws only when the HOME directory itself cannot be
|
|
6
|
+
* created (nothing to isolate into); individual passthrough links and the
|
|
7
|
+
* gitconfig are SOFT failures collected into the return value — the caller
|
|
8
|
+
* decides whether the failed surface is load-bearing for this agent's
|
|
9
|
+
* runtime + LLM source (e.g. a broken Claude link only matters for
|
|
10
|
+
* claude-code on runtime_auth; a parall-source agent runs fine in a fake
|
|
11
|
+
* HOME with no links, exactly like a hosted K8s pod).
|
|
12
|
+
*/
|
|
13
|
+
export function ensureIsolatedHome(spec, agentId, log, platform = process.platform) {
|
|
14
|
+
fs.mkdirSync(spec.homeDir, { recursive: true });
|
|
15
|
+
const failures = [];
|
|
16
|
+
const attempt = (label, fn) => {
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
failures.push(`${label}: ${String(err)}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
// The .claude link is directory-level: a dangling directory symlink cannot
|
|
25
|
+
// be mkdir'd through (unlike the .claude.json FILE link, which writes can
|
|
26
|
+
// create through), so materialize the real target first — a fresh host
|
|
27
|
+
// where the operator never ran Claude would otherwise strand the child on
|
|
28
|
+
// ENOENT when it tries to create session state under ~/.claude/.
|
|
29
|
+
attempt('.claude link', () => {
|
|
30
|
+
fs.mkdirSync(path.join(spec.claudeStateRoot, '.claude'), { recursive: true });
|
|
31
|
+
ensureLink(path.join(spec.homeDir, '.claude'), path.join(spec.claudeStateRoot, '.claude'), agentId, log);
|
|
32
|
+
});
|
|
33
|
+
attempt('.claude.json link', () => ensureLink(path.join(spec.homeDir, '.claude.json'), path.join(spec.claudeStateRoot, '.claude.json'), agentId, log));
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
attempt('Library/Keychains link', () => {
|
|
36
|
+
fs.mkdirSync(path.join(spec.homeDir, 'Library'), { recursive: true });
|
|
37
|
+
ensureLink(path.join(spec.homeDir, 'Library', 'Keychains'), path.join(spec.systemHome, 'Library', 'Keychains'), agentId, log);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
attempt('.gitconfig', () => {
|
|
41
|
+
const gitconfig = path.join(spec.homeDir, '.gitconfig');
|
|
42
|
+
if (!fs.existsSync(gitconfig)) {
|
|
43
|
+
fs.writeFileSync(gitconfig, `[user]\n\tname = ${gitConfigValue(spec.gitUserName)}\n\temail = ${gitConfigValue(spec.gitUserEmail)}\n`, { mode: 0o644 });
|
|
44
|
+
log.info(`agent ${agentId}: wrote per-agent .gitconfig (${spec.gitUserName})`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return failures;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Serialize a git-config value: flatten line/tab control chars onto one ini line, then
|
|
51
|
+
* always double-quote with backslash/quote escaping — display names may
|
|
52
|
+
* legally carry `"` or `\`, which an unquoted value turns into `bad config
|
|
53
|
+
* line` for every git command in the isolated HOME.
|
|
54
|
+
*/
|
|
55
|
+
function gitConfigValue(value) {
|
|
56
|
+
const flat = value.replace(/[\r\n\t]+/g, ' ').trim();
|
|
57
|
+
return `"${flat.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Idempotent passthrough link with self-healing:
|
|
61
|
+
* - missing → create (a dangling link is fine — e.g. .claude.json before the
|
|
62
|
+
* operator's first Claude run; writes through it create the real file)
|
|
63
|
+
* - correct symlink → no-op
|
|
64
|
+
* - wrong-target symlink → relink
|
|
65
|
+
* - regular file/dir squatting on the link name (an atomic-rename swap or a
|
|
66
|
+
* stray write) → preserve it aside, relink, warn. Never delete user data.
|
|
67
|
+
*/
|
|
68
|
+
function ensureLink(linkPath, target, agentId, log) {
|
|
69
|
+
let existing;
|
|
70
|
+
try {
|
|
71
|
+
existing = fs.lstatSync(linkPath);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err.code !== 'ENOENT')
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
if (existing) {
|
|
78
|
+
if (existing.isSymbolicLink()) {
|
|
79
|
+
const current = fs.readlinkSync(linkPath);
|
|
80
|
+
if (path.resolve(path.dirname(linkPath), current) === path.resolve(target))
|
|
81
|
+
return;
|
|
82
|
+
fs.unlinkSync(linkPath);
|
|
83
|
+
log.info(`agent ${agentId}: relinking ${linkPath} → ${target}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const preserved = `${linkPath}.pre-isolation.${Date.now()}`;
|
|
87
|
+
fs.renameSync(linkPath, preserved);
|
|
88
|
+
log.warn(`agent ${agentId}: ${linkPath} was a real ${existing.isDirectory() ? 'directory' : 'file'}, ` +
|
|
89
|
+
`not the expected passthrough symlink — preserved at ${preserved} and relinked`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
fs.symlinkSync(target, linkPath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* K8s branch: each agent has an isolated claude home, but all of them share
|
|
96
|
+
* one runtime-auth OAuth credential file. Link the agent's
|
|
97
|
+
* `.claude/.credentials.json` back to the shared copy under rootClaudeHome.
|
|
98
|
+
* (Moved verbatim from DaemonSupervisor; local isolation above does not use
|
|
99
|
+
* this — its directory-level .claude link already carries the credential.)
|
|
100
|
+
*/
|
|
101
|
+
export function ensureSharedCredentialLink(rootClaudeHome, agentClaudeHome, agentId, log) {
|
|
102
|
+
const sharedCredentials = path.resolve(sharedClaudeCredentialsFileFor(rootClaudeHome));
|
|
103
|
+
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
104
|
+
const agentCredentialsDir = path.dirname(agentCredentials);
|
|
105
|
+
fs.mkdirSync(path.dirname(sharedCredentials), { recursive: true });
|
|
106
|
+
fs.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
107
|
+
try {
|
|
108
|
+
const existing = fs.lstatSync(agentCredentials);
|
|
109
|
+
if (existing.isSymbolicLink()) {
|
|
110
|
+
const currentTarget = fs.readlinkSync(agentCredentials);
|
|
111
|
+
if (path.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
fs.unlinkSync(agentCredentials);
|
|
115
|
+
}
|
|
116
|
+
else if (existing.isDirectory()) {
|
|
117
|
+
log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
fs.unlinkSync(agentCredentials);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err.code !== 'ENOENT') {
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
fs.symlinkSync(sharedCredentials, agentCredentials);
|
|
130
|
+
}
|
package/dist/runtimes.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface AgentDirs {
|
|
|
11
11
|
stateDir: string;
|
|
12
12
|
workspaceDir: string;
|
|
13
13
|
claudeHome: string;
|
|
14
|
+
/**
|
|
15
|
+
* Per-agent fake HOME (local credential isolation, home-isolation.ts).
|
|
16
|
+
* Unset = legacy shared-HOME behavior and all K8s daemons. When set, the
|
|
17
|
+
* child's $HOME points here so third-party CLI credentials it writes stay
|
|
18
|
+
* per-agent; runtime login state is passed back through explicit links.
|
|
19
|
+
*/
|
|
20
|
+
homeDir?: string;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* Resolve a runtime adapter, preferring overlay bundle paths when available.
|
package/dist/runtimes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimes.d.ts","sourceRoot":"","sources":["../src/runtimes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtimes.d.ts","sourceRoot":"","sources":["../src/runtimes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAa,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,YAAY,EAAE,cAAc,EAAE,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CACN,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,EAAE,CAAC,EAAE,cAAc,GAClB,MAAM,CAAC,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAerE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAInD"}
|
package/dist/runtimes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { clearAllProviderCreds, llmSource } from '@parall/agent-core';
|
|
4
5
|
import { resolveBundleDir } from './config.js';
|
|
@@ -14,6 +15,12 @@ function buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
|
|
|
14
15
|
env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
|
|
15
16
|
if (pc)
|
|
16
17
|
env.PRLL_PROVIDER_CONFIG = JSON.stringify(pc);
|
|
18
|
+
// HOME isolation happens HERE and nowhere earlier: runtime binary
|
|
19
|
+
// resolution (applyRuntimeBinaryEnv) must see the operator's real HOME to
|
|
20
|
+
// derive nvm/Homebrew/pnpm paths, so the supervisor applies it to baseEnv
|
|
21
|
+
// BEFORE buildEnv. Do not hoist this assignment above that call site.
|
|
22
|
+
if (dirs.homeDir)
|
|
23
|
+
env.HOME = dirs.homeDir;
|
|
17
24
|
delete env.PRLL_DAEMON_MODE;
|
|
18
25
|
return env;
|
|
19
26
|
}
|
|
@@ -45,6 +52,19 @@ const codexAdapter = {
|
|
|
45
52
|
if (baseEnv.KUBERNETES_SERVICE_HOST || llmSource(pc) !== 'runtime_auth') {
|
|
46
53
|
env.PRLL_CODEX_HOME = path.join(dirs.stateDir, '.codex');
|
|
47
54
|
}
|
|
55
|
+
else if (dirs.homeDir && !env.CODEX_HOME) {
|
|
56
|
+
// runtime_auth + isolated HOME: the bridge's fallback is $HOME/.codex,
|
|
57
|
+
// which under the fake HOME would strand the child in a 401 loop.
|
|
58
|
+
// Point CODEX_HOME at the operator's real ~/.codex (where `codex login`
|
|
59
|
+
// writes) — baseEnv.HOME is still the daemon's real HOME here, the fake
|
|
60
|
+
// HOME was applied to env only. Operator overrides win over this
|
|
61
|
+
// default: an exported CODEX_HOME (already in env via baseEnv) skips
|
|
62
|
+
// this branch, and an exported PRLL_CODEX_HOME rides through untouched
|
|
63
|
+
// and outranks CODEX_HOME in the bridge's resolution — the same
|
|
64
|
+
// explicit-redirect contract the shared-HOME runtime_auth path has
|
|
65
|
+
// always honored.
|
|
66
|
+
env.CODEX_HOME = path.join(baseEnv.HOME || os.homedir(), '.codex');
|
|
67
|
+
}
|
|
48
68
|
return env;
|
|
49
69
|
},
|
|
50
70
|
};
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -179,6 +179,14 @@ export declare class DaemonSupervisor {
|
|
|
179
179
|
private clearAgentConfigRefreshRetry;
|
|
180
180
|
private restartChildNow;
|
|
181
181
|
private spawnAgent;
|
|
182
|
+
/**
|
|
183
|
+
* Local credential isolation (home-isolation.ts): per-agent opt-in via
|
|
184
|
+
* daemon_config.home_isolation (unset = legacy shared HOME for existing
|
|
185
|
+
* agents), machine-level kill switch PRLL_DAEMON_HOME_ISOLATION=0. K8s
|
|
186
|
+
* pods never take this path — they keep the isolated claude-home +
|
|
187
|
+
* shared-credential-link branch in spawnAgentOnce.
|
|
188
|
+
*/
|
|
189
|
+
private buildHomeIsolationSpec;
|
|
182
190
|
private spawnAgentOnce;
|
|
183
191
|
private startChild;
|
|
184
192
|
/**
|
|
@@ -188,6 +196,5 @@ export declare class DaemonSupervisor {
|
|
|
188
196
|
* may start a replacement child immediately after awaiting.
|
|
189
197
|
*/
|
|
190
198
|
private terminateChild;
|
|
191
|
-
private ensureSharedCredentialLink;
|
|
192
199
|
}
|
|
193
200
|
//# sourceMappingURL=supervisor.d.ts.map
|
package/dist/supervisor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/supervisor.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
|
-
import { effectiveLLMSourceExplicit } from '@parall/agent-core';
|
|
5
|
+
import { effectiveLLMSourceExplicit, llmSource } from '@parall/agent-core';
|
|
5
6
|
import { ParallWs, } from '@parall/sdk';
|
|
6
7
|
import { installClip, parseSource } from './clip-runtime/clip-installer.js';
|
|
7
8
|
import { parseIpcCommands } from './clip-runtime/manifest.js';
|
|
8
9
|
import { BrowserProfilePool, ClipProcessManager, ClipProvider, HubClient, } from './clip-runtime/index.js';
|
|
9
|
-
import {
|
|
10
|
+
import { agentClaudeHomeFor, agentHomeDirFor, agentStateDirFor, agentWorkspaceDirFor, } from './config.js';
|
|
10
11
|
import { listDirectory } from './filesystem.js';
|
|
12
|
+
import { ensureIsolatedHome, ensureSharedCredentialLink, } from './home-isolation.js';
|
|
11
13
|
import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
|
|
12
14
|
import { applyRuntimeBinaryEnv } from './runtime-bin-resolver.js';
|
|
13
15
|
import { detectedRuntimesEqual, detectRuntimes, summarizeDetectedRuntimes, } from './runtime-detector.js';
|
|
@@ -442,6 +444,28 @@ export class DaemonSupervisor {
|
|
|
442
444
|
await this.spawnAgent(userId, orgId, a);
|
|
443
445
|
}
|
|
444
446
|
else {
|
|
447
|
+
// Catch up on a home_isolation flip missed while the WS was down
|
|
448
|
+
// (machine.workspace.setup.requested is delivered live only). The
|
|
449
|
+
// isolation spec lives on ChildState, so a plain child respawn would
|
|
450
|
+
// keep the stale mode — rebuild the state from the fresh entry, the
|
|
451
|
+
// same terminate+spawn shape as handleWorkspaceSetupRequested.
|
|
452
|
+
const isolationChanged = !existing.shuttingDown &&
|
|
453
|
+
!!existing.homeIsolation !== !!this.buildHomeIsolationSpec(a, userId);
|
|
454
|
+
if (isolationChanged) {
|
|
455
|
+
this.log.info(`agent ${userId}: home isolation changed while disconnected — rebuilding child`);
|
|
456
|
+
existing.shuttingDown = true;
|
|
457
|
+
if (existing.restartTimer) {
|
|
458
|
+
clearTimeout(existing.restartTimer);
|
|
459
|
+
existing.restartTimer = null;
|
|
460
|
+
}
|
|
461
|
+
await this.terminateChild(existing);
|
|
462
|
+
this.children.delete(userId);
|
|
463
|
+
const orgId = this.machineOrgId;
|
|
464
|
+
if (orgId) {
|
|
465
|
+
await this.spawnAgent(userId, orgId, a);
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
445
469
|
// Catch up on per-agent provider_config changes missed while the WS
|
|
446
470
|
// was down (machine.agent_config.updated is delivered live only):
|
|
447
471
|
// refresh the cache, and respawn a live child when the stored config
|
|
@@ -1581,6 +1605,28 @@ export class DaemonSupervisor {
|
|
|
1581
1605
|
});
|
|
1582
1606
|
}
|
|
1583
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Local credential isolation (home-isolation.ts): per-agent opt-in via
|
|
1610
|
+
* daemon_config.home_isolation (unset = legacy shared HOME for existing
|
|
1611
|
+
* agents), machine-level kill switch PRLL_DAEMON_HOME_ISOLATION=0. K8s
|
|
1612
|
+
* pods never take this path — they keep the isolated claude-home +
|
|
1613
|
+
* shared-credential-link branch in spawnAgentOnce.
|
|
1614
|
+
*/
|
|
1615
|
+
buildHomeIsolationSpec(attached, agentId) {
|
|
1616
|
+
if (process.env.KUBERNETES_SERVICE_HOST)
|
|
1617
|
+
return undefined;
|
|
1618
|
+
if (this.config.homeIsolationDisabled)
|
|
1619
|
+
return undefined;
|
|
1620
|
+
if (attached.daemon_config?.home_isolation !== true)
|
|
1621
|
+
return undefined;
|
|
1622
|
+
return {
|
|
1623
|
+
homeDir: agentHomeDirFor(this.config.rootStateDir, agentId),
|
|
1624
|
+
claudeStateRoot: this.config.rootClaudeHome,
|
|
1625
|
+
systemHome: os.homedir(),
|
|
1626
|
+
gitUserName: attached.user?.display_name?.trim() || agentId,
|
|
1627
|
+
gitUserEmail: `${agentId}@noreply.parall.com`,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1584
1630
|
async spawnAgentOnce(agentId, orgId, attached) {
|
|
1585
1631
|
let credential;
|
|
1586
1632
|
try {
|
|
@@ -1593,14 +1639,15 @@ export class DaemonSupervisor {
|
|
|
1593
1639
|
const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
|
|
1594
1640
|
const defaultWorkspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
|
|
1595
1641
|
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
1642
|
+
const homeIsolation = this.buildHomeIsolationSpec(attached, agentId);
|
|
1596
1643
|
const claudeHome = isK8s
|
|
1597
1644
|
? agentClaudeHomeFor(this.config.rootClaudeHome, agentId)
|
|
1598
|
-
: this.config.rootClaudeHome;
|
|
1645
|
+
: (homeIsolation?.homeDir ?? this.config.rootClaudeHome);
|
|
1599
1646
|
try {
|
|
1600
1647
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
1601
1648
|
if (isK8s) {
|
|
1602
1649
|
fs.mkdirSync(claudeHome, { recursive: true });
|
|
1603
|
-
this.
|
|
1650
|
+
ensureSharedCredentialLink(this.config.rootClaudeHome, claudeHome, agentId, this.log);
|
|
1604
1651
|
}
|
|
1605
1652
|
}
|
|
1606
1653
|
catch (err) {
|
|
@@ -1635,6 +1682,7 @@ export class DaemonSupervisor {
|
|
|
1635
1682
|
// resolved at startChild time (see resolveProviderConfig) so a respawn
|
|
1636
1683
|
// after a machine llm_source change picks up the new source.
|
|
1637
1684
|
providerConfig: attached.provider_config,
|
|
1685
|
+
homeIsolation,
|
|
1638
1686
|
child: null,
|
|
1639
1687
|
credential,
|
|
1640
1688
|
restartAttempts: 0,
|
|
@@ -1660,26 +1708,60 @@ export class DaemonSupervisor {
|
|
|
1660
1708
|
return;
|
|
1661
1709
|
}
|
|
1662
1710
|
assertAgentKey(state.credential.api_key);
|
|
1663
|
-
const adapter = getRuntimeAdapter(state.runtimeType);
|
|
1664
|
-
const dirs = {
|
|
1665
|
-
stateDir: agentStateDirFor(this.config.rootStateDir, state.agentId),
|
|
1666
|
-
workspaceDir: state.workspacePath,
|
|
1667
|
-
claudeHome: state.claudeHome,
|
|
1668
|
-
};
|
|
1669
|
-
let baseEnv = { ...process.env, PRLL_API_URL: this.config.apiUrl };
|
|
1670
|
-
if (this.machineId)
|
|
1671
|
-
baseEnv.PRLL_MACHINE_ID = this.machineId;
|
|
1672
|
-
baseEnv = applyRuntimeBinaryEnv(state.runtimeType, baseEnv, this.log);
|
|
1673
1711
|
// An explicit per-agent provider_config (llm_source or BYO creds) wins;
|
|
1674
1712
|
// otherwise defer to the CURRENT machine-level llm_source. The server sends
|
|
1675
1713
|
// an empty `{}` for self-hosted agents (per-agent provider_config is
|
|
1676
1714
|
// rejected there), and `{} ?? machineLlmSource` would keep the `{}` and
|
|
1677
1715
|
// resolve to "parall", silently shadowing the machine's choice — so check
|
|
1678
1716
|
// for an explicit signal. Resolved here (not at spawn) so a respawn after a
|
|
1679
|
-
// machine llm_source change applies the new source without a full restart
|
|
1717
|
+
// machine llm_source change applies the new source without a full restart —
|
|
1718
|
+
// and BEFORE isolation setup, so the fallback decision below can consult it.
|
|
1680
1719
|
const effectiveProviderConfig = effectiveLLMSourceExplicit(state.providerConfig)
|
|
1681
1720
|
? state.providerConfig
|
|
1682
1721
|
: { llm_source: this.machineLlmSource };
|
|
1722
|
+
// Re-run isolation setup on every (re)start so a passthrough link broken
|
|
1723
|
+
// mid-lifetime heals before the next child sees the fake HOME.
|
|
1724
|
+
let isolation = state.homeIsolation;
|
|
1725
|
+
if (isolation) {
|
|
1726
|
+
try {
|
|
1727
|
+
const failures = ensureIsolatedHome(isolation, state.agentId, this.log);
|
|
1728
|
+
if (failures.length > 0) {
|
|
1729
|
+
// Only claude-code on runtime_auth NEEDS the passthrough links (the
|
|
1730
|
+
// operator's shared login). Every other runtime × source combo runs
|
|
1731
|
+
// fine in a bare fake HOME — exactly like a hosted K8s pod — so a
|
|
1732
|
+
// link failure (e.g. Windows without symlink privileges) must not
|
|
1733
|
+
// cost those agents their credential isolation too.
|
|
1734
|
+
const needsPassthrough = state.runtimeType === 'claude-code' &&
|
|
1735
|
+
llmSource(effectiveProviderConfig) === 'runtime_auth';
|
|
1736
|
+
if (needsPassthrough) {
|
|
1737
|
+
this.log.warn(`agent ${state.agentId}: home isolation passthrough failed (${failures.join('; ')}) — falling back to the shared HOME for this spawn so the runtime keeps its login`);
|
|
1738
|
+
isolation = undefined;
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
this.log.warn(`agent ${state.agentId}: home isolation passthrough incomplete (${failures.join('; ')}) — keeping the fake HOME (this runtime/source does not need the shared-login links)`);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
catch (err) {
|
|
1746
|
+
// Hard failure: the fake HOME itself could not be created — nothing
|
|
1747
|
+
// to isolate into; fall back to the shared HOME for this spawn.
|
|
1748
|
+
this.log.warn(`agent ${state.agentId}: home isolation setup failed — falling back to the shared HOME for this spawn: ${String(err)}`);
|
|
1749
|
+
isolation = undefined;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const adapter = getRuntimeAdapter(state.runtimeType);
|
|
1753
|
+
const dirs = {
|
|
1754
|
+
stateDir: agentStateDirFor(this.config.rootStateDir, state.agentId),
|
|
1755
|
+
workspaceDir: state.workspacePath,
|
|
1756
|
+
// Only the isolation-fallback case replaces claudeHome: K8s (isolation
|
|
1757
|
+
// never set) and legacy-shared children keep their state.claudeHome.
|
|
1758
|
+
claudeHome: state.homeIsolation && !isolation ? this.config.rootClaudeHome : state.claudeHome,
|
|
1759
|
+
homeDir: isolation?.homeDir,
|
|
1760
|
+
};
|
|
1761
|
+
let baseEnv = { ...process.env, PRLL_API_URL: this.config.apiUrl };
|
|
1762
|
+
if (this.machineId)
|
|
1763
|
+
baseEnv.PRLL_MACHINE_ID = this.machineId;
|
|
1764
|
+
baseEnv = applyRuntimeBinaryEnv(state.runtimeType, baseEnv, this.log);
|
|
1683
1765
|
const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
|
|
1684
1766
|
const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(' ')}` : adapter.bin;
|
|
1685
1767
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
|
|
@@ -1774,34 +1856,4 @@ export class DaemonSupervisor {
|
|
|
1774
1856
|
child.once('exit', () => clearTimeout(hardKill));
|
|
1775
1857
|
});
|
|
1776
1858
|
}
|
|
1777
|
-
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
1778
|
-
const sharedCredentials = path.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
|
|
1779
|
-
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
1780
|
-
const agentCredentialsDir = path.dirname(agentCredentials);
|
|
1781
|
-
fs.mkdirSync(path.dirname(sharedCredentials), { recursive: true });
|
|
1782
|
-
fs.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
1783
|
-
try {
|
|
1784
|
-
const existing = fs.lstatSync(agentCredentials);
|
|
1785
|
-
if (existing.isSymbolicLink()) {
|
|
1786
|
-
const currentTarget = fs.readlinkSync(agentCredentials);
|
|
1787
|
-
if (path.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
fs.unlinkSync(agentCredentials);
|
|
1791
|
-
}
|
|
1792
|
-
else if (existing.isDirectory()) {
|
|
1793
|
-
this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
else {
|
|
1797
|
-
fs.unlinkSync(agentCredentials);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
catch (err) {
|
|
1801
|
-
if (err.code !== 'ENOENT') {
|
|
1802
|
-
throw err;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
fs.symlinkSync(sharedCredentials, agentCredentials);
|
|
1806
|
-
}
|
|
1807
1859
|
}
|