@kendoo.agentdesk/agentdesk 0.18.5 → 0.19.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/CHANGELOG.md CHANGED
@@ -8,6 +8,16 @@ All user-facing changes to AgentDesk. Each entry is tagged:
8
8
 
9
9
  Internal refactors, infrastructure changes, and architectural notes are not listed here.
10
10
 
11
+ ## [0.19.0] — 2026-04-18
12
+
13
+ ### Changed
14
+ - `[CLI]` Session scoping rewritten: `HOME` is no longer redirected to a scratch directory. Instead, only the identity-sensitive surface is scoped via tool-specific env vars — `GH_CONFIG_DIR` for the gh CLI, `GIT_CONFIG_GLOBAL` + `GIT_AUTHOR_*` / `GIT_COMMITTER_*` for git, and per-project tracker tokens. Claude Code and other tools now find their own state (auth, cache, etc.) at the real `$HOME` where they expect it. The previous approach was too aggressive: it broke Claude Code's lookup of `~/.claude.json` and caused immediate session crashes. Kernel-level protection of sensitive paths (`~/.ssh`, `~/.aws`, `~/.config/gh`, etc.) is unchanged — the sandbox still denies reads/writes there.
15
+
16
+ ## [0.18.6] — 2026-04-18
17
+
18
+ ### Fixed
19
+ - `[CLI]` Scratch HOME now also symlinks `~/.claude.json` (Claude Code's auth/config file), not just the `~/.claude/` directory. Sessions were starting Claude successfully but failing to find its config file at the redirected HOME root.
20
+
11
21
  ## [0.18.5] — 2026-04-18
12
22
 
13
23
  ### Fixed
@@ -1,78 +1,53 @@
1
- // Per-session scratch HOME for identity isolation.
1
+ // Per-session scoped identity.
2
2
  //
3
- // When a session runs, we point HOME / GH_CONFIG_DIR / XDG_CONFIG_HOME at a
4
- // private scratch directory containing only this project's git + gh config.
5
- // This means `gh`, `git`, `ssh` inside the session see only the identity we
6
- // put there — the user's global accounts, other projects' tokens, or keys
7
- // in the real $HOME aren't discovered by default lookups.
3
+ // Goal: isolate the project's git/gh/tracker identity from the user's
4
+ // global state, without breaking other tools that rely on finding their
5
+ // own state in the real $HOME.
8
6
  //
9
- // This is Tier 1 (scoped-env). It prevents accidental cross-project use.
10
- // Tier 2 (sandbox-exec / bwrap) layers a kernel-enforced boundary on top.
7
+ // Strategy: DO NOT redirect HOME (earlier versions did, and that broke
8
+ // Claude Code's own ~/.claude.json lookup). Instead, set tool-specific
9
+ // env vars that each tool's documentation says will override their
10
+ // per-user config:
11
+ // - gh: GH_CONFIG_DIR → scratch/.config/gh
12
+ // - git: GIT_CONFIG_GLOBAL → scratch/.gitconfig
13
+ // GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL
14
+ // GIT_COMMITTER_NAME / GIT_COMMITTER_EMAIL
15
+ // - credentials: GITHUB_TOKEN, GH_TOKEN, LINEAR_API_KEY, JIRA_*
16
+ //
17
+ // The kernel sandbox (session-isolation.mjs) still applies a deny-list
18
+ // to sensitive paths (~/.ssh, ~/.aws, ~/.config/gh, etc.) so a confused
19
+ // agent can't read other projects' tokens even though HOME is the real
20
+ // one.
11
21
 
12
- import { mkdirSync, writeFileSync, chmodSync, rmSync, existsSync, symlinkSync } from "fs";
22
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
13
23
  import { join } from "path";
14
- import { tmpdir, homedir } from "os";
24
+ import { tmpdir } from "os";
15
25
  import { randomUUID } from "crypto";
16
26
 
17
- // Tools whose own state must follow the user into a scratch HOME. Without
18
- // these symlinks the tool looks for its auth/config at <scratch>/... and
19
- // fails because we redirected HOME.
20
- // ~/.claude — Claude Code's auth token and settings
21
- // ~/.npm — npm cache (agents run npm during sessions)
22
- // ~/.cache — generic XDG cache
23
- // ~/.local/state — generic XDG state
24
- const HOME_PASSTHROUGH = [
25
- ".claude",
26
- ".npm",
27
- ".cache",
28
- ".local",
29
- ];
30
-
31
- // Build a session scratch HOME. Returns { home, env, cleanup }.
32
- // projectId — identifier used to make the dir path readable
33
- // sessionId — unique per session; guarantees parallel sessions don't collide
34
- // creds — { LINEAR_API_KEY?, JIRA_EMAIL?, JIRA_API_TOKEN?, GITHUB_TOKEN? }
35
- // commitIdentity — { name, email } for git user.* (falls back to sensible defaults)
36
- //
37
- // The caller spawns the Claude child with:
38
- // { ...process.env, ...env }
39
- // …and MUST call cleanup() when the session ends (success, crash, signal).
40
27
  export function createScratchHome({ projectId, sessionId, creds = {}, commitIdentity = {} }) {
41
28
  const base = join(tmpdir(), "agentdesk-sessions", `${safe(projectId)}-${sessionId || randomUUID().slice(0, 8)}`);
42
- mkdirSync(base, { recursive: true, mode: 0o700 });
43
- mkdirSync(join(base, ".config"), { recursive: true, mode: 0o700 });
44
- mkdirSync(join(base, ".config", "gh"), { recursive: true, mode: 0o700 });
45
-
46
- // Symlink the user's real state dirs for tools that need their own auth/cache
47
- // (Claude Code itself, npm, etc). Without this the tools look for their
48
- // auth at <scratch>/... and fail because we redirected HOME.
49
- const realHome = homedir();
50
- for (const dir of HOME_PASSTHROUGH) {
51
- const src = join(realHome, dir);
52
- const dst = join(base, dir);
53
- if (!existsSync(src)) continue;
54
- try { symlinkSync(src, dst); } catch {}
55
- }
29
+ const ghConfigDir = join(base, "gh");
30
+ mkdirSync(ghConfigDir, { recursive: true, mode: 0o700 });
56
31
 
57
- // --- gh: scoped hosts.yml with only this project's GitHub token ---
32
+ // --- gh: a scoped hosts.yml overrides the user's ~/.config/gh/ when we
33
+ // point GH_CONFIG_DIR at this dir. Only this project's token lives
34
+ // here. Falls back to empty (no stored auth) if no token configured.
58
35
  if (creds.GITHUB_TOKEN) {
59
- const hostsPath = join(base, ".config", "gh", "hosts.yml");
60
- const hostsYaml =
61
- `github.com:\n` +
62
- ` oauth_token: ${creds.GITHUB_TOKEN}\n` +
63
- ` git_protocol: https\n`;
64
- writeFileSync(hostsPath, hostsYaml, { mode: 0o600 });
36
+ writeFileSync(
37
+ join(ghConfigDir, "hosts.yml"),
38
+ `github.com:\n oauth_token: ${creds.GITHUB_TOKEN}\n git_protocol: https\n`,
39
+ { mode: 0o600 }
40
+ );
65
41
  }
66
42
 
67
- // --- git: scoped gitconfig with commit identity and a credential helper
68
- // that echoes this token for github.com HTTPS pushes. This bypasses
69
- // macOS Keychain, SSH keys, and the global git-credential-manager. ---
43
+ // --- git: a scoped gitconfig overrides the user's ~/.gitconfig when we
44
+ // point GIT_CONFIG_GLOBAL at this file. The credential helper
45
+ // script echoes the project's GITHUB_TOKEN for HTTPS pushes so
46
+ // git push authenticates as this project's identity — not the
47
+ // user's global SSH key or credential manager.
70
48
  const helperPath = join(base, "gh-credential-helper.sh");
71
49
  const helperScript = [
72
50
  `#!/usr/bin/env bash`,
73
- `# Per-session credential helper. Echoes the scoped GitHub token to git`,
74
- `# so HTTPS pushes authenticate as this session's identity, regardless of`,
75
- `# the user's global git-credential state.`,
76
51
  `if [ "$1" = "get" ]; then`,
77
52
  ` while IFS= read -r line; do [ -z "$line" ] && break; done`,
78
53
  ` echo "username=x-access-token"`,
@@ -82,6 +57,7 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
82
57
  ].join("\n");
83
58
  writeFileSync(helperPath, helperScript, { mode: 0o700 });
84
59
 
60
+ const gitconfigPath = join(base, "gitconfig");
85
61
  const gitconfig = [
86
62
  `[user]`,
87
63
  `\tname = ${gitQuote(commitIdentity.name || "AgentDesk")}`,
@@ -89,34 +65,39 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
89
65
  ``,
90
66
  `[credential "https://github.com"]`,
91
67
  `\thelper = ${gitQuote(helperPath)}`,
92
- ``,
93
- // Stop the global credential helper (osxkeychain, gcm, libsecret) from
94
- // being consulted. Our helper above is the only one.
95
68
  `[credential]`,
96
69
  `\thelper = `,
97
70
  `\thelper = ${gitQuote(helperPath)}`,
98
71
  ``,
99
72
  ].join("\n");
100
- writeFileSync(join(base, ".gitconfig"), gitconfig, { mode: 0o600 });
73
+ writeFileSync(gitconfigPath, gitconfig, { mode: 0o600 });
101
74
 
102
- // --- Scoped env for the child process ---
75
+ // --- Scoped env for the child. HOME stays the user's real HOME — Claude
76
+ // Code and other tools find their own state. Only the identity-
77
+ // sensitive surface is redirected.
103
78
  const env = {
104
- HOME: base,
105
- XDG_CONFIG_HOME: join(base, ".config"),
106
- GH_CONFIG_DIR: join(base, ".config", "gh"),
107
-
108
- // Explicit GITHUB_TOKEN overrides gh's stored auth (which we've
109
- // already replaced with our hosts.yml, but belt + suspenders).
79
+ // gh: uses GH_CONFIG_DIR for its own config. Writing hosts.yml there
80
+ // with our token means `gh auth` operations hit this project's account.
81
+ GH_CONFIG_DIR: ghConfigDir,
82
+ // git: GIT_CONFIG_GLOBAL overrides ~/.gitconfig for this process tree
83
+ // (git 2.32+). Our gitconfig sets user.name/email and a credential
84
+ // helper that only knows this project's token.
85
+ GIT_CONFIG_GLOBAL: gitconfigPath,
86
+ // Belt-and-braces commit identity — overrides even if git ignores
87
+ // GIT_CONFIG_GLOBAL for any reason.
88
+ GIT_AUTHOR_NAME: commitIdentity.name || "AgentDesk",
89
+ GIT_AUTHOR_EMAIL: commitIdentity.email || "agentdesk@local",
90
+ GIT_COMMITTER_NAME: commitIdentity.name || "AgentDesk",
91
+ GIT_COMMITTER_EMAIL: commitIdentity.email || "agentdesk@local",
92
+ // Explicit GITHUB_TOKEN/GH_TOKEN overrides gh's stored auth (which
93
+ // we've already replaced anyway, but belt + suspenders).
110
94
  GITHUB_TOKEN: creds.GITHUB_TOKEN || "",
111
95
  GH_TOKEN: creds.GITHUB_TOKEN || "",
112
-
113
- // Linear / Jira — only this project's credentials travel into the session.
96
+ // Tracker-specific env — only this project's credentials reach the agent.
114
97
  LINEAR_API_KEY: creds.LINEAR_API_KEY || "",
115
98
  JIRA_EMAIL: creds.JIRA_EMAIL || "",
116
99
  JIRA_API_TOKEN: creds.JIRA_API_TOKEN || "",
117
100
  };
118
-
119
- // Drop empty-string entries so child processes don't see ambiguous empty vars.
120
101
  for (const k of Object.keys(env)) if (env[k] === "") delete env[k];
121
102
 
122
103
  let cleaned = false;
@@ -125,11 +106,6 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
125
106
  cleaned = true;
126
107
  try { if (existsSync(base)) rmSync(base, { recursive: true, force: true }); } catch {}
127
108
  }
128
-
129
- // Best-effort cleanup if the parent process exits without an explicit call.
130
- // Don't hijack SIGINT/SIGTERM — the daemon has its own signal handling and
131
- // each session registering a process.exit() handler would kill the daemon
132
- // as soon as one session saw a signal. /tmp is cleaned by the OS anyway.
133
109
  process.once("exit", cleanup);
134
110
 
135
111
  return { home: base, env, cleanup };
@@ -140,7 +116,6 @@ function safe(s) {
140
116
  }
141
117
 
142
118
  function gitQuote(s) {
143
- // git config values: wrap in quotes if they contain whitespace or shell-sensitive chars.
144
119
  if (/[\s"\\#;]/.test(s)) return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
145
120
  return s;
146
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {