@kendoo.agentdesk/agentdesk 0.24.0 → 0.25.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/cli/daemon.mjs CHANGED
@@ -92,7 +92,16 @@ function logSessionMetadata(sessionId, metadata) {
92
92
 
93
93
  // --- Find project on local filesystem ---
94
94
 
95
+ // AD-47: server-pushed projectName flows into path.join() here. Validate
96
+ // against a strict character set so values like "../../etc" can't stat
97
+ // arbitrary host paths or land in projects.json with traversal sequences
98
+ // for later use.
99
+ const PROJECT_NAME_RE = /^[A-Za-z0-9._-]{1,64}$/;
100
+
95
101
  function findProjectLocally(projectName) {
102
+ if (!PROJECT_NAME_RE.test(String(projectName || ""))) {
103
+ return null;
104
+ }
96
105
  const home = process.env.HOME || process.env.USERPROFILE;
97
106
  // Search common code directories for a folder matching the project name
98
107
  const searchRoots = [
@@ -318,6 +327,27 @@ export async function runDaemon() {
318
327
  }
319
328
  }
320
329
 
330
+ // AD-34 confirmation helper. Prints session details and asks y/N at the local
331
+ // terminal. Returns true if the user accepts. If stdin isn't a TTY (running
332
+ // headless / no terminal), refuse — the safer default. Set
333
+ // AGENTDESK_DAEMON_AUTO_ACCEPT=1 to skip this prompt for trusted automation.
334
+ async function confirmIncomingSession({ project, taskId, prompt }) {
335
+ if (!process.stdin.isTTY) {
336
+ console.log(" Refusing server-pushed session: no terminal to confirm at. Set AGENTDESK_DAEMON_AUTO_ACCEPT=1 to allow.");
337
+ return false;
338
+ }
339
+ const promptPreview = String(prompt || "(no prompt)").trim().slice(0, 240).replace(/\s+/g, " ");
340
+ console.log("");
341
+ console.log(` Incoming session for project: ${project.name}`);
342
+ if (taskId) console.log(` Task: ${taskId}`);
343
+ console.log(` Prompt: ${promptPreview}${String(prompt || "").length > 240 ? "..." : ""}`);
344
+ console.log("");
345
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
346
+ const answer = await new Promise(resolve => rl.question(" Approve this session? [y/N] ", resolve));
347
+ rl.close();
348
+ return /^y(es)?$/i.test(String(answer).trim());
349
+ }
350
+
321
351
  // 4. Session handling
322
352
 
323
353
  async function handleStartSession({ sessionId, projectId, taskId: remoteTaskId, prompt, phased, screenshots: screenshotsOverride }) {
@@ -329,6 +359,21 @@ export async function runDaemon() {
329
359
  return;
330
360
  }
331
361
 
362
+ // AD-34: server-pushed sessions get a human confirmation prompt at the
363
+ // local terminal before any code runs. If an attacker steals the user's
364
+ // api_key, they can POST to /api/daemon/sessions, but cannot push a
365
+ // command through to the victim machine without someone at the keyboard
366
+ // saying yes. Opt out via AGENTDESK_DAEMON_AUTO_ACCEPT=1 for trusted
367
+ // automation environments.
368
+ if (process.env.AGENTDESK_DAEMON_AUTO_ACCEPT !== "1") {
369
+ const accepted = await confirmIncomingSession({ project, taskId: remoteTaskId, prompt });
370
+ if (!accepted) {
371
+ console.log(` ${yellow}Rejected by user:${reset} ${dim}${sessionId}${reset}`);
372
+ send({ type: "daemon:error", sessionId, error: "Session declined by daemon owner" });
373
+ return;
374
+ }
375
+ }
376
+
332
377
  // Enforce max 1 concurrent session — set flag BEFORE any async work to prevent race
333
378
  if (activeSession) {
334
379
  console.log(` ${red}Rejected:${reset} session already running ${dim}(${activeSession.sessionId})${reset}`);
package/cli/login.mjs CHANGED
@@ -81,11 +81,12 @@ export async function runLogin() {
81
81
 
82
82
  const state = randomUUID();
83
83
 
84
- // Start a local server to receive the API key callback
84
+ // Start a local server to receive the API key callback. AD-48: no CORS
85
+ // headers — the dashboard hits this endpoint as a top-level navigation
86
+ // (window.location.href = ...), which doesn't require CORS. Removing the
87
+ // wildcard ACAO closes the timing oracle that let any origin XHR-probe
88
+ // whether a login was in progress on this port.
85
89
  const server = createServer((req, res) => {
86
- // Allow CORS from agentdesk.live
87
- res.setHeader("Access-Control-Allow-Origin", "*");
88
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
89
90
  if (req.method === "OPTIONS") { res.writeHead(200); res.end(); return; }
90
91
 
91
92
  const url = new URL(req.url, `http://localhost`);
@@ -24,6 +24,16 @@ import { join } from "path";
24
24
  import { tmpdir } from "os";
25
25
  import { randomUUID } from "crypto";
26
26
 
27
+ // AD-46: refuse any value with embedded newlines before it lands in YAML or
28
+ // gitconfig. Real GitHub PATs / emails / names won't have them, but a server-
29
+ // pushed identity field with a stray \n could inject an extra config line.
30
+ function assertNoNewline(name, value) {
31
+ if (typeof value !== "string") return;
32
+ if (/[\r\n]/.test(value)) {
33
+ throw new Error(`Refusing to write scratch config: ${name} contains a newline`);
34
+ }
35
+ }
36
+
27
37
  export function createScratchHome({ projectId, sessionId, creds = {}, commitIdentity = {} }) {
28
38
  if (!creds.GITHUB_TOKEN) {
29
39
  // Caller is responsible for running session-preflight before reaching
@@ -31,6 +41,10 @@ export function createScratchHome({ projectId, sessionId, creds = {}, commitIden
31
41
  // fail loudly instead.
32
42
  throw new Error("createScratchHome called without GITHUB_TOKEN — preflight should have caught this");
33
43
  }
44
+ // AD-46: validate inputs before they're concatenated into structured files.
45
+ assertNoNewline("GITHUB_TOKEN", creds.GITHUB_TOKEN);
46
+ assertNoNewline("commitIdentity.name", commitIdentity.name);
47
+ assertNoNewline("commitIdentity.email", commitIdentity.email);
34
48
 
35
49
  const base = join(tmpdir(), "agentdesk-sessions", `${safe(projectId)}-${sessionId || randomUUID().slice(0, 8)}`);
36
50
  const ghConfigDir = join(base, "gh");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {