@kendoo.agentdesk/agentdesk 0.16.3 → 0.17.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/daemon.mjs +1 -0
- package/cli/orchestrator.mjs +65 -10
- package/cli/session-isolation.mjs +160 -0
- 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.1] — 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `[CLI]` Sessions now run inside a kernel-enforced sandbox when one is available — `sandbox-exec` on macOS, `bwrap` (bubblewrap) on Linux. Writes outside the project directory and the session's scratch home are blocked at the kernel level; reads from known credential locations (`~/.ssh`, `~/.aws`, `~/.config/gh`, `~/.gitconfig`, `~/.netrc`, etc.) are denied so a confused agent cannot slurp up other projects' tokens. If the tool isn't installed (Linux without `bubblewrap` / unsupported platform), sessions fall back to the Tier 1 scoped-env isolation and a one-line notice explains why. Set `AGENTDESK_NO_SANDBOX=1` to opt out for debugging.
|
|
15
|
+
|
|
16
|
+
## [0.17.0] — 2026-04-18
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `[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.
|
|
20
|
+
|
|
11
21
|
## [0.16.3] — 2026-04-18
|
|
12
22
|
|
|
13
23
|
### Changed
|
package/cli/daemon.mjs
CHANGED
package/cli/orchestrator.mjs
CHANGED
|
@@ -7,6 +7,8 @@ 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";
|
|
11
|
+
import { wrapIsolatedSpawn, probeIsolation } from "./session-isolation.mjs";
|
|
10
12
|
|
|
11
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")).version;
|
|
@@ -72,10 +74,23 @@ function modelLabelForPhase(phase, phaseModels) {
|
|
|
72
74
|
export async function runOrchestrator({
|
|
73
75
|
taskId, taskLink, description, createTask, tracker, config,
|
|
74
76
|
project, team, teamSections, sessionUrl, cwd,
|
|
75
|
-
onEvent, apiKey, serverUrl, soloAgent, childStrategy,
|
|
77
|
+
onEvent, apiKey, serverUrl, soloAgent, childStrategy, sessionId,
|
|
76
78
|
}) {
|
|
77
79
|
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
78
|
-
|
|
80
|
+
|
|
81
|
+
// Build a per-session scratch HOME that isolates THIS project's identity
|
|
82
|
+
// from the user's global git/gh/ssh state and from other projects'
|
|
83
|
+
// credentials. Cleanup runs at session end regardless of outcome.
|
|
84
|
+
const sandbox = createScratchHome({
|
|
85
|
+
projectId: project?.name,
|
|
86
|
+
sessionId,
|
|
87
|
+
creds: trackerCreds,
|
|
88
|
+
commitIdentity: {
|
|
89
|
+
name: config?.identityBadge || "AgentDesk",
|
|
90
|
+
email: trackerCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
79
94
|
const startTime = Date.now();
|
|
80
95
|
let totalInputTokens = 0;
|
|
81
96
|
let totalOutputTokens = 0;
|
|
@@ -109,9 +124,21 @@ export async function runOrchestrator({
|
|
|
109
124
|
emit({ type: "phase:change", phase: initialPhase, model: representativeModel });
|
|
110
125
|
|
|
111
126
|
const modelArgs = modelArgsForPhase("EXECUTION", config?.phaseModels);
|
|
127
|
+
const wrapped = wrapIsolatedSpawn({
|
|
128
|
+
cmd: "claude",
|
|
129
|
+
args: ["-p", fullPrompt, ...modelArgs, "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep", "--verbose", "--output-format", "stream-json"],
|
|
130
|
+
cwd,
|
|
131
|
+
scratchHome: sandbox.home,
|
|
132
|
+
sessionId,
|
|
133
|
+
});
|
|
134
|
+
if (wrapped.isolation.kind !== "none") {
|
|
135
|
+
console.error(`[agentdesk] session isolation: ${wrapped.isolation.kind}`);
|
|
136
|
+
} else if (wrapped.isolation.reason) {
|
|
137
|
+
console.error(`[agentdesk] hard isolation unavailable (${wrapped.isolation.reason}) — scoped-env only`);
|
|
138
|
+
}
|
|
112
139
|
const child = spawn(
|
|
113
|
-
|
|
114
|
-
|
|
140
|
+
wrapped.cmd,
|
|
141
|
+
wrapped.args,
|
|
115
142
|
{ stdio: ["pipe", "pipe", "inherit"], shell: false, env, cwd }
|
|
116
143
|
);
|
|
117
144
|
child.stdin.end();
|
|
@@ -200,15 +227,24 @@ export async function runOrchestrator({
|
|
|
200
227
|
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
201
228
|
}
|
|
202
229
|
|
|
230
|
+
sandbox.cleanup();
|
|
231
|
+
|
|
203
232
|
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff: isHandoff };
|
|
204
233
|
}
|
|
205
234
|
|
|
206
235
|
// --- Phased orchestrator: runs 3 sequential Claude processes ---
|
|
207
236
|
|
|
208
|
-
async function runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs = [] }) {
|
|
237
|
+
async function runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs = [], scratchHome, sessionId }) {
|
|
238
|
+
const wrapped = wrapIsolatedSpawn({
|
|
239
|
+
cmd: "claude",
|
|
240
|
+
args: ["-p", prompt, ...modelArgs, "--allowedTools", "Bash,Read,Edit,Write,Glob,Grep", "--verbose", "--output-format", "stream-json"],
|
|
241
|
+
cwd,
|
|
242
|
+
scratchHome,
|
|
243
|
+
sessionId,
|
|
244
|
+
});
|
|
209
245
|
const child = spawn(
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
wrapped.cmd,
|
|
247
|
+
wrapped.args,
|
|
212
248
|
{ stdio: ["pipe", "pipe", "inherit"], shell: false, env, cwd }
|
|
213
249
|
);
|
|
214
250
|
child.stdin.end();
|
|
@@ -245,13 +281,30 @@ async function runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs = [
|
|
|
245
281
|
export async function runPhasedOrchestrator({
|
|
246
282
|
taskId, taskLink, description, createTask, tracker, config,
|
|
247
283
|
project, team, teamSections, sessionUrl, cwd,
|
|
248
|
-
onEvent, apiKey, serverUrl, onChild,
|
|
284
|
+
onEvent, apiKey, serverUrl, onChild, sessionId,
|
|
249
285
|
}) {
|
|
250
286
|
const trackerCreds = await fetchTrackerCredentials(project?.name, apiKey, serverUrl);
|
|
251
|
-
|
|
287
|
+
|
|
288
|
+
const sandbox = createScratchHome({
|
|
289
|
+
projectId: project?.name,
|
|
290
|
+
sessionId,
|
|
291
|
+
creds: trackerCreds,
|
|
292
|
+
commitIdentity: {
|
|
293
|
+
name: config?.identityBadge || "AgentDesk",
|
|
294
|
+
email: trackerCreds.JIRA_EMAIL || `agentdesk@local`,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const env = { ...process.env, ...loadDotEnv(cwd), ...sandbox.env };
|
|
252
298
|
const startTime = Date.now();
|
|
253
299
|
const teamNames = teamSections.names;
|
|
254
300
|
|
|
301
|
+
const probe = probeIsolation();
|
|
302
|
+
if (probe.kind !== "none") {
|
|
303
|
+
console.error(`[agentdesk] session isolation: ${probe.kind}`);
|
|
304
|
+
} else {
|
|
305
|
+
console.error(`[agentdesk] hard isolation unavailable (${probe.reason}) — scoped-env only`);
|
|
306
|
+
}
|
|
307
|
+
|
|
255
308
|
function timestamp() {
|
|
256
309
|
const d = new Date();
|
|
257
310
|
return [d.getHours(), d.getMinutes(), d.getSeconds()].map(n => String(n).padStart(2, "0")).join(":");
|
|
@@ -304,7 +357,7 @@ export async function runPhasedOrchestrator({
|
|
|
304
357
|
});
|
|
305
358
|
|
|
306
359
|
const modelArgs = modelArgsForPhase(phase, config?.phaseModels);
|
|
307
|
-
const result = await runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs });
|
|
360
|
+
const result = await runSinglePhase({ prompt, cwd, env, teamNames, emit, modelArgs, scratchHome: sandbox.home, sessionId });
|
|
308
361
|
|
|
309
362
|
if (onChild) onChild(result.child);
|
|
310
363
|
|
|
@@ -366,5 +419,7 @@ export async function runPhasedOrchestrator({
|
|
|
366
419
|
emit({ type: "session:end", duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
|
|
367
420
|
}
|
|
368
421
|
|
|
422
|
+
sandbox.cleanup();
|
|
423
|
+
|
|
369
424
|
return { duration, steps: totalSteps, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, handoff };
|
|
370
425
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Tier 2 kernel-enforced isolation.
|
|
2
|
+
//
|
|
3
|
+
// On top of the scoped HOME from session-sandbox.mjs, wrap the Claude child
|
|
4
|
+
// process in a kernel sandbox when one is available:
|
|
5
|
+
// - macOS: `sandbox-exec` (TrustedBSD sandbox, same primitive App Sandbox uses)
|
|
6
|
+
// - Linux: `bwrap` (bubblewrap — mount namespaces, same primitive Docker uses)
|
|
7
|
+
//
|
|
8
|
+
// Failure mode: if the tool isn't present or the profile can't be written,
|
|
9
|
+
// we emit a one-line notice and spawn the child as normal. The scoped HOME
|
|
10
|
+
// from Phase E still applies — isolation just degrades from "kernel boundary"
|
|
11
|
+
// to "scoped env."
|
|
12
|
+
//
|
|
13
|
+
// Escape hatch: AGENTDESK_NO_SANDBOX=1 skips this layer entirely.
|
|
14
|
+
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { existsSync, writeFileSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { platform, homedir } from "os";
|
|
19
|
+
|
|
20
|
+
// Detect which kernel-isolation tool is usable. Cached after first probe.
|
|
21
|
+
let cachedProbe = null;
|
|
22
|
+
export function probeIsolation() {
|
|
23
|
+
if (cachedProbe) return cachedProbe;
|
|
24
|
+
if (process.env.AGENTDESK_NO_SANDBOX === "1") {
|
|
25
|
+
cachedProbe = { kind: "none", reason: "AGENTDESK_NO_SANDBOX=1 set" };
|
|
26
|
+
return cachedProbe;
|
|
27
|
+
}
|
|
28
|
+
if (platform() === "darwin") {
|
|
29
|
+
try {
|
|
30
|
+
execSync("command -v sandbox-exec", { stdio: "pipe" });
|
|
31
|
+
cachedProbe = { kind: "sandbox-exec" };
|
|
32
|
+
return cachedProbe;
|
|
33
|
+
} catch {
|
|
34
|
+
cachedProbe = { kind: "none", reason: "sandbox-exec not found" };
|
|
35
|
+
return cachedProbe;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (platform() === "linux") {
|
|
39
|
+
try {
|
|
40
|
+
execSync("command -v bwrap", { stdio: "pipe" });
|
|
41
|
+
cachedProbe = { kind: "bwrap" };
|
|
42
|
+
return cachedProbe;
|
|
43
|
+
} catch {
|
|
44
|
+
cachedProbe = { kind: "none", reason: "bubblewrap not installed — apt install bubblewrap / brew install bubblewrap" };
|
|
45
|
+
return cachedProbe;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
cachedProbe = { kind: "none", reason: `no isolation tool for platform ${platform()}` };
|
|
49
|
+
return cachedProbe;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build the spawn arguments for an isolated claude invocation. Takes the
|
|
53
|
+
// original command/args/options and returns the wrapped form, plus a flag
|
|
54
|
+
// indicating which isolation mode is active.
|
|
55
|
+
export function wrapIsolatedSpawn({ cmd, args, cwd, scratchHome, sessionId }) {
|
|
56
|
+
const probe = probeIsolation();
|
|
57
|
+
if (probe.kind === "none") {
|
|
58
|
+
return { cmd, args, isolation: { kind: "none", reason: probe.reason } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (probe.kind === "sandbox-exec") {
|
|
62
|
+
const profilePath = join(scratchHome, "sandbox.sb");
|
|
63
|
+
writeFileSync(profilePath, macosProfile({ cwd, scratchHome }), { mode: 0o600 });
|
|
64
|
+
return {
|
|
65
|
+
cmd: "sandbox-exec",
|
|
66
|
+
args: ["-f", profilePath, cmd, ...args],
|
|
67
|
+
isolation: { kind: "sandbox-exec" },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (probe.kind === "bwrap") {
|
|
72
|
+
return {
|
|
73
|
+
cmd: "bwrap",
|
|
74
|
+
args: [...bwrapArgs({ cwd, scratchHome }), cmd, ...args],
|
|
75
|
+
isolation: { kind: "bwrap" },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { cmd, args, isolation: { kind: "none", reason: "unknown probe kind" } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- macOS sandbox-exec profile ---------------------------------------------
|
|
83
|
+
|
|
84
|
+
function macosProfile({ cwd, scratchHome }) {
|
|
85
|
+
const home = homedir();
|
|
86
|
+
// Known-sensitive user paths where credentials typically live. Reads denied
|
|
87
|
+
// even though general reads are allowed, so a confused agent can't slurp
|
|
88
|
+
// up other projects' tokens or the user's SSH keys.
|
|
89
|
+
const denyReadPaths = [
|
|
90
|
+
join(home, ".ssh"),
|
|
91
|
+
join(home, ".aws"),
|
|
92
|
+
join(home, ".gcloud"),
|
|
93
|
+
join(home, ".config", "gh"), // real gh config (scratch is under $TMPDIR)
|
|
94
|
+
join(home, ".docker"),
|
|
95
|
+
join(home, ".kube"),
|
|
96
|
+
join(home, ".agentdesk"),
|
|
97
|
+
join(home, ".config", "agentdesk"),
|
|
98
|
+
];
|
|
99
|
+
const denyReadLiterals = [
|
|
100
|
+
join(home, ".netrc"),
|
|
101
|
+
join(home, ".gitconfig"), // real gitconfig (scratch has its own)
|
|
102
|
+
join(home, ".npmrc"),
|
|
103
|
+
join(home, ".pypirc"),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Writes allowed only here. Everywhere else: read-only by default.
|
|
107
|
+
const writeSubpaths = [
|
|
108
|
+
cwd,
|
|
109
|
+
scratchHome,
|
|
110
|
+
"/tmp",
|
|
111
|
+
"/private/tmp",
|
|
112
|
+
"/private/var/folders", // macOS TMPDIR lives here
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const sb = [
|
|
116
|
+
`(version 1)`,
|
|
117
|
+
`(allow default)`,
|
|
118
|
+
``,
|
|
119
|
+
`; Deny reads to known credential locations`,
|
|
120
|
+
...denyReadPaths.map(p => `(deny file-read* (subpath ${sbString(p)}))`),
|
|
121
|
+
...denyReadLiterals.map(p => `(deny file-read* (literal ${sbString(p)}))`),
|
|
122
|
+
``,
|
|
123
|
+
`; Writes: deny everywhere, then allow the project dir + scratch + tmp`,
|
|
124
|
+
`(deny file-write*)`,
|
|
125
|
+
...writeSubpaths.map(p => `(allow file-write* (subpath ${sbString(p)}))`),
|
|
126
|
+
``,
|
|
127
|
+
`; Allow writes to devices so stdio, /dev/null, tty, ptys all work`,
|
|
128
|
+
`(allow file-write* (subpath "/dev"))`,
|
|
129
|
+
``,
|
|
130
|
+
].join("\n");
|
|
131
|
+
|
|
132
|
+
return sb;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sbString(s) {
|
|
136
|
+
return `"${String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Linux bwrap args --------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function bwrapArgs({ cwd, scratchHome }) {
|
|
142
|
+
// Everything read-only by default via ro-bind of /. Writable exceptions:
|
|
143
|
+
// the project dir, the scratch HOME, and a private /tmp. /home/<user> is
|
|
144
|
+
// replaced with a tmpfs so the agent can't browse the real home dir.
|
|
145
|
+
const home = homedir();
|
|
146
|
+
return [
|
|
147
|
+
"--ro-bind", "/", "/",
|
|
148
|
+
"--dev-bind", "/dev", "/dev",
|
|
149
|
+
"--proc", "/proc",
|
|
150
|
+
"--tmpfs", "/tmp",
|
|
151
|
+
"--tmpfs", home, // blank user home
|
|
152
|
+
"--bind", cwd, cwd, // project dir writable
|
|
153
|
+
"--bind", scratchHome, scratchHome, // scratch writable (HOME env var points here)
|
|
154
|
+
"--die-with-parent",
|
|
155
|
+
"--unshare-ipc",
|
|
156
|
+
"--unshare-uts",
|
|
157
|
+
"--unshare-pid",
|
|
158
|
+
// Network is NOT unshared — agents need network for tracker APIs and git.
|
|
159
|
+
];
|
|
160
|
+
}
|
|
@@ -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,
|