@kendoo.agentdesk/agentdesk 0.18.6 → 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 +5 -0
- package/cli/session-sandbox.mjs +56 -84
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ 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
|
+
|
|
11
16
|
## [0.18.6] — 2026-04-18
|
|
12
17
|
|
|
13
18
|
### Fixed
|
package/cli/session-sandbox.mjs
CHANGED
|
@@ -1,81 +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.json — Claude Code's auth/config file
|
|
21
|
-
// ~/.claude — Claude Code's state dir (backups, caches)
|
|
22
|
-
// ~/.npm — npm cache (agents run npm during sessions)
|
|
23
|
-
// ~/.cache — generic XDG cache
|
|
24
|
-
// ~/.local — generic XDG state
|
|
25
|
-
// Works for both files and directories — symlinkSync handles both.
|
|
26
|
-
const HOME_PASSTHROUGH = [
|
|
27
|
-
".claude.json",
|
|
28
|
-
".claude",
|
|
29
|
-
".npm",
|
|
30
|
-
".cache",
|
|
31
|
-
".local",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
// Build a session scratch HOME. Returns { home, env, cleanup }.
|
|
35
|
-
// projectId — identifier used to make the dir path readable
|
|
36
|
-
// sessionId — unique per session; guarantees parallel sessions don't collide
|
|
37
|
-
// creds — { LINEAR_API_KEY?, JIRA_EMAIL?, JIRA_API_TOKEN?, GITHUB_TOKEN? }
|
|
38
|
-
// commitIdentity — { name, email } for git user.* (falls back to sensible defaults)
|
|
39
|
-
//
|
|
40
|
-
// The caller spawns the Claude child with:
|
|
41
|
-
// { ...process.env, ...env }
|
|
42
|
-
// …and MUST call cleanup() when the session ends (success, crash, signal).
|
|
43
27
|
export function createScratchHome({ projectId, sessionId, creds = {}, commitIdentity = {} }) {
|
|
44
28
|
const base = join(tmpdir(), "agentdesk-sessions", `${safe(projectId)}-${sessionId || randomUUID().slice(0, 8)}`);
|
|
45
|
-
|
|
46
|
-
mkdirSync(
|
|
47
|
-
mkdirSync(join(base, ".config", "gh"), { recursive: true, mode: 0o700 });
|
|
48
|
-
|
|
49
|
-
// Symlink the user's real state dirs for tools that need their own auth/cache
|
|
50
|
-
// (Claude Code itself, npm, etc). Without this the tools look for their
|
|
51
|
-
// auth at <scratch>/... and fail because we redirected HOME.
|
|
52
|
-
const realHome = homedir();
|
|
53
|
-
for (const dir of HOME_PASSTHROUGH) {
|
|
54
|
-
const src = join(realHome, dir);
|
|
55
|
-
const dst = join(base, dir);
|
|
56
|
-
if (!existsSync(src)) continue;
|
|
57
|
-
try { symlinkSync(src, dst); } catch {}
|
|
58
|
-
}
|
|
29
|
+
const ghConfigDir = join(base, "gh");
|
|
30
|
+
mkdirSync(ghConfigDir, { recursive: true, mode: 0o700 });
|
|
59
31
|
|
|
60
|
-
// --- 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.
|
|
61
35
|
if (creds.GITHUB_TOKEN) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`github.com:\n
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
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
|
+
);
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
// --- git: scoped gitconfig
|
|
71
|
-
//
|
|
72
|
-
//
|
|
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.
|
|
73
48
|
const helperPath = join(base, "gh-credential-helper.sh");
|
|
74
49
|
const helperScript = [
|
|
75
50
|
`#!/usr/bin/env bash`,
|
|
76
|
-
`# Per-session credential helper. Echoes the scoped GitHub token to git`,
|
|
77
|
-
`# so HTTPS pushes authenticate as this session's identity, regardless of`,
|
|
78
|
-
`# the user's global git-credential state.`,
|
|
79
51
|
`if [ "$1" = "get" ]; then`,
|
|
80
52
|
` while IFS= read -r line; do [ -z "$line" ] && break; done`,
|
|
81
53
|
` echo "username=x-access-token"`,
|
|
@@ -85,6 +57,7 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
|
|
|
85
57
|
].join("\n");
|
|
86
58
|
writeFileSync(helperPath, helperScript, { mode: 0o700 });
|
|
87
59
|
|
|
60
|
+
const gitconfigPath = join(base, "gitconfig");
|
|
88
61
|
const gitconfig = [
|
|
89
62
|
`[user]`,
|
|
90
63
|
`\tname = ${gitQuote(commitIdentity.name || "AgentDesk")}`,
|
|
@@ -92,34 +65,39 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
|
|
|
92
65
|
``,
|
|
93
66
|
`[credential "https://github.com"]`,
|
|
94
67
|
`\thelper = ${gitQuote(helperPath)}`,
|
|
95
|
-
``,
|
|
96
|
-
// Stop the global credential helper (osxkeychain, gcm, libsecret) from
|
|
97
|
-
// being consulted. Our helper above is the only one.
|
|
98
68
|
`[credential]`,
|
|
99
69
|
`\thelper = `,
|
|
100
70
|
`\thelper = ${gitQuote(helperPath)}`,
|
|
101
71
|
``,
|
|
102
72
|
].join("\n");
|
|
103
|
-
writeFileSync(
|
|
73
|
+
writeFileSync(gitconfigPath, gitconfig, { mode: 0o600 });
|
|
104
74
|
|
|
105
|
-
// --- 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.
|
|
106
78
|
const env = {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
GH_CONFIG_DIR:
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
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).
|
|
113
94
|
GITHUB_TOKEN: creds.GITHUB_TOKEN || "",
|
|
114
95
|
GH_TOKEN: creds.GITHUB_TOKEN || "",
|
|
115
|
-
|
|
116
|
-
// Linear / Jira — only this project's credentials travel into the session.
|
|
96
|
+
// Tracker-specific env — only this project's credentials reach the agent.
|
|
117
97
|
LINEAR_API_KEY: creds.LINEAR_API_KEY || "",
|
|
118
98
|
JIRA_EMAIL: creds.JIRA_EMAIL || "",
|
|
119
99
|
JIRA_API_TOKEN: creds.JIRA_API_TOKEN || "",
|
|
120
100
|
};
|
|
121
|
-
|
|
122
|
-
// Drop empty-string entries so child processes don't see ambiguous empty vars.
|
|
123
101
|
for (const k of Object.keys(env)) if (env[k] === "") delete env[k];
|
|
124
102
|
|
|
125
103
|
let cleaned = false;
|
|
@@ -128,11 +106,6 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
|
|
|
128
106
|
cleaned = true;
|
|
129
107
|
try { if (existsSync(base)) rmSync(base, { recursive: true, force: true }); } catch {}
|
|
130
108
|
}
|
|
131
|
-
|
|
132
|
-
// Best-effort cleanup if the parent process exits without an explicit call.
|
|
133
|
-
// Don't hijack SIGINT/SIGTERM — the daemon has its own signal handling and
|
|
134
|
-
// each session registering a process.exit() handler would kill the daemon
|
|
135
|
-
// as soon as one session saw a signal. /tmp is cleaned by the OS anyway.
|
|
136
109
|
process.once("exit", cleanup);
|
|
137
110
|
|
|
138
111
|
return { home: base, env, cleanup };
|
|
@@ -143,7 +116,6 @@ function safe(s) {
|
|
|
143
116
|
}
|
|
144
117
|
|
|
145
118
|
function gitQuote(s) {
|
|
146
|
-
// git config values: wrap in quotes if they contain whitespace or shell-sensitive chars.
|
|
147
119
|
if (/[\s"\\#;]/.test(s)) return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
148
120
|
return s;
|
|
149
121
|
}
|