@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 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
- createInstr += `Create a Linear issue using the GraphQL API:\n- Endpoint: https://api.linear.app/graphql\n- Auth: Authorization: $LINEAR_API_KEY\n- Set the title based on the description below\n- After creation, use the returned identifier (e.g., KEN-530) as the task ID for the rest of the session\n`;
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
- 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- Set the summary based on the description below\n- After creation, use the returned key (e.g., PROJ-42) as the task ID for the rest of the session\n`;
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
  }
@@ -1,81 +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.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
- mkdirSync(base, { recursive: true, mode: 0o700 });
46
- mkdirSync(join(base, ".config"), { recursive: true, mode: 0o700 });
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 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.
61
35
  if (creds.GITHUB_TOKEN) {
62
- const hostsPath = join(base, ".config", "gh", "hosts.yml");
63
- const hostsYaml =
64
- `github.com:\n` +
65
- ` oauth_token: ${creds.GITHUB_TOKEN}\n` +
66
- ` git_protocol: https\n`;
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 with commit identity and a credential helper
71
- // that echoes this token for github.com HTTPS pushes. This bypasses
72
- // 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.
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(join(base, ".gitconfig"), gitconfig, { mode: 0o600 });
73
+ writeFileSync(gitconfigPath, gitconfig, { mode: 0o600 });
104
74
 
105
- // --- 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.
106
78
  const env = {
107
- HOME: base,
108
- XDG_CONFIG_HOME: join(base, ".config"),
109
- GH_CONFIG_DIR: join(base, ".config", "gh"),
110
-
111
- // Explicit GITHUB_TOKEN overrides gh's stored auth (which we've
112
- // 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).
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.18.6",
3
+ "version": "0.19.1",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {