@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 +10 -0
- package/cli/session-sandbox.mjs +56 -81
- package/package.json +1 -1
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
|
package/cli/session-sandbox.mjs
CHANGED
|
@@ -1,78 +1,53 @@
|
|
|
1
|
-
// Per-session
|
|
1
|
+
// Per-session scoped identity.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
10
|
-
//
|
|
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,
|
|
22
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
|
|
13
23
|
import { join } from "path";
|
|
14
|
-
import { tmpdir
|
|
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
|
-
|
|
43
|
-
mkdirSync(
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
`github.com:\n
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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(
|
|
73
|
+
writeFileSync(gitconfigPath, gitconfig, { mode: 0o600 });
|
|
101
74
|
|
|
102
|
-
// --- Scoped env for the child
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
GH_CONFIG_DIR:
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
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
|
}
|