@kendoo.agentdesk/agentdesk 0.18.6 → 0.19.1
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/prompt.mjs +24 -2
- package/cli/session-sandbox.mjs +56 -84
- 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.1] — 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `[CLI]` Creating a new task from a description now locks the task to the project configured in settings. The prompt previously told the agent to "create a Jira issue" without naming a project, so Claude was free to pick any project its token had access to. Jira and Linear create-from-description paths now explicitly pass the configured project key / team key and tell the agent not to choose a different one.
|
|
15
|
+
|
|
16
|
+
## [0.19.0] — 2026-04-18
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `[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.
|
|
20
|
+
|
|
11
21
|
## [0.18.6] — 2026-04-18
|
|
12
22
|
|
|
13
23
|
### Fixed
|
package/cli/prompt.mjs
CHANGED
|
@@ -61,9 +61,23 @@ export function buildPrompt({ taskId, taskLink, description, createTask, tracker
|
|
|
61
61
|
if (createTask && description) {
|
|
62
62
|
let createInstr = "\n\n## CREATE TASK (MANDATORY — FIRST ACTION IN INTAKE)\n\nNo task ID was provided — only a description. Jane MUST create a new task in the tracker as the VERY FIRST action before anything else.\n\n";
|
|
63
63
|
if (tracker === "linear") {
|
|
64
|
-
|
|
64
|
+
const linearTeamKey = config.linear?.teamKey;
|
|
65
|
+
createInstr += `Create a Linear issue using the GraphQL API:\n- Endpoint: https://api.linear.app/graphql\n- Auth: Authorization: $LINEAR_API_KEY\n`;
|
|
66
|
+
if (linearTeamKey) {
|
|
67
|
+
createInstr += `- **Team: MUST be \`${linearTeamKey}\`.** First resolve the team id: \`{ teams(filter: { key: { eq: "${linearTeamKey}" } }) { nodes { id } } }\`, then pass that id as \`teamId\` in \`issueCreate\`. Do NOT pick any other team, even if your account has access to others.\n`;
|
|
68
|
+
} else {
|
|
69
|
+
createInstr += `- ⚠ No Linear team key is configured. Ask the user which team this issue should live in BEFORE creating it.\n`;
|
|
70
|
+
}
|
|
71
|
+
createInstr += `- Set the title based on the description below\n- After creation, use the returned identifier (e.g., ${linearTeamKey || "KEN"}-530) as the task ID for the rest of the session\n`;
|
|
65
72
|
} else if (tracker === "jira") {
|
|
66
|
-
|
|
73
|
+
const jiraProject = config.jira?.project;
|
|
74
|
+
createInstr += `Create a Jira issue:\n- Endpoint: ${config.jira?.baseUrl || ""}/rest/api/3/issue\n- Auth: Basic auth with $JIRA_EMAIL and $JIRA_API_TOKEN\n`;
|
|
75
|
+
if (jiraProject) {
|
|
76
|
+
createInstr += `- **Project: MUST be \`${jiraProject}\`.** Set \`fields.project.key = "${jiraProject}"\` in the create payload. Do NOT pick any other project, even if your account has access to others.\n`;
|
|
77
|
+
} else {
|
|
78
|
+
createInstr += `- ⚠ No Jira project key is configured. Ask the user which project this task should live in BEFORE creating it.\n`;
|
|
79
|
+
}
|
|
80
|
+
createInstr += `- Set the summary based on the description below\n- After creation, use the returned key (e.g., ${jiraProject || "PROJ"}-42) as the task ID for the rest of the session\n`;
|
|
67
81
|
} else if (tracker === "github") {
|
|
68
82
|
createInstr += `Create a GitHub issue:\n- Run: gh issue create --title "..." --body "..."\n- After creation, use the returned issue number as the task ID for the rest of the session\n`;
|
|
69
83
|
}
|
|
@@ -442,9 +456,17 @@ export function buildPhasedPrompt({ phase, taskId, taskLink, description, create
|
|
|
442
456
|
if (phase === "INTAKE" && createTask && description) {
|
|
443
457
|
let createInstr = "\n\n## CREATE TASK (MANDATORY — FIRST ACTION)\n\nNo task ID was provided. Jane MUST create a new task in the tracker as the VERY FIRST action.\n\n";
|
|
444
458
|
if (tracker === "linear") {
|
|
459
|
+
const linearTeamKey = config.linear?.teamKey;
|
|
445
460
|
createInstr += `Create a Linear issue using the GraphQL API.\n`;
|
|
461
|
+
if (linearTeamKey) {
|
|
462
|
+
createInstr += `**Team MUST be \`${linearTeamKey}\`.** Resolve its id via \`{ teams(filter: { key: { eq: "${linearTeamKey}" } }) { nodes { id } } }\` and pass as \`teamId\` in \`issueCreate\`. Do NOT pick any other team.\n`;
|
|
463
|
+
}
|
|
446
464
|
} else if (tracker === "jira") {
|
|
465
|
+
const jiraProject = config.jira?.project;
|
|
447
466
|
createInstr += `Create a Jira issue at ${config.jira?.baseUrl || ""}.\n`;
|
|
467
|
+
if (jiraProject) {
|
|
468
|
+
createInstr += `**Project MUST be \`${jiraProject}\`.** Set \`fields.project.key = "${jiraProject}"\` in the create payload. Do NOT pick any other project.\n`;
|
|
469
|
+
}
|
|
448
470
|
} else if (tracker === "github") {
|
|
449
471
|
createInstr += `Create a GitHub issue: gh issue create --title "..." --body "..."\n`;
|
|
450
472
|
}
|
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
|
}
|