@parall/daemon 1.42.0 → 1.43.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.
@@ -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
@@ -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;CAC9B,CAAC;AAuBF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE7E;AAyBD,wBAAgB,yBAAyB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CA8DpB;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;;;;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"}
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
+ }
@@ -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.
@@ -1 +1 @@
1
- {"version":3,"file":"runtimes.d.ts","sourceRoot":"","sources":["../src/runtimes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,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;CACpB;AAuED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAerE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAInD"}
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,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
2
3
  import * as path from 'node:path';
3
- import { clearAllProviderCreds } from '@parall/agent-core';
4
+ import { clearAllProviderCreds, llmSource } from '@parall/agent-core';
4
5
  import { resolveBundleDir } from './config.js';
5
6
  export { clearAllProviderCreds };
6
7
  function buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
@@ -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
  }
@@ -31,7 +38,33 @@ const codexAdapter = {
31
38
  args: [],
32
39
  buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
33
40
  const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
34
- env.PRLL_CODEX_HOME = path.join(dirs.stateDir, '.codex');
41
+ // CODEX_HOME placement: shared with the operator ONLY for local
42
+ // runtime_auth agents — they need the credential `codex login` writes to
43
+ // CODEX_HOME / ~/.codex (a headless child cannot log in itself; an
44
+ // isolated home strands them in a permanent 401 loop), and they never
45
+ // write the [model_providers.parall] block. Every other source keeps the
46
+ // isolated per-agent home: a parall-mode bridge rewrites that provider
47
+ // block with ITS OWN api_url and threads select the provider by fixed
48
+ // name, so through a shared config.toml a staging daemon's rewrite would
49
+ // route a coexisting prod agent's traffic (and its agk_ key) to the
50
+ // staging endpoint. K8s daemons always isolate, mirroring the Claude
51
+ // home policy (supervisor spawnAgentOnce).
52
+ if (baseEnv.KUBERNETES_SERVICE_HOST || llmSource(pc) !== 'runtime_auth') {
53
+ env.PRLL_CODEX_HOME = path.join(dirs.stateDir, '.codex');
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
+ }
35
68
  return env;
36
69
  },
37
70
  };
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
@@ -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 { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from './config.js';
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.ensureSharedCredentialLink(claudeHome, agentId);
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
  }