@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 +45 -0
- package/cli/login.mjs +5 -4
- package/cli/session-sandbox.mjs +14 -0
- package/package.json +1 -1
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`);
|
package/cli/session-sandbox.mjs
CHANGED
|
@@ -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");
|