@kendoo.agentdesk/agentdesk 0.16.2 → 0.17.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/README.md +7 -3
- package/cli/daemon.mjs +1 -0
- package/cli/orchestrator.mjs +32 -4
- package/cli/session-sandbox.mjs +121 -0
- package/cli/team.mjs +1 -0
- 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.17.0] — 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `[CLI]` Each session now runs with a scoped identity. Before spawning, agentdesk builds a private scratch home containing only this project's tracker credentials, git commit identity, and a scoped GitHub credential helper. `gh`, `git`, and `ssh` inside the session see only what we put there — your global accounts, other projects' tokens, and keys outside the scratch dir aren't discovered by default lookups. Prevents accidental cross-project identity use on shared machines.
|
|
15
|
+
|
|
16
|
+
## [0.16.3] — 2026-04-18
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `[UI]` Docs and Guide pages updated to reflect the new tracker-setup flow: dedicated-user vs personal-account tradeoff, verify-and-echo step, config sync behavior, and the identity-badge field. README aligned with the same copy.
|
|
20
|
+
|
|
11
21
|
## [0.16.2] — 2026-04-18
|
|
12
22
|
|
|
13
23
|
### Added
|
package/README.md
CHANGED
|
@@ -43,7 +43,11 @@ Opens your browser to [agentdesk.live](https://agentdesk.live) where you sign up
|
|
|
43
43
|
agentdesk init
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
A guided wizard walks you through it: picks up your project type, asks which task tracker you use (Linear, Jira, GitHub Issues, or none), shows the tradeoff between a **dedicated AgentDesk user** and your personal account with step-by-step setup for either, verifies the credentials, echoes "Posting as: …" so you can confirm which identity will appear on tickets, then writes `.agentdesk.json`.
|
|
47
|
+
|
|
48
|
+
Re-running `agentdesk init` in a project that already has config offers a "tracker only" shortcut. Use `agentdesk init --quick` to skip the narrative copy for scripted setups.
|
|
49
|
+
|
|
50
|
+
`.agentdesk.json` is kept in sync with the server: every CLI run fetches the authoritative config and rewrites the local file as a credential-free snapshot. Credentials live only on the server, encrypted. If a local edit disagreed with the server's value, a one-line warning lists the overridden fields.
|
|
47
51
|
|
|
48
52
|
### 4. Run a team session
|
|
49
53
|
|
|
@@ -89,9 +93,9 @@ agentdesk update Update to the latest version
|
|
|
89
93
|
Settings can be managed in two ways:
|
|
90
94
|
|
|
91
95
|
1. **Web UI** — go to [agentdesk.live](https://agentdesk.live), select a project, and click the gear icon to configure tracker, team composition, custom agents, commands, and instructions.
|
|
92
|
-
2. **Local file** — `.agentdesk.json` in your project root.
|
|
96
|
+
2. **Local file** — `.agentdesk.json` in your project root. Acts as a credential-free cache of the server config, rewritten on every CLI run. If the server is unreachable, `.agentdesk.json` is used as-is (and never mutated).
|
|
93
97
|
|
|
94
|
-
The
|
|
98
|
+
The server is the source of truth. If you edit `.agentdesk.json` directly while the server has a different value for the same field, the server wins and the CLI prints a one-line warning listing the overridden fields so you notice. To push local edits back up, re-run `agentdesk init` (the wizard writes to the server).
|
|
95
99
|
|
|
96
100
|
### Task trackers
|
|
97
101
|
|
package/cli/daemon.mjs
CHANGED
package/cli/orchestrator.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { join, dirname } from "path";
|
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { buildPrompt, buildSoloPrompt, buildPhasedPrompt } from "./prompt.mjs";
|
|
9
9
|
import { createStreamParser } from "./stream-parser.mjs";
|
|
10
|
+
import { createScratchHome } from "./session-sandbox.mjs";
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
|
|
@@ -72,10 +73,23 @@ function modelLabelForPhase(phase, phaseModels) {
|
|
|
72
73
|
export async function runOrchestrator({
|
|
73
74
|
taskId, taskLink, description, createTask, tracker, config,
|
|
74
75
|
project, team, teamSections, sessionUrl, cwd,
|
|
75
|
-
onEvent, apiKey, serverUrl, soloAgent, childStrategy,
|
|
76
|
+
onEvent, apiKey, serverUrl, soloAgent, childStrategy, sessionId,
|
|
76
77
|
}) {
|
|
77
78
|
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
78
|
-
|
|
79
|
+
|
|
80
|
+
// Build a per-session scratch HOME that isolates THIS project's identity
|
|
81
|
+
// from the user's global git/gh/ssh state and from other projects'
|
|
82
|
+
// credentials. Cleanup runs at session end regardless of outcome.
|
|
83
|
+
const sandbox = createScratchHome({
|
|
84
|
+
projectId: project?.name,
|
|
85
|
+
sessionId,
|
|
86
|
+
creds: trackerCreds,
|
|
87
|
+
commitIdentity: {
|
|
88
|
+
name: config?.identityBadge || "AgentDesk",
|
|
89
|
+
email: trackerCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
79
93
|
const startTime = Date.now();
|
|
80
94
|
let totalInputTokens = 0;
|
|
81
95
|
let totalOutputTokens = 0;
|
|
@@ -200,6 +214,8 @@ export async function runOrchestrator({
|
|
|
200
214
|
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
201
215
|
}
|
|
202
216
|
|
|
217
|
+
sandbox.cleanup();
|
|
218
|
+
|
|
203
219
|
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff: isHandoff };
|
|
204
220
|
}
|
|
205
221
|
|
|
@@ -245,10 +261,20 @@ async function runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs = [
|
|
|
245
261
|
export async function runPhasedOrchestrator({
|
|
246
262
|
taskId, taskLink, description, createTask, tracker, config,
|
|
247
263
|
project, team, teamSections, sessionUrl, cwd,
|
|
248
|
-
onEvent, apiKey, serverUrl, onChild,
|
|
264
|
+
onEvent, apiKey, serverUrl, onChild, sessionId,
|
|
249
265
|
}) {
|
|
250
266
|
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
251
|
-
|
|
267
|
+
|
|
268
|
+
const sandbox = createScratchHome({
|
|
269
|
+
projectId: project?.name,
|
|
270
|
+
sessionId,
|
|
271
|
+
creds: trackerCreds,
|
|
272
|
+
commitIdentity: {
|
|
273
|
+
name: config?.identityBadge || "AgentDesk",
|
|
274
|
+
email: trackerCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
252
278
|
const startTime = Date.now();
|
|
253
279
|
const teamNames = teamSections.names;
|
|
254
280
|
|
|
@@ -366,5 +392,7 @@ export async function runPhasedOrchestrator({
|
|
|
366
392
|
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
sandbox.cleanup();
|
|
396
|
+
|
|
369
397
|
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff };
|
|
370
398
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Per-session scratch HOME for identity isolation.
|
|
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.
|
|
8
|
+
//
|
|
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.
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, writeFileSync, chmodSync, rmSync, existsSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { randomUUID } from "crypto";
|
|
16
|
+
|
|
17
|
+
// Build a session scratch HOME. Returns { home, env, cleanup }.
|
|
18
|
+
// projectId — identifier used to make the dir path readable
|
|
19
|
+
// sessionId — unique per session; guarantees parallel sessions don't collide
|
|
20
|
+
// creds — { LINEAR_API_KEY?, JIRA_EMAIL?, JIRA_API_TOKEN?, GITHUB_TOKEN? }
|
|
21
|
+
// commitIdentity — { name, email } for git user.* (falls back to sensible defaults)
|
|
22
|
+
//
|
|
23
|
+
// The caller spawns the Claude child with:
|
|
24
|
+
// { ...process.env, ...env }
|
|
25
|
+
// …and MUST call cleanup() when the session ends (success, crash, signal).
|
|
26
|
+
export function createScratchHome({ projectId, sessionId, creds = {}, commitIdentity = {} }) {
|
|
27
|
+
const base = join(tmpdir(), "agentdesk-sessions", `${safe(projectId)}-${sessionId || randomUUID().slice(0, 8)}`);
|
|
28
|
+
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
29
|
+
mkdirSync(join(base, ".config"), { recursive: true, mode: 0o700 });
|
|
30
|
+
mkdirSync(join(base, ".config", "gh"), { recursive: true, mode: 0o700 });
|
|
31
|
+
|
|
32
|
+
// --- gh: scoped hosts.yml with only this project's GitHub token ---
|
|
33
|
+
if (creds.GITHUB_TOKEN) {
|
|
34
|
+
const hostsPath = join(base, ".config", "gh", "hosts.yml");
|
|
35
|
+
const hostsYaml =
|
|
36
|
+
`github.com:\n` +
|
|
37
|
+
` oauth_token: ${creds.GITHUB_TOKEN}\n` +
|
|
38
|
+
` git_protocol: https\n`;
|
|
39
|
+
writeFileSync(hostsPath, hostsYaml, { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- git: scoped gitconfig with commit identity and a credential helper
|
|
43
|
+
// that echoes this token for github.com HTTPS pushes. This bypasses
|
|
44
|
+
// macOS Keychain, SSH keys, and the global git-credential-manager. ---
|
|
45
|
+
const helperPath = join(base, "gh-credential-helper.sh");
|
|
46
|
+
const helperScript = [
|
|
47
|
+
`#!/usr/bin/env bash`,
|
|
48
|
+
`# Per-session credential helper. Echoes the scoped GitHub token to git`,
|
|
49
|
+
`# so HTTPS pushes authenticate as this session's identity, regardless of`,
|
|
50
|
+
`# the user's global git-credential state.`,
|
|
51
|
+
`if [ "$1" = "get" ]; then`,
|
|
52
|
+
` while IFS= read -r line; do [ -z "$line" ] && break; done`,
|
|
53
|
+
` echo "username=x-access-token"`,
|
|
54
|
+
` echo "password=${creds.GITHUB_TOKEN || ""}"`,
|
|
55
|
+
`fi`,
|
|
56
|
+
``,
|
|
57
|
+
].join("\n");
|
|
58
|
+
writeFileSync(helperPath, helperScript, { mode: 0o700 });
|
|
59
|
+
|
|
60
|
+
const gitconfig = [
|
|
61
|
+
`[user]`,
|
|
62
|
+
`\tname = ${gitQuote(commitIdentity.name || "AgentDesk")}`,
|
|
63
|
+
`\temail = ${gitQuote(commitIdentity.email || "agentdesk@local")}`,
|
|
64
|
+
``,
|
|
65
|
+
`[credential "https://github.com"]`,
|
|
66
|
+
`\thelper = ${gitQuote(helperPath)}`,
|
|
67
|
+
``,
|
|
68
|
+
// Stop the global credential helper (osxkeychain, gcm, libsecret) from
|
|
69
|
+
// being consulted. Our helper above is the only one.
|
|
70
|
+
`[credential]`,
|
|
71
|
+
`\thelper = `,
|
|
72
|
+
`\thelper = ${gitQuote(helperPath)}`,
|
|
73
|
+
``,
|
|
74
|
+
].join("\n");
|
|
75
|
+
writeFileSync(join(base, ".gitconfig"), gitconfig, { mode: 0o600 });
|
|
76
|
+
|
|
77
|
+
// --- Scoped env for the child process ---
|
|
78
|
+
const env = {
|
|
79
|
+
HOME: base,
|
|
80
|
+
XDG_CONFIG_HOME: join(base, ".config"),
|
|
81
|
+
GH_CONFIG_DIR: join(base, ".config", "gh"),
|
|
82
|
+
|
|
83
|
+
// Explicit GITHUB_TOKEN overrides gh's stored auth (which we've
|
|
84
|
+
// already replaced with our hosts.yml, but belt + suspenders).
|
|
85
|
+
GITHUB_TOKEN: creds.GITHUB_TOKEN || "",
|
|
86
|
+
GH_TOKEN: creds.GITHUB_TOKEN || "",
|
|
87
|
+
|
|
88
|
+
// Linear / Jira — only this project's credentials travel into the session.
|
|
89
|
+
LINEAR_API_KEY: creds.LINEAR_API_KEY || "",
|
|
90
|
+
JIRA_EMAIL: creds.JIRA_EMAIL || "",
|
|
91
|
+
JIRA_API_TOKEN: creds.JIRA_API_TOKEN || "",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Drop empty-string entries so child processes don't see ambiguous empty vars.
|
|
95
|
+
for (const k of Object.keys(env)) if (env[k] === "") delete env[k];
|
|
96
|
+
|
|
97
|
+
let cleaned = false;
|
|
98
|
+
function cleanup() {
|
|
99
|
+
if (cleaned) return;
|
|
100
|
+
cleaned = true;
|
|
101
|
+
try { if (existsSync(base)) rmSync(base, { recursive: true, force: true }); } catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Best-effort cleanup if the parent process exits without an explicit call.
|
|
105
|
+
// Don't hijack SIGINT/SIGTERM — the daemon has its own signal handling and
|
|
106
|
+
// each session registering a process.exit() handler would kill the daemon
|
|
107
|
+
// as soon as one session saw a signal. /tmp is cleaned by the OS anyway.
|
|
108
|
+
process.once("exit", cleanup);
|
|
109
|
+
|
|
110
|
+
return { home: base, env, cleanup };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function safe(s) {
|
|
114
|
+
return String(s || "unknown").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 40);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function gitQuote(s) {
|
|
118
|
+
// git config values: wrap in quotes if they contain whitespace or shell-sensitive chars.
|
|
119
|
+
if (/[\s"\\#;]/.test(s)) return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
120
|
+
return s;
|
|
121
|
+
}
|
package/cli/team.mjs
CHANGED
|
@@ -210,6 +210,7 @@ export async function runTeam(taskId, opts = {}) {
|
|
|
210
210
|
const result = await orchestrate({
|
|
211
211
|
taskId, taskLink, description, createTask, tracker, config,
|
|
212
212
|
project, team, teamSections, sessionUrl, cwd,
|
|
213
|
+
sessionId,
|
|
213
214
|
onEvent: vizSend,
|
|
214
215
|
apiKey,
|
|
215
216
|
serverUrl: agentdeskServer,
|